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:
parent
2a2a60883d
commit
6c5e3c97f8
|
|
@ -17,11 +17,10 @@ use crate::cef;
|
|||
use crate::cli::Cli;
|
||||
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
use crate::persist::PersistentData;
|
||||
use crate::persist;
|
||||
use crate::preferences;
|
||||
use crate::render::{RenderError, RenderState};
|
||||
use crate::window::Window;
|
||||
use crate::workspace_layout;
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ pub(crate) struct App {
|
|||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
persistent_data: PersistentData,
|
||||
#[cfg_attr(not(target_os = "macos"), expect(unused))]
|
||||
preferences: Preferences,
|
||||
cli: Cli,
|
||||
|
|
@ -93,9 +91,6 @@ impl App {
|
|||
}
|
||||
});
|
||||
|
||||
let mut persistent_data = PersistentData::default();
|
||||
persistent_data.load_from_disk();
|
||||
|
||||
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
|
||||
|
||||
Self {
|
||||
|
|
@ -119,7 +114,6 @@ impl App {
|
|||
start_render_sender,
|
||||
web_communication_initialized: false,
|
||||
web_communication_startup_buffer: Vec::new(),
|
||||
persistent_data,
|
||||
preferences,
|
||||
cli,
|
||||
startup_time: None,
|
||||
|
|
@ -285,17 +279,24 @@ impl App {
|
|||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
|
||||
self.persistent_data.write_document(id, document);
|
||||
DesktopFrontendMessage::PersistenceWriteState { state } => {
|
||||
persist::write_state(state);
|
||||
}
|
||||
DesktopFrontendMessage::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 } => {
|
||||
self.persistent_data.delete_document(&id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
|
||||
self.persistent_data.set_current_document(id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
|
||||
self.persistent_data.force_document_order(ids);
|
||||
persist::delete_document(&id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
|
||||
preferences::write(preferences);
|
||||
|
|
@ -305,30 +306,6 @@ impl App {
|
|||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||
responses.push(message);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout: layout } => {
|
||||
workspace_layout::write(&layout);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadWorkspaceLayout => {
|
||||
if let Some(workspace_layout) = workspace_layout::read() {
|
||||
let message = DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout };
|
||||
responses.push(message);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadDocuments => {
|
||||
// Open all documents in persisted tab order, then select the current one
|
||||
for (id, document) in self.persistent_data.documents() {
|
||||
responses.push(DesktopWrapperMessage::LoadDocument {
|
||||
id,
|
||||
document,
|
||||
to_front: false,
|
||||
select_after_open: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(id) = self.persistent_data.current_document_id() {
|
||||
responses.push(DesktopWrapperMessage::SelectDocument { id });
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::OpenLaunchDocuments => {
|
||||
if self.cli.files.is_empty() {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
|||
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
||||
pub(crate) const APP_WORKSPACE_LAYOUT_FILE_NAME: &str = "workspace_layout.ron";
|
||||
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||
|
||||
// CEF configuration constants
|
||||
|
|
|
|||
|
|
@ -84,3 +84,11 @@ impl AsRef<Path> for TempDir {
|
|||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
||||
pub(crate) fn delete_old_cef_browser_directory() {
|
||||
let old_browser_dir = crate::dirs::app_data_dir().join("browser");
|
||||
if old_browser_dir.is_dir() {
|
||||
let _ = std::fs::remove_dir_all(&old_browser_dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ mod persist;
|
|||
mod preferences;
|
||||
mod render;
|
||||
mod window;
|
||||
mod workspace_layout;
|
||||
|
||||
pub(crate) mod consts;
|
||||
|
||||
|
|
@ -65,6 +64,9 @@ pub fn start() {
|
|||
|
||||
dirs::app_tmp_dir_cleanup();
|
||||
|
||||
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
||||
dirs::delete_old_cef_browser_directory();
|
||||
|
||||
let prefs = preferences::read();
|
||||
|
||||
// Must be called before event loop initialization or native window integrations will break
|
||||
|
|
|
|||
|
|
@ -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) struct PersistentData {
|
||||
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();
|
||||
pub(crate) fn read_state() -> PersistedState {
|
||||
let path = state_file_path();
|
||||
let data = match std::fs::read_to_string(&path) {
|
||||
Ok(d) => d,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
tracing::info!("No persistent data file found at {path:?}, starting fresh");
|
||||
return;
|
||||
return PersistedState::default();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read persistent data from disk: {e}");
|
||||
return;
|
||||
return PersistedState::default();
|
||||
}
|
||||
};
|
||||
let loaded = match ron::from_str(&data) {
|
||||
Ok(d) => d,
|
||||
Err(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;
|
||||
}
|
||||
};
|
||||
*self = loaded;
|
||||
|
||||
self.garbage_collect_document_files();
|
||||
if let Err(e) = std::fs::write(state_file_path(), data) {
|
||||
tracing::error!("Failed to write persistent data to disk: {e}");
|
||||
}
|
||||
garbage_collect_document_files(&state);
|
||||
}
|
||||
|
||||
fn garbage_collect_document_files(&self) {
|
||||
let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect();
|
||||
pub(crate) fn write_document_content(id: DocumentId, document_content: String) {
|
||||
if let Err(e) = std::fs::write(document_content_path(&id), document_content) {
|
||||
tracing::error!("Failed to write document {id:?} to disk: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn 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 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -49,41 +49,18 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
|||
let message = FrontendMessage::UpdateFullscreen { fullscreen };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::LoadDocument {
|
||||
id,
|
||||
document,
|
||||
to_front,
|
||||
select_after_open,
|
||||
} => {
|
||||
let message = PortfolioMessage::OpenDocumentFileWithId {
|
||||
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,
|
||||
};
|
||||
DesktopWrapperMessage::LoadDocumentContent { id, document } => {
|
||||
let message = PersistentStateMessage::LoadDocument { document_id: id, document };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::SelectDocument { id } => {
|
||||
let message = PortfolioMessage::SelectDocument { document_id: id };
|
||||
DesktopWrapperMessage::LoadPersistedState { state } => {
|
||||
let message = PersistentStateMessage::LoadState { state };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::LoadPreferences { preferences } => {
|
||||
let message = PreferencesMessage::Load { preferences };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout } => match ron::from_str(&workspace_layout) {
|
||||
Ok(layout) => {
|
||||
let message = PortfolioMessage::LoadWorkspaceLayout { layout };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize workspace layout: {e}");
|
||||
}
|
||||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
DesktopWrapperMessage::MenuEvent { id } => {
|
||||
if let Some(message) = crate::utils::menu::parse_item_path(id) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarge
|
|||
use graphite_editor::messages::prelude::FrontendMessage;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
||||
use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
||||
|
||||
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
|
||||
match message {
|
||||
|
|
@ -67,36 +67,23 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||
dispatcher.respond(DesktopFrontendMessage::UpdateUIScale { scale });
|
||||
return Some(FrontendMessage::UpdateUIScale { scale });
|
||||
}
|
||||
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
|
||||
id: document_id,
|
||||
document: Document {
|
||||
content: document,
|
||||
name: details.name,
|
||||
path: details.path,
|
||||
is_saved: details.is_saved,
|
||||
},
|
||||
});
|
||||
FrontendMessage::TriggerPersistenceReadState => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceReadState);
|
||||
}
|
||||
FrontendMessage::TriggerPersistenceRemoveDocument { document_id } => {
|
||||
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 });
|
||||
}
|
||||
FrontendMessage::UpdateActiveDocument { document_id } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id: document_id });
|
||||
|
||||
// Forward this to update the UI
|
||||
return Some(FrontendMessage::UpdateActiveDocument { document_id });
|
||||
}
|
||||
FrontendMessage::UpdateOpenDocumentsList { open_documents } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateDocumentsList {
|
||||
ids: open_documents.iter().map(|document| document.id).collect(),
|
||||
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
|
||||
id: document_id,
|
||||
document_serialized_content: document,
|
||||
});
|
||||
|
||||
// Forward this to update the UI
|
||||
return Some(FrontendMessage::UpdateOpenDocumentsList { open_documents });
|
||||
}
|
||||
FrontendMessage::TriggerLoadAutoSaveDocuments => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadDocuments);
|
||||
}
|
||||
FrontendMessage::TriggerOpenLaunchDocuments => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments);
|
||||
|
|
@ -107,16 +94,6 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||
FrontendMessage::TriggerLoadPreferences => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
||||
}
|
||||
FrontendMessage::TriggerSaveWorkspaceLayout { workspace_layout } => {
|
||||
let Ok(workspace_layout) = ron::ser::to_string_pretty(&workspace_layout, ron::ser::PrettyConfig::default()) else {
|
||||
tracing::error!("Failed to serialize workspace layout");
|
||||
return None;
|
||||
};
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout });
|
||||
}
|
||||
FrontendMessage::TriggerLoadWorkspaceLayout => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadWorkspaceLayout);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
FrontendMessage::UpdateLayout {
|
||||
layout_target: LayoutTarget::MenuBar,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||
|
||||
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_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
|
||||
pub use graphite_editor::messages::prelude::DocumentId;
|
||||
|
|
@ -42,26 +42,22 @@ pub enum DesktopFrontendMessage {
|
|||
UpdateOverlays(vello::Scene),
|
||||
PersistenceWriteDocument {
|
||||
id: DocumentId,
|
||||
document: Document,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
PersistenceDeleteDocument {
|
||||
id: DocumentId,
|
||||
},
|
||||
PersistenceUpdateCurrentDocument {
|
||||
id: DocumentId,
|
||||
},
|
||||
PersistenceLoadDocuments,
|
||||
PersistenceUpdateDocumentsList {
|
||||
ids: Vec<DocumentId>,
|
||||
},
|
||||
PersistenceWritePreferences {
|
||||
preferences: Preferences,
|
||||
},
|
||||
PersistenceLoadPreferences,
|
||||
PersistenceWriteWorkspaceLayout {
|
||||
workspace_layout: String,
|
||||
PersistenceWriteState {
|
||||
state: PersistedState,
|
||||
},
|
||||
PersistenceReadState,
|
||||
PersistenceReadDocument {
|
||||
id: DocumentId,
|
||||
},
|
||||
PersistenceLoadWorkspaceLayout,
|
||||
UpdateMenu {
|
||||
entries: Vec<MenuItem>,
|
||||
},
|
||||
|
|
@ -85,66 +81,20 @@ pub enum DesktopFrontendMessage {
|
|||
pub enum DesktopWrapperMessage {
|
||||
FromWeb(Box<EditorMessage>),
|
||||
Input(InputMessage),
|
||||
FileDialogResult {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
context: OpenFileDialogContext,
|
||||
},
|
||||
SaveFileDialogResult {
|
||||
path: PathBuf,
|
||||
context: SaveFileDialogContext,
|
||||
},
|
||||
OpenFile {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
ImportFile {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
FileDialogResult { path: PathBuf, content: Vec<u8>, context: OpenFileDialogContext },
|
||||
SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext },
|
||||
OpenFile { path: PathBuf, content: Vec<u8> },
|
||||
ImportFile { path: PathBuf, content: Vec<u8> },
|
||||
PollNodeGraphEvaluation,
|
||||
UpdateMaximized {
|
||||
maximized: bool,
|
||||
},
|
||||
UpdateFullscreen {
|
||||
fullscreen: bool,
|
||||
},
|
||||
LoadDocument {
|
||||
id: DocumentId,
|
||||
document: Document,
|
||||
to_front: bool,
|
||||
select_after_open: bool,
|
||||
},
|
||||
SelectDocument {
|
||||
id: DocumentId,
|
||||
},
|
||||
LoadPreferences {
|
||||
preferences: Preferences,
|
||||
},
|
||||
LoadWorkspaceLayout {
|
||||
workspace_layout: String,
|
||||
},
|
||||
MenuEvent {
|
||||
id: String,
|
||||
},
|
||||
ClipboardReadResult {
|
||||
content: Option<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,
|
||||
UpdateMaximized { maximized: bool },
|
||||
UpdateFullscreen { fullscreen: bool },
|
||||
LoadDocumentContent { id: DocumentId, document: String },
|
||||
LoadPersistedState { state: PersistedState },
|
||||
LoadPreferences { preferences: Preferences },
|
||||
MenuEvent { id: String },
|
||||
ClipboardReadResult { content: Option<String> },
|
||||
PointerLockMove { x: f64, y: f64 },
|
||||
LoadThirdPartyLicenses { text: String },
|
||||
}
|
||||
|
||||
pub struct FileFilter {
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ impl Dispatcher {
|
|||
document_id,
|
||||
document,
|
||||
input: &self.message_handlers.input_preprocessor_message_handler,
|
||||
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
|
||||
cached_data: &self.message_handlers.portfolio_message_handler.cached_data,
|
||||
node_graph: &self.message_handlers.portfolio_message_handler.executor,
|
||||
preferences: &self.message_handlers.preferences_message_handler,
|
||||
viewport: &self.message_handlers.viewport_message_handler,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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::frontend::utility_types::EyedropperPreviewImage;
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage};
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||
|
|
@ -113,7 +113,15 @@ pub enum FrontendMessage {
|
|||
font: Font,
|
||||
url: String,
|
||||
},
|
||||
TriggerPersistenceRemoveDocument {
|
||||
TriggerPersistenceReadState,
|
||||
TriggerPersistenceReadDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
},
|
||||
TriggerPersistenceWriteState {
|
||||
state: PersistedState,
|
||||
},
|
||||
TriggerPersistenceDeleteDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
},
|
||||
|
|
@ -121,26 +129,15 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
document: String,
|
||||
details: DocumentDetails,
|
||||
},
|
||||
TriggerLoadAutoSaveDocuments,
|
||||
TriggerOpenLaunchDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerLoadWorkspaceLayout,
|
||||
TriggerOpen,
|
||||
TriggerImport,
|
||||
TriggerSavePreferences {
|
||||
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
|
||||
preferences: PreferencesMessageHandler,
|
||||
},
|
||||
TriggerSaveWorkspaceLayout {
|
||||
#[serde(rename = "workspaceLayout")]
|
||||
workspace_layout: WorkspacePanelLayout,
|
||||
},
|
||||
TriggerSaveActiveDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
},
|
||||
TriggerTextCommit,
|
||||
TriggerVisitLink {
|
||||
url: String,
|
||||
|
|
@ -291,7 +288,7 @@ pub enum FrontendMessage {
|
|||
},
|
||||
UpdateOpenDocumentsList {
|
||||
#[serde(rename = "openDocuments")]
|
||||
open_documents: Vec<OpenDocument>,
|
||||
open_documents: Vec<DocumentInfo>,
|
||||
},
|
||||
UpdateWirePathInProgress {
|
||||
#[serde(rename = "wirePath")]
|
||||
|
|
|
|||
|
|
@ -1,29 +1,12 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[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))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PersistedDocumentInfo {
|
||||
pub struct DocumentInfo {
|
||||
pub id: DocumentId,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
|
|
@ -34,8 +17,10 @@ pub struct PersistedDocumentInfo {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PersistedState {
|
||||
pub documents: Vec<PersistedDocumentInfo>,
|
||||
pub documents: Vec<DocumentInfo>,
|
||||
pub current_document: Option<DocumentId>,
|
||||
#[serde(default)]
|
||||
pub workspace_layout: Option<WorkspacePanelLayout>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::messages::portfolio::document::properties_panel::properties_panel_mes
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
|
||||
use crate::messages::portfolio::utility_types::{PanelType, PersistentData};
|
||||
use crate::messages::portfolio::utility_types::{CachedData, PanelType};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
|
||||
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
||||
|
|
@ -52,7 +52,7 @@ use std::time::Duration;
|
|||
pub struct DocumentMessageContext<'a> {
|
||||
pub document_id: DocumentId,
|
||||
pub ipp: &'a InputPreprocessorMessageHandler,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub cached_data: &'a CachedData,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub current_tool: &'a ToolType,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
|
|
@ -193,7 +193,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
let DocumentMessageContext {
|
||||
document_id,
|
||||
ipp,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
executor,
|
||||
viewport,
|
||||
current_tool,
|
||||
|
|
@ -231,7 +231,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
selection_network_path: &self.selection_network_path,
|
||||
document_name: self.name.as_str(),
|
||||
executor,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
properties_panel_open,
|
||||
};
|
||||
self.properties_panel_message_handler.process_message(message, responses, context);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
|
|||
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
|
||||
NumberInputSettings, Vec2InputSettings, WidgetOverride,
|
||||
};
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::portfolio::utility_types::CachedData;
|
||||
use crate::messages::prelude::Message;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use glam::DVec2;
|
||||
|
|
@ -29,7 +29,7 @@ use serde_json::Value;
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
pub struct NodePropertiesContext<'a> {
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub cached_data: &'a CachedData,
|
||||
pub responses: &'a mut VecDeque<Message>,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub network_interface: &'a mut NodeNetworkInterface,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use super::utility_types::FrontendGraphDataType;
|
|||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||
use crate::messages::portfolio::utility_types::{FontCatalogStyle, PersistentData};
|
||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
|
||||
use crate::messages::prelude::*;
|
||||
use choice::enum_choice;
|
||||
use dyn_any::DynAny;
|
||||
|
|
@ -793,7 +793,7 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p
|
|||
|
||||
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
||||
let ParameterWidgetsInfo {
|
||||
persistent_data,
|
||||
cached_data,
|
||||
document_node,
|
||||
node_id,
|
||||
index,
|
||||
|
|
@ -813,7 +813,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||
first_widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
DropdownInput::new(vec![
|
||||
persistent_data
|
||||
cached_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
|
|
@ -866,7 +866,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||
})
|
||||
.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)
|
||||
.widget_instance(),
|
||||
]);
|
||||
|
|
@ -876,7 +876,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||
second_row.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
DropdownInput::new({
|
||||
persistent_data
|
||||
cached_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
|
|
@ -914,7 +914,7 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||
.unwrap_or_default()
|
||||
})
|
||||
.selected_index(
|
||||
persistent_data
|
||||
cached_data
|
||||
.font_catalog
|
||||
.0
|
||||
.iter()
|
||||
|
|
@ -2208,7 +2208,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
|||
}
|
||||
|
||||
pub struct ParameterWidgetsInfo<'a> {
|
||||
persistent_data: &'a PersistentData,
|
||||
cached_data: &'a CachedData,
|
||||
network_interface: &'a NodeNetworkInterface,
|
||||
selection_network_path: &'a [NodeId],
|
||||
document_node: Option<&'a DocumentNode>,
|
||||
|
|
@ -2231,7 +2231,7 @@ impl<'a> ParameterWidgetsInfo<'a> {
|
|||
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
|
||||
|
||||
ParameterWidgetsInfo {
|
||||
persistent_data: context.persistent_data,
|
||||
cached_data: context.cached_data,
|
||||
network_interface: context.network_interface,
|
||||
selection_network_path: context.selection_network_path,
|
||||
document_node,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use graphene_std::uuid::NodeId;
|
|||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::portfolio::utility_types::CachedData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ pub struct PropertiesPanelMessageContext<'a> {
|
|||
pub selection_network_path: &'a [NodeId],
|
||||
pub document_name: &'a str,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub cached_data: &'a CachedData,
|
||||
pub properties_panel_open: bool,
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
|||
selection_network_path,
|
||||
document_name,
|
||||
executor,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
properties_panel_open,
|
||||
} = context;
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
|||
}
|
||||
|
||||
let mut node_properties_context = NodePropertiesContext {
|
||||
persistent_data,
|
||||
cached_data,
|
||||
responses,
|
||||
network_interface,
|
||||
selection_network_path,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ mod portfolio_message_handler;
|
|||
|
||||
pub mod document;
|
||||
pub mod document_migration;
|
||||
pub mod persistent_state;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
||||
#[doc(inline)]
|
||||
pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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;);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType, WorkspacePanelLayout};
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use super::persistent_state::PersistentStateMessage;
|
||||
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState};
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::utility_types::FontCatalog;
|
||||
use crate::messages::prelude::*;
|
||||
|
|
@ -15,6 +16,8 @@ pub enum PortfolioMessage {
|
|||
// Sub-messages
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
#[child]
|
||||
PersistentState(PersistentStateMessage),
|
||||
|
||||
// Messages
|
||||
Init,
|
||||
|
|
@ -61,8 +64,12 @@ pub enum PortfolioMessage {
|
|||
LoadDocumentResources {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
LoadWorkspaceLayout {
|
||||
layout: WorkspacePanelLayout,
|
||||
LoadPersistedState {
|
||||
state: PersistedState,
|
||||
},
|
||||
LoadDocumentContent {
|
||||
document_id: DocumentId,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
MoveAllPanelTabs {
|
||||
source_group: PanelGroupId,
|
||||
|
|
@ -93,15 +100,13 @@ pub enum PortfolioMessage {
|
|||
document_path: Option<PathBuf>,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
OpenDocumentFileWithId {
|
||||
LoadDocument {
|
||||
document_id: DocumentId,
|
||||
document_name: Option<String>,
|
||||
document_path: Option<PathBuf>,
|
||||
document_is_auto_saved: bool,
|
||||
document_is_saved: bool,
|
||||
document_serialized_content: String,
|
||||
to_front: bool,
|
||||
select_after_open: bool,
|
||||
},
|
||||
OpenImage {
|
||||
name: Option<String>,
|
||||
|
|
@ -186,7 +191,6 @@ pub enum PortfolioMessage {
|
|||
UpdateDocumentWidgets,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateWorkspacePanelLayout,
|
||||
SaveWorkspaceLayout,
|
||||
ResetWorkspaceLayout,
|
||||
ResetPanelGroupSizes {
|
||||
/// Path of child indices from the root to the split node whose children's sizes should be reset to defaults.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::utility_types::{PanelLayoutSubdivision, PanelType, PersistentData, WorkspacePanelLayout};
|
||||
use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
||||
use super::utility_types::{CachedData, PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
|
||||
use crate::application::{Editor, generate_uuid};
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||
use crate::messages::animation::TimingInformation;
|
||||
use crate::messages::clipboard::utility_types::ClipboardContent;
|
||||
use crate::messages::dialog::simple_dialogs;
|
||||
use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument};
|
||||
use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
|
|
@ -50,10 +51,12 @@ pub struct PortfolioMessageContext<'a> {
|
|||
#[derive(Debug, Default, ExtractField)]
|
||||
pub struct PortfolioMessageHandler {
|
||||
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
|
||||
unloaded_documents: HashMap<DocumentId, DocumentInfo>,
|
||||
document_ids: VecDeque<DocumentId>,
|
||||
pub(crate) active_document_id: Option<DocumentId>,
|
||||
persistent_state: PersistentStateMessageHandler,
|
||||
pub cached_data: CachedData,
|
||||
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||
pub persistent_data: PersistentData,
|
||||
pub executor: NodeGraphExecutor,
|
||||
pub selection_mode: SelectionMode,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
|
|
@ -82,7 +85,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
let document_inputs = DocumentMessageContext {
|
||||
document_id,
|
||||
ipp,
|
||||
persistent_data: &self.persistent_data,
|
||||
cached_data: &self.cached_data,
|
||||
executor: &mut self.executor,
|
||||
current_tool,
|
||||
preferences,
|
||||
|
|
@ -94,9 +97,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document.process_message(message, responses, document_inputs)
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PersistentState(message) => {
|
||||
let context = PersistentStateMessageContext {
|
||||
persisted_state: self.persisted_state_snapshot(),
|
||||
};
|
||||
self.persistent_state.process_message(message, responses, context);
|
||||
}
|
||||
|
||||
// Messages
|
||||
PortfolioMessage::Init => {
|
||||
responses.add(PersistentStateMessage::ReadState);
|
||||
|
||||
// Initialize the frontend with environment information
|
||||
responses.add(FrontendMessage::UpdatePlatform {
|
||||
platform: Editor::environment().into(),
|
||||
|
|
@ -104,14 +115,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
// Tell frontend to load persistent preferences
|
||||
responses.add(FrontendMessage::TriggerLoadPreferences);
|
||||
responses.add(FrontendMessage::TriggerLoadWorkspaceLayout);
|
||||
|
||||
// Before loading any documents, initially prepare the welcome screen buttons layout
|
||||
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
||||
|
||||
// Tell frontend to load persistent auto-saved documents (placed early so IndexedDB reads overlap with subsequent UI setup)
|
||||
responses.add(FrontendMessage::TriggerLoadAutoSaveDocuments);
|
||||
|
||||
// Tell frontend to load documents passed in as launch arguments
|
||||
responses.add(FrontendMessage::TriggerOpenLaunchDocuments);
|
||||
|
||||
|
|
@ -147,7 +154,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
let document_inputs = DocumentMessageContext {
|
||||
document_id,
|
||||
ipp,
|
||||
persistent_data: &self.persistent_data,
|
||||
cached_data: &self.cached_data,
|
||||
executor: &mut self.executor,
|
||||
current_tool,
|
||||
preferences,
|
||||
|
|
@ -168,25 +175,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
PortfolioMessage::AutoSaveAllDocuments => {
|
||||
for (document_id, document) in self.documents.iter_mut() {
|
||||
if !document.is_auto_saved() {
|
||||
for document_id in self.document_ids.iter() {
|
||||
if let Some(document) = self.documents.get_mut(document_id)
|
||||
&& !document.is_auto_saved()
|
||||
{
|
||||
document.set_auto_save_state(true);
|
||||
responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
PortfolioMessage::AutoSaveDocument { document_id } => {
|
||||
let document = self.documents.get(&document_id).unwrap();
|
||||
responses.add(FrontendMessage::TriggerPersistenceWriteDocument {
|
||||
let Some(document) = self.document(document_id) else { return };
|
||||
responses.add(PersistentStateMessage::WriteDocument {
|
||||
document_id,
|
||||
document: document.serialize_document(),
|
||||
details: DocumentDetails {
|
||||
name: document.name.clone(),
|
||||
path: document.path.clone(),
|
||||
is_saved: document.is_saved(),
|
||||
is_auto_saved: document.is_auto_saved(),
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
PortfolioMessage::CloseActiveDocumentWithConfirmation => {
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
|
|
@ -206,7 +209,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
for document_id in &self.document_ids {
|
||||
responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id: *document_id });
|
||||
responses.add(PersistentStateMessage::DeleteDocument { document_id: *document_id });
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::DestroyAllDocuments);
|
||||
|
|
@ -221,7 +224,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
PortfolioMessage::CloseDocument { document_id } => {
|
||||
// Is this the last document?
|
||||
if self.documents.len() == 1 && self.document_ids[0] == document_id {
|
||||
if self.document_ids.len() == 1 && self.document_ids[0] == document_id {
|
||||
// Clear UI layouts that assume the existence of a document
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayersPanel);
|
||||
|
|
@ -231,13 +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)
|
||||
responses.add(PortfolioMessage::DeleteDocument { document_id });
|
||||
responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id });
|
||||
responses.add(PersistentStateMessage::DeleteDocument { document_id });
|
||||
|
||||
// Send the new list of document tab names
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
}
|
||||
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
|
||||
let target_document = self.documents.get(&document_id).unwrap();
|
||||
let Some(target_document) = self.document(document_id) else {
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(PortfolioMessage::CloseDocument { document_id });
|
||||
return;
|
||||
};
|
||||
if target_document.is_saved() {
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(PortfolioMessage::CloseDocument { document_id });
|
||||
|
|
@ -335,6 +342,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
PortfolioMessage::DeleteDocument { document_id } => {
|
||||
let document_index = self.document_index(document_id);
|
||||
self.documents.remove(&document_id);
|
||||
self.unloaded_documents.remove(&document_id);
|
||||
self.document_ids.remove(document_index);
|
||||
|
||||
if self.document_ids.is_empty() {
|
||||
|
|
@ -354,12 +362,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
PortfolioMessage::DestroyAllDocuments => {
|
||||
// Empty the list of internal document data
|
||||
self.documents.clear();
|
||||
self.unloaded_documents.clear();
|
||||
self.document_ids.clear();
|
||||
self.active_document_id = None;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PersistentStateMessage::WriteState);
|
||||
}
|
||||
PortfolioMessage::FontCatalogLoaded { catalog } => {
|
||||
self.persistent_data.font_catalog = catalog;
|
||||
self.cached_data.font_catalog = catalog;
|
||||
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
|
||||
|
|
@ -370,23 +380,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
responses.add(PortfolioMessage::LoadFontData { font });
|
||||
}
|
||||
PortfolioMessage::LoadFontData { font } => {
|
||||
if let Some(style) = self.persistent_data.font_catalog.find_font_style_in_catalog(&font) {
|
||||
if let Some(style) = self.cached_data.font_catalog.find_font_style_in_catalog(&font) {
|
||||
let font = Font::new(font.font_family, style.to_named_style());
|
||||
|
||||
if !self.persistent_data.font_cache.loaded_font(&font) {
|
||||
if !self.cached_data.font_cache.loaded_font(&font) {
|
||||
responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url });
|
||||
}
|
||||
}
|
||||
}
|
||||
PortfolioMessage::FontLoaded { font_family, font_style, data } => {
|
||||
let font = Font::new(font_family, font_style);
|
||||
self.persistent_data.font_cache.insert(font, data);
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
self.cached_data.font_cache.insert(font, data);
|
||||
self.executor.update_font_cache(self.cached_data.font_cache.clone());
|
||||
|
||||
for document_id in self.document_ids.iter() {
|
||||
let node_to_inspect = self.node_to_inspect();
|
||||
|
||||
let Some(document) = self.documents.get_mut(document_id) else {
|
||||
if self.unloaded_documents.contains_key(document_id) {
|
||||
continue;
|
||||
}
|
||||
log::error!("Tried to render non-existent document");
|
||||
continue;
|
||||
};
|
||||
|
|
@ -407,16 +420,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview,
|
||||
});
|
||||
|
||||
if let Ok(message) = self.executor.submit_node_graph_evaluation(
|
||||
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
||||
*document_id,
|
||||
physical_resolution,
|
||||
scale,
|
||||
timing_information,
|
||||
node_to_inspect,
|
||||
true,
|
||||
pointer_position,
|
||||
) {
|
||||
if let Ok(message) = self
|
||||
.executor
|
||||
.submit_node_graph_evaluation(document, *document_id, physical_resolution, scale, timing_information, node_to_inspect, true, pointer_position)
|
||||
{
|
||||
responses.add_front(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -431,7 +438,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||
PortfolioMessage::LoadDocumentResources { document_id } => {
|
||||
let catalog = &self.persistent_data.font_catalog;
|
||||
let catalog = &self.cached_data.font_catalog;
|
||||
|
||||
if catalog.0.is_empty() {
|
||||
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
|
||||
|
|
@ -442,16 +449,52 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document.load_layer_resources(responses);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::LoadWorkspaceLayout { layout } => {
|
||||
PortfolioMessage::LoadPersistedState { state } => {
|
||||
if let Some(layout) = state.workspace_layout {
|
||||
self.workspace_panel_layout = layout;
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
}
|
||||
|
||||
// Refresh all visible panels since the layout may have changed
|
||||
for group_id in self.workspace_panel_layout.root.all_group_ids() {
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group_id).and_then(|g| g.active_panel_type()) {
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
let PersistedState {
|
||||
documents,
|
||||
current_document,
|
||||
workspace_layout: _,
|
||||
} = state;
|
||||
|
||||
for info in documents {
|
||||
if !self.document_ids.contains(&info.id) {
|
||||
self.document_ids.push_back(info.id);
|
||||
}
|
||||
if !self.documents.contains_key(&info.id) {
|
||||
self.unloaded_documents.insert(info.id, info);
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
|
||||
let select_document_id = current_document.filter(|id| self.document_ids.contains(id)).or_else(|| self.document_ids.front().copied());
|
||||
if let Some(document_id) = select_document_id {
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
}
|
||||
}
|
||||
PortfolioMessage::LoadDocumentContent {
|
||||
document_id,
|
||||
document_serialized_content,
|
||||
} => {
|
||||
let Some(info) = self.unloaded_documents.remove(&document_id) else {
|
||||
log::error!("Tried to load content for non existent document");
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(PortfolioMessage::LoadDocument {
|
||||
document_id,
|
||||
document_name: Some(info.name),
|
||||
document_path: info.path,
|
||||
document_is_auto_saved: true,
|
||||
document_is_saved: info.is_saved,
|
||||
document_serialized_content,
|
||||
});
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
}
|
||||
PortfolioMessage::NewDocumentWithName { name } => {
|
||||
let mut new_document = DocumentMessageHandler::default();
|
||||
|
|
@ -465,7 +508,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
|
||||
}
|
||||
|
||||
self.load_document(new_document, document_id, responses, false);
|
||||
self.load_document(new_document, document_id, responses);
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
}
|
||||
PortfolioMessage::MoveAllPanelTabs {
|
||||
|
|
@ -515,7 +558,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the new active tab
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(target_group).and_then(|g| g.active_panel_type()) {
|
||||
|
|
@ -565,7 +607,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the moved panel's content in its new location
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
|
|
@ -661,30 +702,28 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document_path,
|
||||
document_serialized_content,
|
||||
} => {
|
||||
responses.add(PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id: DocumentId(generate_uuid()),
|
||||
let document_id = DocumentId(generate_uuid());
|
||||
responses.add(PortfolioMessage::LoadDocument {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved: false,
|
||||
document_is_saved: true,
|
||||
document_serialized_content,
|
||||
to_front: false,
|
||||
select_after_open: true,
|
||||
});
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
}
|
||||
PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen => {
|
||||
self.reset_node_definitions_on_open = !self.reset_node_definitions_on_open;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
PortfolioMessage::OpenDocumentFileWithId {
|
||||
PortfolioMessage::LoadDocument {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
to_front,
|
||||
select_after_open,
|
||||
} => {
|
||||
// Upgrade the document being opened to use fresh copies of all nodes
|
||||
let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content);
|
||||
|
|
@ -777,11 +816,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, responses, to_front);
|
||||
|
||||
if select_after_open {
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
}
|
||||
self.load_document(document, document_id, responses);
|
||||
}
|
||||
PortfolioMessage::OpenImage { name, image } => {
|
||||
responses.add(PortfolioMessage::NewDocumentWithName {
|
||||
|
|
@ -1126,7 +1161,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
mouse,
|
||||
parent_and_insert_index,
|
||||
} => {
|
||||
if self.documents.is_empty() {
|
||||
if self.document_ids.is_empty() {
|
||||
responses.add(PortfolioMessage::OpenImage { name, image });
|
||||
} else {
|
||||
responses.add(DocumentMessage::PasteImage {
|
||||
|
|
@ -1144,7 +1179,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
mouse,
|
||||
parent_and_insert_index,
|
||||
} => {
|
||||
if self.documents.is_empty() {
|
||||
if self.document_ids.is_empty() {
|
||||
responses.add(PortfolioMessage::OpenSvg { name, svg });
|
||||
} else {
|
||||
responses.add(DocumentMessage::PasteSvg {
|
||||
|
|
@ -1200,7 +1235,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
}
|
||||
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
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group).and_then(|g| g.active_panel_type()) {
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
|
|
@ -1315,7 +1348,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the new panel group's active tab
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(new_id).and_then(|g| g.active_panel_type()) {
|
||||
|
|
@ -1340,13 +1372,32 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
node_graph_open = document.is_graph_overlay_open();
|
||||
}
|
||||
|
||||
if self.unloaded_documents.contains_key(&document_id) {
|
||||
let already_selected = self.active_document_id == Some(document_id);
|
||||
|
||||
self.active_document_id = Some(document_id);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(FrontendMessage::UpdateActiveDocument { document_id });
|
||||
|
||||
if !already_selected {
|
||||
responses.add(PersistentStateMessage::ReadDocument { document_id });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.documents.contains_key(&document_id) {
|
||||
warn!("Tried to read non existent document");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new active document ID
|
||||
self.active_document_id = Some(document_id);
|
||||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(FrontendMessage::UpdateActiveDocument { document_id });
|
||||
responses.add(FrontendMessage::TriggerSaveActiveDocument { document_id });
|
||||
responses.add(ToolMessage::InitTools);
|
||||
responses.add(NodeGraphMessage::Init);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
|
@ -1382,7 +1433,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
artboard_name,
|
||||
artboard_count,
|
||||
} => {
|
||||
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
|
||||
let document_id = self.active_document_id.expect("Tried to render non-existent document");
|
||||
let document = self.documents.get_mut(&document_id).expect("Tried to render non-existent document");
|
||||
let export_config = ExportConfig {
|
||||
name,
|
||||
file_type,
|
||||
|
|
@ -1392,7 +1444,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
artboard_count,
|
||||
..Default::default()
|
||||
};
|
||||
let result = self.executor.submit_document_export(document, self.active_document_id.unwrap(), export_config);
|
||||
let result = self.executor.submit_document_export(document, document_id, export_config);
|
||||
|
||||
if let Err(description) = result {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
|
|
@ -1491,7 +1543,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
PortfolioMessage::TogglePropertiesPanelOpen => {
|
||||
if self.workspace_panel_layout.focus_document {
|
||||
|
|
@ -1536,11 +1587,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
false => self.workspace_panel_layout.clone(),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateWorkspacePanelLayout { panel_layout });
|
||||
}
|
||||
PortfolioMessage::SaveWorkspaceLayout => {
|
||||
responses.add(FrontendMessage::TriggerSaveWorkspaceLayout {
|
||||
workspace_layout: self.workspace_panel_layout.clone(),
|
||||
});
|
||||
responses.add(PersistentStateMessage::WriteState);
|
||||
}
|
||||
PortfolioMessage::ResetWorkspaceLayout => {
|
||||
// Destroy layouts for all currently visible non-document panels
|
||||
|
|
@ -1562,7 +1609,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
PortfolioMessage::ResetPanelGroupSizes { split_path } => {
|
||||
|
|
@ -1578,7 +1624,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
node.recalculate_default_sizes();
|
||||
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
PortfolioMessage::SetPanelGroupSizes { split_path, sizes } => {
|
||||
// Walk the tree to the target split node using the path
|
||||
|
|
@ -1596,29 +1641,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
}
|
||||
PortfolioMessage::UpdateOpenDocumentsList => {
|
||||
// Send the list of document tab names
|
||||
let open_documents = self
|
||||
.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 open_documents = self.document_ids.iter().filter_map(|id| self.document_details(*id)).collect::<Vec<_>>();
|
||||
|
||||
let no_open_documents = open_documents.is_empty();
|
||||
|
||||
responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents });
|
||||
responses.add(PersistentStateMessage::WriteState);
|
||||
|
||||
if no_open_documents {
|
||||
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
||||
|
|
@ -1682,11 +1714,11 @@ impl PortfolioMessageHandler {
|
|||
}
|
||||
|
||||
pub fn active_document(&self) -> Option<&DocumentMessageHandler> {
|
||||
self.active_document_id.and_then(|id| self.documents.get(&id))
|
||||
self.active_document_id.and_then(|id| self.document(id))
|
||||
}
|
||||
|
||||
pub fn active_document_mut(&mut self) -> Option<&mut DocumentMessageHandler> {
|
||||
self.active_document_id.and_then(|id| self.documents.get_mut(&id))
|
||||
self.active_document_id.and_then(|id| self.document_mut(id))
|
||||
}
|
||||
|
||||
pub fn active_document_id(&self) -> Option<DocumentId> {
|
||||
|
|
@ -1694,12 +1726,29 @@ impl PortfolioMessageHandler {
|
|||
}
|
||||
|
||||
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 {
|
||||
let mut doc_title_numbers = self
|
||||
.ordered_document_iterator()
|
||||
.document_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.document_details(*id))
|
||||
.filter_map(|doc| {
|
||||
doc.name
|
||||
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
||||
|
|
@ -1744,12 +1793,12 @@ impl PortfolioMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
|
||||
if to_front {
|
||||
self.document_ids.push_front(document_id);
|
||||
} else {
|
||||
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>) {
|
||||
let is_new_document = !self.document_ids.contains(&document_id);
|
||||
if is_new_document {
|
||||
self.document_ids.push_back(document_id);
|
||||
}
|
||||
self.unloaded_documents.remove(&document_id);
|
||||
new_document.update_layers_panel_control_bar_widgets(
|
||||
self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
|
||||
responses,
|
||||
|
|
@ -1768,12 +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
|
||||
responses.add(DocumentMessage::GraphViewOverlay { open: false });
|
||||
if is_new_document {
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the open documents in order.
|
||||
pub fn ordered_document_iterator(&self) -> impl Iterator<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 {
|
||||
|
|
@ -1781,7 +1832,10 @@ impl PortfolioMessageHandler {
|
|||
}
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
|
|
@ -1801,6 +1855,19 @@ impl PortfolioMessageHandler {
|
|||
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.
|
||||
pub fn node_to_inspect(&self) -> Option<NodeId> {
|
||||
// Skip if the Data panel is not open
|
||||
|
|
@ -1808,7 +1875,7 @@ impl PortfolioMessageHandler {
|
|||
return None;
|
||||
}
|
||||
|
||||
let document = self.documents.get(&self.active_document_id?)?;
|
||||
let document = self.document(self.active_document_id?)?;
|
||||
let selected_nodes = document.network_interface.selected_nodes().0;
|
||||
|
||||
// Skip if there is not exactly one selected node
|
||||
|
|
@ -1854,7 +1921,6 @@ impl PortfolioMessageHandler {
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
|
||||
/// Destroy the stored layout for a panel that is no longer the active tab.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use graphene_std::raster::Image;
|
|||
use graphene_std::text::{Font, FontCache};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PersistentData {
|
||||
pub struct CachedData {
|
||||
pub font_cache: FontCache,
|
||||
pub font_catalog: FontCatalog,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
|
||||
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::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
|
||||
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_
|
|||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::portfolio::utility_types::CachedData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
||||
use crate::messages::tool::utility_types::{HintData, ToolType};
|
||||
|
|
@ -18,7 +18,7 @@ pub struct ToolMessageContext<'a> {
|
|||
pub document_id: DocumentId,
|
||||
pub document: &'a mut DocumentMessageHandler,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub cached_data: &'a CachedData,
|
||||
pub node_graph: &'a NodeGraphExecutor,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
pub viewport: &'a ViewportMessageHandler,
|
||||
|
|
@ -39,7 +39,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
document_id,
|
||||
document,
|
||||
input,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
|
|
@ -126,7 +126,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
};
|
||||
|
||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||
|
|
@ -217,7 +217,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
||||
|
||||
// Register initial properties
|
||||
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, persistent_data);
|
||||
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, cached_data);
|
||||
|
||||
// Notify the frontend about the initial active tool
|
||||
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
|
||||
|
|
@ -234,7 +234,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
};
|
||||
|
||||
// Set initial hints and cursor
|
||||
|
|
@ -258,7 +258,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
ToolMessage::RefreshToolOptions => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, persistent_data);
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, cached_data);
|
||||
}
|
||||
ToolMessage::RefreshToolShelf => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
|
|
@ -346,7 +346,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||
node_graph,
|
||||
preferences,
|
||||
viewport,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
};
|
||||
if matches!(tool_message, ToolMessage::UpdateHints) {
|
||||
if graph_view_overlay_open {
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ impl Fsm for SelectToolFsmState {
|
|||
document,
|
||||
input,
|
||||
viewport,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
..
|
||||
} = tool_action_data;
|
||||
|
||||
|
|
@ -632,7 +632,7 @@ impl Fsm for SelectToolFsmState {
|
|||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
||||
|
||||
if is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) {
|
||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &persistent_data.font_cache);
|
||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache);
|
||||
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::portfolio::utility_types::{FontCatalog, FontCatalogStyle, PersistentData};
|
||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
|
|
@ -230,8 +230,8 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
|
|||
}
|
||||
|
||||
impl ToolRefreshOptions for TextTool {
|
||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions, &persistent_data.font_catalog);
|
||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, cached_data: &CachedData) {
|
||||
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 {
|
||||
|
|
@ -573,11 +573,11 @@ impl Fsm for TextToolFsmState {
|
|||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
persistent_data,
|
||||
cached_data,
|
||||
viewport,
|
||||
..
|
||||
} = transition_data;
|
||||
let font_cache = &persistent_data.font_cache;
|
||||
let font_cache = &cached_data.font_cache;
|
||||
let fill_color = COLOR_OVERLAY_BLUE_05;
|
||||
|
||||
let ToolMessage::Text(event) = event else { return self };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
|||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::portfolio::utility_types::CachedData;
|
||||
use crate::messages::preferences::PreferencesMessageHandler;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
||||
|
|
@ -24,7 +24,7 @@ pub struct ToolActionMessageContext<'a> {
|
|||
pub document_id: DocumentId,
|
||||
pub global_tool_data: &'a DocumentToolData,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub persistent_data: &'a PersistentData,
|
||||
pub cached_data: &'a CachedData,
|
||||
pub shape_editor: &'a mut ShapeState,
|
||||
pub node_graph: &'a NodeGraphExecutor,
|
||||
pub preferences: &'a PreferencesMessageHandler,
|
||||
|
|
@ -37,11 +37,11 @@ impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mu
|
|||
type Tool = dyn ToolCommon + Send + Sync;
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ impl EditorTestUtils {
|
|||
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 document_id = portfolio.active_document_id.unwrap();
|
||||
let exector = &mut portfolio.executor;
|
||||
let document = portfolio.documents.get_mut(&document_id).unwrap();
|
||||
let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents);
|
||||
let document = documents.get_mut(&document_id).unwrap();
|
||||
|
||||
let instrumented = match exector.update_node_graph_instrumented(document) {
|
||||
let instrumented = match executor.update_node_graph_instrumented(document) {
|
||||
Ok(instrumented) => instrumented,
|
||||
Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")),
|
||||
};
|
||||
|
||||
if let Err(e) = exector.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) {
|
||||
if let Err(e) = executor.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) {
|
||||
return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}"));
|
||||
}
|
||||
runtime.run().await;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||
import Panel from "/src/components/window/Panel.svelte";
|
||||
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 DOUBLE_CLICK_MILLISECONDS = 500;
|
||||
|
|
@ -31,9 +31,9 @@
|
|||
$: if (subdivision) sizeOverrides = {};
|
||||
// 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) : [];
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
|
||||
const name = doc.details.name;
|
||||
const unsaved = !doc.details.is_saved;
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: DocumentInfo) => {
|
||||
const name = doc.name;
|
||||
const unsaved = !doc.is_saved;
|
||||
if (!editor.inDevelopmentMode()) return { name, unsaved };
|
||||
|
||||
const tooltipDescription = `Document ID: ${doc.id}`;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
|||
import {
|
||||
saveEditorPreferences,
|
||||
loadEditorPreferences,
|
||||
saveWorkspaceLayout,
|
||||
loadWorkspaceLayout,
|
||||
storeDocument,
|
||||
removeDocument,
|
||||
loadDocuments,
|
||||
saveActiveDocument,
|
||||
writePersistedState,
|
||||
readPersistedState,
|
||||
writePersistedDocument,
|
||||
readPersistedDocument,
|
||||
deletePersistedDocument,
|
||||
} from "/src/utility-functions/persistence";
|
||||
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
|
|
@ -31,33 +30,29 @@ export function createPersistenceManager(subscriptions: SubscriptionsRouter, edi
|
|||
await loadEditorPreferences(editor);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerSaveWorkspaceLayout", async (data) => {
|
||||
await saveWorkspaceLayout(data.workspaceLayout);
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteState", async (data) => {
|
||||
await writePersistedState(data.state);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerLoadWorkspaceLayout", async () => {
|
||||
await loadWorkspaceLayout(editor);
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceReadState", async () => {
|
||||
await readPersistedState(editor);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
|
||||
await storeDocument(data, portfolio);
|
||||
await writePersistedDocument(data);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => {
|
||||
await removeDocument(String(data.documentId), portfolio);
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceReadDocument", async (data) => {
|
||||
await readPersistedDocument(data.documentId, editor);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerLoadAutoSaveDocuments", async () => {
|
||||
await loadDocuments(editor);
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceDeleteDocument", async (data) => {
|
||||
await deletePersistedDocument(String(data.documentId));
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => {
|
||||
// TODO: Could be used to load documents from URL params or similar on launch
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => {
|
||||
await saveActiveDocument(data.documentId);
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyPersistenceManager() {
|
||||
|
|
@ -66,13 +61,12 @@ export function destroyPersistenceManager() {
|
|||
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerSaveWorkspaceLayout");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadWorkspaceLayout");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteState");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadState");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadAutoSaveDocuments");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceReadDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceDeleteDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerSaveActiveDocument");
|
||||
}
|
||||
|
||||
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ import { writable } from "svelte/store";
|
|||
import type { Writable } from "svelte/store";
|
||||
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
||||
import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files";
|
||||
import { storeDocumentTabOrder } from "/src/utility-functions/persistence";
|
||||
import { rasterizeSVG } from "/src/utility-functions/rasterization";
|
||||
import type { EditorWrapper, OpenDocument, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
import type { EditorWrapper, DocumentInfo, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
export type PortfolioStore = ReturnType<typeof createPortfolioStore>;
|
||||
|
||||
type PortfolioStoreState = {
|
||||
unsaved: boolean;
|
||||
documents: OpenDocument[];
|
||||
documents: DocumentInfo[];
|
||||
activeDocumentIndex: number;
|
||||
panelLayout: WorkspacePanelLayout;
|
||||
};
|
||||
|
|
@ -38,7 +37,6 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
|
|||
state.documents = data.openDocuments;
|
||||
return state;
|
||||
});
|
||||
storeDocumentTabOrder({ subscribe });
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ export function onModifyInputField(e: CustomEvent) {
|
|||
|
||||
export async function onBeforeUnload(e: BeforeUnloadEvent, editor: EditorWrapper, portfolioStore: PortfolioStore) {
|
||||
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
|
||||
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
|
||||
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) {
|
||||
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -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 { 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_STORE = "store";
|
||||
|
||||
function emptyPersistedState(): PersistedState {
|
||||
// eslint-disable-next-line camelcase
|
||||
return { documents: [], current_document: undefined };
|
||||
return { documents: [], current_document: undefined, workspace_layout: undefined };
|
||||
}
|
||||
|
||||
function createDocumentInfo(id: bigint, name: string, isSaved: boolean): PersistedDocumentInfo {
|
||||
function createDocumentInfo(id: bigint, name: string, isSaved: boolean): DocumentInfo {
|
||||
// eslint-disable-next-line camelcase
|
||||
return { id, name, is_saved: isSaved };
|
||||
}
|
||||
|
||||
// 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 reordered: PersistedDocumentInfo[] = [];
|
||||
const reordered: DocumentInfo[] = [];
|
||||
|
||||
orderedIds.forEach((id) => {
|
||||
const existing = byId.get(id);
|
||||
|
|
@ -39,18 +37,8 @@ function reorderDocuments(documents: PersistedDocumentInfo[], orderedIds: bigint
|
|||
// State-based persistence (new format)
|
||||
// ====================================
|
||||
|
||||
export async function storeDocumentTabOrder(portfolio: PortfolioStore) {
|
||||
const portfolioData = get(portfolio);
|
||||
const orderedIds = portfolioData.documents.map((doc) => doc.id);
|
||||
|
||||
await databaseUpdate<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;
|
||||
export async function writePersistedDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">) {
|
||||
const { documentId, document } = autoSaveDocument;
|
||||
|
||||
// Update content in the documents store
|
||||
await databaseUpdate<Record<string, string>>("documents", (old) => {
|
||||
|
|
@ -58,91 +46,44 @@ export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersis
|
|||
documents[String(documentId)] = document;
|
||||
return documents;
|
||||
});
|
||||
|
||||
// Update metadata and ordering in the state store
|
||||
const portfolioData = get(portfolio);
|
||||
const orderedIds = portfolioData.documents.map((doc) => doc.id);
|
||||
|
||||
await databaseUpdate<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
|
||||
state.current_document = documentId;
|
||||
state.documents = reorderDocuments(state.documents, orderedIds);
|
||||
return state;
|
||||
});
|
||||
export async function readPersistedDocument(documentId: bigint, editor: EditorWrapper) {
|
||||
const documentContents = await databaseGet<Record<string, string>>("documents");
|
||||
if (!documentContents) return;
|
||||
|
||||
const content = documentContents[String(documentId)];
|
||||
if (content === undefined) return;
|
||||
|
||||
editor.loadDocumentContent(documentId, content);
|
||||
}
|
||||
|
||||
export async function removeDocument(id: string, portfolio: PortfolioStore) {
|
||||
const documentId = BigInt(id);
|
||||
|
||||
export async function deletePersistedDocument(id: string) {
|
||||
// Remove content from the documents store
|
||||
await databaseUpdate<Record<string, string>>("documents", (old) => {
|
||||
const documents = old || {};
|
||||
delete documents[id];
|
||||
return documents;
|
||||
});
|
||||
|
||||
// Update state: remove the entry and update current_document
|
||||
const portfolioData = get(portfolio);
|
||||
const documentCount = portfolioData.documents.length;
|
||||
|
||||
await databaseUpdate<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 garbageCollectDocuments();
|
||||
|
||||
const state = await databaseGet<PersistedState>("state");
|
||||
const documentContents = await databaseGet<Record<string, string>>("documents");
|
||||
if (!state || !documentContents || state.documents.length === 0) return;
|
||||
|
||||
// Find the current document (or fall back to the last document in the list)
|
||||
const currentId = state.current_document;
|
||||
const currentEntry = currentId !== undefined ? state.documents.find((doc) => doc.id === currentId) : undefined;
|
||||
const current = currentEntry || state.documents[state.documents.length - 1];
|
||||
|
||||
// Open all documents in persisted tab order, then select the current one
|
||||
state.documents.forEach((entry) => {
|
||||
const content = documentContents[String(entry.id)];
|
||||
if (content === undefined) return;
|
||||
|
||||
editor.openAutoSavedDocument(entry.id, entry.name, entry.is_saved, content, false);
|
||||
});
|
||||
|
||||
editor.selectDocument(current.id);
|
||||
}
|
||||
|
||||
export async function saveActiveDocument(documentId: bigint) {
|
||||
await databaseUpdate<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;
|
||||
});
|
||||
if (!state) return;
|
||||
editor.loadPersistedState(state);
|
||||
}
|
||||
|
||||
export async function saveEditorPreferences(preferences: unknown) {
|
||||
|
|
@ -154,15 +95,6 @@ export async function loadEditorPreferences(editor: EditorWrapper) {
|
|||
editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined);
|
||||
}
|
||||
|
||||
export async function saveWorkspaceLayout(layout: unknown) {
|
||||
await databaseSet("workspace_layout", layout);
|
||||
}
|
||||
|
||||
export async function loadWorkspaceLayout(editor: EditorWrapper) {
|
||||
const layout = await databaseGet<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"
|
||||
async function garbageCollectDocuments() {
|
||||
const state = await databaseGet<PersistedState>("state");
|
||||
|
|
@ -197,6 +129,7 @@ export async function wipeDocuments() {
|
|||
async function wipeOldFormat() {
|
||||
await databaseDelete("documents_tab_order");
|
||||
await databaseDelete("current_document_id");
|
||||
await databaseDelete("workspace_layout");
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
|
|
@ -209,7 +142,7 @@ async function migrateToNewFormat() {
|
|||
|
||||
// Build the new "state" and "documents" from the old format
|
||||
const newDocumentContents: Record<string, string> = {};
|
||||
const newDocumentInfos: PersistedDocumentInfo[] = [];
|
||||
const newDocumentInfos: DocumentInfo[] = [];
|
||||
|
||||
if (oldDocuments) {
|
||||
Object.values(oldDocuments).forEach((value) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
"verbatimModuleSyntax": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"baseUrl": ".",
|
||||
"paths": { "/*": ["./*"] },
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -379,13 +379,23 @@ impl EditorWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadWorkspaceLayout)]
|
||||
pub fn load_workspace_layout(&self, layout: JsValue) {
|
||||
let Ok(layout) = serde_wasm_bindgen::from_value(layout) else {
|
||||
log::error!("Failed to deserialize workspace layout");
|
||||
#[wasm_bindgen(js_name = loadPersistedState)]
|
||||
pub fn load_persisted_state(&self, state: JsValue) {
|
||||
let Ok(state) = serde_wasm_bindgen::from_value(state) else {
|
||||
log::error!("Failed to deserialize persisted state");
|
||||
return;
|
||||
};
|
||||
let message = PortfolioMessage::LoadWorkspaceLayout { layout };
|
||||
|
||||
let message = 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);
|
||||
}
|
||||
|
||||
|
|
@ -414,22 +424,6 @@ impl EditorWrapper {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = openAutoSavedDocument)]
|
||||
pub fn open_auto_saved_document(&self, document_id: u64, document_name: String, document_is_saved: bool, document_serialized_content: String, to_front: bool) {
|
||||
let document_id = DocumentId(document_id);
|
||||
let message = PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name: Some(document_name),
|
||||
document_path: None,
|
||||
document_is_auto_saved: true,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
to_front,
|
||||
select_after_open: false,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = triggerAutoSave)]
|
||||
pub fn trigger_auto_save(&self, document_id: u64) {
|
||||
let document_id = DocumentId(document_id);
|
||||
|
|
|
|||
Loading…
Reference in New Issue