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, pub project: Option, pub error_message: Option, pub tabs: Vec, pub active_tab: Option, pub hovered_tab: Option, pub expanded_folders: std::collections::HashSet, // 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), ProjectLoaded(Result), CloseProject, SaveProject, Exit, // Entity selection SelectCharacter(String), // Quick actions ValidateProject, NewCharacter, // Tab management OpenCharacterTab(String), OpenFileTab(PathBuf), SwitchTab(usize), CloseTab(usize), TabHovered(Option), // File editing EditorAction(usize, EditorMessage), // File tree ToggleFolder(String), EventOccurred(Event), } impl Editor { pub fn new() -> (Self, Task) { (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 { 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 { iced::event::listen().map(Message::EventOccurred) } }