Files
storybook/storybook-editor/src/app.rs
Sienna Meridian Satterwhite 16deb5d237 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
2026-02-13 21:52:03 +00:00

278 lines
9.1 KiB
Rust

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)
}
}