diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 7486e74e0e..7281a9e598 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -17,11 +17,10 @@ use crate::cef; use crate::cli::Cli; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::event::{AppEvent, AppEventScheduler}; -use crate::persist::PersistentData; +use crate::persist; use crate::preferences; use crate::render::{RenderError, RenderState}; use crate::window::Window; -use crate::workspace_layout; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; @@ -46,7 +45,6 @@ pub(crate) struct App { start_render_sender: SyncSender<()>, web_communication_initialized: bool, web_communication_startup_buffer: Vec>, - persistent_data: PersistentData, #[cfg_attr(not(target_os = "macos"), expect(unused))] preferences: Preferences, cli: Cli, @@ -93,9 +91,6 @@ impl App { } }); - let mut persistent_data = PersistentData::default(); - persistent_data.load_from_disk(); - let desktop_wrapper = DesktopWrapper::new(rand::rng().random()); Self { @@ -119,7 +114,6 @@ impl App { start_render_sender, web_communication_initialized: false, web_communication_startup_buffer: Vec::new(), - persistent_data, preferences, cli, startup_time: None, @@ -285,17 +279,24 @@ impl App { window.request_redraw(); } } - DesktopFrontendMessage::PersistenceWriteDocument { id, document } => { - self.persistent_data.write_document(id, document); + DesktopFrontendMessage::PersistenceWriteState { state } => { + persist::write_state(state); } - DesktopFrontendMessage::PersistenceDeleteDocument { id } => { - self.persistent_data.delete_document(&id); + DesktopFrontendMessage::PersistenceReadState => { + responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() }); } - DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => { - self.persistent_data.set_current_document(id); + DesktopFrontendMessage::PersistenceReadDocument { id } => { + if let Some(document_serialized_content) = persist::read_document_content(&id) { + responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document_serialized_content }); + } else { + tracing::error!("Failed to read document content for {id:?}"); + } } - DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => { - self.persistent_data.force_document_order(ids); + DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => { + persist::write_document_content(id, document_serialized_content); + } + DesktopFrontendMessage::PersistenceDeleteDocument { id } => { + persist::delete_document(&id); } DesktopFrontendMessage::PersistenceWritePreferences { preferences } => { preferences::write(preferences); @@ -305,30 +306,6 @@ impl App { let message = DesktopWrapperMessage::LoadPreferences { preferences }; responses.push(message); } - DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout: layout } => { - workspace_layout::write(&layout); - } - DesktopFrontendMessage::PersistenceLoadWorkspaceLayout => { - if let Some(workspace_layout) = workspace_layout::read() { - let message = DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout }; - responses.push(message); - } - } - DesktopFrontendMessage::PersistenceLoadDocuments => { - // Open all documents in persisted tab order, then select the current one - for (id, document) in self.persistent_data.documents() { - responses.push(DesktopWrapperMessage::LoadDocument { - id, - document, - to_front: false, - select_after_open: false, - }); - } - - if let Some(id) = self.persistent_data.current_document_id() { - responses.push(DesktopWrapperMessage::SelectDocument { id }); - } - } DesktopFrontendMessage::OpenLaunchDocuments => { if self.cli.files.is_empty() { return; diff --git a/desktop/src/consts.rs b/desktop/src/consts.rs index 9178700fa5..d51594ea16 100644 --- a/desktop/src/consts.rs +++ b/desktop/src/consts.rs @@ -9,7 +9,6 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite"; pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock"; pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron"; pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron"; -pub(crate) const APP_WORKSPACE_LAYOUT_FILE_NAME: &str = "workspace_layout.ron"; pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents"; // CEF configuration constants diff --git a/desktop/src/dirs.rs b/desktop/src/dirs.rs index 0ec18aa2f8..9084cf3a6b 100644 --- a/desktop/src/dirs.rs +++ b/desktop/src/dirs.rs @@ -84,3 +84,11 @@ impl AsRef for TempDir { &self.path } } + +// TODO: Eventually remove this cleanup code for the old "browser" CEF directory +pub(crate) fn delete_old_cef_browser_directory() { + let old_browser_dir = crate::dirs::app_data_dir().join("browser"); + if old_browser_dir.is_dir() { + let _ = std::fs::remove_dir_all(&old_browser_dir); + } +} diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index fda880584f..bf9b31f5c4 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -20,7 +20,6 @@ mod persist; mod preferences; mod render; mod window; -mod workspace_layout; pub(crate) mod consts; @@ -65,6 +64,9 @@ pub fn start() { dirs::app_tmp_dir_cleanup(); + // TODO: Eventually remove this cleanup code for the old "browser" CEF directory + dirs::delete_old_cef_browser_directory(); + let prefs = preferences::read(); // Must be called before event loop initialization or native window integrations will break diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index fb536bfae7..4ae7e67467 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -1,169 +1,91 @@ -use crate::wrapper::messages::{Document, DocumentId, PersistedDocumentInfo}; - -#[derive(Default, serde::Serialize, serde::Deserialize)] -pub(crate) struct PersistentData { - documents: Vec, - current_document: Option, - #[serde(skip)] - document_order: Option>, -} - -impl PersistentData { - pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) { - let info = PersistedDocumentInfo { - id, - name: document.name.clone(), - path: document.path.clone(), - is_saved: document.is_saved, - }; - if let Some(existing) = self.documents.iter_mut().find(|doc| doc.id == id) { - *existing = info; - } else { - self.documents.push(info); +use crate::wrapper::messages::{DocumentId, PersistedState}; + +pub(crate) fn read_state() -> PersistedState { + let path = state_file_path(); + let data = match std::fs::read_to_string(&path) { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + tracing::info!("No persistent data file found at {path:?}, starting fresh"); + return PersistedState::default(); } - - if let Err(e) = std::fs::write(Self::document_content_path(&id), document.content) { - tracing::error!("Failed to write document {id:?} to disk: {e}"); + Err(e) => { + tracing::error!("Failed to read persistent data from disk: {e}"); + return PersistedState::default(); } - - self.flush(); - } - - pub(crate) fn delete_document(&mut self, id: &DocumentId) { - if Some(*id) == self.current_document { - self.current_document = None; - } - - self.documents.retain(|doc| doc.id != *id); - if let Err(e) = std::fs::remove_file(Self::document_content_path(id)) { - tracing::error!("Failed to delete document {id:?} from disk: {e}"); + }; + let loaded = match ron::from_str(&data) { + Ok(d) => d, + Err(e) => { + tracing::error!("Failed to deserialize persistent data: {e}"); + return PersistedState::default(); } + }; - self.flush(); - } + garbage_collect_document_files(&loaded); + loaded +} - pub(crate) fn current_document_id(&self) -> Option { - match self.current_document { - Some(id) => Some(id), - None => Some(self.documents.first()?.id), +pub(crate) fn write_state(state: PersistedState) { + let state: &PersistedState = &state; + let data = match ron::ser::to_string_pretty(state, Default::default()) { + Ok(d) => d, + Err(e) => { + tracing::error!("Failed to serialize persistent data: {e}"); + return; } + }; + if let Err(e) = std::fs::write(state_file_path(), data) { + tracing::error!("Failed to write persistent data to disk: {e}"); } +} - pub(crate) fn documents(&self) -> Vec<(DocumentId, Document)> { - self.documents.iter().filter_map(|doc| Some((doc.id, self.read_document(&doc.id)?))).collect() +pub(crate) fn write_document_content(id: DocumentId, document_content: String) { + if let Err(e) = std::fs::write(document_content_path(&id), document_content) { + tracing::error!("Failed to write document {id:?} to disk: {e}"); } +} - pub(crate) fn set_current_document(&mut self, id: DocumentId) { - self.current_document = Some(id); - self.flush(); - } +pub(crate) fn read_document_content(id: &DocumentId) -> Option { + std::fs::read_to_string(document_content_path(id)).ok() +} - pub(crate) fn force_document_order(&mut self, order: Vec) { - let mut ordered_prefix_length = 0; - for id in &order { - if let Some(offset) = self.documents[ordered_prefix_length..].iter().position(|doc| doc.id == *id) { - let found_index = ordered_prefix_length + offset; - if found_index != ordered_prefix_length { - self.documents[ordered_prefix_length..=found_index].rotate_right(1); - } - ordered_prefix_length += 1; - } - } - self.document_order = Some(order); - self.flush(); +pub(crate) fn delete_document(id: &DocumentId) { + if let Err(e) = std::fs::remove_file(document_content_path(id)) { + tracing::error!("Failed to delete document {id:?} from disk: {e}"); } +} - fn read_document(&self, id: &DocumentId) -> Option { - let info = self.documents.iter().find(|doc| doc.id == *id)?; - let content = std::fs::read_to_string(Self::document_content_path(id)).ok()?; - Some(Document { - content, - name: info.name.clone(), - path: info.path.clone(), - is_saved: info.is_saved, - }) - } +fn garbage_collect_document_files(state: &PersistedState) { + let valid_paths: std::collections::HashSet<_> = state.documents.iter().map(|doc| document_content_path(&doc.id)).collect(); - fn flush(&self) { - let data = match ron::ser::to_string_pretty(self, Default::default()) { - Ok(d) => d, - Err(e) => { - tracing::error!("Failed to serialize persistent data: {e}"); - return; - } - }; - if let Err(e) = std::fs::write(Self::state_file_path(), data) { - tracing::error!("Failed to write persistent data to disk: {e}"); + let directory = crate::dirs::app_autosave_documents_dir(); + let entries = match std::fs::read_dir(&directory) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, + Err(e) => { + tracing::error!("Failed to read autosave documents directory: {e}"); + return; } - } - - pub(crate) fn load_from_disk(&mut self) { - delete_old_cef_browser_directory(); - - let path = Self::state_file_path(); - let data = match std::fs::read_to_string(&path) { - Ok(d) => d, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - tracing::info!("No persistent data file found at {path:?}, starting fresh"); - return; - } - Err(e) => { - tracing::error!("Failed to read persistent data from disk: {e}"); - return; - } - }; - let loaded = match ron::from_str(&data) { - Ok(d) => d, - Err(e) => { - tracing::error!("Failed to deserialize persistent data: {e}"); - return; - } - }; - *self = loaded; - - self.garbage_collect_document_files(); - } - - fn garbage_collect_document_files(&self) { - let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect(); + }; - let directory = crate::dirs::app_autosave_documents_dir(); - let entries = match std::fs::read_dir(&directory) { - Ok(entries) => entries, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, - Err(e) => { - tracing::error!("Failed to read autosave documents directory: {e}"); - return; - } - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() && !valid_paths.contains(&path) { - if let Err(e) = std::fs::remove_file(&path) { - tracing::error!("Failed to remove orphaned document file {path:?}: {e}"); - } + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && !valid_paths.contains(&path) { + if let Err(e) = std::fs::remove_file(&path) { + tracing::error!("Failed to remove orphaned document file {path:?}: {e}"); } } } +} - fn state_file_path() -> std::path::PathBuf { - let mut path = crate::dirs::app_data_dir(); - path.push(crate::consts::APP_STATE_FILE_NAME); - path - } - - fn document_content_path(id: &DocumentId) -> std::path::PathBuf { - let mut path = crate::dirs::app_autosave_documents_dir(); - path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION)); - path - } +fn state_file_path() -> std::path::PathBuf { + let mut path = crate::dirs::app_data_dir(); + path.push(crate::consts::APP_STATE_FILE_NAME); + path } -// TODO: Eventually remove this cleanup code for the old "browser" CEF directory -fn delete_old_cef_browser_directory() { - let old_browser_dir = crate::dirs::app_data_dir().join("browser"); - if old_browser_dir.is_dir() { - let _ = std::fs::remove_dir_all(&old_browser_dir); - } +fn document_content_path(id: &DocumentId) -> std::path::PathBuf { + let mut path = crate::dirs::app_autosave_documents_dir(); + path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION)); + path } diff --git a/desktop/src/workspace_layout.rs b/desktop/src/workspace_layout.rs deleted file mode 100644 index 079fbce42f..0000000000 --- a/desktop/src/workspace_layout.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub(crate) fn write(workspace_layout: &str) { - std::fs::write(file_path(), workspace_layout).unwrap_or_else(|e| { - tracing::error!("Failed to write workspace layout to disk: {e}"); - }); -} - -pub(crate) fn read() -> Option { - std::fs::read_to_string(file_path()).ok() -} - -fn file_path() -> std::path::PathBuf { - let mut path = crate::dirs::app_data_dir(); - path.push(crate::consts::APP_WORKSPACE_LAYOUT_FILE_NAME); - path -} diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 88df1f620d..09b8359f53 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -49,41 +49,21 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess let message = FrontendMessage::UpdateFullscreen { fullscreen }; dispatcher.queue_editor_message(message); } - DesktopWrapperMessage::LoadDocument { - id, - document, - to_front, - select_after_open, - } => { - let message = PortfolioMessage::OpenDocumentFileWithId { + DesktopWrapperMessage::LoadDocumentContent { id, document_serialized_content } => { + let message = PortfolioMessage::LoadDocumentContent { document_id: id, - document_name: Some(document.name), - document_path: document.path, - document_serialized_content: document.content, - document_is_auto_saved: true, - document_is_saved: document.is_saved, - to_front, - select_after_open, + document_serialized_content, }; dispatcher.queue_editor_message(message); } - DesktopWrapperMessage::SelectDocument { id } => { - let message = PortfolioMessage::SelectDocument { document_id: id }; + DesktopWrapperMessage::LoadPersistedState { state } => { + let message = PortfolioMessage::LoadPersistedState { state }; dispatcher.queue_editor_message(message); } DesktopWrapperMessage::LoadPreferences { preferences } => { let message = PreferencesMessage::Load { preferences }; dispatcher.queue_editor_message(message); } - DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout } => match ron::from_str(&workspace_layout) { - Ok(layout) => { - let message = PortfolioMessage::LoadWorkspaceLayout { layout }; - dispatcher.queue_editor_message(message); - } - Err(e) => { - tracing::error!("Failed to deserialize workspace layout: {e}"); - } - }, #[cfg(target_os = "macos")] DesktopWrapperMessage::MenuEvent { id } => { if let Some(message) = crate::utils::menu::parse_item_path(id) { diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index b8a0977412..da2e8a7ff4 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -3,7 +3,7 @@ use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarge use graphite_editor::messages::prelude::FrontendMessage; use super::DesktopWrapperMessageDispatcher; -use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; +use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { @@ -67,36 +67,23 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale }); return Some(FrontendMessage::UpdateUIScale { scale }); } - FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => { - dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument { - id: document_id, - document: Document { - content: document, - name: details.name, - path: details.path, - is_saved: details.is_saved, - }, - }); + FrontendMessage::TriggerPersistenceReadState => { + dispatcher.respond(DesktopFrontendMessage::PersistenceReadState); } - FrontendMessage::TriggerPersistenceRemoveDocument { document_id } => { - dispatcher.respond(DesktopFrontendMessage::PersistenceDeleteDocument { id: document_id }); + FrontendMessage::TriggerPersistenceWriteState { state } => { + dispatcher.respond(DesktopFrontendMessage::PersistenceWriteState { state }); } - FrontendMessage::UpdateActiveDocument { document_id } => { - dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id: document_id }); - - // Forward this to update the UI - return Some(FrontendMessage::UpdateActiveDocument { document_id }); + FrontendMessage::TriggerPersistenceReadDocument { document_id } => { + dispatcher.respond(DesktopFrontendMessage::PersistenceReadDocument { id: document_id }); } - FrontendMessage::UpdateOpenDocumentsList { open_documents } => { - dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateDocumentsList { - ids: open_documents.iter().map(|document| document.id).collect(), - }); - - // Forward this to update the UI - return Some(FrontendMessage::UpdateOpenDocumentsList { open_documents }); + FrontendMessage::TriggerPersistenceDeleteDocument { document_id } => { + dispatcher.respond(DesktopFrontendMessage::PersistenceDeleteDocument { id: document_id }); } - FrontendMessage::TriggerLoadAutoSaveDocuments => { - dispatcher.respond(DesktopFrontendMessage::PersistenceLoadDocuments); + FrontendMessage::TriggerPersistenceWriteDocument { document_id, document } => { + dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument { + id: document_id, + document_serialized_content: document, + }); } FrontendMessage::TriggerOpenLaunchDocuments => { dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments); @@ -107,16 +94,6 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::TriggerLoadPreferences => { dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); } - FrontendMessage::TriggerSaveWorkspaceLayout { workspace_layout } => { - let Ok(workspace_layout) = ron::ser::to_string_pretty(&workspace_layout, ron::ser::PrettyConfig::default()) else { - tracing::error!("Failed to serialize workspace layout"); - return None; - }; - dispatcher.respond(DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout }); - } - FrontendMessage::TriggerLoadWorkspaceLayout => { - dispatcher.respond(DesktopFrontendMessage::PersistenceLoadWorkspaceLayout); - } #[cfg(target_os = "macos")] FrontendMessage::UpdateLayout { layout_target: LayoutTarget::MenuBar, diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index f9c5082c98..865094f4fa 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -42,26 +42,22 @@ pub enum DesktopFrontendMessage { UpdateOverlays(vello::Scene), PersistenceWriteDocument { id: DocumentId, - document: Document, + document_serialized_content: String, }, PersistenceDeleteDocument { id: DocumentId, }, - PersistenceUpdateCurrentDocument { - id: DocumentId, - }, - PersistenceLoadDocuments, - PersistenceUpdateDocumentsList { - ids: Vec, - }, PersistenceWritePreferences { preferences: Preferences, }, PersistenceLoadPreferences, - PersistenceWriteWorkspaceLayout { - workspace_layout: String, + PersistenceWriteState { + state: PersistedState, + }, + PersistenceReadState, + PersistenceReadDocument { + id: DocumentId, }, - PersistenceLoadWorkspaceLayout, UpdateMenu { entries: Vec, }, @@ -85,66 +81,20 @@ pub enum DesktopFrontendMessage { pub enum DesktopWrapperMessage { FromWeb(Box), Input(InputMessage), - FileDialogResult { - path: PathBuf, - content: Vec, - context: OpenFileDialogContext, - }, - SaveFileDialogResult { - path: PathBuf, - context: SaveFileDialogContext, - }, - OpenFile { - path: PathBuf, - content: Vec, - }, - ImportFile { - path: PathBuf, - content: Vec, - }, + FileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, + SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, + OpenFile { path: PathBuf, content: Vec }, + ImportFile { path: PathBuf, content: Vec }, PollNodeGraphEvaluation, - UpdateMaximized { - maximized: bool, - }, - UpdateFullscreen { - fullscreen: bool, - }, - LoadDocument { - id: DocumentId, - document: Document, - to_front: bool, - select_after_open: bool, - }, - SelectDocument { - id: DocumentId, - }, - LoadPreferences { - preferences: Preferences, - }, - LoadWorkspaceLayout { - workspace_layout: String, - }, - MenuEvent { - id: String, - }, - ClipboardReadResult { - content: Option, - }, - PointerLockMove { - x: f64, - y: f64, - }, - LoadThirdPartyLicenses { - text: String, - }, -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct Document { - pub content: String, - pub name: String, - pub path: Option, - pub is_saved: bool, + UpdateMaximized { maximized: bool }, + UpdateFullscreen { fullscreen: bool }, + LoadDocumentContent { id: DocumentId, document_serialized_content: String }, + LoadPersistedState { state: PersistedState }, + LoadPreferences { preferences: Preferences }, + MenuEvent { id: String }, + ClipboardReadResult { content: Option }, + PointerLockMove { x: f64, y: f64 }, + LoadThirdPartyLicenses { text: String }, } pub struct FileFilter { diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index db060cebbf..58ea6a1a34 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -295,7 +295,7 @@ impl Dispatcher { document_id, document, input: &self.message_handlers.input_preprocessor_message_handler, - persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data, + cached_data: &self.message_handlers.portfolio_message_handler.cached_data, node_graph: &self.message_handlers.portfolio_message_handler.executor, preferences: &self.message_handlers.preferences_message_handler, viewport: &self.message_handlers.viewport_message_handler, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c9b7782f91..1e3cfc9d38 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,5 +1,5 @@ use super::IconName; -use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; +use super::utility_types::{MouseCursorIcon, OpenDocument, PersistedState}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::frontend::utility_types::EyedropperPreviewImage; use crate::messages::input_mapper::utility_types::misc::ActionShortcut; @@ -113,7 +113,15 @@ pub enum FrontendMessage { font: Font, url: String, }, - TriggerPersistenceRemoveDocument { + TriggerPersistenceReadState, + TriggerPersistenceReadDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, + }, + TriggerPersistenceWriteState { + state: PersistedState, + }, + TriggerPersistenceDeleteDocument { #[serde(rename = "documentId")] document_id: DocumentId, }, @@ -121,26 +129,15 @@ pub enum FrontendMessage { #[serde(rename = "documentId")] document_id: DocumentId, document: String, - details: DocumentDetails, }, - TriggerLoadAutoSaveDocuments, TriggerOpenLaunchDocuments, TriggerLoadPreferences, - TriggerLoadWorkspaceLayout, TriggerOpen, TriggerImport, TriggerSavePreferences { #[cfg_attr(feature = "wasm", tsify(type = "unknown"))] preferences: PreferencesMessageHandler, }, - TriggerSaveWorkspaceLayout { - #[serde(rename = "workspaceLayout")] - workspace_layout: WorkspacePanelLayout, - }, - TriggerSaveActiveDocument { - #[serde(rename = "documentId")] - document_id: DocumentId, - }, TriggerTextCommit, TriggerVisitLink { url: String, diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 04ce8cba77..812bdad0a1 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::utility_types::WorkspacePanelLayout; use crate::messages::prelude::*; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -36,6 +37,8 @@ pub struct PersistedDocumentInfo { pub struct PersistedState { pub documents: Vec, pub current_document: Option, + #[serde(default)] + pub workspace_layout: Option, } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 6158297296..2cd7744d93 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -21,7 +21,7 @@ use crate::messages::portfolio::document::properties_panel::properties_panel_mes use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; -use crate::messages::portfolio::utility_types::{PanelType, PersistentData}; +use crate::messages::portfolio::utility_types::{CachedData, PanelType}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; @@ -52,7 +52,7 @@ use std::time::Duration; pub struct DocumentMessageContext<'a> { pub document_id: DocumentId, pub ipp: &'a InputPreprocessorMessageHandler, - pub persistent_data: &'a PersistentData, + pub cached_data: &'a CachedData, pub executor: &'a mut NodeGraphExecutor, pub current_tool: &'a ToolType, pub preferences: &'a PreferencesMessageHandler, @@ -193,7 +193,7 @@ impl MessageHandler> for DocumentMes let DocumentMessageContext { document_id, ipp, - persistent_data, + cached_data, executor, viewport, current_tool, @@ -231,7 +231,7 @@ impl MessageHandler> for DocumentMes selection_network_path: &self.selection_network_path, document_name: self.name.as_str(), executor, - persistent_data, + cached_data, properties_panel_open, }; self.properties_panel_message_handler.process_message(message, responses, context); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 5ede5649e5..3ee48af260 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -8,7 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, NumberInputSettings, Vec2InputSettings, WidgetOverride, }; -use crate::messages::portfolio::utility_types::PersistentData; +use crate::messages::portfolio::utility_types::CachedData; use crate::messages::prelude::Message; use crate::node_graph_executor::NodeGraphExecutor; use glam::DVec2; @@ -29,7 +29,7 @@ use serde_json::Value; use std::collections::{HashMap, VecDeque}; pub struct NodePropertiesContext<'a> { - pub persistent_data: &'a PersistentData, + pub cached_data: &'a CachedData, pub responses: &'a mut VecDeque, pub executor: &'a mut NodeGraphExecutor, pub network_interface: &'a mut NodeNetworkInterface, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 788f6e5e8b..3a2c6a778b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -5,7 +5,7 @@ use super::utility_types::FrontendGraphDataType; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface}; -use crate::messages::portfolio::utility_types::{FontCatalogStyle, PersistentData}; +use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle}; use crate::messages::prelude::*; use choice::enum_choice; use dyn_any::DynAny; @@ -793,7 +793,7 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec, Option>) { let ParameterWidgetsInfo { - persistent_data, + cached_data, document_node, node_id, index, @@ -813,7 +813,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec (Vec>(), ]) - .selected_index(persistent_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32)) + .selected_index(cached_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32)) .virtual_scrolling(true) .widget_instance(), ]); @@ -876,7 +876,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec (Vec } pub struct ParameterWidgetsInfo<'a> { - persistent_data: &'a PersistentData, + cached_data: &'a CachedData, network_interface: &'a NodeNetworkInterface, selection_network_path: &'a [NodeId], document_node: Option<&'a DocumentNode>, @@ -2231,7 +2231,7 @@ impl<'a> ParameterWidgetsInfo<'a> { let document_node = context.network_interface.document_node(&node_id, context.selection_network_path); ParameterWidgetsInfo { - persistent_data: context.persistent_data, + cached_data: context.cached_data, network_interface: context.network_interface, selection_network_path: context.selection_network_path, document_node, diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index b5706ec1e5..8e8052d5a3 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -3,7 +3,7 @@ use graphene_std::uuid::NodeId; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; -use crate::messages::portfolio::utility_types::PersistentData; +use crate::messages::portfolio::utility_types::CachedData; use crate::messages::prelude::*; use crate::node_graph_executor::NodeGraphExecutor; @@ -13,7 +13,7 @@ pub struct PropertiesPanelMessageContext<'a> { pub selection_network_path: &'a [NodeId], pub document_name: &'a str, pub executor: &'a mut NodeGraphExecutor, - pub persistent_data: &'a PersistentData, + pub cached_data: &'a CachedData, pub properties_panel_open: bool, } @@ -28,7 +28,7 @@ impl MessageHandler> f selection_network_path, document_name, executor, - persistent_data, + cached_data, properties_panel_open, } = context; @@ -46,7 +46,7 @@ impl MessageHandler> f } let mut node_properties_context = NodePropertiesContext { - persistent_data, + cached_data, responses, network_interface, selection_network_path, diff --git a/editor/src/messages/portfolio/mod.rs b/editor/src/messages/portfolio/mod.rs index 3c9d53e229..f0f8c442c5 100644 --- a/editor/src/messages/portfolio/mod.rs +++ b/editor/src/messages/portfolio/mod.rs @@ -3,8 +3,11 @@ mod portfolio_message_handler; pub mod document; pub mod document_migration; +pub mod persistent_state; pub mod utility_types; +#[doc(inline)] +pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler}; #[doc(inline)] pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant}; #[doc(inline)] diff --git a/editor/src/messages/portfolio/persistent_state/mod.rs b/editor/src/messages/portfolio/persistent_state/mod.rs new file mode 100644 index 0000000000..e39e54e289 --- /dev/null +++ b/editor/src/messages/portfolio/persistent_state/mod.rs @@ -0,0 +1,7 @@ +mod persistent_state_message; +mod persistent_state_message_handler; + +#[doc(inline)] +pub use persistent_state_message::{PersistentStateMessage, PersistentStateMessageDiscriminant}; +#[doc(inline)] +pub use persistent_state_message_handler::{PersistentStateMessageContext, PersistentStateMessageHandler}; diff --git a/editor/src/messages/portfolio/persistent_state/persistent_state_message.rs b/editor/src/messages/portfolio/persistent_state/persistent_state_message.rs new file mode 100644 index 0000000000..624da30a25 --- /dev/null +++ b/editor/src/messages/portfolio/persistent_state/persistent_state_message.rs @@ -0,0 +1,21 @@ +use crate::messages::prelude::*; + +#[impl_message(Message, PortfolioMessage, PersistentState)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum PersistentStateMessage { + ReadState, + WriteState, + ReadDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, + }, + WriteDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, + document: String, + }, + DeleteDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, + }, +} diff --git a/editor/src/messages/portfolio/persistent_state/persistent_state_message_handler.rs b/editor/src/messages/portfolio/persistent_state/persistent_state_message_handler.rs new file mode 100644 index 0000000000..1df03dcfc2 --- /dev/null +++ b/editor/src/messages/portfolio/persistent_state/persistent_state_message_handler.rs @@ -0,0 +1,38 @@ +use super::persistent_state_message::PersistentStateMessage; +use crate::messages::frontend::utility_types::PersistedState; +use crate::messages::prelude::*; + +#[derive(Default, Debug, Clone, ExtractField)] +pub struct PersistentStateMessageHandler {} + +#[derive(ExtractField)] +pub struct PersistentStateMessageContext { + pub persisted_state: PersistedState, +} + +#[message_handler_data] +impl MessageHandler for PersistentStateMessageHandler { + fn process_message(&mut self, message: PersistentStateMessage, responses: &mut VecDeque, context: PersistentStateMessageContext) { + let PersistentStateMessageContext { persisted_state: state } = context; + + match message { + PersistentStateMessage::ReadState => { + responses.add(FrontendMessage::TriggerPersistenceReadState); + } + PersistentStateMessage::WriteState => { + responses.add(FrontendMessage::TriggerPersistenceWriteState { state }); + } + PersistentStateMessage::ReadDocument { document_id } => { + responses.add(FrontendMessage::TriggerPersistenceReadDocument { document_id }); + } + PersistentStateMessage::WriteDocument { document_id, document } => { + responses.add(FrontendMessage::TriggerPersistenceWriteDocument { document_id, document }); + } + PersistentStateMessage::DeleteDocument { document_id } => { + responses.add(FrontendMessage::TriggerPersistenceDeleteDocument { document_id }); + } + } + } + + advertise_actions!(PersistentStateMessageDiscriminant;); +} diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index a282817394..b5bdc45c1c 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -1,6 +1,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; -use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType, WorkspacePanelLayout}; -use crate::messages::frontend::utility_types::{ExportBounds, FileType}; +use super::persistent_state::PersistentStateMessage; +use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType}; +use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState}; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::utility_types::FontCatalog; use crate::messages::prelude::*; @@ -15,6 +16,8 @@ pub enum PortfolioMessage { // Sub-messages #[child] Document(DocumentMessage), + #[child] + PersistentState(PersistentStateMessage), // Messages Init, @@ -61,8 +64,12 @@ pub enum PortfolioMessage { LoadDocumentResources { document_id: DocumentId, }, - LoadWorkspaceLayout { - layout: WorkspacePanelLayout, + LoadPersistedState { + state: PersistedState, + }, + LoadDocumentContent { + document_id: DocumentId, + document_serialized_content: String, }, MoveAllPanelTabs { source_group: PanelGroupId, @@ -93,15 +100,13 @@ pub enum PortfolioMessage { document_path: Option, document_serialized_content: String, }, - OpenDocumentFileWithId { + LoadDocument { document_id: DocumentId, document_name: Option, document_path: Option, document_is_auto_saved: bool, document_is_saved: bool, document_serialized_content: String, - to_front: bool, - select_after_open: bool, }, OpenImage { name: Option, @@ -186,7 +191,6 @@ pub enum PortfolioMessage { UpdateDocumentWidgets, UpdateOpenDocumentsList, UpdateWorkspacePanelLayout, - SaveWorkspaceLayout, ResetWorkspaceLayout, ResetPanelGroupSizes { /// Path of child indices from the root to the split node whose children's sizes should be reset to defaults. diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 53019b9b84..5a2827b36e 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,12 +1,13 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::network_interface; -use super::utility_types::{PanelLayoutSubdivision, PanelType, PersistentData, WorkspacePanelLayout}; +use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler}; +use super::utility_types::{CachedData, PanelLayoutSubdivision, PanelType, WorkspacePanelLayout}; use crate::application::{Editor, generate_uuid}; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::dialog::simple_dialogs; -use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument}; +use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument, PersistedDocumentInfo, PersistedState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual}; use crate::messages::layout::utility_types::widget_prelude::*; @@ -50,10 +51,12 @@ pub struct PortfolioMessageContext<'a> { #[derive(Debug, Default, ExtractField)] pub struct PortfolioMessageHandler { pub documents: HashMap, + unloaded_documents: HashMap, document_ids: VecDeque, pub(crate) active_document_id: Option, + persistent_state: PersistentStateMessageHandler, + pub cached_data: CachedData, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], - pub persistent_data: PersistentData, pub executor: NodeGraphExecutor, pub selection_mode: SelectionMode, pub reset_node_definitions_on_open: bool, @@ -82,7 +85,7 @@ impl MessageHandler> for Portfolio let document_inputs = DocumentMessageContext { document_id, ipp, - persistent_data: &self.persistent_data, + cached_data: &self.cached_data, executor: &mut self.executor, current_tool, preferences, @@ -94,6 +97,12 @@ impl MessageHandler> for Portfolio document.process_message(message, responses, document_inputs) } } + PortfolioMessage::PersistentState(message) => { + let context = PersistentStateMessageContext { + persisted_state: self.persisted_state_snapshot(), + }; + self.persistent_state.process_message(message, responses, context); + } // Messages PortfolioMessage::Init => { @@ -104,14 +113,11 @@ impl MessageHandler> for Portfolio // Tell frontend to load persistent preferences responses.add(FrontendMessage::TriggerLoadPreferences); - responses.add(FrontendMessage::TriggerLoadWorkspaceLayout); + responses.add(PersistentStateMessage::ReadState); // Before loading any documents, initially prepare the welcome screen buttons layout responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); - // Tell frontend to load persistent auto-saved documents (placed early so IndexedDB reads overlap with subsequent UI setup) - responses.add(FrontendMessage::TriggerLoadAutoSaveDocuments); - // Tell frontend to load documents passed in as launch arguments responses.add(FrontendMessage::TriggerOpenLaunchDocuments); @@ -147,7 +153,7 @@ impl MessageHandler> for Portfolio let document_inputs = DocumentMessageContext { document_id, ipp, - persistent_data: &self.persistent_data, + cached_data: &self.cached_data, executor: &mut self.executor, current_tool, preferences, @@ -168,25 +174,21 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::AutoSaveAllDocuments => { - for (document_id, document) in self.documents.iter_mut() { - if !document.is_auto_saved() { + for document_id in self.document_ids.iter() { + if let Some(document) = self.documents.get_mut(document_id) + && !document.is_auto_saved() + { document.set_auto_save_state(true); responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id }); } } } PortfolioMessage::AutoSaveDocument { document_id } => { - let document = self.documents.get(&document_id).unwrap(); - responses.add(FrontendMessage::TriggerPersistenceWriteDocument { + let Some(document) = self.document(document_id) else { return }; + responses.add(PersistentStateMessage::WriteDocument { document_id, document: document.serialize_document(), - details: DocumentDetails { - name: document.name.clone(), - path: document.path.clone(), - is_saved: document.is_saved(), - is_auto_saved: document.is_auto_saved(), - }, - }) + }); } PortfolioMessage::CloseActiveDocumentWithConfirmation => { if let Some(document_id) = self.active_document_id { @@ -206,7 +208,7 @@ impl MessageHandler> for Portfolio } for document_id in &self.document_ids { - responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id: *document_id }); + responses.add(PersistentStateMessage::DeleteDocument { document_id: *document_id }); } responses.add(PortfolioMessage::DestroyAllDocuments); @@ -221,7 +223,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::CloseDocument { document_id } => { // Is this the last document? - if self.documents.len() == 1 && self.document_ids[0] == document_id { + if self.document_ids.len() == 1 && self.document_ids[0] == document_id { // Clear UI layouts that assume the existence of a document responses.add(PropertiesPanelMessage::Clear); responses.add(DocumentMessage::ClearLayersPanel); @@ -231,13 +233,17 @@ impl MessageHandler> for Portfolio // Actually delete the document (delay to delete document is required to let the document and properties panel messages above get processed) responses.add(PortfolioMessage::DeleteDocument { document_id }); - responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id }); + responses.add(PersistentStateMessage::DeleteDocument { document_id }); // Send the new list of document tab names responses.add(PortfolioMessage::UpdateOpenDocumentsList); } PortfolioMessage::CloseDocumentWithConfirmation { document_id } => { - let target_document = self.documents.get(&document_id).unwrap(); + let Some(target_document) = self.document(document_id) else { + responses.add(EventMessage::ToolAbort); + responses.add(PortfolioMessage::CloseDocument { document_id }); + return; + }; if target_document.is_saved() { responses.add(EventMessage::ToolAbort); responses.add(PortfolioMessage::CloseDocument { document_id }); @@ -335,6 +341,7 @@ impl MessageHandler> for Portfolio PortfolioMessage::DeleteDocument { document_id } => { let document_index = self.document_index(document_id); self.documents.remove(&document_id); + self.unloaded_documents.remove(&document_id); self.document_ids.remove(document_index); if self.document_ids.is_empty() { @@ -354,12 +361,14 @@ impl MessageHandler> for Portfolio PortfolioMessage::DestroyAllDocuments => { // Empty the list of internal document data self.documents.clear(); + self.unloaded_documents.clear(); self.document_ids.clear(); self.active_document_id = None; responses.add(MenuBarMessage::SendLayout); + responses.add(PersistentStateMessage::WriteState); } PortfolioMessage::FontCatalogLoaded { catalog } => { - self.persistent_data.font_catalog = catalog; + self.cached_data.font_catalog = catalog; if let Some(document_id) = self.active_document_id { responses.add(PortfolioMessage::LoadDocumentResources { document_id }); @@ -370,23 +379,26 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::LoadFontData { font }); } PortfolioMessage::LoadFontData { font } => { - if let Some(style) = self.persistent_data.font_catalog.find_font_style_in_catalog(&font) { + if let Some(style) = self.cached_data.font_catalog.find_font_style_in_catalog(&font) { let font = Font::new(font.font_family, style.to_named_style()); - if !self.persistent_data.font_cache.loaded_font(&font) { + if !self.cached_data.font_cache.loaded_font(&font) { responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url }); } } } PortfolioMessage::FontLoaded { font_family, font_style, data } => { let font = Font::new(font_family, font_style); - self.persistent_data.font_cache.insert(font, data); - self.executor.update_font_cache(self.persistent_data.font_cache.clone()); + self.cached_data.font_cache.insert(font, data); + self.executor.update_font_cache(self.cached_data.font_cache.clone()); for document_id in self.document_ids.iter() { let node_to_inspect = self.node_to_inspect(); let Some(document) = self.documents.get_mut(document_id) else { + if self.unloaded_documents.contains_key(document_id) { + continue; + } log::error!("Tried to render non-existent document"); continue; }; @@ -407,16 +419,10 @@ impl MessageHandler> for Portfolio active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview, }); - if let Ok(message) = self.executor.submit_node_graph_evaluation( - self.documents.get_mut(document_id).expect("Tried to render non-existent document"), - *document_id, - physical_resolution, - scale, - timing_information, - node_to_inspect, - true, - pointer_position, - ) { + if let Ok(message) = self + .executor + .submit_node_graph_evaluation(document, *document_id, physical_resolution, scale, timing_information, node_to_inspect, true, pointer_position) + { responses.add_front(message); } } @@ -431,7 +437,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), PortfolioMessage::LoadDocumentResources { document_id } => { - let catalog = &self.persistent_data.font_catalog; + let catalog = &self.cached_data.font_catalog; if catalog.0.is_empty() { responses.add_front(FrontendMessage::TriggerFontCatalogLoad); @@ -442,17 +448,53 @@ impl MessageHandler> for Portfolio document.load_layer_resources(responses); } } - PortfolioMessage::LoadWorkspaceLayout { layout } => { - self.workspace_panel_layout = layout; - responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); + PortfolioMessage::LoadPersistedState { state } => { + if let Some(layout) = state.workspace_layout { + self.workspace_panel_layout = layout; + responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); + } - // Refresh all visible panels since the layout may have changed - for group_id in self.workspace_panel_layout.root.all_group_ids() { - if let Some(panel_type) = self.workspace_panel_layout.panel_group(group_id).and_then(|g| g.active_panel_type()) { - self.refresh_panel_content(panel_type, responses); + let PersistedState { + documents, + current_document, + workspace_layout: _, + } = state; + + for info in documents { + if !self.document_ids.contains(&info.id) { + self.document_ids.push_back(info.id); } + if !self.documents.contains_key(&info.id) { + self.unloaded_documents.insert(info.id, info); + } + } + + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + + let select_document_id = current_document.filter(|id| self.document_ids.contains(id)).or_else(|| self.document_ids.front().copied()); + if let Some(document_id) = select_document_id { + responses.add(PortfolioMessage::SelectDocument { document_id }); } } + PortfolioMessage::LoadDocumentContent { + document_id, + document_serialized_content, + } => { + let Some(info) = self.unloaded_documents.remove(&document_id) else { + log::error!("Tried to load content for non existent document"); + return; + }; + + responses.add(PortfolioMessage::LoadDocument { + document_id, + document_name: Some(info.name), + document_path: info.path, + document_is_auto_saved: true, + document_is_saved: info.is_saved, + document_serialized_content, + }); + responses.add(PortfolioMessage::SelectDocument { document_id }); + } PortfolioMessage::NewDocumentWithName { name } => { let mut new_document = DocumentMessageHandler::default(); new_document.name = name; @@ -465,7 +507,7 @@ impl MessageHandler> for Portfolio responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); } - self.load_document(new_document, document_id, responses, false); + self.load_document(new_document, document_id, responses); responses.add(PortfolioMessage::SelectDocument { document_id }); } PortfolioMessage::MoveAllPanelTabs { @@ -515,7 +557,6 @@ impl MessageHandler> for Portfolio responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); // Refresh the new active tab if let Some(panel_type) = self.workspace_panel_layout.panel_group(target_group).and_then(|g| g.active_panel_type()) { @@ -565,7 +606,6 @@ impl MessageHandler> for Portfolio responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); // Refresh the moved panel's content in its new location self.refresh_panel_content(panel_type, responses); @@ -661,30 +701,28 @@ impl MessageHandler> for Portfolio document_path, document_serialized_content, } => { - responses.add(PortfolioMessage::OpenDocumentFileWithId { - document_id: DocumentId(generate_uuid()), + let document_id = DocumentId(generate_uuid()); + responses.add(PortfolioMessage::LoadDocument { + document_id, document_name, document_path, document_is_auto_saved: false, document_is_saved: true, document_serialized_content, - to_front: false, - select_after_open: true, }); + responses.add(PortfolioMessage::SelectDocument { document_id }); } PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen => { self.reset_node_definitions_on_open = !self.reset_node_definitions_on_open; responses.add(MenuBarMessage::SendLayout); } - PortfolioMessage::OpenDocumentFileWithId { + PortfolioMessage::LoadDocument { document_id, document_name, document_path, document_is_auto_saved, document_is_saved, document_serialized_content, - to_front, - select_after_open, } => { // Upgrade the document being opened to use fresh copies of all nodes let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content); @@ -777,11 +815,7 @@ impl MessageHandler> for Portfolio } // Load the document into the portfolio so it opens in the editor - self.load_document(document, document_id, responses, to_front); - - if select_after_open { - responses.add(PortfolioMessage::SelectDocument { document_id }); - } + self.load_document(document, document_id, responses); } PortfolioMessage::OpenImage { name, image } => { responses.add(PortfolioMessage::NewDocumentWithName { @@ -1126,7 +1160,7 @@ impl MessageHandler> for Portfolio mouse, parent_and_insert_index, } => { - if self.documents.is_empty() { + if self.document_ids.is_empty() { responses.add(PortfolioMessage::OpenImage { name, image }); } else { responses.add(DocumentMessage::PasteImage { @@ -1144,7 +1178,7 @@ impl MessageHandler> for Portfolio mouse, parent_and_insert_index, } => { - if self.documents.is_empty() { + if self.document_ids.is_empty() { responses.add(PortfolioMessage::OpenSvg { name, svg }); } else { responses.add(DocumentMessage::PasteSvg { @@ -1200,7 +1234,6 @@ impl MessageHandler> for Portfolio } responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); } } PortfolioMessage::RequestWelcomeScreenButtonsLayout => { @@ -1280,7 +1313,6 @@ impl MessageHandler> for Portfolio // Send the layout update first so the frontend mounts the new panel component before it receives content responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); if let Some(panel_type) = self.workspace_panel_layout.panel_group(group).and_then(|g| g.active_panel_type()) { self.refresh_panel_content(panel_type, responses); @@ -1315,7 +1347,6 @@ impl MessageHandler> for Portfolio responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); // Refresh the new panel group's active tab if let Some(panel_type) = self.workspace_panel_layout.panel_group(new_id).and_then(|g| g.active_panel_type()) { @@ -1340,13 +1371,32 @@ impl MessageHandler> for Portfolio node_graph_open = document.is_graph_overlay_open(); } + if self.unloaded_documents.contains_key(&document_id) { + let already_selected = self.active_document_id == Some(document_id); + + self.active_document_id = Some(document_id); + responses.add(MenuBarMessage::SendLayout); + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + responses.add(FrontendMessage::UpdateActiveDocument { document_id }); + + if !already_selected { + responses.add(PersistentStateMessage::ReadDocument { document_id }); + } + + return; + } + + if !self.documents.contains_key(&document_id) { + warn!("Tried to read non existent document"); + return; + } + // Set the new active document ID self.active_document_id = Some(document_id); responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(FrontendMessage::UpdateActiveDocument { document_id }); - responses.add(FrontendMessage::TriggerSaveActiveDocument { document_id }); responses.add(ToolMessage::InitTools); responses.add(NodeGraphMessage::Init); responses.add(OverlaysMessage::Draw); @@ -1382,7 +1432,8 @@ impl MessageHandler> for Portfolio artboard_name, artboard_count, } => { - let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document"); + let document_id = self.active_document_id.expect("Tried to render non-existent document"); + let document = self.documents.get_mut(&document_id).expect("Tried to render non-existent document"); let export_config = ExportConfig { name, file_type, @@ -1392,7 +1443,7 @@ impl MessageHandler> for Portfolio artboard_count, ..Default::default() }; - let result = self.executor.submit_document_export(document, self.active_document_id.unwrap(), export_config); + let result = self.executor.submit_document_export(document, document_id, export_config); if let Err(description) = result { responses.add(DialogMessage::DisplayDialogError { @@ -1491,7 +1542,6 @@ impl MessageHandler> for Portfolio responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); } PortfolioMessage::TogglePropertiesPanelOpen => { if self.workspace_panel_layout.focus_document { @@ -1536,11 +1586,7 @@ impl MessageHandler> for Portfolio false => self.workspace_panel_layout.clone(), }; responses.add(FrontendMessage::UpdateWorkspacePanelLayout { panel_layout }); - } - PortfolioMessage::SaveWorkspaceLayout => { - responses.add(FrontendMessage::TriggerSaveWorkspaceLayout { - workspace_layout: self.workspace_panel_layout.clone(), - }); + responses.add(PersistentStateMessage::WriteState); } PortfolioMessage::ResetWorkspaceLayout => { // Destroy layouts for all currently visible non-document panels @@ -1562,7 +1608,6 @@ impl MessageHandler> for Portfolio } responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); responses.add(MenuBarMessage::SendLayout); } PortfolioMessage::ResetPanelGroupSizes { split_path } => { @@ -1578,7 +1623,6 @@ impl MessageHandler> for Portfolio node.recalculate_default_sizes(); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); } PortfolioMessage::SetPanelGroupSizes { split_path, sizes } => { // Walk the tree to the target split node using the path @@ -1596,7 +1640,7 @@ impl MessageHandler> for Portfolio } } - responses.add(PortfolioMessage::SaveWorkspaceLayout); + responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); } PortfolioMessage::UpdateOpenDocumentsList => { // Send the list of document tab names @@ -1604,21 +1648,34 @@ impl MessageHandler> for Portfolio .document_ids .iter() .filter_map(|id| { - self.documents.get(id).map(|document| OpenDocument { - id: *id, - details: DocumentDetails { - name: document.name.clone(), - path: document.path.clone(), - is_saved: document.is_saved(), - is_auto_saved: document.is_auto_saved(), - }, - }) + if let Some(document) = self.documents.get(id) { + Some(OpenDocument { + id: *id, + details: DocumentDetails { + name: document.name.clone(), + path: document.path.clone(), + is_saved: document.is_saved(), + is_auto_saved: document.is_auto_saved(), + }, + }) + } else { + self.unloaded_documents.get(id).map(|details| OpenDocument { + id: *id, + details: DocumentDetails { + name: details.name.clone(), + path: details.path.clone(), + is_saved: details.is_saved, + is_auto_saved: true, + }, + }) + } }) .collect::>(); let no_open_documents = open_documents.is_empty(); responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents }); + responses.add(PersistentStateMessage::WriteState); if no_open_documents { responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); @@ -1682,11 +1739,11 @@ impl PortfolioMessageHandler { } pub fn active_document(&self) -> Option<&DocumentMessageHandler> { - self.active_document_id.and_then(|id| self.documents.get(&id)) + self.active_document_id.and_then(|id| self.document(id)) } pub fn active_document_mut(&mut self) -> Option<&mut DocumentMessageHandler> { - self.active_document_id.and_then(|id| self.documents.get_mut(&id)) + self.active_document_id.and_then(|id| self.document_mut(id)) } pub fn active_document_id(&self) -> Option { @@ -1694,12 +1751,29 @@ impl PortfolioMessageHandler { } pub fn unsaved_document_names(&self) -> Vec { - self.documents.values().filter(|document| !document.is_saved()).map(|document| document.name.clone()).collect() + self.document_ids + .iter() + .filter_map(|id| self.document_details(*id)) + .filter(|details| !details.is_saved) + .map(|details| details.name) + .collect() + } + + pub fn persisted_state_snapshot(&self) -> PersistedState { + let documents = self.document_ids.iter().filter_map(|id| self.document_details(*id)).collect::>(); + + PersistedState { + documents, + current_document: self.active_document_id, + workspace_layout: Some(self.workspace_panel_layout.clone()), + } } pub fn generate_new_document_name(&self) -> String { let mut doc_title_numbers = self - .ordered_document_iterator() + .document_ids + .iter() + .filter_map(|id| self.document_details(*id)) .filter_map(|doc| { doc.name .rsplit_once(DEFAULT_DOCUMENT_NAME) @@ -1744,12 +1818,12 @@ impl PortfolioMessageHandler { } } - fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque, to_front: bool) { - if to_front { - self.document_ids.push_front(document_id); - } else { + fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque) { + let is_new_document = !self.document_ids.contains(&document_id); + if is_new_document { self.document_ids.push_back(document_id); } + self.unloaded_documents.remove(&document_id); new_document.update_layers_panel_control_bar_widgets( self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document, responses, @@ -1768,12 +1842,14 @@ impl PortfolioMessageHandler { // TODO: Remove this and find a way to fix the issue where creating a new document when the node graph is open causes the transform in the new document to be incorrect responses.add(DocumentMessage::GraphViewOverlay { open: false }); - responses.add(PortfolioMessage::UpdateOpenDocumentsList); + if is_new_document { + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + } } /// Returns an iterator over the open documents in order. pub fn ordered_document_iterator(&self) -> impl Iterator { - self.document_ids.iter().map(|id| self.documents.get(id).expect("Document id was not found in the document hashmap")) + self.document_ids.iter().filter_map(|id| self.document(*id)) } fn document_index(&self, document_id: DocumentId) -> usize { @@ -1781,7 +1857,10 @@ impl PortfolioMessageHandler { } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else { + let Some(document_id) = self.active_document_id else { + return Err("No active document".to_string()); + }; + let Some(active_document) = self.documents.get_mut(&document_id) else { return Err("No active document".to_string()); }; @@ -1801,6 +1880,19 @@ impl PortfolioMessageHandler { result } + fn document_details(&self, document_id: DocumentId) -> Option { + if let Some(document) = self.documents.get(&document_id) { + Some(PersistedDocumentInfo { + id: document_id, + name: document.name.clone(), + path: document.path.clone(), + is_saved: document.is_saved(), + }) + } else { + self.unloaded_documents.get(&document_id).cloned() + } + } + /// Get the ID of the selected node that should be used as the current source for the Data panel. pub fn node_to_inspect(&self) -> Option { // Skip if the Data panel is not open @@ -1808,7 +1900,7 @@ impl PortfolioMessageHandler { return None; } - let document = self.documents.get(&self.active_document_id?)?; + let document = self.document(self.active_document_id?)?; let selected_nodes = document.network_interface.selected_nodes().0; // Skip if there is not exactly one selected node @@ -1854,7 +1946,6 @@ impl PortfolioMessageHandler { responses.add(MenuBarMessage::SendLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - responses.add(PortfolioMessage::SaveWorkspaceLayout); } /// Destroy the stored layout for a panel that is no longer the active tab. diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index 5aff092da6..f7e0ebb386 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -3,7 +3,7 @@ use graphene_std::raster::Image; use graphene_std::text::{Font, FontCache}; #[derive(Debug, Default)] -pub struct PersistentData { +pub struct CachedData { pub font_cache: FontCache, pub font_catalog: FontCatalog, } diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 29f6022767..e085b0918b 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -4,7 +4,7 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_ use crate::application::generate_uuid; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; -use crate::messages::portfolio::utility_types::PersistentData; +use crate::messages::portfolio::utility_types::CachedData; use crate::messages::prelude::*; use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext; use crate::messages::tool::utility_types::{HintData, ToolType}; @@ -18,7 +18,7 @@ pub struct ToolMessageContext<'a> { pub document_id: DocumentId, pub document: &'a mut DocumentMessageHandler, pub input: &'a InputPreprocessorMessageHandler, - pub persistent_data: &'a PersistentData, + pub cached_data: &'a CachedData, pub node_graph: &'a NodeGraphExecutor, pub preferences: &'a PreferencesMessageHandler, pub viewport: &'a ViewportMessageHandler, @@ -39,7 +39,7 @@ impl MessageHandler> for ToolMessageHandler document_id, document, input, - persistent_data, + cached_data, node_graph, preferences, viewport, @@ -126,7 +126,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - persistent_data, + cached_data, }; if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort { @@ -217,7 +217,7 @@ impl MessageHandler> for ToolMessageHandler tool_data.tools.get(active_tool).unwrap().activate(responses); // Register initial properties - tool_data.tools.get(active_tool).unwrap().refresh_options(responses, persistent_data); + tool_data.tools.get(active_tool).unwrap().refresh_options(responses, cached_data); // Notify the frontend about the initial active tool tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool); @@ -234,7 +234,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - persistent_data, + cached_data, }; // Set initial hints and cursor @@ -258,7 +258,7 @@ impl MessageHandler> for ToolMessageHandler ToolMessage::RefreshToolOptions => { let tool_data = &mut self.tool_state.tool_data; - tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, persistent_data); + tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, cached_data); } ToolMessage::RefreshToolShelf => { let tool_data = &mut self.tool_state.tool_data; @@ -346,7 +346,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - persistent_data, + cached_data, }; if matches!(tool_message, ToolMessage::UpdateHints) { if graph_view_overlay_open { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index b8c6751565..0eb1518f46 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -605,7 +605,7 @@ impl Fsm for SelectToolFsmState { document, input, viewport, - persistent_data, + cached_data, .. } = tool_action_data; @@ -632,7 +632,7 @@ impl Fsm for SelectToolFsmState { overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None); if is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) { - let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &persistent_data.font_cache); + let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache); overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None); } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index c5471f0bff..cb969d3175 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -7,7 +7,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; -use crate::messages::portfolio::utility_types::{FontCatalog, FontCatalogStyle, PersistentData}; +use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle}; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name}; @@ -230,8 +230,8 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec, persistent_data: &PersistentData) { - self.send_layout(responses, LayoutTarget::ToolOptions, &persistent_data.font_catalog); + fn refresh_options(&self, responses: &mut VecDeque, cached_data: &CachedData) { + self.send_layout(responses, LayoutTarget::ToolOptions, &cached_data.font_catalog); } } @@ -302,7 +302,7 @@ impl<'a> MessageHandler> for Text } } - self.send_layout(responses, LayoutTarget::ToolOptions, &context.persistent_data.font_catalog); + self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog); } fn actions(&self) -> ActionList { @@ -573,11 +573,11 @@ impl Fsm for TextToolFsmState { document, global_tool_data, input, - persistent_data, + cached_data, viewport, .. } = transition_data; - let font_cache = &persistent_data.font_cache; + let font_cache = &cached_data.font_cache; let fill_color = COLOR_OVERLAY_BLUE_05; let ToolMessage::Text(event) = event else { return self }; diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index c54f3c1cd3..566a2af75c 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -9,7 +9,7 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; -use crate::messages::portfolio::utility_types::PersistentData; +use crate::messages::portfolio::utility_types::CachedData; use crate::messages::preferences::PreferencesMessageHandler; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; @@ -24,7 +24,7 @@ pub struct ToolActionMessageContext<'a> { pub document_id: DocumentId, pub global_tool_data: &'a DocumentToolData, pub input: &'a InputPreprocessorMessageHandler, - pub persistent_data: &'a PersistentData, + pub cached_data: &'a CachedData, pub shape_editor: &'a mut ShapeState, pub node_graph: &'a NodeGraphExecutor, pub preferences: &'a PreferencesMessageHandler, @@ -37,11 +37,11 @@ impl ToolCommon for T where T: for<'a, 'b> MessageHandler, _persistent_data: &PersistentData); + fn refresh_options(&self, responses: &mut VecDeque, _cached_data: &CachedData); } impl ToolRefreshOptions for T { - fn refresh_options(&self, responses: &mut VecDeque, _persistent_data: &PersistentData) { + fn refresh_options(&self, responses: &mut VecDeque, _cached_data: &CachedData) { self.send_layout(responses, LayoutTarget::ToolOptions); } } diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 40d6c30a1c..23552601e6 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -36,15 +36,15 @@ impl EditorTestUtils { async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result { let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; let document_id = portfolio.active_document_id.unwrap(); - let exector = &mut portfolio.executor; - let document = portfolio.documents.get_mut(&document_id).unwrap(); + let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents); + let document = documents.get_mut(&document_id).unwrap(); - let instrumented = match exector.update_node_graph_instrumented(document) { + let instrumented = match executor.update_node_graph_instrumented(document) { Ok(instrumented) => instrumented, Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")), }; - if let Err(e) = exector.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) { + if let Err(e) = executor.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) { return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}")); } runtime.run().await; diff --git a/frontend/src/managers/persistence.ts b/frontend/src/managers/persistence.ts index 4907891f46..87c598a5f0 100644 --- a/frontend/src/managers/persistence.ts +++ b/frontend/src/managers/persistence.ts @@ -3,12 +3,11 @@ import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { saveEditorPreferences, loadEditorPreferences, - saveWorkspaceLayout, - loadWorkspaceLayout, - storeDocument, - removeDocument, - loadDocuments, - saveActiveDocument, + writePersistedState, + readPersistedState, + writePersistedDocument, + readPersistedDocument, + deletePersistedDocument, } from "/src/utility-functions/persistence"; import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; @@ -31,33 +30,29 @@ export function createPersistenceManager(subscriptions: SubscriptionsRouter, edi await loadEditorPreferences(editor); }); - subscriptions.subscribeFrontendMessage("TriggerSaveWorkspaceLayout", async (data) => { - await saveWorkspaceLayout(data.workspaceLayout); + subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteState", async (data) => { + await writePersistedState(data.state); }); - subscriptions.subscribeFrontendMessage("TriggerLoadWorkspaceLayout", async () => { - await loadWorkspaceLayout(editor); + subscriptions.subscribeFrontendMessage("TriggerPersistenceReadState", async () => { + await readPersistedState(editor); }); subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => { - await storeDocument(data, portfolio); + await writePersistedDocument(data); }); - subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => { - await removeDocument(String(data.documentId), portfolio); + subscriptions.subscribeFrontendMessage("TriggerPersistenceReadDocument", async (data) => { + await readPersistedDocument(data.documentId, editor); }); - subscriptions.subscribeFrontendMessage("TriggerLoadAutoSaveDocuments", async () => { - await loadDocuments(editor); + subscriptions.subscribeFrontendMessage("TriggerPersistenceDeleteDocument", async (data) => { + await deletePersistedDocument(String(data.documentId)); }); subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => { // TODO: Could be used to load documents from URL params or similar on launch }); - - subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => { - await saveActiveDocument(data.documentId); - }); } export function destroyPersistenceManager() { @@ -66,13 +61,12 @@ export function destroyPersistenceManager() { subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences"); subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences"); - subscriptions.unsubscribeFrontendMessage("TriggerSaveWorkspaceLayout"); - subscriptions.unsubscribeFrontendMessage("TriggerLoadWorkspaceLayout"); + subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteState"); + subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadState"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument"); - subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument"); - subscriptions.unsubscribeFrontendMessage("TriggerLoadAutoSaveDocuments"); + subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadDocument"); + subscriptions.unsubscribeFrontendMessage("TriggerPersistenceDeleteDocument"); subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments"); - subscriptions.unsubscribeFrontendMessage("TriggerSaveActiveDocument"); } // Self-accepting HMR: tear down the old instance and re-create with the new module's code diff --git a/frontend/src/stores/portfolio.ts b/frontend/src/stores/portfolio.ts index 60af4e1afe..d839c81143 100644 --- a/frontend/src/stores/portfolio.ts +++ b/frontend/src/stores/portfolio.ts @@ -2,7 +2,6 @@ import { writable } from "svelte/store"; import type { Writable } from "svelte/store"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files"; -import { storeDocumentTabOrder } from "/src/utility-functions/persistence"; import { rasterizeSVG } from "/src/utility-functions/rasterization"; import type { EditorWrapper, OpenDocument, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper"; @@ -38,7 +37,6 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor: state.documents = data.openDocuments; return state; }); - storeDocumentTabOrder({ subscribe }); }); subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => { diff --git a/frontend/src/utility-functions/persistence.ts b/frontend/src/utility-functions/persistence.ts index 921f054a2a..15e8aee15b 100644 --- a/frontend/src/utility-functions/persistence.ts +++ b/frontend/src/utility-functions/persistence.ts @@ -1,5 +1,3 @@ -import { get } from "svelte/store"; -import type { PortfolioStore } from "/src/stores/portfolio"; import type { MessageBody } from "/src/subscriptions-router"; import type { EditorWrapper, PersistedDocumentInfo, PersistedState } from "/wrapper/pkg/graphite_wasm_wrapper"; @@ -8,7 +6,7 @@ const PERSISTENCE_STORE = "store"; function emptyPersistedState(): PersistedState { // eslint-disable-next-line camelcase - return { documents: [], current_document: undefined }; + return { documents: [], current_document: undefined, workspace_layout: undefined }; } function createDocumentInfo(id: bigint, name: string, isSaved: boolean): PersistedDocumentInfo { @@ -39,18 +37,8 @@ function reorderDocuments(documents: PersistedDocumentInfo[], orderedIds: bigint // State-based persistence (new format) // ==================================== -export async function storeDocumentTabOrder(portfolio: PortfolioStore) { - const portfolioData = get(portfolio); - const orderedIds = portfolioData.documents.map((doc) => doc.id); - - await databaseUpdate("state", (old) => { - const state = old || emptyPersistedState(); - return { ...state, documents: reorderDocuments(state.documents, orderedIds) }; - }); -} - -export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">, portfolio: PortfolioStore) { - const { documentId, document, details } = autoSaveDocument; +export async function writePersistedDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">) { + const { documentId, document } = autoSaveDocument; // Update content in the documents store await databaseUpdate>("documents", (old) => { @@ -58,91 +46,44 @@ export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersis documents[String(documentId)] = document; return documents; }); +} - // Update metadata and ordering in the state store - const portfolioData = get(portfolio); - const orderedIds = portfolioData.documents.map((doc) => doc.id); - - await databaseUpdate("state", (old) => { - const state = old || emptyPersistedState(); +export async function readPersistedDocument(documentId: bigint, editor: EditorWrapper) { + const documentContents = await databaseGet>("documents"); + if (!documentContents) return; - // Update (or add) the document info entry - const entry = createDocumentInfo(documentId, details.name, details.is_saved); - const existingIndex = state.documents.findIndex((doc) => doc.id === documentId); - if (existingIndex !== -1) { - state.documents[existingIndex] = entry; - } else { - state.documents.push(entry); - } + const content = documentContents[String(documentId)]; + if (content === undefined) return; - // eslint-disable-next-line camelcase - state.current_document = documentId; - state.documents = reorderDocuments(state.documents, orderedIds); - return state; - }); + editor.loadDocumentContent(documentId, content); } -export async function removeDocument(id: string, portfolio: PortfolioStore) { - const documentId = BigInt(id); - +export async function deletePersistedDocument(id: string) { // Remove content from the documents store await databaseUpdate>("documents", (old) => { const documents = old || {}; delete documents[id]; return documents; }); +} - // Update state: remove the entry and update current_document - const portfolioData = get(portfolio); - const documentCount = portfolioData.documents.length; - - await databaseUpdate("state", (old) => { - const state: PersistedState = old || emptyPersistedState(); - state.documents = state.documents.filter((doc) => doc.id !== documentId); - - if (state.current_document === documentId) { - // eslint-disable-next-line camelcase - state.current_document = documentCount > 0 ? portfolioData.documents[portfolioData.activeDocumentIndex].id : undefined; - } - - return state; - }); +export async function writePersistedState(state: PersistedState) { + // Keep state ordered and normalized before writing. + state.documents = reorderDocuments( + state.documents, + state.documents.map((entry) => entry.id), + ); + await databaseSet("state", state); + await garbageCollectDocuments(); } -export async function loadDocuments(editor: EditorWrapper) { +export async function readPersistedState(editor: EditorWrapper) { await migrateToNewFormat(); await garbageCollectDocuments(); const state = await databaseGet("state"); - const documentContents = await databaseGet>("documents"); - if (!state || !documentContents || state.documents.length === 0) return; - - // Find the current document (or fall back to the last document in the list) - const currentId = state.current_document; - const currentEntry = currentId !== undefined ? state.documents.find((doc) => doc.id === currentId) : undefined; - const current = currentEntry || state.documents[state.documents.length - 1]; - - // Open all documents in persisted tab order, then select the current one - state.documents.forEach((entry) => { - const content = documentContents[String(entry.id)]; - if (content === undefined) return; - - editor.openAutoSavedDocument(entry.id, entry.name, entry.is_saved, content, false); - }); - - editor.selectDocument(current.id); -} - -export async function saveActiveDocument(documentId: bigint) { - await databaseUpdate("state", (old) => { - const state: PersistedState = old || emptyPersistedState(); - - const exists = state.documents.some((doc) => doc.id === documentId); - // eslint-disable-next-line camelcase - if (exists) state.current_document = documentId; - - return state; - }); + if (!state) return; + editor.loadPersistedState(state); } export async function saveEditorPreferences(preferences: unknown) { @@ -154,15 +95,6 @@ export async function loadEditorPreferences(editor: EditorWrapper) { editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined); } -export async function saveWorkspaceLayout(layout: unknown) { - await databaseSet("workspace_layout", layout); -} - -export async function loadWorkspaceLayout(editor: EditorWrapper) { - const layout = await databaseGet>("workspace_layout"); - if (layout) editor.loadWorkspaceLayout(layout); -} - // Remove orphaned entries from the "documents" content store that have no corresponding entry in "state" async function garbageCollectDocuments() { const state = await databaseGet("state"); @@ -197,6 +129,7 @@ export async function wipeDocuments() { async function wipeOldFormat() { await databaseDelete("documents_tab_order"); await databaseDelete("current_document_id"); + await databaseDelete("workspace_layout"); } // TODO: Eventually remove this document upgrade code diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index bda46057a9..7fb23125bf 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,7 +8,6 @@ "verbatimModuleSyntax": true, "sourceMap": true, "types": ["node"], - "baseUrl": ".", "paths": { "/*": ["./*"] }, "lib": ["ESNext", "DOM", "DOM.Iterable"] }, diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index a1d60e1605..f6c407a791 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -379,13 +379,23 @@ impl EditorWrapper { } } - #[wasm_bindgen(js_name = loadWorkspaceLayout)] - pub fn load_workspace_layout(&self, layout: JsValue) { - let Ok(layout) = serde_wasm_bindgen::from_value(layout) else { - log::error!("Failed to deserialize workspace layout"); + #[wasm_bindgen(js_name = loadPersistedState)] + pub fn load_persisted_state(&self, state: JsValue) { + let Ok(state) = serde_wasm_bindgen::from_value(state) else { + log::error!("Failed to deserialize persisted state"); return; }; - let message = PortfolioMessage::LoadWorkspaceLayout { layout }; + + let message = PortfolioMessage::LoadPersistedState { state }; + self.dispatch(message); + } + + #[wasm_bindgen(js_name = loadDocumentContent)] + pub fn load_document_content(&self, document_id: u64, document_serialized_content: String) { + let message = PortfolioMessage::LoadDocumentContent { + document_id: DocumentId(document_id), + document_serialized_content, + }; self.dispatch(message); } @@ -414,22 +424,6 @@ impl EditorWrapper { self.dispatch(message); } - #[wasm_bindgen(js_name = openAutoSavedDocument)] - pub fn open_auto_saved_document(&self, document_id: u64, document_name: String, document_is_saved: bool, document_serialized_content: String, to_front: bool) { - let document_id = DocumentId(document_id); - let message = PortfolioMessage::OpenDocumentFileWithId { - document_id, - document_name: Some(document_name), - document_path: None, - document_is_auto_saved: true, - document_is_saved, - document_serialized_content, - to_front, - select_after_open: false, - }; - self.dispatch(message); - } - #[wasm_bindgen(js_name = triggerAutoSave)] pub fn trigger_auto_save(&self, document_id: u64) { let document_id = DocumentId(document_id);