Refactor persistence to combine document handling and workspace layout (#4031)

* Unify editor state persistence

* Review

* Fix

* Remove redundant DocumentDetails

* LoadDocumentContent indirection
This commit is contained in:
Timon 2026-04-19 11:31:21 +02:00 committed by GitHub
parent 2a2a60883d
commit 6c5e3c97f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 562 additions and 699 deletions

View File

@ -17,11 +17,10 @@ use crate::cef;
use crate::cli::Cli; use crate::cli::Cli;
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
use crate::event::{AppEvent, AppEventScheduler}; use crate::event::{AppEvent, AppEventScheduler};
use crate::persist::PersistentData; use crate::persist;
use crate::preferences; use crate::preferences;
use crate::render::{RenderError, RenderState}; use crate::render::{RenderError, RenderState};
use crate::window::Window; use crate::window::Window;
use crate::workspace_layout;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences}; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
@ -46,7 +45,6 @@ pub(crate) struct App {
start_render_sender: SyncSender<()>, start_render_sender: SyncSender<()>,
web_communication_initialized: bool, web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>, web_communication_startup_buffer: Vec<Vec<u8>>,
persistent_data: PersistentData,
#[cfg_attr(not(target_os = "macos"), expect(unused))] #[cfg_attr(not(target_os = "macos"), expect(unused))]
preferences: Preferences, preferences: Preferences,
cli: Cli, 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()); let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
Self { Self {
@ -119,7 +114,6 @@ impl App {
start_render_sender, start_render_sender,
web_communication_initialized: false, web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(), web_communication_startup_buffer: Vec::new(),
persistent_data,
preferences, preferences,
cli, cli,
startup_time: None, startup_time: None,
@ -285,17 +279,24 @@ impl App {
window.request_redraw(); window.request_redraw();
} }
} }
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => { DesktopFrontendMessage::PersistenceWriteState { state } => {
self.persistent_data.write_document(id, document); persist::write_state(state);
}
DesktopFrontendMessage::PersistenceReadState => {
responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
}
DesktopFrontendMessage::PersistenceReadDocument { id } => {
if let Some(document) = persist::read_document_content(&id) {
responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document });
} else {
tracing::error!("Failed to read document content for {id:?}");
}
}
DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => {
persist::write_document_content(id, document_serialized_content);
} }
DesktopFrontendMessage::PersistenceDeleteDocument { id } => { DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
self.persistent_data.delete_document(&id); persist::delete_document(&id);
}
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
self.persistent_data.set_current_document(id);
}
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
self.persistent_data.force_document_order(ids);
} }
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => { DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
preferences::write(preferences); preferences::write(preferences);
@ -305,30 +306,6 @@ impl App {
let message = DesktopWrapperMessage::LoadPreferences { preferences }; let message = DesktopWrapperMessage::LoadPreferences { preferences };
responses.push(message); 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 => { DesktopFrontendMessage::OpenLaunchDocuments => {
if self.cli.files.is_empty() { if self.cli.files.is_empty() {
return; return;

View File

@ -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_LOCK_FILE_NAME: &str = "instance.lock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron"; pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.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"; pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
// CEF configuration constants // CEF configuration constants

View File

@ -84,3 +84,11 @@ impl AsRef<Path> for TempDir {
&self.path &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);
}
}

View File

@ -20,7 +20,6 @@ mod persist;
mod preferences; mod preferences;
mod render; mod render;
mod window; mod window;
mod workspace_layout;
pub(crate) mod consts; pub(crate) mod consts;
@ -65,6 +64,9 @@ pub fn start() {
dirs::app_tmp_dir_cleanup(); 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(); let prefs = preferences::read();
// Must be called before event loop initialization or native window integrations will break // Must be called before event loop initialization or native window integrations will break

View File

@ -1,131 +1,63 @@
use crate::wrapper::messages::{Document, DocumentId, PersistedDocumentInfo}; use crate::wrapper::messages::{DocumentId, PersistedState};
#[derive(Default, serde::Serialize, serde::Deserialize)] pub(crate) fn read_state() -> PersistedState {
pub(crate) struct PersistentData { let path = state_file_path();
documents: Vec<PersistedDocumentInfo>,
current_document: Option<DocumentId>,
#[serde(skip)]
document_order: Option<Vec<DocumentId>>,
}
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);
}
if let Err(e) = std::fs::write(Self::document_content_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}
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}");
}
self.flush();
}
pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
match self.current_document {
Some(id) => Some(id),
None => Some(self.documents.first()?.id),
}
}
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 set_current_document(&mut self, id: DocumentId) {
self.current_document = Some(id);
self.flush();
}
pub(crate) fn force_document_order(&mut self, order: Vec<DocumentId>) {
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();
}
fn read_document(&self, id: &DocumentId) -> Option<Document> {
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 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}");
}
}
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) { let data = match std::fs::read_to_string(&path) {
Ok(d) => d, Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("No persistent data file found at {path:?}, starting fresh"); tracing::info!("No persistent data file found at {path:?}, starting fresh");
return; return PersistedState::default();
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to read persistent data from disk: {e}"); tracing::error!("Failed to read persistent data from disk: {e}");
return; return PersistedState::default();
} }
}; };
let loaded = match ron::from_str(&data) { let loaded = match ron::from_str(&data) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
tracing::error!("Failed to deserialize persistent data: {e}"); tracing::error!("Failed to deserialize persistent data: {e}");
return PersistedState::default();
}
};
garbage_collect_document_files(&loaded);
loaded
}
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; return;
} }
}; };
*self = loaded; if let Err(e) = std::fs::write(state_file_path(), data) {
tracing::error!("Failed to write persistent data to disk: {e}");
self.garbage_collect_document_files(); }
garbage_collect_document_files(&state);
} }
fn garbage_collect_document_files(&self) { pub(crate) fn write_document_content(id: DocumentId, document_content: String) {
let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect(); 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 read_document_content(id: &DocumentId) -> Option<String> {
std::fs::read_to_string(document_content_path(id)).ok()
}
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 garbage_collect_document_files(state: &PersistedState) {
let valid_paths: std::collections::HashSet<_> = state.documents.iter().map(|doc| document_content_path(&doc.id)).collect();
let directory = crate::dirs::app_autosave_documents_dir(); let directory = crate::dirs::app_autosave_documents_dir();
let entries = match std::fs::read_dir(&directory) { let entries = match std::fs::read_dir(&directory) {
@ -158,12 +90,3 @@ impl PersistentData {
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION)); path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
path 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);
}
}

View File

@ -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<String> {
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
}

View File

@ -49,41 +49,18 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
let message = FrontendMessage::UpdateFullscreen { fullscreen }; let message = FrontendMessage::UpdateFullscreen { fullscreen };
dispatcher.queue_editor_message(message); dispatcher.queue_editor_message(message);
} }
DesktopWrapperMessage::LoadDocument { DesktopWrapperMessage::LoadDocumentContent { id, document } => {
id, let message = PersistentStateMessage::LoadDocument { document_id: id, document };
document,
to_front,
select_after_open,
} => {
let message = PortfolioMessage::OpenDocumentFileWithId {
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,
};
dispatcher.queue_editor_message(message); dispatcher.queue_editor_message(message);
} }
DesktopWrapperMessage::SelectDocument { id } => { DesktopWrapperMessage::LoadPersistedState { state } => {
let message = PortfolioMessage::SelectDocument { document_id: id }; let message = PersistentStateMessage::LoadState { state };
dispatcher.queue_editor_message(message); dispatcher.queue_editor_message(message);
} }
DesktopWrapperMessage::LoadPreferences { preferences } => { DesktopWrapperMessage::LoadPreferences { preferences } => {
let message = PreferencesMessage::Load { preferences }; let message = PreferencesMessage::Load { preferences };
dispatcher.queue_editor_message(message); 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")] #[cfg(target_os = "macos")]
DesktopWrapperMessage::MenuEvent { id } => { DesktopWrapperMessage::MenuEvent { id } => {
if let Some(message) = crate::utils::menu::parse_item_path(id) { if let Some(message) = crate::utils::menu::parse_item_path(id) {

View File

@ -3,7 +3,7 @@ use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarge
use graphite_editor::messages::prelude::FrontendMessage; use graphite_editor::messages::prelude::FrontendMessage;
use super::DesktopWrapperMessageDispatcher; 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<FrontendMessage> { pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message { match message {
@ -67,36 +67,23 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale }); dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale });
return Some(FrontendMessage::UpdateUIScale { scale }); return Some(FrontendMessage::UpdateUIScale { scale });
} }
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => { FrontendMessage::TriggerPersistenceReadState => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument { dispatcher.respond(DesktopFrontendMessage::PersistenceReadState);
id: document_id,
document: Document {
content: document,
name: details.name,
path: details.path,
is_saved: details.is_saved,
},
});
} }
FrontendMessage::TriggerPersistenceRemoveDocument { document_id } => { FrontendMessage::TriggerPersistenceWriteState { state } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteState { state });
}
FrontendMessage::TriggerPersistenceReadDocument { document_id } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceReadDocument { id: document_id });
}
FrontendMessage::TriggerPersistenceDeleteDocument { document_id } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceDeleteDocument { id: document_id }); dispatcher.respond(DesktopFrontendMessage::PersistenceDeleteDocument { id: document_id });
} }
FrontendMessage::UpdateActiveDocument { document_id } => { FrontendMessage::TriggerPersistenceWriteDocument { document_id, document } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id: document_id }); dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
id: document_id,
// Forward this to update the UI document_serialized_content: document,
return Some(FrontendMessage::UpdateActiveDocument { 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::TriggerLoadAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadDocuments);
} }
FrontendMessage::TriggerOpenLaunchDocuments => { FrontendMessage::TriggerOpenLaunchDocuments => {
dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments); dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments);
@ -107,16 +94,6 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::TriggerLoadPreferences => { FrontendMessage::TriggerLoadPreferences => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); 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")] #[cfg(target_os = "macos")]
FrontendMessage::UpdateLayout { FrontendMessage::UpdateLayout {
layout_target: LayoutTarget::MenuBar, layout_target: LayoutTarget::MenuBar,

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
pub use graphite_editor::messages::frontend::utility_types::{PersistedDocumentInfo, PersistedState}; pub use graphite_editor::messages::frontend::utility_types::{DocumentInfo, PersistedState};
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys}; pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
pub use graphite_editor::messages::prelude::DocumentId; pub use graphite_editor::messages::prelude::DocumentId;
@ -42,26 +42,22 @@ pub enum DesktopFrontendMessage {
UpdateOverlays(vello::Scene), UpdateOverlays(vello::Scene),
PersistenceWriteDocument { PersistenceWriteDocument {
id: DocumentId, id: DocumentId,
document: Document, document_serialized_content: String,
}, },
PersistenceDeleteDocument { PersistenceDeleteDocument {
id: DocumentId, id: DocumentId,
}, },
PersistenceUpdateCurrentDocument {
id: DocumentId,
},
PersistenceLoadDocuments,
PersistenceUpdateDocumentsList {
ids: Vec<DocumentId>,
},
PersistenceWritePreferences { PersistenceWritePreferences {
preferences: Preferences, preferences: Preferences,
}, },
PersistenceLoadPreferences, PersistenceLoadPreferences,
PersistenceWriteWorkspaceLayout { PersistenceWriteState {
workspace_layout: String, state: PersistedState,
},
PersistenceReadState,
PersistenceReadDocument {
id: DocumentId,
}, },
PersistenceLoadWorkspaceLayout,
UpdateMenu { UpdateMenu {
entries: Vec<MenuItem>, entries: Vec<MenuItem>,
}, },
@ -85,66 +81,20 @@ pub enum DesktopFrontendMessage {
pub enum DesktopWrapperMessage { pub enum DesktopWrapperMessage {
FromWeb(Box<EditorMessage>), FromWeb(Box<EditorMessage>),
Input(InputMessage), Input(InputMessage),
FileDialogResult { FileDialogResult { path: PathBuf, content: Vec<u8>, context: OpenFileDialogContext },
path: PathBuf, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext },
content: Vec<u8>, OpenFile { path: PathBuf, content: Vec<u8> },
context: OpenFileDialogContext, ImportFile { path: PathBuf, content: Vec<u8> },
},
SaveFileDialogResult {
path: PathBuf,
context: SaveFileDialogContext,
},
OpenFile {
path: PathBuf,
content: Vec<u8>,
},
ImportFile {
path: PathBuf,
content: Vec<u8>,
},
PollNodeGraphEvaluation, PollNodeGraphEvaluation,
UpdateMaximized { UpdateMaximized { maximized: bool },
maximized: bool, UpdateFullscreen { fullscreen: bool },
}, LoadDocumentContent { id: DocumentId, document: String },
UpdateFullscreen { LoadPersistedState { state: PersistedState },
fullscreen: bool, LoadPreferences { preferences: Preferences },
}, MenuEvent { id: String },
LoadDocument { ClipboardReadResult { content: Option<String> },
id: DocumentId, PointerLockMove { x: f64, y: f64 },
document: Document, LoadThirdPartyLicenses { text: String },
to_front: bool,
select_after_open: bool,
},
SelectDocument {
id: DocumentId,
},
LoadPreferences {
preferences: Preferences,
},
LoadWorkspaceLayout {
workspace_layout: String,
},
MenuEvent {
id: String,
},
ClipboardReadResult {
content: Option<String>,
},
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<PathBuf>,
pub is_saved: bool,
} }
pub struct FileFilter { pub struct FileFilter {

View File

@ -295,7 +295,7 @@ impl Dispatcher {
document_id, document_id,
document, document,
input: &self.message_handlers.input_preprocessor_message_handler, 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, node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler, preferences: &self.message_handlers.preferences_message_handler,
viewport: &self.message_handlers.viewport_message_handler, viewport: &self.message_handlers.viewport_message_handler,

View File

@ -1,7 +1,7 @@
use super::IconName; use super::IconName;
use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use super::utility_types::{MouseCursorIcon, PersistedState};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::frontend::utility_types::EyedropperPreviewImage; use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage};
use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{ use crate::messages::portfolio::document::node_graph::utility_types::{
@ -113,7 +113,15 @@ pub enum FrontendMessage {
font: Font, font: Font,
url: String, url: String,
}, },
TriggerPersistenceRemoveDocument { TriggerPersistenceReadState,
TriggerPersistenceReadDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
},
TriggerPersistenceWriteState {
state: PersistedState,
},
TriggerPersistenceDeleteDocument {
#[serde(rename = "documentId")] #[serde(rename = "documentId")]
document_id: DocumentId, document_id: DocumentId,
}, },
@ -121,26 +129,15 @@ pub enum FrontendMessage {
#[serde(rename = "documentId")] #[serde(rename = "documentId")]
document_id: DocumentId, document_id: DocumentId,
document: String, document: String,
details: DocumentDetails,
}, },
TriggerLoadAutoSaveDocuments,
TriggerOpenLaunchDocuments, TriggerOpenLaunchDocuments,
TriggerLoadPreferences, TriggerLoadPreferences,
TriggerLoadWorkspaceLayout,
TriggerOpen, TriggerOpen,
TriggerImport, TriggerImport,
TriggerSavePreferences { TriggerSavePreferences {
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))] #[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
preferences: PreferencesMessageHandler, preferences: PreferencesMessageHandler,
}, },
TriggerSaveWorkspaceLayout {
#[serde(rename = "workspaceLayout")]
workspace_layout: WorkspacePanelLayout,
},
TriggerSaveActiveDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
},
TriggerTextCommit, TriggerTextCommit,
TriggerVisitLink { TriggerVisitLink {
url: String, url: String,
@ -291,7 +288,7 @@ pub enum FrontendMessage {
}, },
UpdateOpenDocumentsList { UpdateOpenDocumentsList {
#[serde(rename = "openDocuments")] #[serde(rename = "openDocuments")]
open_documents: Vec<OpenDocument>, open_documents: Vec<DocumentInfo>,
}, },
UpdateWirePathInProgress { UpdateWirePathInProgress {
#[serde(rename = "wirePath")] #[serde(rename = "wirePath")]

View File

@ -1,29 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
use crate::messages::prelude::*; use crate::messages::prelude::*;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct OpenDocument {
pub id: DocumentId,
pub details: DocumentDetails,
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DocumentDetails {
pub name: String,
pub path: Option<PathBuf>,
#[serde(alias = "isSaved")]
pub is_saved: bool,
#[serde(alias = "isAutoSaved")]
pub is_auto_saved: bool,
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedDocumentInfo { pub struct DocumentInfo {
pub id: DocumentId, pub id: DocumentId,
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
@ -34,8 +17,10 @@ pub struct PersistedDocumentInfo {
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedState { pub struct PersistedState {
pub documents: Vec<PersistedDocumentInfo>, pub documents: Vec<DocumentInfo>,
pub current_document: Option<DocumentId>, pub current_document: Option<DocumentId>,
#[serde(default)]
pub workspace_layout: Option<WorkspacePanelLayout>,
} }
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]

View File

@ -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::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; 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::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::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; 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; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
@ -52,7 +52,7 @@ use std::time::Duration;
pub struct DocumentMessageContext<'a> { pub struct DocumentMessageContext<'a> {
pub document_id: DocumentId, pub document_id: DocumentId,
pub ipp: &'a InputPreprocessorMessageHandler, pub ipp: &'a InputPreprocessorMessageHandler,
pub persistent_data: &'a PersistentData, pub cached_data: &'a CachedData,
pub executor: &'a mut NodeGraphExecutor, pub executor: &'a mut NodeGraphExecutor,
pub current_tool: &'a ToolType, pub current_tool: &'a ToolType,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
@ -193,7 +193,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let DocumentMessageContext { let DocumentMessageContext {
document_id, document_id,
ipp, ipp,
persistent_data, cached_data,
executor, executor,
viewport, viewport,
current_tool, current_tool,
@ -231,7 +231,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
selection_network_path: &self.selection_network_path, selection_network_path: &self.selection_network_path,
document_name: self.name.as_str(), document_name: self.name.as_str(),
executor, executor,
persistent_data, cached_data,
properties_panel_open, properties_panel_open,
}; };
self.properties_panel_message_handler.process_message(message, responses, context); self.properties_panel_message_handler.process_message(message, responses, context);

View File

@ -8,7 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
NumberInputSettings, Vec2InputSettings, WidgetOverride, NumberInputSettings, Vec2InputSettings, WidgetOverride,
}; };
use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::portfolio::utility_types::CachedData;
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;
use glam::DVec2; use glam::DVec2;
@ -29,7 +29,7 @@ use serde_json::Value;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
pub struct NodePropertiesContext<'a> { pub struct NodePropertiesContext<'a> {
pub persistent_data: &'a PersistentData, pub cached_data: &'a CachedData,
pub responses: &'a mut VecDeque<Message>, pub responses: &'a mut VecDeque<Message>,
pub executor: &'a mut NodeGraphExecutor, pub executor: &'a mut NodeGraphExecutor,
pub network_interface: &'a mut NodeNetworkInterface, pub network_interface: &'a mut NodeNetworkInterface,

View File

@ -5,7 +5,7 @@ use super::utility_types::FrontendGraphDataType;
use crate::messages::layout::utility_types::widget_prelude::*; 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::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface}; 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 crate::messages::prelude::*;
use choice::enum_choice; use choice::enum_choice;
use dyn_any::DynAny; 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<WidgetInstance>, Option<Vec<WidgetInstance>>) { pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
let ParameterWidgetsInfo { let ParameterWidgetsInfo {
persistent_data, cached_data,
document_node, document_node,
node_id, node_id,
index, index,
@ -813,7 +813,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
first_widgets.extend_from_slice(&[ first_widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new(vec![ DropdownInput::new(vec![
persistent_data cached_data
.font_catalog .font_catalog
.0 .0
.iter() .iter()
@ -866,7 +866,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
}) })
.collect::<Vec<_>>(), .collect::<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) .virtual_scrolling(true)
.widget_instance(), .widget_instance(),
]); ]);
@ -876,7 +876,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
second_row.extend_from_slice(&[ second_row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new({ DropdownInput::new({
persistent_data cached_data
.font_catalog .font_catalog
.0 .0
.iter() .iter()
@ -914,7 +914,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
.unwrap_or_default() .unwrap_or_default()
}) })
.selected_index( .selected_index(
persistent_data cached_data
.font_catalog .font_catalog
.0 .0
.iter() .iter()
@ -2208,7 +2208,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
} }
pub struct ParameterWidgetsInfo<'a> { pub struct ParameterWidgetsInfo<'a> {
persistent_data: &'a PersistentData, cached_data: &'a CachedData,
network_interface: &'a NodeNetworkInterface, network_interface: &'a NodeNetworkInterface,
selection_network_path: &'a [NodeId], selection_network_path: &'a [NodeId],
document_node: Option<&'a DocumentNode>, 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); let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
ParameterWidgetsInfo { ParameterWidgetsInfo {
persistent_data: context.persistent_data, cached_data: context.cached_data,
network_interface: context.network_interface, network_interface: context.network_interface,
selection_network_path: context.selection_network_path, selection_network_path: context.selection_network_path,
document_node, document_node,

View File

@ -3,7 +3,7 @@ use graphene_std::uuid::NodeId;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; 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::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::messages::prelude::*;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;
@ -13,7 +13,7 @@ pub struct PropertiesPanelMessageContext<'a> {
pub selection_network_path: &'a [NodeId], pub selection_network_path: &'a [NodeId],
pub document_name: &'a str, pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor, pub executor: &'a mut NodeGraphExecutor,
pub persistent_data: &'a PersistentData, pub cached_data: &'a CachedData,
pub properties_panel_open: bool, pub properties_panel_open: bool,
} }
@ -28,7 +28,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
selection_network_path, selection_network_path,
document_name, document_name,
executor, executor,
persistent_data, cached_data,
properties_panel_open, properties_panel_open,
} = context; } = context;
@ -46,7 +46,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
} }
let mut node_properties_context = NodePropertiesContext { let mut node_properties_context = NodePropertiesContext {
persistent_data, cached_data,
responses, responses,
network_interface, network_interface,
selection_network_path, selection_network_path,

View File

@ -3,8 +3,11 @@ mod portfolio_message_handler;
pub mod document; pub mod document;
pub mod document_migration; pub mod document_migration;
pub mod persistent_state;
pub mod utility_types; pub mod utility_types;
#[doc(inline)]
pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
#[doc(inline)] #[doc(inline)]
pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant}; pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
#[doc(inline)] #[doc(inline)]

View File

@ -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};

View File

@ -0,0 +1,30 @@
use crate::messages::frontend::utility_types::PersistedState;
use crate::messages::prelude::*;
#[impl_message(Message, PortfolioMessage, PersistentState)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum PersistentStateMessage {
ReadState,
WriteState,
LoadState {
state: PersistedState,
},
ReadDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
},
WriteDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
document: String,
},
DeleteDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
},
LoadDocument {
#[serde(rename = "documentId")]
document_id: DocumentId,
document: String,
},
}

View File

@ -0,0 +1,54 @@
use crate::messages::frontend::utility_types::PersistedState;
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
use crate::messages::prelude::*;
#[derive(Default, Debug, Clone, ExtractField)]
pub struct PersistentStateMessageHandler {
loaded: bool,
}
#[derive(ExtractField)]
pub struct PersistentStateMessageContext {
pub persisted_state: PersistedState,
}
#[message_handler_data]
impl MessageHandler<PersistentStateMessage, PersistentStateMessageContext> for PersistentStateMessageHandler {
fn process_message(&mut self, message: PersistentStateMessage, responses: &mut VecDeque<Message>, context: PersistentStateMessageContext) {
let PersistentStateMessageContext { persisted_state: state } = context;
match message {
PersistentStateMessage::ReadState => {
responses.add(FrontendMessage::TriggerPersistenceReadState);
}
PersistentStateMessage::WriteState => {
if !self.loaded && (state.documents.is_empty() && (state.workspace_layout == Some(WorkspacePanelLayout::default()) || state.workspace_layout.is_none())) {
return;
}
self.loaded = true;
responses.add(FrontendMessage::TriggerPersistenceWriteState { state });
}
PersistentStateMessage::LoadState { state } => {
self.loaded = true;
responses.add(PortfolioMessage::LoadPersistedState { 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 });
}
PersistentStateMessage::LoadDocument { document_id, document } => {
responses.add(PortfolioMessage::LoadDocumentContent {
document_id,
document_serialized_content: document,
});
}
}
}
advertise_actions!(PersistentStateMessageDiscriminant;);
}

View File

@ -1,6 +1,7 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType, WorkspacePanelLayout}; use super::persistent_state::PersistentStateMessage;
use crate::messages::frontend::utility_types::{ExportBounds, FileType}; 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::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::utility_types::FontCatalog; use crate::messages::portfolio::utility_types::FontCatalog;
use crate::messages::prelude::*; use crate::messages::prelude::*;
@ -15,6 +16,8 @@ pub enum PortfolioMessage {
// Sub-messages // Sub-messages
#[child] #[child]
Document(DocumentMessage), Document(DocumentMessage),
#[child]
PersistentState(PersistentStateMessage),
// Messages // Messages
Init, Init,
@ -61,8 +64,12 @@ pub enum PortfolioMessage {
LoadDocumentResources { LoadDocumentResources {
document_id: DocumentId, document_id: DocumentId,
}, },
LoadWorkspaceLayout { LoadPersistedState {
layout: WorkspacePanelLayout, state: PersistedState,
},
LoadDocumentContent {
document_id: DocumentId,
document_serialized_content: String,
}, },
MoveAllPanelTabs { MoveAllPanelTabs {
source_group: PanelGroupId, source_group: PanelGroupId,
@ -93,15 +100,13 @@ pub enum PortfolioMessage {
document_path: Option<PathBuf>, document_path: Option<PathBuf>,
document_serialized_content: String, document_serialized_content: String,
}, },
OpenDocumentFileWithId { LoadDocument {
document_id: DocumentId, document_id: DocumentId,
document_name: Option<String>, document_name: Option<String>,
document_path: Option<PathBuf>, document_path: Option<PathBuf>,
document_is_auto_saved: bool, document_is_auto_saved: bool,
document_is_saved: bool, document_is_saved: bool,
document_serialized_content: String, document_serialized_content: String,
to_front: bool,
select_after_open: bool,
}, },
OpenImage { OpenImage {
name: Option<String>, name: Option<String>,
@ -186,7 +191,6 @@ pub enum PortfolioMessage {
UpdateDocumentWidgets, UpdateDocumentWidgets,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdateWorkspacePanelLayout, UpdateWorkspacePanelLayout,
SaveWorkspaceLayout,
ResetWorkspaceLayout, ResetWorkspaceLayout,
ResetPanelGroupSizes { ResetPanelGroupSizes {
/// Path of child indices from the root to the split node whose children's sizes should be reset to defaults. /// Path of child indices from the root to the split node whose children's sizes should be reset to defaults.

View File

@ -1,12 +1,13 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface; 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::application::{Editor, generate_uuid};
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
use crate::messages::animation::TimingInformation; use crate::messages::animation::TimingInformation;
use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::dialog::simple_dialogs; use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument}; use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key; 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::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
@ -50,10 +51,12 @@ pub struct PortfolioMessageContext<'a> {
#[derive(Debug, Default, ExtractField)] #[derive(Debug, Default, ExtractField)]
pub struct PortfolioMessageHandler { pub struct PortfolioMessageHandler {
pub documents: HashMap<DocumentId, DocumentMessageHandler>, pub documents: HashMap<DocumentId, DocumentMessageHandler>,
unloaded_documents: HashMap<DocumentId, DocumentInfo>,
document_ids: VecDeque<DocumentId>, document_ids: VecDeque<DocumentId>,
pub(crate) active_document_id: Option<DocumentId>, pub(crate) active_document_id: Option<DocumentId>,
persistent_state: PersistentStateMessageHandler,
pub cached_data: CachedData,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize], copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor, pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode, pub selection_mode: SelectionMode,
pub reset_node_definitions_on_open: bool, pub reset_node_definitions_on_open: bool,
@ -82,7 +85,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_inputs = DocumentMessageContext { let document_inputs = DocumentMessageContext {
document_id, document_id,
ipp, ipp,
persistent_data: &self.persistent_data, cached_data: &self.cached_data,
executor: &mut self.executor, executor: &mut self.executor,
current_tool, current_tool,
preferences, preferences,
@ -94,9 +97,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.process_message(message, responses, document_inputs) 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 // Messages
PortfolioMessage::Init => { PortfolioMessage::Init => {
responses.add(PersistentStateMessage::ReadState);
// Initialize the frontend with environment information // Initialize the frontend with environment information
responses.add(FrontendMessage::UpdatePlatform { responses.add(FrontendMessage::UpdatePlatform {
platform: Editor::environment().into(), platform: Editor::environment().into(),
@ -104,14 +115,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Tell frontend to load persistent preferences // Tell frontend to load persistent preferences
responses.add(FrontendMessage::TriggerLoadPreferences); responses.add(FrontendMessage::TriggerLoadPreferences);
responses.add(FrontendMessage::TriggerLoadWorkspaceLayout);
// Before loading any documents, initially prepare the welcome screen buttons layout // Before loading any documents, initially prepare the welcome screen buttons layout
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); 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 // Tell frontend to load documents passed in as launch arguments
responses.add(FrontendMessage::TriggerOpenLaunchDocuments); responses.add(FrontendMessage::TriggerOpenLaunchDocuments);
@ -147,7 +154,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_inputs = DocumentMessageContext { let document_inputs = DocumentMessageContext {
document_id, document_id,
ipp, ipp,
persistent_data: &self.persistent_data, cached_data: &self.cached_data,
executor: &mut self.executor, executor: &mut self.executor,
current_tool, current_tool,
preferences, preferences,
@ -168,25 +175,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
PortfolioMessage::AutoSaveAllDocuments => { PortfolioMessage::AutoSaveAllDocuments => {
for (document_id, document) in self.documents.iter_mut() { for document_id in self.document_ids.iter() {
if !document.is_auto_saved() { if let Some(document) = self.documents.get_mut(document_id)
&& !document.is_auto_saved()
{
document.set_auto_save_state(true); document.set_auto_save_state(true);
responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id }); responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id });
} }
} }
} }
PortfolioMessage::AutoSaveDocument { document_id } => { PortfolioMessage::AutoSaveDocument { document_id } => {
let document = self.documents.get(&document_id).unwrap(); let Some(document) = self.document(document_id) else { return };
responses.add(FrontendMessage::TriggerPersistenceWriteDocument { responses.add(PersistentStateMessage::WriteDocument {
document_id, document_id,
document: document.serialize_document(), 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 => { PortfolioMessage::CloseActiveDocumentWithConfirmation => {
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
@ -206,7 +209,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
for document_id in &self.document_ids { 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); responses.add(PortfolioMessage::DestroyAllDocuments);
@ -221,7 +224,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
PortfolioMessage::CloseDocument { document_id } => { PortfolioMessage::CloseDocument { document_id } => {
// Is this the last document? // 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 // Clear UI layouts that assume the existence of a document
responses.add(PropertiesPanelMessage::Clear); responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel); responses.add(DocumentMessage::ClearLayersPanel);
@ -231,13 +234,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Actually delete the document (delay to delete document is required to let the document and properties panel messages above get processed) // 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(PortfolioMessage::DeleteDocument { document_id });
responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id }); responses.add(PersistentStateMessage::DeleteDocument { document_id });
// Send the new list of document tab names // Send the new list of document tab names
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
} }
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => { 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() { if target_document.is_saved() {
responses.add(EventMessage::ToolAbort); responses.add(EventMessage::ToolAbort);
responses.add(PortfolioMessage::CloseDocument { document_id }); responses.add(PortfolioMessage::CloseDocument { document_id });
@ -335,6 +342,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::DeleteDocument { document_id } => { PortfolioMessage::DeleteDocument { document_id } => {
let document_index = self.document_index(document_id); let document_index = self.document_index(document_id);
self.documents.remove(&document_id); self.documents.remove(&document_id);
self.unloaded_documents.remove(&document_id);
self.document_ids.remove(document_index); self.document_ids.remove(document_index);
if self.document_ids.is_empty() { if self.document_ids.is_empty() {
@ -354,12 +362,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::DestroyAllDocuments => { PortfolioMessage::DestroyAllDocuments => {
// Empty the list of internal document data // Empty the list of internal document data
self.documents.clear(); self.documents.clear();
self.unloaded_documents.clear();
self.document_ids.clear(); self.document_ids.clear();
self.active_document_id = None; self.active_document_id = None;
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PersistentStateMessage::WriteState);
} }
PortfolioMessage::FontCatalogLoaded { catalog } => { PortfolioMessage::FontCatalogLoaded { catalog } => {
self.persistent_data.font_catalog = catalog; self.cached_data.font_catalog = catalog;
if let Some(document_id) = self.active_document_id { if let Some(document_id) = self.active_document_id {
responses.add(PortfolioMessage::LoadDocumentResources { document_id }); responses.add(PortfolioMessage::LoadDocumentResources { document_id });
@ -370,23 +380,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(PortfolioMessage::LoadFontData { font }); responses.add(PortfolioMessage::LoadFontData { font });
} }
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()); 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 }); responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url });
} }
} }
} }
PortfolioMessage::FontLoaded { font_family, font_style, data } => { PortfolioMessage::FontLoaded { font_family, font_style, data } => {
let font = Font::new(font_family, font_style); let font = Font::new(font_family, font_style);
self.persistent_data.font_cache.insert(font, data); self.cached_data.font_cache.insert(font, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone()); self.executor.update_font_cache(self.cached_data.font_cache.clone());
for document_id in self.document_ids.iter() { for document_id in self.document_ids.iter() {
let node_to_inspect = self.node_to_inspect(); let node_to_inspect = self.node_to_inspect();
let Some(document) = self.documents.get_mut(document_id) else { 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"); log::error!("Tried to render non-existent document");
continue; continue;
}; };
@ -407,16 +420,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview, active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview,
}); });
if let Ok(message) = self.executor.submit_node_graph_evaluation( if let Ok(message) = self
self.documents.get_mut(document_id).expect("Tried to render non-existent document"), .executor
*document_id, .submit_node_graph_evaluation(document, *document_id, physical_resolution, scale, timing_information, node_to_inspect, true, pointer_position)
physical_resolution, {
scale,
timing_information,
node_to_inspect,
true,
pointer_position,
) {
responses.add_front(message); responses.add_front(message);
} }
} }
@ -431,7 +438,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::LoadDocumentResources { document_id } => { PortfolioMessage::LoadDocumentResources { document_id } => {
let catalog = &self.persistent_data.font_catalog; let catalog = &self.cached_data.font_catalog;
if catalog.0.is_empty() { if catalog.0.is_empty() {
responses.add_front(FrontendMessage::TriggerFontCatalogLoad); responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
@ -442,16 +449,52 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.load_layer_resources(responses); document.load_layer_resources(responses);
} }
} }
PortfolioMessage::LoadWorkspaceLayout { layout } => { PortfolioMessage::LoadPersistedState { state } => {
if let Some(layout) = state.workspace_layout {
self.workspace_panel_layout = layout; self.workspace_panel_layout = layout;
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
}
// Refresh all visible panels since the layout may have changed let PersistedState {
for group_id in self.workspace_panel_layout.root.all_group_ids() { documents,
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group_id).and_then(|g| g.active_panel_type()) { current_document,
self.refresh_panel_content(panel_type, responses); 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 } => { PortfolioMessage::NewDocumentWithName { name } => {
let mut new_document = DocumentMessageHandler::default(); let mut new_document = DocumentMessageHandler::default();
@ -465,7 +508,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); 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 }); responses.add(PortfolioMessage::SelectDocument { document_id });
} }
PortfolioMessage::MoveAllPanelTabs { PortfolioMessage::MoveAllPanelTabs {
@ -515,7 +558,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
// Refresh the new active tab // 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()) { if let Some(panel_type) = self.workspace_panel_layout.panel_group(target_group).and_then(|g| g.active_panel_type()) {
@ -565,7 +607,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
// Refresh the moved panel's content in its new location // Refresh the moved panel's content in its new location
self.refresh_panel_content(panel_type, responses); self.refresh_panel_content(panel_type, responses);
@ -661,30 +702,28 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document_path, document_path,
document_serialized_content, document_serialized_content,
} => { } => {
responses.add(PortfolioMessage::OpenDocumentFileWithId { let document_id = DocumentId(generate_uuid());
document_id: DocumentId(generate_uuid()), responses.add(PortfolioMessage::LoadDocument {
document_id,
document_name, document_name,
document_path, document_path,
document_is_auto_saved: false, document_is_auto_saved: false,
document_is_saved: true, document_is_saved: true,
document_serialized_content, document_serialized_content,
to_front: false,
select_after_open: true,
}); });
responses.add(PortfolioMessage::SelectDocument { document_id });
} }
PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen => { PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen => {
self.reset_node_definitions_on_open = !self.reset_node_definitions_on_open; self.reset_node_definitions_on_open = !self.reset_node_definitions_on_open;
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
} }
PortfolioMessage::OpenDocumentFileWithId { PortfolioMessage::LoadDocument {
document_id, document_id,
document_name, document_name,
document_path, document_path,
document_is_auto_saved, document_is_auto_saved,
document_is_saved, document_is_saved,
document_serialized_content, document_serialized_content,
to_front,
select_after_open,
} => { } => {
// Upgrade the document being opened to use fresh copies of all nodes // 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); let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content);
@ -777,11 +816,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
// Load the document into the portfolio so it opens in the editor // Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, responses, to_front); self.load_document(document, document_id, responses);
if select_after_open {
responses.add(PortfolioMessage::SelectDocument { document_id });
}
} }
PortfolioMessage::OpenImage { name, image } => { PortfolioMessage::OpenImage { name, image } => {
responses.add(PortfolioMessage::NewDocumentWithName { responses.add(PortfolioMessage::NewDocumentWithName {
@ -1126,7 +1161,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
mouse, mouse,
parent_and_insert_index, parent_and_insert_index,
} => { } => {
if self.documents.is_empty() { if self.document_ids.is_empty() {
responses.add(PortfolioMessage::OpenImage { name, image }); responses.add(PortfolioMessage::OpenImage { name, image });
} else { } else {
responses.add(DocumentMessage::PasteImage { responses.add(DocumentMessage::PasteImage {
@ -1144,7 +1179,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
mouse, mouse,
parent_and_insert_index, parent_and_insert_index,
} => { } => {
if self.documents.is_empty() { if self.document_ids.is_empty() {
responses.add(PortfolioMessage::OpenSvg { name, svg }); responses.add(PortfolioMessage::OpenSvg { name, svg });
} else { } else {
responses.add(DocumentMessage::PasteSvg { responses.add(DocumentMessage::PasteSvg {
@ -1200,7 +1235,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
} }
} }
PortfolioMessage::RequestWelcomeScreenButtonsLayout => { PortfolioMessage::RequestWelcomeScreenButtonsLayout => {
@ -1280,7 +1314,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Send the layout update first so the frontend mounts the new panel component before it receives content // Send the layout update first so the frontend mounts the new panel component before it receives content
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); 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()) { 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); self.refresh_panel_content(panel_type, responses);
@ -1315,7 +1348,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
// Refresh the new panel group's active tab // 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()) { if let Some(panel_type) = self.workspace_panel_layout.panel_group(new_id).and_then(|g| g.active_panel_type()) {
@ -1340,13 +1372,32 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
node_graph_open = document.is_graph_overlay_open(); 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 // Set the new active document ID
self.active_document_id = Some(document_id); self.active_document_id = Some(document_id);
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(FrontendMessage::UpdateActiveDocument { document_id }); responses.add(FrontendMessage::UpdateActiveDocument { document_id });
responses.add(FrontendMessage::TriggerSaveActiveDocument { document_id });
responses.add(ToolMessage::InitTools); responses.add(ToolMessage::InitTools);
responses.add(NodeGraphMessage::Init); responses.add(NodeGraphMessage::Init);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@ -1382,7 +1433,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
artboard_name, artboard_name,
artboard_count, 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 { let export_config = ExportConfig {
name, name,
file_type, file_type,
@ -1392,7 +1444,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
artboard_count, artboard_count,
..Default::default() ..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 { if let Err(description) = result {
responses.add(DialogMessage::DisplayDialogError { responses.add(DialogMessage::DisplayDialogError {
@ -1491,7 +1543,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
} }
PortfolioMessage::TogglePropertiesPanelOpen => { PortfolioMessage::TogglePropertiesPanelOpen => {
if self.workspace_panel_layout.focus_document { if self.workspace_panel_layout.focus_document {
@ -1536,11 +1587,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
false => self.workspace_panel_layout.clone(), false => self.workspace_panel_layout.clone(),
}; };
responses.add(FrontendMessage::UpdateWorkspacePanelLayout { panel_layout }); responses.add(FrontendMessage::UpdateWorkspacePanelLayout { panel_layout });
} responses.add(PersistentStateMessage::WriteState);
PortfolioMessage::SaveWorkspaceLayout => {
responses.add(FrontendMessage::TriggerSaveWorkspaceLayout {
workspace_layout: self.workspace_panel_layout.clone(),
});
} }
PortfolioMessage::ResetWorkspaceLayout => { PortfolioMessage::ResetWorkspaceLayout => {
// Destroy layouts for all currently visible non-document panels // Destroy layouts for all currently visible non-document panels
@ -1562,7 +1609,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
} }
PortfolioMessage::ResetPanelGroupSizes { split_path } => { PortfolioMessage::ResetPanelGroupSizes { split_path } => {
@ -1578,7 +1624,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
node.recalculate_default_sizes(); node.recalculate_default_sizes();
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
} }
PortfolioMessage::SetPanelGroupSizes { split_path, sizes } => { PortfolioMessage::SetPanelGroupSizes { split_path, sizes } => {
// Walk the tree to the target split node using the path // Walk the tree to the target split node using the path
@ -1596,29 +1641,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
responses.add(PortfolioMessage::SaveWorkspaceLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
} }
PortfolioMessage::UpdateOpenDocumentsList => { PortfolioMessage::UpdateOpenDocumentsList => {
// Send the list of document tab names // Send the list of document tab names
let open_documents = self let open_documents = self.document_ids.iter().filter_map(|id| self.document_details(*id)).collect::<Vec<_>>();
.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(),
},
})
})
.collect::<Vec<_>>();
let no_open_documents = open_documents.is_empty(); let no_open_documents = open_documents.is_empty();
responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents }); responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents });
responses.add(PersistentStateMessage::WriteState);
if no_open_documents { if no_open_documents {
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
@ -1682,11 +1714,11 @@ impl PortfolioMessageHandler {
} }
pub fn active_document(&self) -> Option<&DocumentMessageHandler> { 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> { 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<DocumentId> { pub fn active_document_id(&self) -> Option<DocumentId> {
@ -1694,12 +1726,29 @@ impl PortfolioMessageHandler {
} }
pub fn unsaved_document_names(&self) -> Vec<String> { pub fn unsaved_document_names(&self) -> Vec<String> {
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::<Vec<_>>();
PersistedState {
documents,
current_document: self.active_document_id,
workspace_layout: Some(self.workspace_panel_layout.clone()),
}
} }
pub fn generate_new_document_name(&self) -> String { pub fn generate_new_document_name(&self) -> String {
let mut doc_title_numbers = self let mut doc_title_numbers = self
.ordered_document_iterator() .document_ids
.iter()
.filter_map(|id| self.document_details(*id))
.filter_map(|doc| { .filter_map(|doc| {
doc.name doc.name
.rsplit_once(DEFAULT_DOCUMENT_NAME) .rsplit_once(DEFAULT_DOCUMENT_NAME)
@ -1744,12 +1793,12 @@ impl PortfolioMessageHandler {
} }
} }
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) { fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
if to_front { let is_new_document = !self.document_ids.contains(&document_id);
self.document_ids.push_front(document_id); if is_new_document {
} else {
self.document_ids.push_back(document_id); self.document_ids.push_back(document_id);
} }
self.unloaded_documents.remove(&document_id);
new_document.update_layers_panel_control_bar_widgets( new_document.update_layers_panel_control_bar_widgets(
self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document, self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
responses, responses,
@ -1768,12 +1817,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 // 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(DocumentMessage::GraphViewOverlay { open: false });
if is_new_document {
responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(PortfolioMessage::UpdateOpenDocumentsList);
} }
}
/// Returns an iterator over the open documents in order. /// Returns an iterator over the open documents in order.
pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> { pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> {
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 { fn document_index(&self, document_id: DocumentId) -> usize {
@ -1781,7 +1832,10 @@ impl PortfolioMessageHandler {
} }
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> 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()); return Err("No active document".to_string());
}; };
@ -1801,6 +1855,19 @@ impl PortfolioMessageHandler {
result result
} }
fn document_details(&self, document_id: DocumentId) -> Option<DocumentInfo> {
if let Some(document) = self.documents.get(&document_id) {
Some(DocumentInfo {
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. /// 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<NodeId> { pub fn node_to_inspect(&self) -> Option<NodeId> {
// Skip if the Data panel is not open // Skip if the Data panel is not open
@ -1808,7 +1875,7 @@ impl PortfolioMessageHandler {
return None; 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; let selected_nodes = document.network_interface.selected_nodes().0;
// Skip if there is not exactly one selected node // Skip if there is not exactly one selected node
@ -1854,7 +1921,6 @@ impl PortfolioMessageHandler {
responses.add(MenuBarMessage::SendLayout); responses.add(MenuBarMessage::SendLayout);
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
responses.add(PortfolioMessage::SaveWorkspaceLayout);
} }
/// Destroy the stored layout for a panel that is no longer the active tab. /// Destroy the stored layout for a panel that is no longer the active tab.

View File

@ -3,7 +3,7 @@ use graphene_std::raster::Image;
use graphene_std::text::{Font, FontCache}; use graphene_std::text::{Font, FontCache};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct PersistentData { pub struct CachedData {
pub font_cache: FontCache, pub font_cache: FontCache,
pub font_catalog: FontCatalog, pub font_catalog: FontCatalog,
} }

View File

@ -27,6 +27,7 @@ pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, Nod
pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler}; pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageContext, OverlaysMessageDiscriminant, OverlaysMessageHandler};
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler}; pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};

View File

@ -4,7 +4,7 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; 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::prelude::*;
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext; use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
use crate::messages::tool::utility_types::{HintData, ToolType}; use crate::messages::tool::utility_types::{HintData, ToolType};
@ -18,7 +18,7 @@ pub struct ToolMessageContext<'a> {
pub document_id: DocumentId, pub document_id: DocumentId,
pub document: &'a mut DocumentMessageHandler, pub document: &'a mut DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler, pub input: &'a InputPreprocessorMessageHandler,
pub persistent_data: &'a PersistentData, pub cached_data: &'a CachedData,
pub node_graph: &'a NodeGraphExecutor, pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
pub viewport: &'a ViewportMessageHandler, pub viewport: &'a ViewportMessageHandler,
@ -39,7 +39,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
document_id, document_id,
document, document,
input, input,
persistent_data, cached_data,
node_graph, node_graph,
preferences, preferences,
viewport, viewport,
@ -126,7 +126,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph, node_graph,
preferences, preferences,
viewport, viewport,
persistent_data, cached_data,
}; };
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort { if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
@ -217,7 +217,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
tool_data.tools.get(active_tool).unwrap().activate(responses); tool_data.tools.get(active_tool).unwrap().activate(responses);
// Register initial properties // 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 // Notify the frontend about the initial active tool
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool); tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
@ -234,7 +234,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph, node_graph,
preferences, preferences,
viewport, viewport,
persistent_data, cached_data,
}; };
// Set initial hints and cursor // Set initial hints and cursor
@ -258,7 +258,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
ToolMessage::RefreshToolOptions => { ToolMessage::RefreshToolOptions => {
let tool_data = &mut self.tool_state.tool_data; 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 => { ToolMessage::RefreshToolShelf => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
@ -346,7 +346,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph, node_graph,
preferences, preferences,
viewport, viewport,
persistent_data, cached_data,
}; };
if matches!(tool_message, ToolMessage::UpdateHints) { if matches!(tool_message, ToolMessage::UpdateHints) {
if graph_view_overlay_open { if graph_view_overlay_open {

View File

@ -605,7 +605,7 @@ impl Fsm for SelectToolFsmState {
document, document,
input, input,
viewport, viewport,
persistent_data, cached_data,
.. ..
} = tool_action_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); 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)) { 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); overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
} }
} }

View File

@ -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::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; 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::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; 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}; 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<Widge
} }
impl ToolRefreshOptions for TextTool { impl ToolRefreshOptions for TextTool {
fn refresh_options(&self, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) { fn refresh_options(&self, responses: &mut VecDeque<Message>, cached_data: &CachedData) {
self.send_layout(responses, LayoutTarget::ToolOptions, &persistent_data.font_catalog); self.send_layout(responses, LayoutTarget::ToolOptions, &cached_data.font_catalog);
} }
} }
@ -302,7 +302,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> 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 { fn actions(&self) -> ActionList {
@ -573,11 +573,11 @@ impl Fsm for TextToolFsmState {
document, document,
global_tool_data, global_tool_data,
input, input,
persistent_data, cached_data,
viewport, viewport,
.. ..
} = transition_data; } = transition_data;
let font_cache = &persistent_data.font_cache; let font_cache = &cached_data.font_cache;
let fill_color = COLOR_OVERLAY_BLUE_05; let fill_color = COLOR_OVERLAY_BLUE_05;
let ToolMessage::Text(event) = event else { return self }; let ToolMessage::Text(event) = event else { return self };

View File

@ -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::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; 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::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
@ -24,7 +24,7 @@ pub struct ToolActionMessageContext<'a> {
pub document_id: DocumentId, pub document_id: DocumentId,
pub global_tool_data: &'a DocumentToolData, pub global_tool_data: &'a DocumentToolData,
pub input: &'a InputPreprocessorMessageHandler, pub input: &'a InputPreprocessorMessageHandler,
pub persistent_data: &'a PersistentData, pub cached_data: &'a CachedData,
pub shape_editor: &'a mut ShapeState, pub shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor, pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler, pub preferences: &'a PreferencesMessageHandler,
@ -37,11 +37,11 @@ impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mu
type Tool = dyn ToolCommon + Send + Sync; type Tool = dyn ToolCommon + Send + Sync;
pub trait ToolRefreshOptions { pub trait ToolRefreshOptions {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData); fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData);
} }
impl<T: LayoutHolder> ToolRefreshOptions for T { impl<T: LayoutHolder> ToolRefreshOptions for T {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData) { fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
self.send_layout(responses, LayoutTarget::ToolOptions); self.send_layout(responses, LayoutTarget::ToolOptions);
} }
} }

View File

@ -36,15 +36,15 @@ impl EditorTestUtils {
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result<Instrumented, String> { async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Result<Instrumented, String> {
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
let document_id = portfolio.active_document_id.unwrap(); let document_id = portfolio.active_document_id.unwrap();
let exector = &mut portfolio.executor; let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents);
let document = portfolio.documents.get_mut(&document_id).unwrap(); 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, Ok(instrumented) => instrumented,
Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")), 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}")); return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}"));
} }
runtime.run().await; runtime.run().await;

View File

@ -4,7 +4,7 @@
import LayoutRow from "/src/components/layout/LayoutRow.svelte"; import LayoutRow from "/src/components/layout/LayoutRow.svelte";
import Panel from "/src/components/window/Panel.svelte"; import Panel from "/src/components/window/Panel.svelte";
import type { PortfolioStore } from "/src/stores/portfolio"; import type { PortfolioStore } from "/src/stores/portfolio";
import type { EditorWrapper, OpenDocument, PanelGroupState, PanelLayoutSubdivision } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { DocumentInfo, EditorWrapper, PanelGroupState, PanelLayoutSubdivision } from "/wrapper/pkg/graphite_wasm_wrapper";
const MIN_PANEL_SIZE = 100; const MIN_PANEL_SIZE = 100;
const DOUBLE_CLICK_MILLISECONDS = 500; const DOUBLE_CLICK_MILLISECONDS = 500;
@ -31,9 +31,9 @@
$: if (subdivision) sizeOverrides = {}; $: if (subdivision) sizeOverrides = {};
// Reactive array of resolved sizes (merging backend defaults with local overrides) // Reactive array of resolved sizes (merging backend defaults with local overrides)
$: resolvedSizes = subdivision && "Split" in subdivision ? subdivision.Split.children.map((child, index) => sizeOverrides[index] ?? child.size) : []; $: resolvedSizes = subdivision && "Split" in subdivision ? subdivision.Split.children.map((child, index) => sizeOverrides[index] ?? child.size) : [];
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => { $: documentTabLabels = $portfolio.documents.map((doc: DocumentInfo) => {
const name = doc.details.name; const name = doc.name;
const unsaved = !doc.details.is_saved; const unsaved = !doc.is_saved;
if (!editor.inDevelopmentMode()) return { name, unsaved }; if (!editor.inDevelopmentMode()) return { name, unsaved };
const tooltipDescription = `Document ID: ${doc.id}`; const tooltipDescription = `Document ID: ${doc.id}`;

View File

@ -3,12 +3,11 @@ import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { import {
saveEditorPreferences, saveEditorPreferences,
loadEditorPreferences, loadEditorPreferences,
saveWorkspaceLayout, writePersistedState,
loadWorkspaceLayout, readPersistedState,
storeDocument, writePersistedDocument,
removeDocument, readPersistedDocument,
loadDocuments, deletePersistedDocument,
saveActiveDocument,
} from "/src/utility-functions/persistence"; } from "/src/utility-functions/persistence";
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
@ -31,33 +30,29 @@ export function createPersistenceManager(subscriptions: SubscriptionsRouter, edi
await loadEditorPreferences(editor); await loadEditorPreferences(editor);
}); });
subscriptions.subscribeFrontendMessage("TriggerSaveWorkspaceLayout", async (data) => { subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteState", async (data) => {
await saveWorkspaceLayout(data.workspaceLayout); await writePersistedState(data.state);
}); });
subscriptions.subscribeFrontendMessage("TriggerLoadWorkspaceLayout", async () => { subscriptions.subscribeFrontendMessage("TriggerPersistenceReadState", async () => {
await loadWorkspaceLayout(editor); await readPersistedState(editor);
}); });
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => { subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
await storeDocument(data, portfolio); await writePersistedDocument(data);
}); });
subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => { subscriptions.subscribeFrontendMessage("TriggerPersistenceReadDocument", async (data) => {
await removeDocument(String(data.documentId), portfolio); await readPersistedDocument(data.documentId, editor);
}); });
subscriptions.subscribeFrontendMessage("TriggerLoadAutoSaveDocuments", async () => { subscriptions.subscribeFrontendMessage("TriggerPersistenceDeleteDocument", async (data) => {
await loadDocuments(editor); await deletePersistedDocument(String(data.documentId));
}); });
subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => { subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => {
// TODO: Could be used to load documents from URL params or similar on launch // 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() { export function destroyPersistenceManager() {
@ -66,13 +61,12 @@ export function destroyPersistenceManager() {
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences"); subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences"); subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
subscriptions.unsubscribeFrontendMessage("TriggerSaveWorkspaceLayout"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteState");
subscriptions.unsubscribeFrontendMessage("TriggerLoadWorkspaceLayout"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadState");
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadDocument");
subscriptions.unsubscribeFrontendMessage("TriggerLoadAutoSaveDocuments"); subscriptions.unsubscribeFrontendMessage("TriggerPersistenceDeleteDocument");
subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments"); subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments");
subscriptions.unsubscribeFrontendMessage("TriggerSaveActiveDocument");
} }
// Self-accepting HMR: tear down the old instance and re-create with the new module's code // Self-accepting HMR: tear down the old instance and re-create with the new module's code

View File

@ -2,15 +2,14 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { SubscriptionsRouter } from "/src/subscriptions-router"; import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files"; import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files";
import { storeDocumentTabOrder } from "/src/utility-functions/persistence";
import { rasterizeSVG } from "/src/utility-functions/rasterization"; import { rasterizeSVG } from "/src/utility-functions/rasterization";
import type { EditorWrapper, OpenDocument, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { EditorWrapper, DocumentInfo, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper";
export type PortfolioStore = ReturnType<typeof createPortfolioStore>; export type PortfolioStore = ReturnType<typeof createPortfolioStore>;
type PortfolioStoreState = { type PortfolioStoreState = {
unsaved: boolean; unsaved: boolean;
documents: OpenDocument[]; documents: DocumentInfo[];
activeDocumentIndex: number; activeDocumentIndex: number;
panelLayout: WorkspacePanelLayout; panelLayout: WorkspacePanelLayout;
}; };
@ -38,7 +37,6 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
state.documents = data.openDocuments; state.documents = data.openDocuments;
return state; return state;
}); });
storeDocumentTabOrder({ subscribe });
}); });
subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => { subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {

View File

@ -247,7 +247,7 @@ export function onModifyInputField(e: CustomEvent) {
export async function onBeforeUnload(e: BeforeUnloadEvent, editor: EditorWrapper, portfolioStore: PortfolioStore) { export async function onBeforeUnload(e: BeforeUnloadEvent, editor: EditorWrapper, portfolioStore: PortfolioStore) {
const activeDocument = get(portfolioStore).documents[get(portfolioStore).activeDocumentIndex]; const activeDocument = get(portfolioStore).documents[get(portfolioStore).activeDocumentIndex];
if (activeDocument && !activeDocument.details.is_auto_saved) editor.triggerAutoSave(activeDocument.id); if (activeDocument) editor.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost // Skip the message if the editor crashed, since work is already lost
if (await editor.hasCrashed()) return; if (await editor.hasCrashed()) return;
@ -255,7 +255,7 @@ export async function onBeforeUnload(e: BeforeUnloadEvent, editor: EditorWrapper
// Skip the message during development, since it's annoying when testing // Skip the message during development, since it's annoying when testing
if (await editor.inDevelopmentMode()) return; if (await editor.inDevelopmentMode()) return;
const allDocumentsSaved = get(portfolioStore).documents.reduce((acc, doc) => acc && doc.details.is_saved, true); const allDocumentsSaved = get(portfolioStore).documents.reduce((acc, doc) => acc && doc.is_saved, true);
if (!allDocumentsSaved) { if (!allDocumentsSaved) {
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?"; e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
e.preventDefault(); e.preventDefault();

View File

@ -1,25 +1,23 @@
import { get } from "svelte/store";
import type { PortfolioStore } from "/src/stores/portfolio";
import type { MessageBody } from "/src/subscriptions-router"; import type { MessageBody } from "/src/subscriptions-router";
import type { EditorWrapper, PersistedDocumentInfo, PersistedState } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { DocumentInfo, EditorWrapper, PersistedState } from "/wrapper/pkg/graphite_wasm_wrapper";
const PERSISTENCE_DB = "graphite"; const PERSISTENCE_DB = "graphite";
const PERSISTENCE_STORE = "store"; const PERSISTENCE_STORE = "store";
function emptyPersistedState(): PersistedState { function emptyPersistedState(): PersistedState {
// eslint-disable-next-line camelcase // 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 { function createDocumentInfo(id: bigint, name: string, isSaved: boolean): DocumentInfo {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
return { id, name, is_saved: isSaved }; return { id, name, is_saved: isSaved };
} }
// Reorder document entries to match the given ID ordering, appending any unmentioned entries at the end // Reorder document entries to match the given ID ordering, appending any unmentioned entries at the end
function reorderDocuments(documents: PersistedDocumentInfo[], orderedIds: bigint[]): PersistedDocumentInfo[] { function reorderDocuments(documents: DocumentInfo[], orderedIds: bigint[]): DocumentInfo[] {
const byId = new Map(documents.map((entry) => [entry.id, entry])); const byId = new Map(documents.map((entry) => [entry.id, entry]));
const reordered: PersistedDocumentInfo[] = []; const reordered: DocumentInfo[] = [];
orderedIds.forEach((id) => { orderedIds.forEach((id) => {
const existing = byId.get(id); const existing = byId.get(id);
@ -39,18 +37,8 @@ function reorderDocuments(documents: PersistedDocumentInfo[], orderedIds: bigint
// State-based persistence (new format) // State-based persistence (new format)
// ==================================== // ====================================
export async function storeDocumentTabOrder(portfolio: PortfolioStore) { export async function writePersistedDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">) {
const portfolioData = get(portfolio); const { documentId, document } = autoSaveDocument;
const orderedIds = portfolioData.documents.map((doc) => doc.id);
await databaseUpdate<PersistedState>("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;
// Update content in the documents store // Update content in the documents store
await databaseUpdate<Record<string, string>>("documents", (old) => { await databaseUpdate<Record<string, string>>("documents", (old) => {
@ -58,91 +46,44 @@ export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersis
documents[String(documentId)] = document; documents[String(documentId)] = document;
return documents; return documents;
}); });
// Update metadata and ordering in the state store
const portfolioData = get(portfolio);
const orderedIds = portfolioData.documents.map((doc) => doc.id);
await databaseUpdate<PersistedState>("state", (old) => {
const state = old || emptyPersistedState();
// 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);
} }
// eslint-disable-next-line camelcase export async function readPersistedDocument(documentId: bigint, editor: EditorWrapper) {
state.current_document = documentId; const documentContents = await databaseGet<Record<string, string>>("documents");
state.documents = reorderDocuments(state.documents, orderedIds); if (!documentContents) return;
return state;
}); const content = documentContents[String(documentId)];
if (content === undefined) return;
editor.loadDocumentContent(documentId, content);
} }
export async function removeDocument(id: string, portfolio: PortfolioStore) { export async function deletePersistedDocument(id: string) {
const documentId = BigInt(id);
// Remove content from the documents store // Remove content from the documents store
await databaseUpdate<Record<string, string>>("documents", (old) => { await databaseUpdate<Record<string, string>>("documents", (old) => {
const documents = old || {}; const documents = old || {};
delete documents[id]; delete documents[id];
return documents; return documents;
}); });
// Update state: remove the entry and update current_document
const portfolioData = get(portfolio);
const documentCount = portfolioData.documents.length;
await databaseUpdate<PersistedState>("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 migrateToNewFormat();
await garbageCollectDocuments(); await garbageCollectDocuments();
const state = await databaseGet<PersistedState>("state"); const state = await databaseGet<PersistedState>("state");
const documentContents = await databaseGet<Record<string, string>>("documents"); if (!state) return;
if (!state || !documentContents || state.documents.length === 0) return; editor.loadPersistedState(state);
// 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<PersistedState>("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;
});
} }
export async function saveEditorPreferences(preferences: unknown) { export async function saveEditorPreferences(preferences: unknown) {
@ -154,15 +95,6 @@ export async function loadEditorPreferences(editor: EditorWrapper) {
editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined); 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<Record<string, unknown>>("workspace_layout");
if (layout) editor.loadWorkspaceLayout(layout);
}
// Remove orphaned entries from the "documents" content store that have no corresponding entry in "state" // Remove orphaned entries from the "documents" content store that have no corresponding entry in "state"
async function garbageCollectDocuments() { async function garbageCollectDocuments() {
const state = await databaseGet<PersistedState>("state"); const state = await databaseGet<PersistedState>("state");
@ -197,6 +129,7 @@ export async function wipeDocuments() {
async function wipeOldFormat() { async function wipeOldFormat() {
await databaseDelete("documents_tab_order"); await databaseDelete("documents_tab_order");
await databaseDelete("current_document_id"); await databaseDelete("current_document_id");
await databaseDelete("workspace_layout");
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
@ -209,7 +142,7 @@ async function migrateToNewFormat() {
// Build the new "state" and "documents" from the old format // Build the new "state" and "documents" from the old format
const newDocumentContents: Record<string, string> = {}; const newDocumentContents: Record<string, string> = {};
const newDocumentInfos: PersistedDocumentInfo[] = []; const newDocumentInfos: DocumentInfo[] = [];
if (oldDocuments) { if (oldDocuments) {
Object.values(oldDocuments).forEach((value) => { Object.values(oldDocuments).forEach((value) => {

View File

@ -8,7 +8,6 @@
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"sourceMap": true, "sourceMap": true,
"types": ["node"], "types": ["node"],
"baseUrl": ".",
"paths": { "/*": ["./*"] }, "paths": { "/*": ["./*"] },
"lib": ["ESNext", "DOM", "DOM.Iterable"] "lib": ["ESNext", "DOM", "DOM.Iterable"]
}, },

View File

@ -379,13 +379,23 @@ impl EditorWrapper {
} }
} }
#[wasm_bindgen(js_name = loadWorkspaceLayout)] #[wasm_bindgen(js_name = loadPersistedState)]
pub fn load_workspace_layout(&self, layout: JsValue) { pub fn load_persisted_state(&self, state: JsValue) {
let Ok(layout) = serde_wasm_bindgen::from_value(layout) else { let Ok(state) = serde_wasm_bindgen::from_value(state) else {
log::error!("Failed to deserialize workspace layout"); log::error!("Failed to deserialize persisted state");
return; return;
}; };
let message = PortfolioMessage::LoadWorkspaceLayout { layout };
let message = PersistentStateMessage::LoadState { state };
self.dispatch(message);
}
#[wasm_bindgen(js_name = loadDocumentContent)]
pub fn load_document_content(&self, document_id: u64, document: String) {
let message = PersistentStateMessage::LoadDocument {
document_id: DocumentId(document_id),
document: document,
};
self.dispatch(message); self.dispatch(message);
} }
@ -414,22 +424,6 @@ impl EditorWrapper {
self.dispatch(message); 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)] #[wasm_bindgen(js_name = triggerAutoSave)]
pub fn trigger_auto_save(&self, document_id: u64) { pub fn trigger_auto_save(&self, document_id: u64) {
let document_id = DocumentId(document_id); let document_id = DocumentId(document_id);