release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES: - Relationship syntax now requires blocks for all participants - Removed self/other perspective blocks from relationships - Replaced 'guard' keyword with 'if' for behavior tree decorators Language Features: - Add tree-sitter grammar with improved if/condition disambiguation - Add comprehensive tutorial and reference documentation - Add SBIR v0.2.0 binary format specification - Add resource linking system for behaviors and schedules - Add year-long schedule patterns (day, season, recurrence) - Add behavior tree enhancements (named nodes, decorators) Documentation: - Complete tutorial series (9 chapters) with baker family examples - Complete reference documentation for all language features - SBIR v0.2.0 specification with binary format details - Added locations and institutions documentation Examples: - Convert all examples to baker family scenario - Add comprehensive working examples Tooling: - Zed extension with LSP integration - Tree-sitter grammar for syntax highlighting - Build scripts and development tools Version Updates: - Main package: 0.1.0 → 0.2.0 - Tree-sitter grammar: 0.1.0 → 0.2.0 - Zed extension: 0.1.0 → 0.2.0 - Storybook editor: 0.1.0 → 0.2.0
This commit is contained in:
277
storybook-editor/src/app.rs
Normal file
277
storybook-editor/src/app.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use iced::mouse;
|
||||
/// Main application state and logic
|
||||
use iced::{
|
||||
Element,
|
||||
Event,
|
||||
Subscription,
|
||||
Task,
|
||||
};
|
||||
use iced_code_editor::Message as EditorMessage;
|
||||
|
||||
use crate::{
|
||||
file_editor_state::FileEditorState,
|
||||
recent_projects::RecentProjects,
|
||||
ui,
|
||||
};
|
||||
|
||||
/// Represents a tab in the editor
|
||||
#[derive(Debug)]
|
||||
pub enum Tab {
|
||||
Character(String), // Character name
|
||||
File(FileEditorState), // File editor state
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn title(&self) -> String {
|
||||
match self {
|
||||
| Tab::Character(name) => name.clone(),
|
||||
| Tab::File(state) => state.filename(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Editor {
|
||||
pub project_path: Option<PathBuf>,
|
||||
pub project: Option<storybook::Project>,
|
||||
pub error_message: Option<String>,
|
||||
pub tabs: Vec<Tab>,
|
||||
pub active_tab: Option<usize>,
|
||||
pub hovered_tab: Option<usize>,
|
||||
pub expanded_folders: std::collections::HashSet<String>, // Track which folders are expanded
|
||||
pub recent_projects: RecentProjects,
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
project_path: None,
|
||||
project: None,
|
||||
error_message: None,
|
||||
tabs: Vec::new(),
|
||||
active_tab: None,
|
||||
hovered_tab: None,
|
||||
expanded_folders: std::collections::HashSet::new(),
|
||||
recent_projects: RecentProjects::load(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
// Menu actions
|
||||
OpenProjectDialog,
|
||||
ProjectSelected(Option<PathBuf>),
|
||||
ProjectLoaded(Result<storybook::Project, String>),
|
||||
CloseProject,
|
||||
SaveProject,
|
||||
Exit,
|
||||
// Entity selection
|
||||
SelectCharacter(String),
|
||||
// Quick actions
|
||||
ValidateProject,
|
||||
NewCharacter,
|
||||
// Tab management
|
||||
OpenCharacterTab(String),
|
||||
OpenFileTab(PathBuf),
|
||||
SwitchTab(usize),
|
||||
CloseTab(usize),
|
||||
TabHovered(Option<usize>),
|
||||
// File editing
|
||||
EditorAction(usize, EditorMessage),
|
||||
// File tree
|
||||
ToggleFolder(String),
|
||||
EventOccurred(Event),
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new() -> (Self, Task<Message>) {
|
||||
(Self::default(), Task::none())
|
||||
}
|
||||
|
||||
pub fn title(&self) -> String {
|
||||
"Storybook Editor".to_string()
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> iced::Theme {
|
||||
crate::theme::storybook_theme()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
| Message::OpenProjectDialog => {
|
||||
// Open file picker dialog
|
||||
Task::perform(
|
||||
async {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Select Storybook Project Directory")
|
||||
.pick_folder()
|
||||
.await
|
||||
.map(|handle| handle.path().to_path_buf())
|
||||
},
|
||||
Message::ProjectSelected,
|
||||
)
|
||||
},
|
||||
| Message::ProjectSelected(Some(path)) => {
|
||||
self.project_path = Some(path.clone());
|
||||
self.error_message = None;
|
||||
|
||||
// Load the project asynchronously
|
||||
Task::perform(
|
||||
async move {
|
||||
match storybook::Project::load(&path) {
|
||||
| Ok(project) => Ok(project),
|
||||
| Err(e) => Err(format!("Failed to load project: {}", e)),
|
||||
}
|
||||
},
|
||||
Message::ProjectLoaded,
|
||||
)
|
||||
},
|
||||
| Message::ProjectSelected(None) => {
|
||||
// User cancelled the dialog
|
||||
Task::none()
|
||||
},
|
||||
| Message::ProjectLoaded(Ok(project)) => {
|
||||
self.project = Some(project);
|
||||
self.error_message = None;
|
||||
|
||||
// Add to recent projects
|
||||
if let Some(ref path) = self.project_path {
|
||||
self.recent_projects.add(path.clone());
|
||||
}
|
||||
|
||||
Task::none()
|
||||
},
|
||||
| Message::ProjectLoaded(Err(error)) => {
|
||||
self.error_message = Some(error);
|
||||
self.project = None;
|
||||
Task::none()
|
||||
},
|
||||
| Message::CloseProject => {
|
||||
self.project_path = None;
|
||||
self.project = None;
|
||||
self.error_message = None;
|
||||
Task::none()
|
||||
},
|
||||
| Message::SaveProject => {
|
||||
// TODO: Implement save functionality
|
||||
Task::none()
|
||||
},
|
||||
| Message::Exit => iced::exit(),
|
||||
| Message::SelectCharacter(name) => {
|
||||
// Open character in a new tab (or switch to existing tab)
|
||||
Task::done(Message::OpenCharacterTab(name))
|
||||
},
|
||||
| Message::OpenCharacterTab(name) => {
|
||||
// Check if tab already exists
|
||||
if let Some(idx) = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|t| matches!(t, Tab::Character(n) if n == &name))
|
||||
{
|
||||
self.active_tab = Some(idx);
|
||||
} else {
|
||||
// For now, just create a character tab
|
||||
// TODO: In the future, find the .sb file and open it as a File tab
|
||||
self.tabs.push(Tab::Character(name));
|
||||
self.active_tab = Some(self.tabs.len() - 1);
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::OpenFileTab(path) => {
|
||||
// Check if tab already exists
|
||||
if let Some(idx) = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|t| matches!(t, Tab::File(state) if state.path == path))
|
||||
{
|
||||
self.active_tab = Some(idx);
|
||||
} else {
|
||||
// Load file content
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
let state = FileEditorState::new(path, content);
|
||||
self.tabs.push(Tab::File(state));
|
||||
self.active_tab = Some(self.tabs.len() - 1);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::EditorAction(tab_idx, editor_msg) => {
|
||||
// Update the code editor
|
||||
if let Some(Tab::File(state)) = self.tabs.get_mut(tab_idx) {
|
||||
return state
|
||||
.editor
|
||||
.update(&editor_msg)
|
||||
.map(move |msg| Message::EditorAction(tab_idx, msg));
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::ToggleFolder(folder_path) => {
|
||||
// Toggle folder expanded/collapsed state
|
||||
if self.expanded_folders.contains(&folder_path) {
|
||||
self.expanded_folders.remove(&folder_path);
|
||||
} else {
|
||||
self.expanded_folders.insert(folder_path);
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::SwitchTab(idx) => {
|
||||
if idx < self.tabs.len() {
|
||||
self.active_tab = Some(idx);
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::CloseTab(idx) => {
|
||||
if idx < self.tabs.len() {
|
||||
self.tabs.remove(idx);
|
||||
// Adjust active tab
|
||||
if self.tabs.is_empty() {
|
||||
self.active_tab = None;
|
||||
} else if let Some(active) = self.active_tab {
|
||||
if active >= self.tabs.len() {
|
||||
self.active_tab = Some(self.tabs.len() - 1);
|
||||
} else if active > idx {
|
||||
self.active_tab = Some(active - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::ValidateProject => {
|
||||
// TODO: Run validation and show results
|
||||
// For now, just clear any error
|
||||
if self.project.is_some() {
|
||||
self.error_message = Some("✓ Project validated successfully!".to_string());
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
| Message::NewCharacter => {
|
||||
// TODO: Open character creation dialog
|
||||
self.error_message = Some("New character creation coming soon!".to_string());
|
||||
Task::none()
|
||||
},
|
||||
| Message::TabHovered(idx) => {
|
||||
self.hovered_tab = idx;
|
||||
Task::none()
|
||||
},
|
||||
| Message::EventOccurred(event) => {
|
||||
// Handle middle-click to close tab
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event {
|
||||
if let Some(idx) = self.hovered_tab {
|
||||
return Task::done(Message::CloseTab(idx));
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<'_, Message> {
|
||||
ui::view(self)
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
iced::event::listen().map(Message::EventOccurred)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user