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
278 lines
9.1 KiB
Rust
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)
|
|
}
|
|
}
|