From bfb0b4bb8ccf9d91607c34d31cc86053b4edc9d0 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 00:31:23 +0000 Subject: [PATCH 01/31] Make file name and document name identical --- desktop/src/app.rs | 3 +- editor/src/consts.rs | 1 + .../document/document_message_handler.rs | 38 ++++++++++++------- .../messages/portfolio/portfolio_message.rs | 9 +++-- .../portfolio/portfolio_message_handler.rs | 34 ++++++++++++++--- frontend/wasm/src/editor_api.rs | 6 ++- 6 files changed, 66 insertions(+), 25 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0ec9eaa81e..05fd1db043 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -75,7 +75,8 @@ impl WinitApp { String::new() }); let message = PortfolioMessage::OpenDocumentFile { - document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(), + document_name: None, + document_path: Some(path), document_serialized_content: content, }; let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into())); diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 44a5b1d210..48bdea853f 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -151,6 +151,7 @@ pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; // DOCUMENT pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; +pub const FILE_EXTENSION: &str = "graphite"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5bfa0af7dd..7107e28ae9 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; -use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -76,8 +76,6 @@ pub struct DocumentMessageHandler { /// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel. /// Collapsed means that the expansion arrow isn't set to show the children of these layers. pub collapsed: CollapsedLayers, - /// The name of the document, which is displayed in the tab and title bar of the editor. - pub name: String, /// The full Git commit hash of the Graphite repository that was used to build the editor. /// We save this to provide a hint about which version of the editor was used to create the document. pub commit_hash: String, @@ -104,6 +102,12 @@ pub struct DocumentMessageHandler { // Fields omitted from the saved document format // ============================================= // + /// The name of the document, which is displayed in the tab and title bar of the editor. + #[serde(skip)] + pub name: String, + /// The path of the to the document file. + #[serde(skip)] + pub(crate) path: Option, /// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks #[serde(skip)] breadcrumb_network_path: Vec, @@ -116,9 +120,6 @@ pub struct DocumentMessageHandler { /// Stack of document network snapshots for future history states. #[serde(skip)] document_redo_history: VecDeque, - /// The path of the to the document file. - #[serde(skip)] - path: Option, /// Hash of the document snapshot that was most recently saved to disk by the user. #[serde(skip)] saved_hash: Option, @@ -149,7 +150,6 @@ impl Default for DocumentMessageHandler { // ============================================ network_interface: default_document_network_interface(), collapsed: CollapsedLayers::default(), - name: DEFAULT_DOCUMENT_NAME.to_string(), commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(), document_ptz: PTZ::default(), document_mode: DocumentMode::DesignMode, @@ -162,11 +162,12 @@ impl Default for DocumentMessageHandler { // ============================================= // Fields omitted from the saved document format // ============================================= + name: DEFAULT_DOCUMENT_NAME.to_string(), + path: None, breadcrumb_network_path: Vec::new(), selection_network_path: Vec::new(), document_undo_history: VecDeque::new(), document_redo_history: VecDeque::new(), - path: None, saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, @@ -918,7 +919,20 @@ impl MessageHandler> for DocumentMes responses.add(OverlaysMessage::Draw); } DocumentMessage::RenameDocument { new_name } => { - self.name = new_name; + self.name = new_name.clone(); + + // If the new document name does not match the current path, clear the path. + if let Some(path) = self.path.as_ref() { + let document_name_from_path = if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + }; + if Some(new_name) != document_name_from_path { + self.path = None; + } + } + responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::UpdateNewNodeGraph); } @@ -997,13 +1011,9 @@ impl MessageHandler> for DocumentMes // Update the save status of the just saved document responses.add(PortfolioMessage::UpdateOpenDocumentsList); - let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { - true => self.name.clone(), - false => self.name.clone() + FILE_SAVE_SUFFIX, - }; responses.add(FrontendMessage::TriggerSaveDocument { document_id, - name, + name: self.name.clone() + FILE_SAVE_SUFFIX, path: self.path.clone(), content: self.serialize_document().into_bytes(), }) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 153ad6c039..58be6ab13e 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -6,6 +6,7 @@ use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::raster::Image; use graphene_std::text::Font; +use std::path::PathBuf; #[impl_message(Message, Portfolio)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -68,18 +69,20 @@ pub enum PortfolioMessage { NextDocument, OpenDocument, OpenDocumentFile { - document_name: String, + document_name: Option, + document_path: Option, document_serialized_content: String, }, - ToggleResetNodesToDefinitionsOnOpen, OpenDocumentFileWithId { document_id: DocumentId, - document_name: String, + document_name: Option, + document_path: Option, document_is_auto_saved: bool, document_is_saved: bool, document_serialized_content: String, to_front: bool, }, + ToggleResetNodesToDefinitionsOnOpen, PasteIntoFolder { clipboard: Clipboard, parent: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54ed..0097ad61fb 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -3,7 +3,7 @@ use super::document::utility_types::network_interface; use super::spreadsheet::SpreadsheetMessageHandler; use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; -use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH}; +use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::dialog::simple_dialogs; @@ -408,12 +408,14 @@ impl MessageHandler> for Portfolio } PortfolioMessage::OpenDocumentFile { document_name, + document_path, document_serialized_content, } => { let document_id = DocumentId(generate_uuid()); responses.add(PortfolioMessage::OpenDocumentFileWithId { document_id, document_name, + document_path, document_is_auto_saved: false, document_is_saved: true, document_serialized_content, @@ -428,6 +430,7 @@ impl MessageHandler> for Portfolio PortfolioMessage::OpenDocumentFileWithId { document_id, document_name, + document_path, document_is_auto_saved, document_is_saved, document_serialized_content, @@ -439,10 +442,7 @@ impl MessageHandler> for Portfolio let document_serialized_content = document_migration_string_preprocessing(document_serialized_content); // Deserialize the document - let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| { - document.name.clone_from(&document_name); - document - }); + let document = DocumentMessageHandler::deserialize_document(&document_serialized_content); // Display an error to the user if the document could not be opened let mut document = match document { @@ -503,6 +503,30 @@ impl MessageHandler> for Portfolio document.set_auto_save_state(document_is_auto_saved); document.set_save_state(document_is_saved); + let document_name_from_path = document_path.as_ref().and_then(|path| { + if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + } + }); + + match (document_name, document_path, document_name_from_path) { + (Some(name), _, None) => { + document.name = name; + } + (_, Some(path), Some(name)) => { + document.name = name; + document.path = Some(path); + } + (_, _, Some(name)) => { + document.name = name; + } + _ => { + document.name = DEFAULT_DOCUMENT_NAME.to_string(); + } + } + // Load the document into the portfolio so it opens in the editor self.load_document(document, document_id, responses, to_front); } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 2b81d98b95..cc52877ec7 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -425,7 +425,8 @@ impl EditorHandle { #[wasm_bindgen(js_name = openDocumentFile)] pub fn open_document_file(&self, document_name: String, document_serialized_content: String) { let message = PortfolioMessage::OpenDocumentFile { - document_name, + document_name: Some(document_name), + document_path: None, document_serialized_content, }; self.dispatch(message); @@ -436,7 +437,8 @@ impl EditorHandle { let document_id = DocumentId(document_id); let message = PortfolioMessage::OpenDocumentFileWithId { document_id, - document_name, + document_name: Some(document_name), + document_path: None, document_is_auto_saved: true, document_is_saved, document_serialized_content, From d78317d10add592b8e420c1d47a024307381d770 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 01:49:41 +0000 Subject: [PATCH 02/31] Add save as action --- .../messages/input_mapper/input_mappings.rs | 1 + .../portfolio/document/document_message.rs | 1 + .../document/document_message_handler.rs | 21 ++++++++++++++- .../menu_bar/menu_bar_message_handler.rs | 26 +++++++++++++------ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 1b722301e6..6eb053bfde 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers), entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers), entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument), + entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs), entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers), entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers), entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 89db87c983..b6eb9a4fd6 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -107,6 +107,7 @@ pub enum DocumentMessage { RenderRulers, RenderScrollbars, SaveDocument, + SaveDocumentAs, SavedDocument { path: Option, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7107e28ae9..ee329860ac 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1005,7 +1005,11 @@ impl MessageHandler> for DocumentMes multiplier: scrollbar_multiplier.into(), }); } - DocumentMessage::SaveDocument => { + DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => { + if let DocumentMessage::SaveDocumentAs = message { + self.path = None; + } + self.set_save_state(true); responses.add(PortfolioMessage::AutoSaveActiveDocument); // Update the save status of the just saved document @@ -1020,6 +1024,21 @@ impl MessageHandler> for DocumentMes } DocumentMessage::SavedDocument { path } => { self.path = path; + + // Update the name to match the file stem + let document_name_from_path = self.path.as_ref().and_then(|path| { + if path.extension().is_some_and(|e| e == FILE_EXTENSION) { + path.file_stem().map(|n| n.to_string_lossy().to_string()) + } else { + None + } + }); + if let Some(name) = document_name_from_path { + self.name = name; + + responses.add(PortfolioMessage::UpdateOpenDocumentsList); + responses.add(NodeGraphMessage::UpdateNewNodeGraph); + } } DocumentMessage::SelectParentLayer => { let selected_nodes = self.network_interface.selected_nodes(); diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 0f4b574de8..3aab220345 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -99,14 +99,24 @@ impl LayoutHolder for MenuBarMessageHandler { ..MenuBarEntry::default() }, ], - vec![MenuBarEntry { - label: "Save".into(), - icon: Some("Save".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument), - action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }], + vec![ + MenuBarEntry { + label: "Save".into(), + icon: Some("Save".into()), + shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument), + action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()), + disabled: no_active_document, + ..MenuBarEntry::default() + }, + MenuBarEntry { + label: "Save as".into(), + icon: Some("Save".into()), + shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs), + action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()), + disabled: no_active_document, + ..MenuBarEntry::default() + }, + ], vec![ MenuBarEntry { label: "Import…".into(), From b63d36f5f1562b10713d92ae05818968176d76f2 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 16:17:17 +0000 Subject: [PATCH 03/31] Fix test errors --- editor/src/dispatcher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 0c098833a3..4073a46deb 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -501,7 +501,8 @@ mod test { ); let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile { - document_name: document_name.into(), + document_name: Some(document_name.to_string()), + document_path: None, document_serialized_content, }); From ec8fbfb84a3734786a2469b3ad825850504806d2 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 11 Aug 2025 11:52:20 +0200 Subject: [PATCH 04/31] Add missing save as action --- .../src/messages/portfolio/document/document_message_handler.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index ee329860ac..e5d2c1ffb9 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1556,6 +1556,7 @@ impl MessageHandler> for DocumentMes Noop, Redo, SaveDocument, + SaveDocumentAs, SelectAllLayers, SetSnapping, ToggleGridVisibility, From 6a5897a06494e60cbef1992c44d2521618f5faa7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 18:03:30 +0000 Subject: [PATCH 05/31] Desktop fix drop file open document file message --- desktop/src/app.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 5b425182d2..30d0286f57 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -295,7 +295,8 @@ impl ApplicationHandler for WinitApp { let Some(content) = load_string(&path) else { return }; let message = PortfolioMessage::OpenDocumentFile { - document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()), + document_name: None, + document_path: Some(path), document_serialized_content: content, }; self.dispatch_message(message.into()); From 2ace6e9428ec80e31e26f5b611d2a1e24739b816 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 19:59:14 +0000 Subject: [PATCH 06/31] Address review comments --- .../portfolio/document/document_message_handler.rs | 13 ++----------- .../portfolio/menu_bar/menu_bar_message_handler.rs | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 92a50fb562..338be12e3a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -947,17 +947,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::RenameDocument { new_name } => { self.name = new_name.clone(); - // If the new document name does not match the current path, clear the path. - if let Some(path) = self.path.as_ref() { - let document_name_from_path = if path.extension().is_some_and(|e| e == FILE_EXTENSION) { - path.file_stem().map(|n| n.to_string_lossy().to_string()) - } else { - None - }; - if Some(new_name) != document_name_from_path { - self.path = None; - } - } + self.path = None; + self.set_save_state(false); responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::UpdateNewNodeGraph); diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index f962bbf10e..44e8fd63c9 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -111,7 +111,7 @@ impl LayoutHolder for MenuBarMessageHandler { ..MenuBarEntry::default() }, MenuBarEntry { - label: "Save as".into(), + label: "Save As…".into(), icon: Some("Save".into()), shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs), action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()), From e0cea6633adf6409f77c2583dd2ea008f289371f Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 20:52:15 +0000 Subject: [PATCH 07/31] Replace file save suffix with file extension --- desktop/src/app.rs | 1 - editor/src/consts.rs | 3 +-- .../export_dialog/export_dialog_message_handler.rs | 2 +- .../portfolio/document/document_message_handler.rs | 4 ++-- editor/src/messages/portfolio/portfolio_message.rs | 2 +- .../messages/portfolio/portfolio_message_handler.rs | 4 ++-- editor/src/node_graph_executor.rs | 12 ++---------- editor/src/node_graph_executor/runtime.rs | 2 +- frontend/src/state-providers/portfolio.ts | 6 +++--- frontend/wasm/src/editor_api.rs | 10 +++++----- 10 files changed, 18 insertions(+), 28 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 30d0286f57..98d284f7cf 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -10,7 +10,6 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphene_std::Color; use graphene_std::raster::Image; use graphite_editor::application::Editor; -use graphite_editor::consts::DEFAULT_DOCUMENT_NAME; use graphite_editor::messages::prelude::*; use std::fs; use std::sync::Arc; diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 48bdea853f..0fc75466f7 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -150,9 +150,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; // DOCUMENT -pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const FILE_EXTENSION: &str = "graphite"; -pub const FILE_SAVE_SUFFIX: &str = ".graphite"; +pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1; diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 0fa126917b..624db0ab9f 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -44,7 +44,7 @@ impl MessageHandler> for Exp ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area, ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport { - file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), + name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), file_type: self.file_type, scale_factor: self.scale_factor, bounds: self.bounds, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 338be12e3a..79d8ab1a36 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; -use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; +use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; @@ -1034,7 +1034,7 @@ impl MessageHandler> for DocumentMes responses.add(FrontendMessage::TriggerSaveDocument { document_id, - name: self.name.clone() + FILE_SAVE_SUFFIX, + name: format!("{}.{}", self.name.clone(), FILE_EXTENSION), path: self.path.clone(), content: self.serialize_document().into_bytes(), }) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 0e47d254a4..75717bac42 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -118,7 +118,7 @@ pub enum PortfolioMessage { document_id: DocumentId, }, SubmitDocumentExport { - file_name: String, + name: String, file_type: FileType, scale_factor: f64, bounds: ExportBounds, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index aef9738a30..6bbf996474 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -923,7 +923,7 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::SubmitDocumentExport { - file_name, + name, file_type, scale_factor, bounds, @@ -931,7 +931,7 @@ impl MessageHandler> for Portfolio } => { let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document"); let export_config = ExportConfig { - file_name, + name, file_type, scale_factor, bounds, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 560ff47188..b3584d342d 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,4 +1,3 @@ -use crate::consts::FILE_SAVE_SUFFIX; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::prelude::*; use glam::{DAffine2, DVec2, UVec2}; @@ -232,18 +231,11 @@ impl NodeGraphExecutor { }; let ExportConfig { - file_type, - file_name, - size, - scale_factor, - .. + file_type, name, size, scale_factor, .. } = export_config; let file_suffix = &format!(".{file_type:?}").to_lowercase(); - let name = match file_name.ends_with(FILE_SAVE_SUFFIX) { - true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix), - false => file_name + file_suffix, - }; + let name = name + file_suffix; if file_type == FileType::Svg { responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 55a262ab2c..0838f1781c 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -74,7 +74,7 @@ pub struct GraphUpdate { #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ExportConfig { - pub file_name: String, + pub name: String, pub file_type: FileType, pub scale_factor: f64, pub bounds: ExportBounds, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 41ad207f02..5b3178e9a2 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -62,8 +62,8 @@ export function createPortfolioState(editor: Editor) { } }); editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => { - const extension = editor.handle.fileSaveSuffix(); - const data = await upload(extension, "text"); + const extension = editor.handle.fileExtension(); + const data = await upload("." + extension, "text"); editor.handle.openDocumentFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { @@ -76,7 +76,7 @@ export function createPortfolioState(editor: Editor) { } // In case the user accidentally uploads a Graphite file, open it instead of failing to import it - if (data.filename.endsWith(".graphite")) { + if (data.filename.endsWith("." + editor.handle.fileExtension())) { editor.handle.openDocumentFile(data.filename, data.content.text); return; } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 501e96cde0..9376d7c4ed 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -6,7 +6,7 @@ // use crate::helpers::translate_key; use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; -use editor::consts::FILE_SAVE_SUFFIX; +use editor::consts::FILE_EXTENSION; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -344,10 +344,10 @@ impl EditorHandle { cfg!(debug_assertions) } - /// Get the constant `FILE_SAVE_SUFFIX` - #[wasm_bindgen(js_name = fileSaveSuffix)] - pub fn file_save_suffix(&self) -> String { - FILE_SAVE_SUFFIX.into() + /// Get the constant `FILE_EXTENSION` + #[wasm_bindgen(js_name = fileExtension)] + pub fn file_extension(&self) -> String { + FILE_EXTENSION.into() } /// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that) From 18e73605d083d8b129e0e85afd28c89322867ac7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 11 Aug 2025 20:53:42 +0000 Subject: [PATCH 08/31] Add comment specifying that the upload function takes a html input accept string --- frontend/src/utility-functions/files.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index 6d230433a1..08be7ccdf2 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -22,11 +22,12 @@ export function downloadFile(filename: string, content: ArrayBuffer) { downloadFileBlob(filename, blob); } -export async function upload(acceptedExtensions: string, textOrData: T): Promise> { +// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format +export async function upload(accept: string, textOrData: T): Promise> { return new Promise>((resolve, _) => { const element = document.createElement("input"); element.type = "file"; - element.accept = acceptedExtensions; + element.accept = accept; element.addEventListener( "change", From bcea6f6974df7443d53b63b28f9f2eb52f540ca6 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 10 Aug 2025 17:52:43 +0000 Subject: [PATCH 09/31] Prototyping desktop wrapper api --- desktop/src/editor_api.rs | 237 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 desktop/src/editor_api.rs diff --git a/desktop/src/editor_api.rs b/desktop/src/editor_api.rs new file mode 100644 index 0000000000..e73e625165 --- /dev/null +++ b/desktop/src/editor_api.rs @@ -0,0 +1,237 @@ +use graphene_std::{Color, raster::Image}; +use graphite_editor::{ + application::Editor, + messages::prelude::{DocumentId, DocumentMessage, FrontendMessage, InputPreprocessorMessage, Message, PortfolioMessage}, +}; +use std::{io::Cursor, path::PathBuf}; + +pub struct EditorApi { + editor: Editor, +} + +impl EditorApi { + pub fn new() -> Self { + Self { editor: Editor::new() } + } + + pub fn dispatch(&mut self, message: EditorMessage) -> Vec { + let mut responses = Vec::new(); + match message { + EditorMessage::FromFrontend(data) => { + let string = std::str::from_utf8(&data).unwrap(); + match ron::from_str::(string) { + Ok(message) => { + self.handle_message(message, &mut responses); + } + Err(e) => { + tracing::error!("Failed to deserialize message {:?}", e) + } + } + } + EditorMessage::OpenFileDialogResult { path, content, context } => match context.0 { + OpenFileDialogContextInner::Document => match String::from_utf8(content) { + Ok(content) => { + let message = PortfolioMessage::OpenDocumentFile { + document_name: None, + document_path: Some(path), + document_serialized_content: content, + }; + self.handle_message(message.into(), &mut responses); + } + Err(e) => { + tracing::error!("Failed to deserialize document content: {:?}", e); + } + }, + OpenFileDialogContextInner::Import => { + let extension = path.extension().and_then(|s| s.to_str()); + let name = path.file_stem().map(|s| s.to_string_lossy().to_string()); + match extension { + Some("svg") => match String::from_utf8(content) { + Ok(content) if !content.is_empty() => { + let message = PortfolioMessage::PasteSvg { + name, + svg: content, + mouse: None, + parent_and_insert_index: None, + }; + self.handle_message(message.into(), &mut responses); + } + Ok(_) => { + tracing::warn!("Svg file is empty: {}", path.display()); + } + Err(e) => { + tracing::error!("Failed to deserialize document content: {:?}", e); + } + }, + Some(_) => { + let reader = image::ImageReader::new(Cursor::new(content)); + match reader.decode() { + Ok(image) => { + let width = image.width(); + let height = image.height(); + let image_data = image.to_rgba8(); + let image = Image::::from_image_data(image_data.as_raw(), width, height); + + let message = PortfolioMessage::PasteImage { + name, + image, + mouse: None, + parent_and_insert_index: None, + }; + self.handle_message(message.into(), &mut responses); + } + Err(e) => { + tracing::error!("Failed to decode image: {}: {}", path.display(), e); + } + } + } + _ => { + tracing::warn!("Unsupported file type: {}", path.display()); + } + } + } + }, + EditorMessage::SaveFileDialogResult { path, context } => match context.0 { + SaveFileDialogContextInner::Document { document_id } => { + let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage { + document_id, + message: DocumentMessage::SavedDocument { path: Some(path) }, + }); + self.handle_message(message, &mut responses); + } + SaveFileDialogContextInner::Export => {} + }, + } + responses + } + + fn handle_message(&mut self, message: Message, responses: &mut Vec) { + handle_frontend_messages(self.editor.handle_message(message), responses); + } + + pub fn run_node_graph(&self) {} +} + +fn handle_message(message: Message, responses: &mut Vec) -> Option { + if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message { + let top_left = bounds_of_viewports[0].top_left; + let bottom_right = bounds_of_viewports[0].bottom_right; + responses.push(NativeMessage::UpdateViewportBounds { + x: top_left.x as f32, + y: top_left.y as f32, + width: (bottom_right.x - top_left.x) as f32, + height: (bottom_right.y - top_left.y) as f32, + }); + } + None +} + +fn handle_frontend_messages(messages: Vec, responses: &mut Vec) { + let frontend_messages = messages.into_iter().filter_map(|m| handle_frontend_message(m, responses)).collect::>(); + responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); +} + +fn handle_frontend_message(message: FrontendMessage, responses: &mut Vec) -> Option { + match message { + FrontendMessage::RenderOverlays(overlay_context) => { + responses.push(NativeMessage::UpdateOverlays(overlay_context.take_scene())); + } + FrontendMessage::TriggerOpenDocument => { + responses.push(NativeMessage::OpenFileDialog { + title: "Open Document".to_string(), + filters: vec![FileFilter { + name: "Graphite".to_string(), + extensions: vec!["graphite".to_string()], + }], + context: OpenFileDialogContext(OpenFileDialogContextInner::Document), + }); + } + FrontendMessage::TriggerImport => { + responses.push(NativeMessage::OpenFileDialog { + title: "Import File".to_string(), + filters: vec![ + FileFilter { + name: "Svg".to_string(), + extensions: vec!["svg".to_string()], + }, + FileFilter { + name: "Image".to_string(), + extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()], + }, + ], + context: OpenFileDialogContext(OpenFileDialogContextInner::Import), + }); + } + FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { + responses.push(NativeMessage::SaveFileDialog { + title: "Save Document".to_string(), + default_filename: name, + default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), + content, + context: SaveFileDialogContext(SaveFileDialogContextInner::Document { document_id }), + }); + } + FrontendMessage::TriggerSaveFile { name, content } => { + responses.push(NativeMessage::SaveFileDialog { + title: "Save File".to_string(), + default_filename: name, + default_folder: None, + content, + context: SaveFileDialogContext(SaveFileDialogContextInner::Export), + }); + } + FrontendMessage::TriggerVisitLink { url } => { + responses.push(NativeMessage::OpenUrl(url)); + } + m => return Some(m), + } + None +} + +pub enum NativeMessage { + ToFrontend(Vec), + OpenFileDialog { + title: String, + filters: Vec, + context: OpenFileDialogContext, + }, + SaveFileDialog { + title: String, + default_filename: String, + default_folder: Option, + content: Vec, + context: SaveFileDialogContext, + }, + OpenUrl(String), + UpdateViewport(wgpu::Texture), + UpdateViewportBounds { + x: f32, + y: f32, + width: f32, + height: f32, + }, + UpdateOverlays(vello::Scene), +} + +pub enum EditorMessage { + FromFrontend(Vec), + OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, + SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, +} + +pub struct FileFilter { + pub name: String, + pub extensions: Vec, +} + +pub struct OpenFileDialogContext(OpenFileDialogContextInner); +enum OpenFileDialogContextInner { + Document, + Import, +} + +pub struct SaveFileDialogContext(SaveFileDialogContextInner); +enum SaveFileDialogContextInner { + Document { document_id: DocumentId }, + Export, +} From 7aabc9545f1938978e1bbb77631987cbda7d5fc4 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 14 Aug 2025 15:27:10 +0000 Subject: [PATCH 10/31] Separate into multiple modules --- desktop/src/editor_api.rs | 241 +----------------- desktop/src/editor_api/editor_wrapper.rs | 52 ++++ .../src/editor_api/handle_editor_message.rs | 97 +++++++ .../editor_api/intercept_frontend_message.rs | 62 +++++ desktop/src/editor_api/intercept_message.rs | 20 ++ desktop/src/editor_api/messages.rs | 49 ++++ desktop/src/main.rs | 2 + 7 files changed, 289 insertions(+), 234 deletions(-) create mode 100644 desktop/src/editor_api/editor_wrapper.rs create mode 100644 desktop/src/editor_api/handle_editor_message.rs create mode 100644 desktop/src/editor_api/intercept_frontend_message.rs create mode 100644 desktop/src/editor_api/intercept_message.rs create mode 100644 desktop/src/editor_api/messages.rs diff --git a/desktop/src/editor_api.rs b/desktop/src/editor_api.rs index e73e625165..8fb91d2b0e 100644 --- a/desktop/src/editor_api.rs +++ b/desktop/src/editor_api.rs @@ -1,237 +1,10 @@ -use graphene_std::{Color, raster::Image}; -use graphite_editor::{ - application::Editor, - messages::prelude::{DocumentId, DocumentMessage, FrontendMessage, InputPreprocessorMessage, Message, PortfolioMessage}, -}; -use std::{io::Cursor, path::PathBuf}; +mod editor_wrapper; +pub use editor_wrapper::EditorWrapper; -pub struct EditorApi { - editor: Editor, -} - -impl EditorApi { - pub fn new() -> Self { - Self { editor: Editor::new() } - } - - pub fn dispatch(&mut self, message: EditorMessage) -> Vec { - let mut responses = Vec::new(); - match message { - EditorMessage::FromFrontend(data) => { - let string = std::str::from_utf8(&data).unwrap(); - match ron::from_str::(string) { - Ok(message) => { - self.handle_message(message, &mut responses); - } - Err(e) => { - tracing::error!("Failed to deserialize message {:?}", e) - } - } - } - EditorMessage::OpenFileDialogResult { path, content, context } => match context.0 { - OpenFileDialogContextInner::Document => match String::from_utf8(content) { - Ok(content) => { - let message = PortfolioMessage::OpenDocumentFile { - document_name: None, - document_path: Some(path), - document_serialized_content: content, - }; - self.handle_message(message.into(), &mut responses); - } - Err(e) => { - tracing::error!("Failed to deserialize document content: {:?}", e); - } - }, - OpenFileDialogContextInner::Import => { - let extension = path.extension().and_then(|s| s.to_str()); - let name = path.file_stem().map(|s| s.to_string_lossy().to_string()); - match extension { - Some("svg") => match String::from_utf8(content) { - Ok(content) if !content.is_empty() => { - let message = PortfolioMessage::PasteSvg { - name, - svg: content, - mouse: None, - parent_and_insert_index: None, - }; - self.handle_message(message.into(), &mut responses); - } - Ok(_) => { - tracing::warn!("Svg file is empty: {}", path.display()); - } - Err(e) => { - tracing::error!("Failed to deserialize document content: {:?}", e); - } - }, - Some(_) => { - let reader = image::ImageReader::new(Cursor::new(content)); - match reader.decode() { - Ok(image) => { - let width = image.width(); - let height = image.height(); - let image_data = image.to_rgba8(); - let image = Image::::from_image_data(image_data.as_raw(), width, height); - - let message = PortfolioMessage::PasteImage { - name, - image, - mouse: None, - parent_and_insert_index: None, - }; - self.handle_message(message.into(), &mut responses); - } - Err(e) => { - tracing::error!("Failed to decode image: {}: {}", path.display(), e); - } - } - } - _ => { - tracing::warn!("Unsupported file type: {}", path.display()); - } - } - } - }, - EditorMessage::SaveFileDialogResult { path, context } => match context.0 { - SaveFileDialogContextInner::Document { document_id } => { - let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage { - document_id, - message: DocumentMessage::SavedDocument { path: Some(path) }, - }); - self.handle_message(message, &mut responses); - } - SaveFileDialogContextInner::Export => {} - }, - } - responses - } - - fn handle_message(&mut self, message: Message, responses: &mut Vec) { - handle_frontend_messages(self.editor.handle_message(message), responses); - } - - pub fn run_node_graph(&self) {} -} - -fn handle_message(message: Message, responses: &mut Vec) -> Option { - if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message { - let top_left = bounds_of_viewports[0].top_left; - let bottom_right = bounds_of_viewports[0].bottom_right; - responses.push(NativeMessage::UpdateViewportBounds { - x: top_left.x as f32, - y: top_left.y as f32, - width: (bottom_right.x - top_left.x) as f32, - height: (bottom_right.y - top_left.y) as f32, - }); - } - None -} - -fn handle_frontend_messages(messages: Vec, responses: &mut Vec) { - let frontend_messages = messages.into_iter().filter_map(|m| handle_frontend_message(m, responses)).collect::>(); - responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); -} - -fn handle_frontend_message(message: FrontendMessage, responses: &mut Vec) -> Option { - match message { - FrontendMessage::RenderOverlays(overlay_context) => { - responses.push(NativeMessage::UpdateOverlays(overlay_context.take_scene())); - } - FrontendMessage::TriggerOpenDocument => { - responses.push(NativeMessage::OpenFileDialog { - title: "Open Document".to_string(), - filters: vec![FileFilter { - name: "Graphite".to_string(), - extensions: vec!["graphite".to_string()], - }], - context: OpenFileDialogContext(OpenFileDialogContextInner::Document), - }); - } - FrontendMessage::TriggerImport => { - responses.push(NativeMessage::OpenFileDialog { - title: "Import File".to_string(), - filters: vec![ - FileFilter { - name: "Svg".to_string(), - extensions: vec!["svg".to_string()], - }, - FileFilter { - name: "Image".to_string(), - extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()], - }, - ], - context: OpenFileDialogContext(OpenFileDialogContextInner::Import), - }); - } - FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { - responses.push(NativeMessage::SaveFileDialog { - title: "Save Document".to_string(), - default_filename: name, - default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), - content, - context: SaveFileDialogContext(SaveFileDialogContextInner::Document { document_id }), - }); - } - FrontendMessage::TriggerSaveFile { name, content } => { - responses.push(NativeMessage::SaveFileDialog { - title: "Save File".to_string(), - default_filename: name, - default_folder: None, - content, - context: SaveFileDialogContext(SaveFileDialogContextInner::Export), - }); - } - FrontendMessage::TriggerVisitLink { url } => { - responses.push(NativeMessage::OpenUrl(url)); - } - m => return Some(m), - } - None -} - -pub enum NativeMessage { - ToFrontend(Vec), - OpenFileDialog { - title: String, - filters: Vec, - context: OpenFileDialogContext, - }, - SaveFileDialog { - title: String, - default_filename: String, - default_folder: Option, - content: Vec, - context: SaveFileDialogContext, - }, - OpenUrl(String), - UpdateViewport(wgpu::Texture), - UpdateViewportBounds { - x: f32, - y: f32, - width: f32, - height: f32, - }, - UpdateOverlays(vello::Scene), -} - -pub enum EditorMessage { - FromFrontend(Vec), - OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, - SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, -} - -pub struct FileFilter { - pub name: String, - pub extensions: Vec, -} - -pub struct OpenFileDialogContext(OpenFileDialogContextInner); -enum OpenFileDialogContextInner { - Document, - Import, -} +pub mod messages; +use messages::{EditorMessage, NativeMessage}; -pub struct SaveFileDialogContext(SaveFileDialogContextInner); -enum SaveFileDialogContextInner { - Document { document_id: DocumentId }, - Export, +pub trait EditorApi { + fn dispatch(&mut self, message: EditorMessage) -> Vec; + fn poll() -> Vec; } diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs new file mode 100644 index 0000000000..4f524e62a7 --- /dev/null +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -0,0 +1,52 @@ +use graphite_editor::{application::Editor, messages::prelude::Message}; + +use crate::editor_api::EditorApi; +use crate::editor_api::messages::{EditorMessage, NativeMessage}; + +#[path = "handle_editor_message.rs"] +mod handle_editor_message; + +#[path = "intercept_frontend_message.rs"] +mod intercept_frontend_message; +#[path = "intercept_message.rs"] +mod intercept_message; + +pub struct EditorWrapper { + editor: Editor, +} + +impl EditorApi for EditorWrapper { + fn dispatch(&mut self, message: EditorMessage) -> Vec { + let mut responses = Vec::new(); + handle_editor_message::handle_editor_message(message, &mut responses); + let mut native_responses = Vec::new(); + for message in responses.drain(..) { + self.handle_message(message, &mut native_responses); + } + native_responses + } + + fn poll() -> Vec { + match futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()) { + (has_run, Some(texture)) if has_run => vec![NativeMessage::UpdateViewport((*texture.texture).clone())], + _ => vec![], + } + } +} + +impl EditorWrapper { + pub fn new() -> Self { + Self { editor: Editor::new() } + } + + fn handle_message(&mut self, message: Message, responses: &mut Vec) { + if let Some(message) = intercept_message::intercept_message(message, responses) { + let messages = self.editor.handle_message(message); + let frontend_messages = messages + .into_iter() + .filter_map(|m| intercept_frontend_message::intercept_frontend_message(m, responses)) + .collect::>(); + responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); + } + } +} diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/editor_api/handle_editor_message.rs new file mode 100644 index 0000000000..92c17f7733 --- /dev/null +++ b/desktop/src/editor_api/handle_editor_message.rs @@ -0,0 +1,97 @@ +use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; + +use crate::editor_api::messages::{EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; + +pub(super) fn handle_editor_message(message: EditorMessage, responses: &mut Vec) { + match message { + EditorMessage::FromFrontend(data) => { + let string = std::str::from_utf8(&data).unwrap(); + match ron::from_str::(string) { + Ok(message) => { + responses.push(message); + } + Err(e) => { + tracing::error!("Failed to deserialize message {:?}", e) + } + } + } + EditorMessage::OpenFileDialogResult { path, content, context } => match context { + OpenFileDialogContext::Document => match String::from_utf8(content) { + Ok(content) => { + responses.push( + PortfolioMessage::OpenDocumentFile { + document_name: None, + document_path: Some(path), + document_serialized_content: content, + } + .into(), + ); + } + Err(e) => { + tracing::error!("Failed to deserialize document content: {:?}", e); + } + }, + OpenFileDialogContext::Import => { + let extension = path.extension().and_then(|s| s.to_str()); + let name = path.file_stem().map(|s| s.to_string_lossy().to_string()); + match extension { + Some("svg") => match String::from_utf8(content) { + Ok(content) if !content.is_empty() => { + responses.push( + PortfolioMessage::PasteSvg { + name, + svg: content, + mouse: None, + parent_and_insert_index: None, + } + .into(), + ); + } + Ok(_) => { + tracing::warn!("Svg file is empty: {}", path.display()); + } + Err(e) => { + tracing::error!("Failed to deserialize document content: {:?}", e); + } + }, + Some(_) => { + let reader = image::ImageReader::new(std::io::Cursor::new(content)); + match reader.decode() { + Ok(image) => { + let width = image.width(); + let height = image.height(); + let image_data = image.to_rgba8(); + let image = graphene_std::raster::Image::::from_image_data(image_data.as_raw(), width, height); + + responses.push( + PortfolioMessage::PasteImage { + name, + image, + mouse: None, + parent_and_insert_index: None, + } + .into(), + ); + } + Err(e) => { + tracing::error!("Failed to decode image: {}: {}", path.display(), e); + } + } + } + _ => { + tracing::warn!("Unsupported file type: {}", path.display()); + } + } + } + }, + EditorMessage::SaveFileDialogResult { path, context } => match context { + SaveFileDialogContext::Document { document_id } => { + responses.push(Message::Portfolio(PortfolioMessage::DocumentPassMessage { + document_id, + message: DocumentMessage::SavedDocument { path: Some(path) }, + })); + } + SaveFileDialogContext::Export => {} + }, + } +} diff --git a/desktop/src/editor_api/intercept_frontend_message.rs b/desktop/src/editor_api/intercept_frontend_message.rs new file mode 100644 index 0000000000..43457ea6f1 --- /dev/null +++ b/desktop/src/editor_api/intercept_frontend_message.rs @@ -0,0 +1,62 @@ +use std::path::PathBuf; + +use graphite_editor::messages::prelude::FrontendMessage; + +use crate::editor_api::messages::{FileFilter, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; + +pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &mut Vec) -> Option { + match message { + FrontendMessage::RenderOverlays(overlay_context) => { + responses.push(NativeMessage::UpdateOverlays(overlay_context.take_scene())); + } + FrontendMessage::TriggerOpenDocument => { + responses.push(NativeMessage::OpenFileDialog { + title: "Open Document".to_string(), + filters: vec![FileFilter { + name: "Graphite".to_string(), + extensions: vec!["graphite".to_string()], + }], + context: OpenFileDialogContext::Document, + }); + } + FrontendMessage::TriggerImport => { + responses.push(NativeMessage::OpenFileDialog { + title: "Import File".to_string(), + filters: vec![ + FileFilter { + name: "Svg".to_string(), + extensions: vec!["svg".to_string()], + }, + FileFilter { + name: "Image".to_string(), + extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()], + }, + ], + context: OpenFileDialogContext::Import, + }); + } + FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { + responses.push(NativeMessage::SaveFileDialog { + title: "Save Document".to_string(), + default_filename: name, + default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), + content, + context: SaveFileDialogContext::Document { document_id }, + }); + } + FrontendMessage::TriggerSaveFile { name, content } => { + responses.push(NativeMessage::SaveFileDialog { + title: "Save File".to_string(), + default_filename: name, + default_folder: None, + content, + context: SaveFileDialogContext::Export, + }); + } + FrontendMessage::TriggerVisitLink { url } => { + responses.push(NativeMessage::OpenUrl(url)); + } + m => return Some(m), + } + None +} diff --git a/desktop/src/editor_api/intercept_message.rs b/desktop/src/editor_api/intercept_message.rs new file mode 100644 index 0000000000..2ad2f686fa --- /dev/null +++ b/desktop/src/editor_api/intercept_message.rs @@ -0,0 +1,20 @@ +use graphite_editor::messages::prelude::{InputPreprocessorMessage, Message}; + +use crate::editor_api::messages::NativeMessage; + +pub(super) fn intercept_message(message: Message, responses: &mut Vec) -> Option { + match message { + Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) => { + let top_left = bounds_of_viewports[0].top_left; + let bottom_right = bounds_of_viewports[0].bottom_right; + responses.push(NativeMessage::UpdateViewportBounds { + x: top_left.x as f32, + y: top_left.y as f32, + width: (bottom_right.x - top_left.x) as f32, + height: (bottom_right.y - top_left.y) as f32, + }); + } + m => return Some(m), + } + None +} diff --git a/desktop/src/editor_api/messages.rs b/desktop/src/editor_api/messages.rs new file mode 100644 index 0000000000..a471a20fcb --- /dev/null +++ b/desktop/src/editor_api/messages.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; + +use graphite_editor::messages::prelude::DocumentId; + +pub enum NativeMessage { + ToFrontend(Vec), + OpenFileDialog { + title: String, + filters: Vec, + context: OpenFileDialogContext, + }, + SaveFileDialog { + title: String, + default_filename: String, + default_folder: Option, + content: Vec, + context: SaveFileDialogContext, + }, + OpenUrl(String), + UpdateViewport(wgpu::Texture), + UpdateViewportBounds { + x: f32, + y: f32, + width: f32, + height: f32, + }, + UpdateOverlays(vello::Scene), +} + +pub struct FileFilter { + pub name: String, + pub extensions: Vec, +} + +pub enum EditorMessage { + FromFrontend(Vec), + OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, + SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, +} + +pub enum OpenFileDialogContext { + Document, + Import, +} + +pub enum SaveFileDialogContext { + Document { document_id: DocumentId }, + Export, +} diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 36fe41db82..ae8ac49a9a 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -21,6 +21,8 @@ mod dirs; mod dialogs; +mod editor_api; + #[derive(Debug)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), From ec71b822b3bee4d3232b40d894d02b16136bc6a1 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 14 Aug 2025 18:13:17 +0000 Subject: [PATCH 11/31] Some fixup --- desktop/src/editor_api.rs | 2 ++ desktop/src/editor_api/editor_wrapper.rs | 36 +++++++++++++++---- .../src/editor_api/handle_editor_message.rs | 5 ++- .../editor_api/intercept_frontend_message.rs | 5 +++ desktop/src/editor_api/intercept_message.rs | 25 +++++++------ desktop/src/editor_api/messages.rs | 4 +++ 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/desktop/src/editor_api.rs b/desktop/src/editor_api.rs index 8fb91d2b0e..d422417dbb 100644 --- a/desktop/src/editor_api.rs +++ b/desktop/src/editor_api.rs @@ -4,6 +4,8 @@ pub use editor_wrapper::EditorWrapper; pub mod messages; use messages::{EditorMessage, NativeMessage}; +pub use wgpu_executor::Context as WgpuContext; + pub trait EditorApi { fn dispatch(&mut self, message: EditorMessage) -> Vec; fn poll() -> Vec; diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index 4f524e62a7..64123726f3 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -1,7 +1,9 @@ +use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::{application::Editor, messages::prelude::Message}; +use std::collections::VecDeque; -use crate::editor_api::EditorApi; use crate::editor_api::messages::{EditorMessage, NativeMessage}; +use crate::editor_api::{EditorApi, WgpuContext}; #[path = "handle_editor_message.rs"] mod handle_editor_message; @@ -18,7 +20,7 @@ pub struct EditorWrapper { impl EditorApi for EditorWrapper { fn dispatch(&mut self, message: EditorMessage) -> Vec { let mut responses = Vec::new(); - handle_editor_message::handle_editor_message(message, &mut responses); + handle_editor_message::handle_editor_message(self, message, &mut responses); let mut native_responses = Vec::new(); for message in responses.drain(..) { self.handle_message(message, &mut native_responses); @@ -27,15 +29,26 @@ impl EditorApi for EditorWrapper { } fn poll() -> Vec { - match futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()) { - (has_run, Some(texture)) if has_run => vec![NativeMessage::UpdateViewport((*texture.texture).clone())], - _ => vec![], + let mut responses = Vec::new(); + + let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); + if has_run { + responses.push(NativeMessage::Loopback(EditorMessage::PoolNodeGraphEvaluation)); } + if let Some(texture) = texture { + responses.push(NativeMessage::UpdateViewport((*texture.texture).clone())); + responses.push(NativeMessage::RequestRedraw); + } + + responses } } impl EditorWrapper { - pub fn new() -> Self { + pub fn new(wgpu_context: WgpuContext) -> Self { + let application_io = WasmApplicationIo::new_with_context(wgpu_context); + futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); + Self { editor: Editor::new() } } @@ -49,4 +62,15 @@ impl EditorWrapper { responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); } } + + pub(super) fn poll_node_graph_evaluation(&mut self, responses: &mut Vec) { + let mut node_graph_responses = VecDeque::new(); + let err = self.editor.poll_node_graph_evaluation(&mut node_graph_responses); + if let Err(e) = err { + if e != "No active document" { + tracing::error!("Error poling node graph: {}", e); + } + } + responses.extend(node_graph_responses.drain(..)); + } } diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/editor_api/handle_editor_message.rs index 92c17f7733..4b27dec416 100644 --- a/desktop/src/editor_api/handle_editor_message.rs +++ b/desktop/src/editor_api/handle_editor_message.rs @@ -2,7 +2,9 @@ use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMess use crate::editor_api::messages::{EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; -pub(super) fn handle_editor_message(message: EditorMessage, responses: &mut Vec) { +use super::EditorWrapper; + +pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage, responses: &mut Vec) { match message { EditorMessage::FromFrontend(data) => { let string = std::str::from_utf8(&data).unwrap(); @@ -93,5 +95,6 @@ pub(super) fn handle_editor_message(message: EditorMessage, responses: &mut Vec< } SaveFileDialogContext::Export => {} }, + EditorMessage::PoolNodeGraphEvaluation => editor_wrapper.poll_node_graph_evaluation(responses), } } diff --git a/desktop/src/editor_api/intercept_frontend_message.rs b/desktop/src/editor_api/intercept_frontend_message.rs index 43457ea6f1..1444545fc0 100644 --- a/desktop/src/editor_api/intercept_frontend_message.rs +++ b/desktop/src/editor_api/intercept_frontend_message.rs @@ -40,6 +40,10 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m title: "Save Document".to_string(), default_filename: name, default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), + filters: vec![FileFilter { + name: "Graphite".to_string(), + extensions: vec!["graphite".to_string()], + }], content, context: SaveFileDialogContext::Document { document_id }, }); @@ -49,6 +53,7 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m title: "Save File".to_string(), default_filename: name, default_folder: None, + filters: Vec::new(), content, context: SaveFileDialogContext::Export, }); diff --git a/desktop/src/editor_api/intercept_message.rs b/desktop/src/editor_api/intercept_message.rs index 2ad2f686fa..0834efe004 100644 --- a/desktop/src/editor_api/intercept_message.rs +++ b/desktop/src/editor_api/intercept_message.rs @@ -4,17 +4,20 @@ use crate::editor_api::messages::NativeMessage; pub(super) fn intercept_message(message: Message, responses: &mut Vec) -> Option { match message { - Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) => { - let top_left = bounds_of_viewports[0].top_left; - let bottom_right = bounds_of_viewports[0].bottom_right; - responses.push(NativeMessage::UpdateViewportBounds { - x: top_left.x as f32, - y: top_left.y as f32, - width: (bottom_right.x - top_left.x) as f32, - height: (bottom_right.y - top_left.y) as f32, - }); + Message::InputPreprocessor(message) => { + if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { + let top_left = bounds_of_viewports[0].top_left; + let bottom_right = bounds_of_viewports[0].bottom_right; + responses.push(NativeMessage::UpdateViewportBounds { + x: top_left.x as f32, + y: top_left.y as f32, + width: (bottom_right.x - top_left.x) as f32, + height: (bottom_right.y - top_left.y) as f32, + }); + } + responses.push(NativeMessage::RequestRedraw); + Some(Message::InputPreprocessor(message)) } - m => return Some(m), + m => Some(m), } - None } diff --git a/desktop/src/editor_api/messages.rs b/desktop/src/editor_api/messages.rs index a471a20fcb..5f52f588f0 100644 --- a/desktop/src/editor_api/messages.rs +++ b/desktop/src/editor_api/messages.rs @@ -13,10 +13,12 @@ pub enum NativeMessage { title: String, default_filename: String, default_folder: Option, + filters: Vec, content: Vec, context: SaveFileDialogContext, }, OpenUrl(String), + RequestRedraw, UpdateViewport(wgpu::Texture), UpdateViewportBounds { x: f32, @@ -25,6 +27,7 @@ pub enum NativeMessage { height: f32, }, UpdateOverlays(vello::Scene), + Loopback(EditorMessage), } pub struct FileFilter { @@ -36,6 +39,7 @@ pub enum EditorMessage { FromFrontend(Vec), OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, + PoolNodeGraphEvaluation, } pub enum OpenFileDialogContext { From cc23ae2051d9a55706f7428bf7338f99d4e6dd4f Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 14 Aug 2025 18:14:38 +0000 Subject: [PATCH 12/31] Reimplement most functionality with editor api --- desktop/src/app.rs | 368 ++++++++++++--------------- desktop/src/cef.rs | 16 +- desktop/src/dialogs.rs | 26 -- desktop/src/main.rs | 27 +- desktop/src/render.rs | 2 +- desktop/src/render/graphics_state.rs | 7 +- 6 files changed, 190 insertions(+), 256 deletions(-) delete mode 100644 desktop/src/dialogs.rs diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 98d284f7cf..82324d23f0 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,17 +1,13 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; -use crate::dialogs::dialog_open_graphite_file; -use crate::dialogs::dialog_save_file; -use crate::dialogs::dialog_save_graphite_file; +use crate::editor_api::EditorApi; +use crate::editor_api::EditorWrapper; +use crate::editor_api::WgpuContext; +use crate::editor_api::messages::EditorMessage; +use crate::editor_api::messages::NativeMessage; use crate::render::GraphicsState; -use crate::render::WgpuContext; -use graph_craft::wasm_application_io::WasmApplicationIo; -use graphene_std::Color; -use graphene_std::raster::Image; -use graphite_editor::application::Editor; -use graphite_editor::messages::prelude::*; -use std::fs; +use rfd::AsyncFileDialog; use std::sync::Arc; use std::sync::mpsc::Sender; use std::thread; @@ -37,11 +33,12 @@ pub(crate) struct WinitApp { graphics_state: Option, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy, - editor: Editor, + editor_wrapper: EditorWrapper, } impl WinitApp { pub(crate) fn new(cef_context: cef::Context, window_size_sender: Sender, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy) -> Self { + let editor_wrapper = EditorWrapper::new(wgpu_context.clone()); Self { cef_context, window: None, @@ -50,97 +47,107 @@ impl WinitApp { window_size_sender, wgpu_context, event_loop_proxy, - editor: Editor::new(), + editor_wrapper, } } - fn dispatch_message(&mut self, message: Message) { - let responses = self.editor.handle_message(message); - self.send_messages_to_editor(responses); - } - - fn send_messages_to_editor(&mut self, mut responses: Vec) { - for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) { - let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() }; - if let Some(graphics_state) = &mut self.graphics_state { - let scene = overlay_context.take_scene(); - graphics_state.set_overlays_scene(scene); + fn handle_native_message(&mut self, message: NativeMessage) { + match message { + NativeMessage::ToFrontend(bytes) => { + self.cef_context.send_web_message(bytes.as_slice()); } - } + NativeMessage::OpenFileDialog { title, filters, context } => { + let event_loop_proxy = self.event_loop_proxy.clone(); + let _ = thread::spawn(move || { + let mut dialog = AsyncFileDialog::new().set_title(title); + for filter in filters { + dialog = dialog.add_filter(filter.name, &filter.extensions); + } - for _ in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerOpenDocument)) { - let event_loop_proxy = self.event_loop_proxy.clone(); - let _ = thread::spawn(move || { - let path = futures::executor::block_on(dialog_open_graphite_file()); - if let Some(path) = path { - let content = std::fs::read_to_string(&path).unwrap_or_else(|_| { - tracing::error!("Failed to read file: {}", path.display()); - String::new() - }); - let message = PortfolioMessage::OpenDocumentFile { - document_name: None, - document_path: Some(path), - document_serialized_content: content, - }; - let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into())); - } - }); - } + let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) }; - for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) { - let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else { - unreachable!() - }; - if let Some(path) = path { - let _ = std::fs::write(&path, content); - } else { + if let Some(path) = futures::executor::block_on(show_dialog) + && let Ok(content) = std::fs::read(&path) + { + let message = EditorMessage::OpenFileDialogResult { path, content, context }; + let _ = event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); + } + }); + } + NativeMessage::SaveFileDialog { + title, + default_filename, + default_folder, + filters, + content, + context, + } => { let event_loop_proxy = self.event_loop_proxy.clone(); let _ = thread::spawn(move || { - let path = futures::executor::block_on(dialog_save_graphite_file(name)); - if let Some(path) = path { + let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename); + if let Some(folder) = default_folder { + dialog = dialog.set_directory(folder); + } + for filter in filters { + dialog = dialog.add_filter(filter.name, &filter.extensions); + } + + let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) }; + + if let Some(path) = futures::executor::block_on(show_dialog) { if let Err(e) = std::fs::write(&path, content) { - tracing::error!("Failed to save file: {}: {}", path.display(), e); - } else { - let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage { - document_id, - message: DocumentMessage::SavedDocument { path: Some(path) }, - }); - let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message)); + tracing::error!("Failed to write file: {}: {}", path.display(), e); + return; } + + let message = EditorMessage::SaveFileDialogResult { path, context }; + let _ = event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); } }); } - } - - for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) { - let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() }; - let _ = thread::spawn(move || { - let path = futures::executor::block_on(dialog_save_file(name)); - if let Some(path) = path { - if let Err(e) = std::fs::write(&path, content) { - tracing::error!("Failed to save file: {}: {}", path.display(), e); + NativeMessage::OpenUrl(url) => { + let _ = thread::spawn(move || { + if let Err(e) = open::that(&url) { + tracing::error!("Failed to open URL: {}: {}", url, e); } + }); + } + NativeMessage::RequestRedraw => { + if let Some(window) = &self.window { + window.request_redraw(); } - }); - } + } + NativeMessage::UpdateViewport(texture) => { + if let Some(graphics_state) = &mut self.graphics_state { + graphics_state.bind_viewport_texture(texture); + } + } + NativeMessage::UpdateViewportBounds { x, y, width, height } => { + if let Some(graphics_state) = &mut self.graphics_state + && let Some(window) = &self.window + { + let window_size = window.inner_size(); + + let viewport_offset_x = x / window_size.width as f32; + let viewport_offset_y = y / window_size.height as f32; + graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]); - for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) { - let _ = thread::spawn(move || { - let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() }; - if let Err(e) = open::that(&url) { - tracing::error!("Failed to open URL: {}: {}", url, e); + let viewport_scale_x = width / window_size.width as f32; + let viewport_scale_y = height / window_size.height as f32; + graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]); + } + } + NativeMessage::UpdateOverlays(scene) => { + if let Some(graphics_state) = &mut self.graphics_state { + graphics_state.set_overlays_scene(scene); } - }); + } + NativeMessage::Loopback(editor_message) => self.dispatch_editor_message(editor_message), } + } - if responses.is_empty() { - return; - } - let Ok(message) = ron::to_string(&responses) else { - tracing::error!("Failed to serialize Messages"); - return; - }; - self.cef_context.send_web_message(message.as_bytes()); + fn dispatch_editor_message(&self, message: EditorMessage) { + let _ = self.event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); } } @@ -193,14 +200,19 @@ impl ApplicationHandler for WinitApp { self.graphics_state = Some(graphics_state); tracing::info!("Winit window created and ready"); - - let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone()); - - futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); } fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { match event { + CustomEvent::NativeMessage(message) => { + self.handle_native_message(message); + } + CustomEvent::EditorMessage(message) => { + let responses = self.editor_wrapper.dispatch(message); + for response in responses { + let _ = self.event_loop_proxy.send_event(CustomEvent::NativeMessage(response)); + } + } CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { graphics_state.resize(texture.width(), texture.height()); @@ -217,50 +229,6 @@ impl ApplicationHandler for WinitApp { self.cef_schedule = Some(instant); } } - CustomEvent::DispatchMessage(message) => { - self.dispatch_message(message); - } - CustomEvent::MessageReceived(message) => { - if let Message::InputPreprocessor(_) = &message { - if let Some(window) = &self.window { - window.request_redraw(); - } - } - if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message { - if let Some(graphic_state) = &mut self.graphics_state { - let window_size = self.window.as_ref().unwrap().inner_size(); - let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32); - let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size; - let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size; - let offset = top_left.to_array(); - let scale = (bottom_right - top_left).recip(); - graphic_state.set_viewport_offset(offset); - graphic_state.set_viewport_scale(scale.to_array()); - } else { - panic!("graphics state not intialized, viewport offset might be lost"); - } - } - - self.dispatch_message(message); - } - CustomEvent::NodeGraphRan(texture) => { - if let Some(texture) = texture - && let Some(graphics_state) = &mut self.graphics_state - { - graphics_state.bind_viewport_texture(texture); - } - let mut responses = VecDeque::new(); - let err = self.editor.poll_node_graph_evaluation(&mut responses); - if let Err(e) = err { - if e != "No active document" { - tracing::error!("Error poling node graph: {}", e); - } - } - - for message in responses { - self.dispatch_message(message); - } - } } } @@ -269,75 +237,75 @@ impl ApplicationHandler for WinitApp { match event { // Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881 - WindowEvent::DroppedFile(path) => { - let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()); - let Some(extension) = path.extension().and_then(|s| s.to_str()) else { - tracing::warn!("Unsupported file dropped: {}", path.display()); - // Fine to early return since we don't need to do cef work in this case - return; - }; - let load_string = |path: &std::path::PathBuf| { - let Ok(content) = fs::read_to_string(path) else { - tracing::error!("Failed to read file: {}", path.display()); - return None; - }; - - if content.is_empty() { - tracing::warn!("Dropped file is empty: {}", path.display()); - return None; - } - Some(content) - }; - // TODO: Consider moving this logic to the editor so we have one message to load data which is then demultiplexed in the portfolio message handler - match extension { - "graphite" => { - let Some(content) = load_string(&path) else { return }; - - let message = PortfolioMessage::OpenDocumentFile { - document_name: None, - document_path: Some(path), - document_serialized_content: content, - }; - self.dispatch_message(message.into()); - } - "svg" => { - let Some(content) = load_string(&path) else { return }; - - let message = PortfolioMessage::PasteSvg { - name: path.file_stem().map(|s| s.to_string_lossy().to_string()), - svg: content, - mouse: None, - parent_and_insert_index: None, - }; - self.dispatch_message(message.into()); - } - _ => match image::ImageReader::open(&path) { - Ok(reader) => match reader.decode() { - Ok(image) => { - let width = image.width(); - let height = image.height(); - // TODO: support loading images with more than 8 bits per channel - let image_data = image.to_rgba8(); - let image = Image::::from_image_data(image_data.as_raw(), width, height); - - let message = PortfolioMessage::PasteImage { - name, - image, - mouse: None, - parent_and_insert_index: None, - }; - self.dispatch_message(message.into()); - } - Err(e) => { - tracing::error!("Failed to decode image: {}: {}", path.display(), e); - } - }, - Err(e) => { - tracing::error!("Failed to open image file: {}: {}", path.display(), e); - } - }, - } - } + // WindowEvent::DroppedFile(path) => { + // let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()); + // let Some(extension) = path.extension().and_then(|s| s.to_str()) else { + // tracing::warn!("Unsupported file dropped: {}", path.display()); + // // Fine to early return since we don't need to do cef work in this case + // return; + // }; + // let load_string = |path: &std::path::PathBuf| { + // let Ok(content) = fs::read_to_string(path) else { + // tracing::error!("Failed to read file: {}", path.display()); + // return None; + // }; + + // if content.is_empty() { + // tracing::warn!("Dropped file is empty: {}", path.display()); + // return None; + // } + // Some(content) + // }; + // // TODO: Consider moving this logic to the editor so we have one message to load data which is then demultiplexed in the portfolio message handler + // match extension { + // "graphite" => { + // let Some(content) = load_string(&path) else { return }; + + // let message = PortfolioMessage::OpenDocumentFile { + // document_name: None, + // document_path: Some(path), + // document_serialized_content: content, + // }; + // self.dispatch_message(message.into()); + // } + // "svg" => { + // let Some(content) = load_string(&path) else { return }; + + // let message = PortfolioMessage::PasteSvg { + // name: path.file_stem().map(|s| s.to_string_lossy().to_string()), + // svg: content, + // mouse: None, + // parent_and_insert_index: None, + // }; + // self.dispatch_message(message.into()); + // } + // _ => match image::ImageReader::open(&path) { + // Ok(reader) => match reader.decode() { + // Ok(image) => { + // let width = image.width(); + // let height = image.height(); + // // TODO: support loading images with more than 8 bits per channel + // let image_data = image.to_rgba8(); + // let image = Image::::from_image_data(image_data.as_raw(), width, height); + + // let message = PortfolioMessage::PasteImage { + // name, + // image, + // mouse: None, + // parent_and_insert_index: None, + // }; + // self.dispatch_message(message.into()); + // } + // Err(e) => { + // tracing::error!("Failed to decode image: {}: {}", path.display(), e); + // } + // }, + // Err(e) => { + // tracing::error!("Failed to open image file: {}: {}", path.display(), e); + // } + // }, + // } + // } WindowEvent::CloseRequested => { tracing::info!("The close button was pressed; stopping"); event_loop.exit(); diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index af1e7ec73d..bed0c60e4e 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -1,4 +1,7 @@ -use crate::{CustomEvent, WgpuContext, render::FrameBufferRef}; +use crate::CustomEvent; +use crate::editor_api::WgpuContext; +use crate::editor_api::messages::EditorMessage; +use crate::render::FrameBufferRef; use std::{ sync::{Arc, Mutex, mpsc::Receiver}, time::Instant, @@ -121,14 +124,7 @@ impl CefEventHandler for CefHandler { } fn receive_web_message(&self, message: &[u8]) { - let str = std::str::from_utf8(message).unwrap(); - match ron::from_str(str) { - Ok(message) => { - let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived(message)); - } - Err(e) => { - tracing::error!("Failed to deserialize message {:?}", e) - } - } + let editor_message = EditorMessage::FromFrontend(message.to_vec()); + let _ = self.event_loop_proxy.send_event(CustomEvent::EditorMessage(editor_message)); } } diff --git a/desktop/src/dialogs.rs b/desktop/src/dialogs.rs deleted file mode 100644 index a4a70f0224..0000000000 --- a/desktop/src/dialogs.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; - -use rfd::AsyncFileDialog; - -pub(crate) async fn dialog_open_graphite_file() -> Option { - AsyncFileDialog::new() - .add_filter("Graphite", &["graphite"]) - .set_title("Open Graphite Document") - .pick_file() - .await - .map(|f| f.path().to_path_buf()) -} - -pub(crate) async fn dialog_save_graphite_file(name: String) -> Option { - AsyncFileDialog::new() - .add_filter("Graphite", &["graphite"]) - .set_title("Save Graphite Document") - .set_file_name(name) - .save_file() - .await - .map(|f| f.path().to_path_buf()) -} - -pub(crate) async fn dialog_save_file(name: String) -> Option { - AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf()) -} diff --git a/desktop/src/main.rs b/desktop/src/main.rs index ae8ac49a9a..7e6c94a560 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,8 +1,5 @@ use std::process::exit; -use std::time::Instant; -use std::{fmt::Debug, time::Duration}; - -use graphite_editor::messages::prelude::Message; +use std::time::{Duration, Instant}; use tracing_subscriber::EnvFilter; use winit::event_loop::EventLoop; @@ -12,24 +9,22 @@ mod cef; use cef::{Setup, WindowSize}; mod render; -use render::WgpuContext; mod app; use app::WinitApp; mod dirs; -mod dialogs; - mod editor_api; +use editor_api::messages::{EditorMessage, NativeMessage}; +use editor_api::{EditorApi, EditorWrapper}; -#[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), - DispatchMessage(Message), - MessageReceived(Message), - NodeGraphRan(Option), + NativeMessage(NativeMessage), + EditorMessage(EditorMessage), } fn main() { @@ -48,7 +43,7 @@ fn main() { let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel(); - let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap(); + let wgpu_context = futures::executor::block_on(editor_api::WgpuContext::new()).unwrap(); let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) { Ok(c) => c, Err(cef::InitError::AlreadyRunning) => { @@ -68,10 +63,12 @@ fn main() { std::thread::spawn(move || { loop { let last_render = Instant::now(); - let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); - if has_run { - let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan(texture.map(|t| (*t.texture).clone()))); + + let responses = EditorWrapper::poll(); + for response in responses.into_iter() { + let _ = rendering_loop_proxy.send_event(CustomEvent::NativeMessage(response)); } + let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); std::thread::sleep(sleep); diff --git a/desktop/src/render.rs b/desktop/src/render.rs index 0fe3ad7d0d..df03381f08 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -2,4 +2,4 @@ mod frame_buffer_ref; pub(crate) use frame_buffer_ref::FrameBufferRef; mod graphics_state; -pub(crate) use graphics_state::{GraphicsState, WgpuContext}; +pub(crate) use graphics_state::GraphicsState; diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index 85a9615315..a19d65af8b 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use wgpu_executor::WgpuExecutor; use winit::window::Window; -pub(crate) use wgpu_executor::Context as WgpuContext; +use crate::editor_api::WgpuContext; #[derive(derivative::Derivative)] #[derivative(Debug)] @@ -195,17 +195,14 @@ impl GraphicsState { pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) { self.viewport_texture = Some(viewport_texture); - self.update_bindgroup(); } pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) { self.overlays_texture = Some(overlays_texture); - self.update_bindgroup(); } pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) { self.ui_texture = Some(bind_ui_texture); - self.update_bindgroup(); } pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) { @@ -239,6 +236,8 @@ impl GraphicsState { self.render_overlays(scene); } + self.update_bindgroup(); + let output = self.surface.get_current_texture()?; let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); From 02e838ab63afe26da4d47e68724f2db2ca326854 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Fri, 15 Aug 2025 04:56:31 +0000 Subject: [PATCH 13/31] Fix texture life time crashes --- desktop/src/app.rs | 4 +++- desktop/src/editor_api/editor_wrapper.rs | 10 ++++++---- desktop/src/render/graphics_state.rs | 5 +++-- node-graph/gapplication-io/src/lib.rs | 2 +- node-graph/gstd/src/wasm_application_io.rs | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 82324d23f0..4408af1b02 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -38,7 +38,7 @@ pub(crate) struct WinitApp { impl WinitApp { pub(crate) fn new(cef_context: cef::Context, window_size_sender: Sender, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy) -> Self { - let editor_wrapper = EditorWrapper::new(wgpu_context.clone()); + let editor_wrapper = EditorWrapper::new(); Self { cef_context, window: None, @@ -200,6 +200,8 @@ impl ApplicationHandler for WinitApp { self.graphics_state = Some(graphics_state); tracing::info!("Winit window created and ready"); + + self.editor_wrapper.resume(self.wgpu_context.clone()); } fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index 64123726f3..b179f0860d 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -36,7 +36,7 @@ impl EditorApi for EditorWrapper { responses.push(NativeMessage::Loopback(EditorMessage::PoolNodeGraphEvaluation)); } if let Some(texture) = texture { - responses.push(NativeMessage::UpdateViewport((*texture.texture).clone())); + responses.push(NativeMessage::UpdateViewport(texture.texture)); responses.push(NativeMessage::RequestRedraw); } @@ -45,11 +45,13 @@ impl EditorApi for EditorWrapper { } impl EditorWrapper { - pub fn new(wgpu_context: WgpuContext) -> Self { + pub fn new() -> Self { + Self { editor: Editor::new() } + } + + pub fn resume(&self, wgpu_context: WgpuContext) { let application_io = WasmApplicationIo::new_with_context(wgpu_context); futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); - - Self { editor: Editor::new() } } fn handle_message(&mut self, message: Message, responses: &mut Vec) { diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index a19d65af8b..537045878f 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -195,14 +195,17 @@ impl GraphicsState { pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) { self.viewport_texture = Some(viewport_texture); + self.update_bindgroup(); } pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) { self.overlays_texture = Some(overlays_texture); + self.update_bindgroup(); } pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) { self.ui_texture = Some(bind_ui_texture); + self.update_bindgroup(); } pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) { @@ -236,8 +239,6 @@ impl GraphicsState { self.render_overlays(scene); } - self.update_bindgroup(); - let output = self.surface.get_current_texture()?; let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); diff --git a/node-graph/gapplication-io/src/lib.rs b/node-graph/gapplication-io/src/lib.rs index d53f50cf25..b371b95e1c 100644 --- a/node-graph/gapplication-io/src/lib.rs +++ b/node-graph/gapplication-io/src/lib.rs @@ -52,7 +52,7 @@ impl Size for web_sys::HtmlCanvasElement { #[derive(Debug, Clone)] pub struct ImageTexture { #[cfg(feature = "wgpu")] - pub texture: Arc, + pub texture: wgpu::Texture, #[cfg(not(feature = "wgpu"))] pub texture: (), } diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 80b73abbb4..91771417a4 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -209,7 +209,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl Render, editor: & .await .expect("Failed to render Vello scene"); - RenderOutputType::Texture(ImageTexture { texture: Arc::new(texture) }) + RenderOutputType::Texture(ImageTexture { texture }) } } From 1671c758d767f8bbd9e67257ded86f21e705a8a7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Fri, 15 Aug 2025 05:40:40 +0000 Subject: [PATCH 14/31] Fix scale --- desktop/src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 4408af1b02..bac6eb5098 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -132,8 +132,8 @@ impl WinitApp { let viewport_offset_y = y / window_size.height as f32; graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]); - let viewport_scale_x = width / window_size.width as f32; - let viewport_scale_y = height / window_size.height as f32; + let viewport_scale_x = if width != 0.0 { window_size.width as f32 / width } else { 1.0 }; + let viewport_scale_y = if height != 0.0 { window_size.height as f32 / height } else { 1.0 }; graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]); } } From f1c2d411efc8b99de167a43c23592e26898c891a Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Fri, 15 Aug 2025 18:40:01 +0000 Subject: [PATCH 15/31] Implement editor wrapper message queue --- desktop/src/app.rs | 11 ++-- desktop/src/editor_api/editor_wrapper.rs | 50 +++++++++++++++---- .../src/editor_api/handle_editor_message.rs | 23 +++++---- .../editor_api/intercept_frontend_message.rs | 28 ++++++----- desktop/src/editor_api/messages.rs | 13 +++-- .../document/document_message_handler.rs | 4 ++ 6 files changed, 86 insertions(+), 43 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index bac6eb5098..e36d680d11 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -79,7 +79,6 @@ impl WinitApp { default_filename, default_folder, filters, - content, context, } => { let event_loop_proxy = self.event_loop_proxy.clone(); @@ -95,16 +94,16 @@ impl WinitApp { let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) }; if let Some(path) = futures::executor::block_on(show_dialog) { - if let Err(e) = std::fs::write(&path, content) { - tracing::error!("Failed to write file: {}: {}", path.display(), e); - return; - } - let message = EditorMessage::SaveFileDialogResult { path, context }; let _ = event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); } }); } + NativeMessage::WriteFile { path, content } => { + if let Err(e) = std::fs::write(&path, content) { + tracing::error!("Failed to write file {}: {}", path.display(), e); + } + } NativeMessage::OpenUrl(url) => { let _ = thread::spawn(move || { if let Err(e) = open::that(&url) { diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index b179f0860d..fd95ff5043 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -15,17 +15,29 @@ mod intercept_message; pub struct EditorWrapper { editor: Editor, + queue: VecDeque, +} + +#[allow(clippy::large_enum_variant)] +enum QueuedMessage { + Message(Message), + EditorMessage(EditorMessage), } impl EditorApi for EditorWrapper { fn dispatch(&mut self, message: EditorMessage) -> Vec { + self.queue_editor_message(message); + let mut responses = Vec::new(); - handle_editor_message::handle_editor_message(self, message, &mut responses); - let mut native_responses = Vec::new(); - for message in responses.drain(..) { - self.handle_message(message, &mut native_responses); + + while let Some(queued_message) = self.queue.pop_front() { + match queued_message { + QueuedMessage::Message(message) => self.handle_message(message, &mut responses), + QueuedMessage::EditorMessage(editor_message) => self.handle_editor_message(editor_message, &mut responses), + } } - native_responses + + responses } fn poll() -> Vec { @@ -46,7 +58,10 @@ impl EditorApi for EditorWrapper { impl EditorWrapper { pub fn new() -> Self { - Self { editor: Editor::new() } + Self { + editor: Editor::new(), + queue: VecDeque::new(), + } } pub fn resume(&self, wgpu_context: WgpuContext) { @@ -65,14 +80,27 @@ impl EditorWrapper { } } - pub(super) fn poll_node_graph_evaluation(&mut self, responses: &mut Vec) { - let mut node_graph_responses = VecDeque::new(); - let err = self.editor.poll_node_graph_evaluation(&mut node_graph_responses); - if let Err(e) = err { + fn handle_editor_message(&mut self, message: EditorMessage, responses: &mut Vec) { + handle_editor_message::handle_editor_message(self, message, responses); + } + + pub(super) fn queue_editor_message(&mut self, message: EditorMessage) { + self.queue.push_back(QueuedMessage::EditorMessage(message)); + } + + pub(super) fn queue_message(&mut self, message: Message) { + self.queue.push_back(QueuedMessage::Message(message)); + } + + pub(super) fn poll_node_graph_evaluation(&mut self) { + let mut responses = VecDeque::new(); + if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) { if e != "No active document" { tracing::error!("Error poling node graph: {}", e); } } - responses.extend(node_graph_responses.drain(..)); + while let Some(message) = responses.pop_front() { + self.queue_message(message); + } } } diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/editor_api/handle_editor_message.rs index 4b27dec416..170419296c 100644 --- a/desktop/src/editor_api/handle_editor_message.rs +++ b/desktop/src/editor_api/handle_editor_message.rs @@ -1,16 +1,16 @@ use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; -use crate::editor_api::messages::{EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; +use crate::editor_api::messages::{EditorMessage, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; use super::EditorWrapper; -pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage, responses: &mut Vec) { +pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage, responses: &mut Vec) { match message { EditorMessage::FromFrontend(data) => { let string = std::str::from_utf8(&data).unwrap(); match ron::from_str::(string) { Ok(message) => { - responses.push(message); + editor_wrapper.queue_message(message); } Err(e) => { tracing::error!("Failed to deserialize message {:?}", e) @@ -20,7 +20,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage::OpenFileDialogResult { path, content, context } => match context { OpenFileDialogContext::Document => match String::from_utf8(content) { Ok(content) => { - responses.push( + editor_wrapper.queue_message( PortfolioMessage::OpenDocumentFile { document_name: None, document_path: Some(path), @@ -39,7 +39,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: match extension { Some("svg") => match String::from_utf8(content) { Ok(content) if !content.is_empty() => { - responses.push( + editor_wrapper.queue_message( PortfolioMessage::PasteSvg { name, svg: content, @@ -65,7 +65,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: let image_data = image.to_rgba8(); let image = graphene_std::raster::Image::::from_image_data(image_data.as_raw(), width, height); - responses.push( + editor_wrapper.queue_message( PortfolioMessage::PasteImage { name, image, @@ -87,14 +87,17 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: } }, EditorMessage::SaveFileDialogResult { path, context } => match context { - SaveFileDialogContext::Document { document_id } => { - responses.push(Message::Portfolio(PortfolioMessage::DocumentPassMessage { + SaveFileDialogContext::Document { document_id, content } => { + responses.push(NativeMessage::WriteFile { path: path.clone(), content }); + editor_wrapper.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::SavedDocument { path: Some(path) }, })); } - SaveFileDialogContext::Export => {} + SaveFileDialogContext::Export { content } => { + responses.push(NativeMessage::WriteFile { path, content }); + } }, - EditorMessage::PoolNodeGraphEvaluation => editor_wrapper.poll_node_graph_evaluation(responses), + EditorMessage::PoolNodeGraphEvaluation => editor_wrapper.poll_node_graph_evaluation(), } } diff --git a/desktop/src/editor_api/intercept_frontend_message.rs b/desktop/src/editor_api/intercept_frontend_message.rs index 1444545fc0..27ddc12cc4 100644 --- a/desktop/src/editor_api/intercept_frontend_message.rs +++ b/desktop/src/editor_api/intercept_frontend_message.rs @@ -36,17 +36,20 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m }); } FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { - responses.push(NativeMessage::SaveFileDialog { - title: "Save Document".to_string(), - default_filename: name, - default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), - filters: vec![FileFilter { - name: "Graphite".to_string(), - extensions: vec!["graphite".to_string()], - }], - content, - context: SaveFileDialogContext::Document { document_id }, - }); + if let Some(path) = path { + responses.push(NativeMessage::WriteFile { path, content }); + } else { + responses.push(NativeMessage::SaveFileDialog { + title: "Save Document".to_string(), + default_filename: name, + default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), + filters: vec![FileFilter { + name: "Graphite".to_string(), + extensions: vec!["graphite".to_string()], + }], + context: SaveFileDialogContext::Document { document_id, content }, + }); + } } FrontendMessage::TriggerSaveFile { name, content } => { responses.push(NativeMessage::SaveFileDialog { @@ -54,8 +57,7 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m default_filename: name, default_folder: None, filters: Vec::new(), - content, - context: SaveFileDialogContext::Export, + context: SaveFileDialogContext::Export { content }, }); } FrontendMessage::TriggerVisitLink { url } => { diff --git a/desktop/src/editor_api/messages.rs b/desktop/src/editor_api/messages.rs index 5f52f588f0..7c246df79a 100644 --- a/desktop/src/editor_api/messages.rs +++ b/desktop/src/editor_api/messages.rs @@ -2,6 +2,10 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::DocumentId; +pub enum WrappedMessage { + x, +} + pub enum NativeMessage { ToFrontend(Vec), OpenFileDialog { @@ -14,9 +18,12 @@ pub enum NativeMessage { default_filename: String, default_folder: Option, filters: Vec, - content: Vec, context: SaveFileDialogContext, }, + WriteFile { + path: PathBuf, + content: Vec, + }, OpenUrl(String), RequestRedraw, UpdateViewport(wgpu::Texture), @@ -48,6 +55,6 @@ pub enum OpenFileDialogContext { } pub enum SaveFileDialogContext { - Document { document_id: DocumentId }, - Export, + Document { document_id: DocumentId, content: Vec }, + Export { content: Vec }, } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 79d8ab1a36..7dec9ec06e 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1023,6 +1023,8 @@ impl MessageHandler> for DocumentMes }); } DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => { + dbg!(&self.path); + if let DocumentMessage::SaveDocumentAs = message { self.path = None; } @@ -1042,6 +1044,8 @@ impl MessageHandler> for DocumentMes DocumentMessage::SavedDocument { path } => { self.path = path; + dbg!(&self.path); + // Update the name to match the file stem let document_name_from_path = self.path.as_ref().and_then(|path| { if path.extension().is_some_and(|e| e == FILE_EXTENSION) { From 91c96ffb8a2a45ccdfa53449978ce09c51cf4544 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 16 Aug 2025 13:14:16 +0000 Subject: [PATCH 16/31] Improve performance --- desktop/src/editor_api/editor_wrapper.rs | 53 +++++++++---------- .../src/editor_api/handle_editor_message.rs | 6 +-- desktop/src/editor_api/messages.rs | 4 -- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index fd95ff5043..cb194b78d3 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -15,29 +15,31 @@ mod intercept_message; pub struct EditorWrapper { editor: Editor, - queue: VecDeque, -} - -#[allow(clippy::large_enum_variant)] -enum QueuedMessage { - Message(Message), - EditorMessage(EditorMessage), + queue: VecDeque, + messages: Vec, + responses: Vec, } impl EditorApi for EditorWrapper { fn dispatch(&mut self, message: EditorMessage) -> Vec { self.queue_editor_message(message); - let mut responses = Vec::new(); - - while let Some(queued_message) = self.queue.pop_front() { - match queued_message { - QueuedMessage::Message(message) => self.handle_message(message, &mut responses), - QueuedMessage::EditorMessage(editor_message) => self.handle_editor_message(editor_message, &mut responses), + while !self.queue.is_empty() { + while let Some(message) = self.queue.pop_front() { + self.handle_editor_message(message); } + let frontend_messages = self + .editor + .handle_message(Message::Batched { + messages: std::mem::take(&mut self.messages).into_boxed_slice(), + }) + .into_iter() + .filter_map(|m| intercept_frontend_message::intercept_frontend_message(m, &mut self.responses)) + .collect::>(); + self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); } - responses + std::mem::take(&mut self.responses) } fn poll() -> Vec { @@ -61,6 +63,8 @@ impl EditorWrapper { Self { editor: Editor::new(), queue: VecDeque::new(), + messages: Vec::new(), + responses: Vec::new(), } } @@ -69,27 +73,22 @@ impl EditorWrapper { futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); } - fn handle_message(&mut self, message: Message, responses: &mut Vec) { - if let Some(message) = intercept_message::intercept_message(message, responses) { - let messages = self.editor.handle_message(message); - let frontend_messages = messages - .into_iter() - .filter_map(|m| intercept_frontend_message::intercept_frontend_message(m, responses)) - .collect::>(); - responses.push(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); - } + fn handle_editor_message(&mut self, message: EditorMessage) { + handle_editor_message::handle_editor_message(self, message); } - fn handle_editor_message(&mut self, message: EditorMessage, responses: &mut Vec) { - handle_editor_message::handle_editor_message(self, message, responses); + pub fn respond(&mut self, response: NativeMessage) { + self.responses.push(response); } pub(super) fn queue_editor_message(&mut self, message: EditorMessage) { - self.queue.push_back(QueuedMessage::EditorMessage(message)); + self.queue.push_back(message); } pub(super) fn queue_message(&mut self, message: Message) { - self.queue.push_back(QueuedMessage::Message(message)); + if let Some(message) = intercept_message::intercept_message(message, &mut self.responses) { + self.messages.push(message); + } } pub(super) fn poll_node_graph_evaluation(&mut self) { diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/editor_api/handle_editor_message.rs index 170419296c..961c9fe01f 100644 --- a/desktop/src/editor_api/handle_editor_message.rs +++ b/desktop/src/editor_api/handle_editor_message.rs @@ -4,7 +4,7 @@ use crate::editor_api::messages::{EditorMessage, NativeMessage, OpenFileDialogCo use super::EditorWrapper; -pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage, responses: &mut Vec) { +pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage) { match message { EditorMessage::FromFrontend(data) => { let string = std::str::from_utf8(&data).unwrap(); @@ -88,14 +88,14 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: }, EditorMessage::SaveFileDialogResult { path, context } => match context { SaveFileDialogContext::Document { document_id, content } => { - responses.push(NativeMessage::WriteFile { path: path.clone(), content }); + editor_wrapper.respond(NativeMessage::WriteFile { path: path.clone(), content }); editor_wrapper.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::SavedDocument { path: Some(path) }, })); } SaveFileDialogContext::Export { content } => { - responses.push(NativeMessage::WriteFile { path, content }); + editor_wrapper.respond(NativeMessage::WriteFile { path, content }); } }, EditorMessage::PoolNodeGraphEvaluation => editor_wrapper.poll_node_graph_evaluation(), diff --git a/desktop/src/editor_api/messages.rs b/desktop/src/editor_api/messages.rs index 7c246df79a..7c123bca40 100644 --- a/desktop/src/editor_api/messages.rs +++ b/desktop/src/editor_api/messages.rs @@ -2,10 +2,6 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::DocumentId; -pub enum WrappedMessage { - x, -} - pub enum NativeMessage { ToFrontend(Vec), OpenFileDialog { From 72eeb8f7fca93a43b2f2a58d8c6a49e0c0c50d4d Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 16 Aug 2025 14:26:33 +0000 Subject: [PATCH 17/31] Handle native messages directly without submitting to event loop --- desktop/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index e36d680d11..070c2b8055 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -211,7 +211,7 @@ impl ApplicationHandler for WinitApp { CustomEvent::EditorMessage(message) => { let responses = self.editor_wrapper.dispatch(message); for response in responses { - let _ = self.event_loop_proxy.send_event(CustomEvent::NativeMessage(response)); + self.handle_native_message(response); } } CustomEvent::UiUpdate(texture) => { From 85535a87fa17b5ec269a235b8e58b515c1a729da Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 17 Aug 2025 18:57:07 +0000 Subject: [PATCH 18/31] Fix overlay latency --- desktop/src/app.rs | 24 +++++++++++++----------- desktop/src/main.rs | 6 ++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 070c2b8055..64ae07099c 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -145,8 +145,17 @@ impl WinitApp { } } - fn dispatch_editor_message(&self, message: EditorMessage) { - let _ = self.event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); + fn handle_native_messages(&mut self, messages: Vec) { + for message in messages { + self.handle_native_message(message); + } + } + + fn dispatch_editor_message(&mut self, message: EditorMessage) { + let responses = self.editor_wrapper.dispatch(message); + for response in responses { + self.handle_native_message(response); + } } } @@ -205,15 +214,8 @@ impl ApplicationHandler for WinitApp { fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { match event { - CustomEvent::NativeMessage(message) => { - self.handle_native_message(message); - } - CustomEvent::EditorMessage(message) => { - let responses = self.editor_wrapper.dispatch(message); - for response in responses { - self.handle_native_message(response); - } - } + CustomEvent::NativeMessages(messages) => self.handle_native_messages(messages), + CustomEvent::EditorMessage(message) => self.dispatch_editor_message(message), CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { graphics_state.resize(texture.width(), texture.height()); diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 7e6c94a560..6ce2829d51 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -23,7 +23,7 @@ use editor_api::{EditorApi, EditorWrapper}; pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), - NativeMessage(NativeMessage), + NativeMessages(Vec), EditorMessage(EditorMessage), } @@ -65,9 +65,7 @@ fn main() { let last_render = Instant::now(); let responses = EditorWrapper::poll(); - for response in responses.into_iter() { - let _ = rendering_loop_proxy.send_event(CustomEvent::NativeMessage(response)); - } + let _ = rendering_loop_proxy.send_event(CustomEvent::NativeMessages(responses)); let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); From cc8034f89ff799022d69c4a17f153d533ce0c595 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 08:58:48 +0000 Subject: [PATCH 19/31] Move editor message execution to executor allows no shared state in editor wrapper --- desktop/src/app.rs | 3 +- desktop/src/editor_api.rs | 7 -- desktop/src/editor_api/editor_wrapper.rs | 82 +++++++++++-------- .../src/editor_api/handle_editor_message.rs | 20 ++--- .../editor_api/intercept_frontend_message.rs | 18 ++-- desktop/src/editor_api/intercept_message.rs | 8 +- desktop/src/main.rs | 2 +- 7 files changed, 76 insertions(+), 64 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 64ae07099c..4b2b12024b 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,7 +1,6 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; -use crate::editor_api::EditorApi; use crate::editor_api::EditorWrapper; use crate::editor_api::WgpuContext; use crate::editor_api::messages::EditorMessage; @@ -209,7 +208,7 @@ impl ApplicationHandler for WinitApp { tracing::info!("Winit window created and ready"); - self.editor_wrapper.resume(self.wgpu_context.clone()); + self.editor_wrapper.init(self.wgpu_context.clone()); } fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { diff --git a/desktop/src/editor_api.rs b/desktop/src/editor_api.rs index d422417dbb..673ad18e7e 100644 --- a/desktop/src/editor_api.rs +++ b/desktop/src/editor_api.rs @@ -2,11 +2,4 @@ mod editor_wrapper; pub use editor_wrapper::EditorWrapper; pub mod messages; -use messages::{EditorMessage, NativeMessage}; - pub use wgpu_executor::Context as WgpuContext; - -pub trait EditorApi { - fn dispatch(&mut self, message: EditorMessage) -> Vec; - fn poll() -> Vec; -} diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index cb194b78d3..f5fc310c59 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -2,8 +2,8 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::{application::Editor, messages::prelude::Message}; use std::collections::VecDeque; +use crate::editor_api::WgpuContext; use crate::editor_api::messages::{EditorMessage, NativeMessage}; -use crate::editor_api::{EditorApi, WgpuContext}; #[path = "handle_editor_message.rs"] mod handle_editor_message; @@ -15,34 +15,25 @@ mod intercept_message; pub struct EditorWrapper { editor: Editor, - queue: VecDeque, - messages: Vec, - responses: Vec, } -impl EditorApi for EditorWrapper { - fn dispatch(&mut self, message: EditorMessage) -> Vec { - self.queue_editor_message(message); +impl EditorWrapper { + pub fn new() -> Self { + Self { editor: Editor::new() } + } - while !self.queue.is_empty() { - while let Some(message) = self.queue.pop_front() { - self.handle_editor_message(message); - } - let frontend_messages = self - .editor - .handle_message(Message::Batched { - messages: std::mem::take(&mut self.messages).into_boxed_slice(), - }) - .into_iter() - .filter_map(|m| intercept_frontend_message::intercept_frontend_message(m, &mut self.responses)) - .collect::>(); - self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); - } + pub fn init(&self, wgpu_context: WgpuContext) { + let application_io = WasmApplicationIo::new_with_context(wgpu_context); + futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); + } - std::mem::take(&mut self.responses) + pub fn dispatch(&mut self, message: EditorMessage) -> Vec { + let mut executor = EditorMessageExecutor::new(&mut self.editor); + executor.execute(message); + executor.responses() } - fn poll() -> Vec { + pub fn poll() -> Vec { let mut responses = Vec::new(); let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); @@ -58,35 +49,60 @@ impl EditorApi for EditorWrapper { } } -impl EditorWrapper { - pub fn new() -> Self { +struct EditorMessageExecutor<'a> { + editor: &'a mut Editor, + queue: VecDeque, + messages: Vec, + responses: Vec, +} + +impl<'a> EditorMessageExecutor<'a> { + pub(crate) fn new(editor: &'a mut Editor) -> Self { Self { - editor: Editor::new(), + editor, queue: VecDeque::new(), messages: Vec::new(), responses: Vec::new(), } } - pub fn resume(&self, wgpu_context: WgpuContext) { - let application_io = WasmApplicationIo::new_with_context(wgpu_context); - futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); + pub(crate) fn execute(&mut self, message: EditorMessage) { + self.queue.push_back(message); + self.process_queue(); } - fn handle_editor_message(&mut self, message: EditorMessage) { - handle_editor_message::handle_editor_message(self, message); + pub(crate) fn responses(self) -> Vec { + self.responses + } + + fn process_queue(&mut self) { + while !self.queue.is_empty() || !self.messages.is_empty() { + while let Some(message) = self.queue.pop_front() { + handle_editor_message::handle_editor_message(self, message); + } + let frontend_messages = self + .editor + .handle_message(Message::Batched { + messages: std::mem::take(&mut self.messages).into_boxed_slice(), + }) + .into_iter() + .filter_map(|m| intercept_frontend_message::intercept_frontend_message(self, m)) + .collect::>(); + self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); + } } - pub fn respond(&mut self, response: NativeMessage) { + pub(super) fn respond(&mut self, response: NativeMessage) { self.responses.push(response); } + #[allow(dead_code)] // will be used for features in the future pub(super) fn queue_editor_message(&mut self, message: EditorMessage) { self.queue.push_back(message); } pub(super) fn queue_message(&mut self, message: Message) { - if let Some(message) = intercept_message::intercept_message(message, &mut self.responses) { + if let Some(message) = intercept_message::intercept_message(self, message) { self.messages.push(message); } } diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/editor_api/handle_editor_message.rs index 961c9fe01f..c258e66dda 100644 --- a/desktop/src/editor_api/handle_editor_message.rs +++ b/desktop/src/editor_api/handle_editor_message.rs @@ -2,15 +2,15 @@ use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMess use crate::editor_api::messages::{EditorMessage, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; -use super::EditorWrapper; +use super::EditorMessageExecutor; -pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage) { +pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, message: EditorMessage) { match message { EditorMessage::FromFrontend(data) => { let string = std::str::from_utf8(&data).unwrap(); match ron::from_str::(string) { Ok(message) => { - editor_wrapper.queue_message(message); + executor.queue_message(message); } Err(e) => { tracing::error!("Failed to deserialize message {:?}", e) @@ -20,7 +20,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: EditorMessage::OpenFileDialogResult { path, content, context } => match context { OpenFileDialogContext::Document => match String::from_utf8(content) { Ok(content) => { - editor_wrapper.queue_message( + executor.queue_message( PortfolioMessage::OpenDocumentFile { document_name: None, document_path: Some(path), @@ -39,7 +39,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: match extension { Some("svg") => match String::from_utf8(content) { Ok(content) if !content.is_empty() => { - editor_wrapper.queue_message( + executor.queue_message( PortfolioMessage::PasteSvg { name, svg: content, @@ -65,7 +65,7 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: let image_data = image.to_rgba8(); let image = graphene_std::raster::Image::::from_image_data(image_data.as_raw(), width, height); - editor_wrapper.queue_message( + executor.queue_message( PortfolioMessage::PasteImage { name, image, @@ -88,16 +88,16 @@ pub(super) fn handle_editor_message(editor_wrapper: &mut EditorWrapper, message: }, EditorMessage::SaveFileDialogResult { path, context } => match context { SaveFileDialogContext::Document { document_id, content } => { - editor_wrapper.respond(NativeMessage::WriteFile { path: path.clone(), content }); - editor_wrapper.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { + executor.respond(NativeMessage::WriteFile { path: path.clone(), content }); + executor.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::SavedDocument { path: Some(path) }, })); } SaveFileDialogContext::Export { content } => { - editor_wrapper.respond(NativeMessage::WriteFile { path, content }); + executor.respond(NativeMessage::WriteFile { path, content }); } }, - EditorMessage::PoolNodeGraphEvaluation => editor_wrapper.poll_node_graph_evaluation(), + EditorMessage::PoolNodeGraphEvaluation => executor.poll_node_graph_evaluation(), } } diff --git a/desktop/src/editor_api/intercept_frontend_message.rs b/desktop/src/editor_api/intercept_frontend_message.rs index 27ddc12cc4..7bd9d834d2 100644 --- a/desktop/src/editor_api/intercept_frontend_message.rs +++ b/desktop/src/editor_api/intercept_frontend_message.rs @@ -4,13 +4,15 @@ use graphite_editor::messages::prelude::FrontendMessage; use crate::editor_api::messages::{FileFilter, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; -pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &mut Vec) -> Option { +use super::EditorMessageExecutor; + +pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, message: FrontendMessage) -> Option { match message { FrontendMessage::RenderOverlays(overlay_context) => { - responses.push(NativeMessage::UpdateOverlays(overlay_context.take_scene())); + executor.respond(NativeMessage::UpdateOverlays(overlay_context.take_scene())); } FrontendMessage::TriggerOpenDocument => { - responses.push(NativeMessage::OpenFileDialog { + executor.respond(NativeMessage::OpenFileDialog { title: "Open Document".to_string(), filters: vec![FileFilter { name: "Graphite".to_string(), @@ -20,7 +22,7 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m }); } FrontendMessage::TriggerImport => { - responses.push(NativeMessage::OpenFileDialog { + executor.respond(NativeMessage::OpenFileDialog { title: "Import File".to_string(), filters: vec![ FileFilter { @@ -37,9 +39,9 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m } FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { if let Some(path) = path { - responses.push(NativeMessage::WriteFile { path, content }); + executor.respond(NativeMessage::WriteFile { path, content }); } else { - responses.push(NativeMessage::SaveFileDialog { + executor.respond(NativeMessage::SaveFileDialog { title: "Save Document".to_string(), default_filename: name, default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), @@ -52,7 +54,7 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m } } FrontendMessage::TriggerSaveFile { name, content } => { - responses.push(NativeMessage::SaveFileDialog { + executor.respond(NativeMessage::SaveFileDialog { title: "Save File".to_string(), default_filename: name, default_folder: None, @@ -61,7 +63,7 @@ pub(super) fn intercept_frontend_message(message: FrontendMessage, responses: &m }); } FrontendMessage::TriggerVisitLink { url } => { - responses.push(NativeMessage::OpenUrl(url)); + executor.respond(NativeMessage::OpenUrl(url)); } m => return Some(m), } diff --git a/desktop/src/editor_api/intercept_message.rs b/desktop/src/editor_api/intercept_message.rs index 0834efe004..68a4f8fd6d 100644 --- a/desktop/src/editor_api/intercept_message.rs +++ b/desktop/src/editor_api/intercept_message.rs @@ -2,20 +2,22 @@ use graphite_editor::messages::prelude::{InputPreprocessorMessage, Message}; use crate::editor_api::messages::NativeMessage; -pub(super) fn intercept_message(message: Message, responses: &mut Vec) -> Option { +use super::EditorMessageExecutor; + +pub(super) fn intercept_message(executor: &mut EditorMessageExecutor, message: Message) -> Option { match message { Message::InputPreprocessor(message) => { if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { let top_left = bounds_of_viewports[0].top_left; let bottom_right = bounds_of_viewports[0].bottom_right; - responses.push(NativeMessage::UpdateViewportBounds { + executor.respond(NativeMessage::UpdateViewportBounds { x: top_left.x as f32, y: top_left.y as f32, width: (bottom_right.x - top_left.x) as f32, height: (bottom_right.y - top_left.y) as f32, }); } - responses.push(NativeMessage::RequestRedraw); + executor.respond(NativeMessage::RequestRedraw); Some(Message::InputPreprocessor(message)) } m => Some(m), diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 6ce2829d51..73143c8484 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -16,8 +16,8 @@ use app::WinitApp; mod dirs; mod editor_api; +use editor_api::EditorWrapper; use editor_api::messages::{EditorMessage, NativeMessage}; -use editor_api::{EditorApi, EditorWrapper}; #[allow(clippy::large_enum_variant)] pub(crate) enum CustomEvent { From 6a5e561b7adbc76107462ffca18ef88f1d88e970 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 09:03:14 +0000 Subject: [PATCH 20/31] Small clean up --- desktop/src/app.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 4b2b12024b..702bcc8fd7 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -152,9 +152,7 @@ impl WinitApp { fn dispatch_editor_message(&mut self, message: EditorMessage) { let responses = self.editor_wrapper.dispatch(message); - for response in responses { - self.handle_native_message(response); - } + self.handle_native_messages(responses); } } From ab70daf856d51055c604ac81c7148f3923edf90d Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 14:39:58 +0000 Subject: [PATCH 21/31] Small cleanup --- desktop/src/editor_api/editor_wrapper.rs | 55 +++++++++++------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/editor_api/editor_wrapper.rs index f5fc310c59..ba92143fb4 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/editor_api/editor_wrapper.rs @@ -29,8 +29,8 @@ impl EditorWrapper { pub fn dispatch(&mut self, message: EditorMessage) -> Vec { let mut executor = EditorMessageExecutor::new(&mut self.editor); - executor.execute(message); - executor.responses() + executor.queue(message); + executor.execute() } pub fn poll() -> Vec { @@ -66,38 +66,12 @@ impl<'a> EditorMessageExecutor<'a> { } } - pub(crate) fn execute(&mut self, message: EditorMessage) { - self.queue.push_back(message); + pub(crate) fn execute(mut self) -> Vec { self.process_queue(); - } - - pub(crate) fn responses(self) -> Vec { self.responses } - fn process_queue(&mut self) { - while !self.queue.is_empty() || !self.messages.is_empty() { - while let Some(message) = self.queue.pop_front() { - handle_editor_message::handle_editor_message(self, message); - } - let frontend_messages = self - .editor - .handle_message(Message::Batched { - messages: std::mem::take(&mut self.messages).into_boxed_slice(), - }) - .into_iter() - .filter_map(|m| intercept_frontend_message::intercept_frontend_message(self, m)) - .collect::>(); - self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); - } - } - - pub(super) fn respond(&mut self, response: NativeMessage) { - self.responses.push(response); - } - - #[allow(dead_code)] // will be used for features in the future - pub(super) fn queue_editor_message(&mut self, message: EditorMessage) { + pub(crate) fn queue(&mut self, message: EditorMessage) { self.queue.push_back(message); } @@ -107,6 +81,10 @@ impl<'a> EditorMessageExecutor<'a> { } } + pub(super) fn respond(&mut self, response: NativeMessage) { + self.responses.push(response); + } + pub(super) fn poll_node_graph_evaluation(&mut self) { let mut responses = VecDeque::new(); if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) { @@ -118,4 +96,21 @@ impl<'a> EditorMessageExecutor<'a> { self.queue_message(message); } } + + fn process_queue(&mut self) { + while !self.queue.is_empty() || !self.messages.is_empty() { + while let Some(message) = self.queue.pop_front() { + handle_editor_message::handle_editor_message(self, message); + } + let frontend_messages = self + .editor + .handle_message(Message::Batched { + messages: std::mem::take(&mut self.messages).into_boxed_slice(), + }) + .into_iter() + .filter_map(|m| intercept_frontend_message::intercept_frontend_message(self, m)) + .collect::>(); + self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); + } + } } From 5182db7dcd2f35d6773dcfcbfa43bfba3c3478b0 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 15:58:51 +0000 Subject: [PATCH 22/31] Some renames --- desktop/src/app.rs | 50 +++++++++---------- desktop/src/cef.rs | 8 +-- .../src/{editor_api.rs => desktop_wrapper.rs} | 0 .../editor_wrapper.rs | 26 +++++----- .../handle_editor_message.rs | 16 +++--- .../intercept_frontend_message.rs | 16 +++--- .../intercept_message.rs | 6 +-- .../messages.rs | 12 ++--- desktop/src/main.rs | 14 +++--- desktop/src/render/graphics_state.rs | 2 +- 10 files changed, 75 insertions(+), 75 deletions(-) rename desktop/src/{editor_api.rs => desktop_wrapper.rs} (100%) rename desktop/src/{editor_api => desktop_wrapper}/editor_wrapper.rs (73%) rename desktop/src/{editor_api => desktop_wrapper}/handle_editor_message.rs (80%) rename desktop/src/{editor_api => desktop_wrapper}/intercept_frontend_message.rs (75%) rename desktop/src/{editor_api => desktop_wrapper}/intercept_message.rs (79%) rename desktop/src/{editor_api => desktop_wrapper}/messages.rs (85%) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 702bcc8fd7..7f9c846fcd 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,10 +1,10 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; -use crate::editor_api::EditorWrapper; -use crate::editor_api::WgpuContext; -use crate::editor_api::messages::EditorMessage; -use crate::editor_api::messages::NativeMessage; +use crate::desktop_wrapper::EditorWrapper; +use crate::desktop_wrapper::WgpuContext; +use crate::desktop_wrapper::messages::DesktopFrontendMessage; +use crate::desktop_wrapper::messages::DesktopWrapperMessage; use crate::render::GraphicsState; use rfd::AsyncFileDialog; use std::sync::Arc; @@ -50,12 +50,12 @@ impl WinitApp { } } - fn handle_native_message(&mut self, message: NativeMessage) { + fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) { match message { - NativeMessage::ToFrontend(bytes) => { + DesktopFrontendMessage::ToWeb(bytes) => { self.cef_context.send_web_message(bytes.as_slice()); } - NativeMessage::OpenFileDialog { title, filters, context } => { + DesktopFrontendMessage::OpenFileDialog { title, filters, context } => { let event_loop_proxy = self.event_loop_proxy.clone(); let _ = thread::spawn(move || { let mut dialog = AsyncFileDialog::new().set_title(title); @@ -68,12 +68,12 @@ impl WinitApp { if let Some(path) = futures::executor::block_on(show_dialog) && let Ok(content) = std::fs::read(&path) { - let message = EditorMessage::OpenFileDialogResult { path, content, context }; - let _ = event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); + let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context }; + let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message)); } }); } - NativeMessage::SaveFileDialog { + DesktopFrontendMessage::SaveFileDialog { title, default_filename, default_folder, @@ -93,34 +93,34 @@ impl WinitApp { let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) }; if let Some(path) = futures::executor::block_on(show_dialog) { - let message = EditorMessage::SaveFileDialogResult { path, context }; - let _ = event_loop_proxy.send_event(CustomEvent::EditorMessage(message)); + let message = DesktopWrapperMessage::SaveFileDialogResult { path, context }; + let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message)); } }); } - NativeMessage::WriteFile { path, content } => { + DesktopFrontendMessage::WriteFile { path, content } => { if let Err(e) = std::fs::write(&path, content) { tracing::error!("Failed to write file {}: {}", path.display(), e); } } - NativeMessage::OpenUrl(url) => { + DesktopFrontendMessage::OpenUrl(url) => { let _ = thread::spawn(move || { if let Err(e) = open::that(&url) { tracing::error!("Failed to open URL: {}: {}", url, e); } }); } - NativeMessage::RequestRedraw => { + DesktopFrontendMessage::RequestRedraw => { if let Some(window) = &self.window { window.request_redraw(); } } - NativeMessage::UpdateViewport(texture) => { + DesktopFrontendMessage::UpdateViewport(texture) => { if let Some(graphics_state) = &mut self.graphics_state { graphics_state.bind_viewport_texture(texture); } } - NativeMessage::UpdateViewportBounds { x, y, width, height } => { + DesktopFrontendMessage::UpdateViewportBounds { x, y, width, height } => { if let Some(graphics_state) = &mut self.graphics_state && let Some(window) = &self.window { @@ -135,24 +135,24 @@ impl WinitApp { graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]); } } - NativeMessage::UpdateOverlays(scene) => { + DesktopFrontendMessage::UpdateOverlays(scene) => { if let Some(graphics_state) = &mut self.graphics_state { graphics_state.set_overlays_scene(scene); } } - NativeMessage::Loopback(editor_message) => self.dispatch_editor_message(editor_message), + DesktopFrontendMessage::Loopback(editor_message) => self.dispatch_desktop_wrapper_message(editor_message), } } - fn handle_native_messages(&mut self, messages: Vec) { + fn handle_desktop_frontend_messages(&mut self, messages: Vec) { for message in messages { - self.handle_native_message(message); + self.handle_desktop_frontend_message(message); } } - fn dispatch_editor_message(&mut self, message: EditorMessage) { + fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) { let responses = self.editor_wrapper.dispatch(message); - self.handle_native_messages(responses); + self.handle_desktop_frontend_messages(responses); } } @@ -211,8 +211,8 @@ impl ApplicationHandler for WinitApp { fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { match event { - CustomEvent::NativeMessages(messages) => self.handle_native_messages(messages), - CustomEvent::EditorMessage(message) => self.dispatch_editor_message(message), + CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), + CustomEvent::DesktopFrontendMessages(messages) => self.handle_desktop_frontend_messages(messages), CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { graphics_state.resize(texture.width(), texture.height()); diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index bed0c60e4e..f93391353f 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -1,6 +1,6 @@ use crate::CustomEvent; -use crate::editor_api::WgpuContext; -use crate::editor_api::messages::EditorMessage; +use crate::desktop_wrapper::WgpuContext; +use crate::desktop_wrapper::messages::DesktopWrapperMessage; use crate::render::FrameBufferRef; use std::{ sync::{Arc, Mutex, mpsc::Receiver}, @@ -124,7 +124,7 @@ impl CefEventHandler for CefHandler { } fn receive_web_message(&self, message: &[u8]) { - let editor_message = EditorMessage::FromFrontend(message.to_vec()); - let _ = self.event_loop_proxy.send_event(CustomEvent::EditorMessage(editor_message)); + let editor_message = DesktopWrapperMessage::FromWeb(message.to_vec()); + let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(editor_message)); } } diff --git a/desktop/src/editor_api.rs b/desktop/src/desktop_wrapper.rs similarity index 100% rename from desktop/src/editor_api.rs rename to desktop/src/desktop_wrapper.rs diff --git a/desktop/src/editor_api/editor_wrapper.rs b/desktop/src/desktop_wrapper/editor_wrapper.rs similarity index 73% rename from desktop/src/editor_api/editor_wrapper.rs rename to desktop/src/desktop_wrapper/editor_wrapper.rs index ba92143fb4..7165064b95 100644 --- a/desktop/src/editor_api/editor_wrapper.rs +++ b/desktop/src/desktop_wrapper/editor_wrapper.rs @@ -2,8 +2,8 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::{application::Editor, messages::prelude::Message}; use std::collections::VecDeque; -use crate::editor_api::WgpuContext; -use crate::editor_api::messages::{EditorMessage, NativeMessage}; +use crate::desktop_wrapper::WgpuContext; +use crate::desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; #[path = "handle_editor_message.rs"] mod handle_editor_message; @@ -27,22 +27,22 @@ impl EditorWrapper { futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); } - pub fn dispatch(&mut self, message: EditorMessage) -> Vec { + pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec { let mut executor = EditorMessageExecutor::new(&mut self.editor); executor.queue(message); executor.execute() } - pub fn poll() -> Vec { + pub fn poll() -> Vec { let mut responses = Vec::new(); let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); if has_run { - responses.push(NativeMessage::Loopback(EditorMessage::PoolNodeGraphEvaluation)); + responses.push(DesktopFrontendMessage::Loopback(DesktopWrapperMessage::PollNodeGraphEvaluation)); } if let Some(texture) = texture { - responses.push(NativeMessage::UpdateViewport(texture.texture)); - responses.push(NativeMessage::RequestRedraw); + responses.push(DesktopFrontendMessage::UpdateViewport(texture.texture)); + responses.push(DesktopFrontendMessage::RequestRedraw); } responses @@ -51,9 +51,9 @@ impl EditorWrapper { struct EditorMessageExecutor<'a> { editor: &'a mut Editor, - queue: VecDeque, + queue: VecDeque, messages: Vec, - responses: Vec, + responses: Vec, } impl<'a> EditorMessageExecutor<'a> { @@ -66,12 +66,12 @@ impl<'a> EditorMessageExecutor<'a> { } } - pub(crate) fn execute(mut self) -> Vec { + pub(crate) fn execute(mut self) -> Vec { self.process_queue(); self.responses } - pub(crate) fn queue(&mut self, message: EditorMessage) { + pub(crate) fn queue(&mut self, message: DesktopWrapperMessage) { self.queue.push_back(message); } @@ -81,7 +81,7 @@ impl<'a> EditorMessageExecutor<'a> { } } - pub(super) fn respond(&mut self, response: NativeMessage) { + pub(super) fn respond(&mut self, response: DesktopFrontendMessage) { self.responses.push(response); } @@ -110,7 +110,7 @@ impl<'a> EditorMessageExecutor<'a> { .into_iter() .filter_map(|m| intercept_frontend_message::intercept_frontend_message(self, m)) .collect::>(); - self.respond(NativeMessage::ToFrontend(ron::to_string(&frontend_messages).unwrap().into_bytes())); + self.respond(DesktopFrontendMessage::ToWeb(ron::to_string(&frontend_messages).unwrap().into_bytes())); } } } diff --git a/desktop/src/editor_api/handle_editor_message.rs b/desktop/src/desktop_wrapper/handle_editor_message.rs similarity index 80% rename from desktop/src/editor_api/handle_editor_message.rs rename to desktop/src/desktop_wrapper/handle_editor_message.rs index c258e66dda..5d58055f37 100644 --- a/desktop/src/editor_api/handle_editor_message.rs +++ b/desktop/src/desktop_wrapper/handle_editor_message.rs @@ -1,12 +1,12 @@ use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; -use crate::editor_api::messages::{EditorMessage, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; +use crate::desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, OpenFileDialogContext, SaveFileDialogContext}; use super::EditorMessageExecutor; -pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, message: EditorMessage) { +pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, message: DesktopWrapperMessage) { match message { - EditorMessage::FromFrontend(data) => { + DesktopWrapperMessage::FromWeb(data) => { let string = std::str::from_utf8(&data).unwrap(); match ron::from_str::(string) { Ok(message) => { @@ -17,7 +17,7 @@ pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, messag } } } - EditorMessage::OpenFileDialogResult { path, content, context } => match context { + DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { OpenFileDialogContext::Document => match String::from_utf8(content) { Ok(content) => { executor.queue_message( @@ -86,18 +86,18 @@ pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, messag } } }, - EditorMessage::SaveFileDialogResult { path, context } => match context { + DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context { SaveFileDialogContext::Document { document_id, content } => { - executor.respond(NativeMessage::WriteFile { path: path.clone(), content }); + executor.respond(DesktopFrontendMessage::WriteFile { path: path.clone(), content }); executor.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::SavedDocument { path: Some(path) }, })); } SaveFileDialogContext::Export { content } => { - executor.respond(NativeMessage::WriteFile { path, content }); + executor.respond(DesktopFrontendMessage::WriteFile { path, content }); } }, - EditorMessage::PoolNodeGraphEvaluation => executor.poll_node_graph_evaluation(), + DesktopWrapperMessage::PollNodeGraphEvaluation => executor.poll_node_graph_evaluation(), } } diff --git a/desktop/src/editor_api/intercept_frontend_message.rs b/desktop/src/desktop_wrapper/intercept_frontend_message.rs similarity index 75% rename from desktop/src/editor_api/intercept_frontend_message.rs rename to desktop/src/desktop_wrapper/intercept_frontend_message.rs index 7bd9d834d2..412a13d492 100644 --- a/desktop/src/editor_api/intercept_frontend_message.rs +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -2,17 +2,17 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::FrontendMessage; -use crate::editor_api::messages::{FileFilter, NativeMessage, OpenFileDialogContext, SaveFileDialogContext}; +use crate::desktop_wrapper::messages::{FileFilter, DesktopFrontendMessage, OpenFileDialogContext, SaveFileDialogContext}; use super::EditorMessageExecutor; pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, message: FrontendMessage) -> Option { match message { FrontendMessage::RenderOverlays(overlay_context) => { - executor.respond(NativeMessage::UpdateOverlays(overlay_context.take_scene())); + executor.respond(DesktopFrontendMessage::UpdateOverlays(overlay_context.take_scene())); } FrontendMessage::TriggerOpenDocument => { - executor.respond(NativeMessage::OpenFileDialog { + executor.respond(DesktopFrontendMessage::OpenFileDialog { title: "Open Document".to_string(), filters: vec![FileFilter { name: "Graphite".to_string(), @@ -22,7 +22,7 @@ pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, m }); } FrontendMessage::TriggerImport => { - executor.respond(NativeMessage::OpenFileDialog { + executor.respond(DesktopFrontendMessage::OpenFileDialog { title: "Import File".to_string(), filters: vec![ FileFilter { @@ -39,9 +39,9 @@ pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, m } FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { if let Some(path) = path { - executor.respond(NativeMessage::WriteFile { path, content }); + executor.respond(DesktopFrontendMessage::WriteFile { path, content }); } else { - executor.respond(NativeMessage::SaveFileDialog { + executor.respond(DesktopFrontendMessage::SaveFileDialog { title: "Save Document".to_string(), default_filename: name, default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), @@ -54,7 +54,7 @@ pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, m } } FrontendMessage::TriggerSaveFile { name, content } => { - executor.respond(NativeMessage::SaveFileDialog { + executor.respond(DesktopFrontendMessage::SaveFileDialog { title: "Save File".to_string(), default_filename: name, default_folder: None, @@ -63,7 +63,7 @@ pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, m }); } FrontendMessage::TriggerVisitLink { url } => { - executor.respond(NativeMessage::OpenUrl(url)); + executor.respond(DesktopFrontendMessage::OpenUrl(url)); } m => return Some(m), } diff --git a/desktop/src/editor_api/intercept_message.rs b/desktop/src/desktop_wrapper/intercept_message.rs similarity index 79% rename from desktop/src/editor_api/intercept_message.rs rename to desktop/src/desktop_wrapper/intercept_message.rs index 68a4f8fd6d..6b3e95e9f2 100644 --- a/desktop/src/editor_api/intercept_message.rs +++ b/desktop/src/desktop_wrapper/intercept_message.rs @@ -1,6 +1,6 @@ use graphite_editor::messages::prelude::{InputPreprocessorMessage, Message}; -use crate::editor_api::messages::NativeMessage; +use crate::desktop_wrapper::messages::DesktopFrontendMessage; use super::EditorMessageExecutor; @@ -10,14 +10,14 @@ pub(super) fn intercept_message(executor: &mut EditorMessageExecutor, message: M if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { let top_left = bounds_of_viewports[0].top_left; let bottom_right = bounds_of_viewports[0].bottom_right; - executor.respond(NativeMessage::UpdateViewportBounds { + executor.respond(DesktopFrontendMessage::UpdateViewportBounds { x: top_left.x as f32, y: top_left.y as f32, width: (bottom_right.x - top_left.x) as f32, height: (bottom_right.y - top_left.y) as f32, }); } - executor.respond(NativeMessage::RequestRedraw); + executor.respond(DesktopFrontendMessage::RequestRedraw); Some(Message::InputPreprocessor(message)) } m => Some(m), diff --git a/desktop/src/editor_api/messages.rs b/desktop/src/desktop_wrapper/messages.rs similarity index 85% rename from desktop/src/editor_api/messages.rs rename to desktop/src/desktop_wrapper/messages.rs index 7c123bca40..c2096721da 100644 --- a/desktop/src/editor_api/messages.rs +++ b/desktop/src/desktop_wrapper/messages.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::DocumentId; -pub enum NativeMessage { - ToFrontend(Vec), +pub enum DesktopFrontendMessage { + ToWeb(Vec), OpenFileDialog { title: String, filters: Vec, @@ -30,7 +30,7 @@ pub enum NativeMessage { height: f32, }, UpdateOverlays(vello::Scene), - Loopback(EditorMessage), + Loopback(DesktopWrapperMessage), } pub struct FileFilter { @@ -38,11 +38,11 @@ pub struct FileFilter { pub extensions: Vec, } -pub enum EditorMessage { - FromFrontend(Vec), +pub enum DesktopWrapperMessage { + FromWeb(Vec), OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, - PoolNodeGraphEvaluation, + PollNodeGraphEvaluation, } pub enum OpenFileDialogContext { diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 73143c8484..799796796f 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -15,16 +15,16 @@ use app::WinitApp; mod dirs; -mod editor_api; -use editor_api::EditorWrapper; -use editor_api::messages::{EditorMessage, NativeMessage}; +mod desktop_wrapper; +use desktop_wrapper::EditorWrapper; +use desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; #[allow(clippy::large_enum_variant)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), - NativeMessages(Vec), - EditorMessage(EditorMessage), + DesktopWrapperMessage(DesktopWrapperMessage), + DesktopFrontendMessages(Vec), } fn main() { @@ -43,7 +43,7 @@ fn main() { let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel(); - let wgpu_context = futures::executor::block_on(editor_api::WgpuContext::new()).unwrap(); + let wgpu_context = futures::executor::block_on(desktop_wrapper::WgpuContext::new()).unwrap(); let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) { Ok(c) => c, Err(cef::InitError::AlreadyRunning) => { @@ -65,7 +65,7 @@ fn main() { let last_render = Instant::now(); let responses = EditorWrapper::poll(); - let _ = rendering_loop_proxy.send_event(CustomEvent::NativeMessages(responses)); + let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopFrontendMessages(responses)); let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index 537045878f..8404bce453 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use wgpu_executor::WgpuExecutor; use winit::window::Window; -use crate::editor_api::WgpuContext; +use crate::desktop_wrapper::WgpuContext; #[derive(derivative::Derivative)] #[derivative(Debug)] From 5228025b23e57e0ea010789c357834cf84d7af8d Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 17:14:49 +0000 Subject: [PATCH 23/31] Cleaning up desktop wrapper interface --- desktop/src/app.rs | 20 +++++--------- desktop/src/desktop_wrapper.rs | 2 +- desktop/src/desktop_wrapper/editor_wrapper.rs | 26 +++++++++---------- .../src/desktop_wrapper/intercept_message.rs | 1 - desktop/src/desktop_wrapper/messages.rs | 2 -- desktop/src/main.rs | 16 +++++++++--- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 7f9c846fcd..ebf0cf75d6 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,7 +1,7 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; -use crate::desktop_wrapper::EditorWrapper; +use crate::desktop_wrapper::DesktopWrapper; use crate::desktop_wrapper::WgpuContext; use crate::desktop_wrapper::messages::DesktopFrontendMessage; use crate::desktop_wrapper::messages::DesktopWrapperMessage; @@ -32,12 +32,12 @@ pub(crate) struct WinitApp { graphics_state: Option, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy, - editor_wrapper: EditorWrapper, + desktop_wrapper: DesktopWrapper, } impl WinitApp { pub(crate) fn new(cef_context: cef::Context, window_size_sender: Sender, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy) -> Self { - let editor_wrapper = EditorWrapper::new(); + let desktop_wrapper = DesktopWrapper::new(); Self { cef_context, window: None, @@ -46,7 +46,7 @@ impl WinitApp { window_size_sender, wgpu_context, event_loop_proxy, - editor_wrapper, + desktop_wrapper, } } @@ -110,11 +110,6 @@ impl WinitApp { } }); } - DesktopFrontendMessage::RequestRedraw => { - if let Some(window) = &self.window { - window.request_redraw(); - } - } DesktopFrontendMessage::UpdateViewport(texture) => { if let Some(graphics_state) = &mut self.graphics_state { graphics_state.bind_viewport_texture(texture); @@ -140,7 +135,6 @@ impl WinitApp { graphics_state.set_overlays_scene(scene); } } - DesktopFrontendMessage::Loopback(editor_message) => self.dispatch_desktop_wrapper_message(editor_message), } } @@ -151,7 +145,7 @@ impl WinitApp { } fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) { - let responses = self.editor_wrapper.dispatch(message); + let responses = self.desktop_wrapper.dispatch(message); self.handle_desktop_frontend_messages(responses); } } @@ -206,13 +200,13 @@ impl ApplicationHandler for WinitApp { tracing::info!("Winit window created and ready"); - self.editor_wrapper.init(self.wgpu_context.clone()); + self.desktop_wrapper.init(self.wgpu_context.clone()); } fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { match event { CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), - CustomEvent::DesktopFrontendMessages(messages) => self.handle_desktop_frontend_messages(messages), + CustomEvent::DesktopFrontendMessage(message) => self.handle_desktop_frontend_message(message), CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { graphics_state.resize(texture.width(), texture.height()); diff --git a/desktop/src/desktop_wrapper.rs b/desktop/src/desktop_wrapper.rs index 673ad18e7e..bb9cb25681 100644 --- a/desktop/src/desktop_wrapper.rs +++ b/desktop/src/desktop_wrapper.rs @@ -1,5 +1,5 @@ mod editor_wrapper; -pub use editor_wrapper::EditorWrapper; +pub use editor_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; pub mod messages; pub use wgpu_executor::Context as WgpuContext; diff --git a/desktop/src/desktop_wrapper/editor_wrapper.rs b/desktop/src/desktop_wrapper/editor_wrapper.rs index 7165064b95..01d1146176 100644 --- a/desktop/src/desktop_wrapper/editor_wrapper.rs +++ b/desktop/src/desktop_wrapper/editor_wrapper.rs @@ -13,11 +13,11 @@ mod intercept_frontend_message; #[path = "intercept_message.rs"] mod intercept_message; -pub struct EditorWrapper { +pub struct DesktopWrapper { editor: Editor, } -impl EditorWrapper { +impl DesktopWrapper { pub fn new() -> Self { Self { editor: Editor::new() } } @@ -33,22 +33,20 @@ impl EditorWrapper { executor.execute() } - pub fn poll() -> Vec { - let mut responses = Vec::new(); - - let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); - if has_run { - responses.push(DesktopFrontendMessage::Loopback(DesktopWrapperMessage::PollNodeGraphEvaluation)); - } - if let Some(texture) = texture { - responses.push(DesktopFrontendMessage::UpdateViewport(texture.texture)); - responses.push(DesktopFrontendMessage::RequestRedraw); + pub async fn run_node_graph() -> NodeGraphExecutionResult { + let result = graphite_editor::node_graph_executor::run_node_graph().await; + match result { + (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), + (false, _) => NodeGraphExecutionResult::NotRun, } - - responses } } +pub enum NodeGraphExecutionResult { + HasRun(Option), + NotRun, +} + struct EditorMessageExecutor<'a> { editor: &'a mut Editor, queue: VecDeque, diff --git a/desktop/src/desktop_wrapper/intercept_message.rs b/desktop/src/desktop_wrapper/intercept_message.rs index 6b3e95e9f2..708afff06b 100644 --- a/desktop/src/desktop_wrapper/intercept_message.rs +++ b/desktop/src/desktop_wrapper/intercept_message.rs @@ -17,7 +17,6 @@ pub(super) fn intercept_message(executor: &mut EditorMessageExecutor, message: M height: (bottom_right.y - top_left.y) as f32, }); } - executor.respond(DesktopFrontendMessage::RequestRedraw); Some(Message::InputPreprocessor(message)) } m => Some(m), diff --git a/desktop/src/desktop_wrapper/messages.rs b/desktop/src/desktop_wrapper/messages.rs index c2096721da..c852ee6b3f 100644 --- a/desktop/src/desktop_wrapper/messages.rs +++ b/desktop/src/desktop_wrapper/messages.rs @@ -21,7 +21,6 @@ pub enum DesktopFrontendMessage { content: Vec, }, OpenUrl(String), - RequestRedraw, UpdateViewport(wgpu::Texture), UpdateViewportBounds { x: f32, @@ -30,7 +29,6 @@ pub enum DesktopFrontendMessage { height: f32, }, UpdateOverlays(vello::Scene), - Loopback(DesktopWrapperMessage), } pub struct FileFilter { diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 799796796f..4923c52b6f 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -16,15 +16,15 @@ use app::WinitApp; mod dirs; mod desktop_wrapper; -use desktop_wrapper::EditorWrapper; use desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; +use desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; #[allow(clippy::large_enum_variant)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), DesktopWrapperMessage(DesktopWrapperMessage), - DesktopFrontendMessages(Vec), + DesktopFrontendMessage(DesktopFrontendMessage), } fn main() { @@ -64,8 +64,16 @@ fn main() { loop { let last_render = Instant::now(); - let responses = EditorWrapper::poll(); - let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopFrontendMessages(responses)); + let result = futures::executor::block_on(DesktopWrapper::run_node_graph()); + match result { + NodeGraphExecutionResult::HasRun(texture) => { + let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(DesktopWrapperMessage::PollNodeGraphEvaluation)); + if let Some(texture) = texture { + let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopFrontendMessage(DesktopFrontendMessage::UpdateViewport(texture))); + } + } + NodeGraphExecutionResult::NotRun => {} + } let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); From 4827cfb70a06f3764e39fe098640eda8fb815978 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 17:44:44 +0000 Subject: [PATCH 24/31] Fix formatting --- desktop/src/desktop_wrapper/intercept_frontend_message.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/desktop_wrapper/intercept_frontend_message.rs b/desktop/src/desktop_wrapper/intercept_frontend_message.rs index 412a13d492..798650006a 100644 --- a/desktop/src/desktop_wrapper/intercept_frontend_message.rs +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::FrontendMessage; -use crate::desktop_wrapper::messages::{FileFilter, DesktopFrontendMessage, OpenFileDialogContext, SaveFileDialogContext}; +use crate::desktop_wrapper::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; use super::EditorMessageExecutor; From 2b4cdaffdda19b342e8e10d72c5a065ba666d2d7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 18:15:10 +0000 Subject: [PATCH 25/31] Fix naming --- desktop/src/desktop_wrapper.rs | 49 ++++++++++++++- ...e.rs => handle_desktop_wrapper_message.rs} | 7 +-- .../intercept_frontend_message.rs | 7 +-- .../src/desktop_wrapper/intercept_message.rs | 7 +-- ...{editor_wrapper.rs => message_executor.rs} | 59 +++---------------- 5 files changed, 64 insertions(+), 65 deletions(-) rename desktop/src/desktop_wrapper/{handle_editor_message.rs => handle_desktop_wrapper_message.rs} (91%) rename desktop/src/desktop_wrapper/{editor_wrapper.rs => message_executor.rs} (50%) diff --git a/desktop/src/desktop_wrapper.rs b/desktop/src/desktop_wrapper.rs index bb9cb25681..fc2dc1d3e2 100644 --- a/desktop/src/desktop_wrapper.rs +++ b/desktop/src/desktop_wrapper.rs @@ -1,5 +1,48 @@ -mod editor_wrapper; -pub use editor_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; +use graph_craft::wasm_application_io::WasmApplicationIo; +use graphite_editor::application::Editor; -pub mod messages; pub use wgpu_executor::Context as WgpuContext; + +pub mod messages; +use messages::{DesktopFrontendMessage, DesktopWrapperMessage}; + +mod message_executor; +use message_executor::DesktopWrapperMessageExecutor; + +mod handle_desktop_wrapper_message; +mod intercept_frontend_message; +mod intercept_message; + +pub struct DesktopWrapper { + editor: Editor, +} + +impl DesktopWrapper { + pub fn new() -> Self { + Self { editor: Editor::new() } + } + + pub fn init(&self, wgpu_context: WgpuContext) { + let application_io = WasmApplicationIo::new_with_context(wgpu_context); + futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); + } + + pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec { + let mut executor = DesktopWrapperMessageExecutor::new(&mut self.editor); + executor.queue(message); + executor.execute() + } + + pub async fn run_node_graph() -> NodeGraphExecutionResult { + let result = graphite_editor::node_graph_executor::run_node_graph().await; + match result { + (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), + (false, _) => NodeGraphExecutionResult::NotRun, + } + } +} + +pub enum NodeGraphExecutionResult { + HasRun(Option), + NotRun, +} diff --git a/desktop/src/desktop_wrapper/handle_editor_message.rs b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs similarity index 91% rename from desktop/src/desktop_wrapper/handle_editor_message.rs rename to desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs index 5d58055f37..dd69b84f17 100644 --- a/desktop/src/desktop_wrapper/handle_editor_message.rs +++ b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs @@ -1,10 +1,9 @@ use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; -use crate::desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, OpenFileDialogContext, SaveFileDialogContext}; +use super::DesktopWrapperMessageExecutor; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, OpenFileDialogContext, SaveFileDialogContext}; -use super::EditorMessageExecutor; - -pub(super) fn handle_editor_message(executor: &mut EditorMessageExecutor, message: DesktopWrapperMessage) { +pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessageExecutor, message: DesktopWrapperMessage) { match message { DesktopWrapperMessage::FromWeb(data) => { let string = std::str::from_utf8(&data).unwrap(); diff --git a/desktop/src/desktop_wrapper/intercept_frontend_message.rs b/desktop/src/desktop_wrapper/intercept_frontend_message.rs index 798650006a..019554a25d 100644 --- a/desktop/src/desktop_wrapper/intercept_frontend_message.rs +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -2,11 +2,10 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::FrontendMessage; -use crate::desktop_wrapper::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; +use super::DesktopWrapperMessageExecutor; +use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; -use super::EditorMessageExecutor; - -pub(super) fn intercept_frontend_message(executor: &mut EditorMessageExecutor, message: FrontendMessage) -> Option { +pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExecutor, message: FrontendMessage) -> Option { match message { FrontendMessage::RenderOverlays(overlay_context) => { executor.respond(DesktopFrontendMessage::UpdateOverlays(overlay_context.take_scene())); diff --git a/desktop/src/desktop_wrapper/intercept_message.rs b/desktop/src/desktop_wrapper/intercept_message.rs index 708afff06b..b698368289 100644 --- a/desktop/src/desktop_wrapper/intercept_message.rs +++ b/desktop/src/desktop_wrapper/intercept_message.rs @@ -1,10 +1,9 @@ use graphite_editor::messages::prelude::{InputPreprocessorMessage, Message}; -use crate::desktop_wrapper::messages::DesktopFrontendMessage; +use super::DesktopWrapperMessageExecutor; +use super::messages::DesktopFrontendMessage; -use super::EditorMessageExecutor; - -pub(super) fn intercept_message(executor: &mut EditorMessageExecutor, message: Message) -> Option { +pub(super) fn intercept_message(executor: &mut DesktopWrapperMessageExecutor, message: Message) -> Option { match message { Message::InputPreprocessor(message) => { if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { diff --git a/desktop/src/desktop_wrapper/editor_wrapper.rs b/desktop/src/desktop_wrapper/message_executor.rs similarity index 50% rename from desktop/src/desktop_wrapper/editor_wrapper.rs rename to desktop/src/desktop_wrapper/message_executor.rs index 01d1146176..aca49079f2 100644 --- a/desktop/src/desktop_wrapper/editor_wrapper.rs +++ b/desktop/src/desktop_wrapper/message_executor.rs @@ -1,60 +1,19 @@ -use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::{application::Editor, messages::prelude::Message}; use std::collections::VecDeque; -use crate::desktop_wrapper::WgpuContext; -use crate::desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; +use super::handle_desktop_wrapper_message::handle_desktop_wrapper_message; +use super::intercept_frontend_message::intercept_frontend_message; +use super::intercept_message::intercept_message; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; -#[path = "handle_editor_message.rs"] -mod handle_editor_message; - -#[path = "intercept_frontend_message.rs"] -mod intercept_frontend_message; -#[path = "intercept_message.rs"] -mod intercept_message; - -pub struct DesktopWrapper { - editor: Editor, -} - -impl DesktopWrapper { - pub fn new() -> Self { - Self { editor: Editor::new() } - } - - pub fn init(&self, wgpu_context: WgpuContext) { - let application_io = WasmApplicationIo::new_with_context(wgpu_context); - futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); - } - - pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec { - let mut executor = EditorMessageExecutor::new(&mut self.editor); - executor.queue(message); - executor.execute() - } - - pub async fn run_node_graph() -> NodeGraphExecutionResult { - let result = graphite_editor::node_graph_executor::run_node_graph().await; - match result { - (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), - (false, _) => NodeGraphExecutionResult::NotRun, - } - } -} - -pub enum NodeGraphExecutionResult { - HasRun(Option), - NotRun, -} - -struct EditorMessageExecutor<'a> { +pub(crate) struct DesktopWrapperMessageExecutor<'a> { editor: &'a mut Editor, queue: VecDeque, messages: Vec, responses: Vec, } -impl<'a> EditorMessageExecutor<'a> { +impl<'a> DesktopWrapperMessageExecutor<'a> { pub(crate) fn new(editor: &'a mut Editor) -> Self { Self { editor, @@ -74,7 +33,7 @@ impl<'a> EditorMessageExecutor<'a> { } pub(super) fn queue_message(&mut self, message: Message) { - if let Some(message) = intercept_message::intercept_message(self, message) { + if let Some(message) = intercept_message(self, message) { self.messages.push(message); } } @@ -98,7 +57,7 @@ impl<'a> EditorMessageExecutor<'a> { fn process_queue(&mut self) { while !self.queue.is_empty() || !self.messages.is_empty() { while let Some(message) = self.queue.pop_front() { - handle_editor_message::handle_editor_message(self, message); + handle_desktop_wrapper_message(self, message); } let frontend_messages = self .editor @@ -106,7 +65,7 @@ impl<'a> EditorMessageExecutor<'a> { messages: std::mem::take(&mut self.messages).into_boxed_slice(), }) .into_iter() - .filter_map(|m| intercept_frontend_message::intercept_frontend_message(self, m)) + .filter_map(|m| intercept_frontend_message(self, m)) .collect::>(); self.respond(DesktopFrontendMessage::ToWeb(ron::to_string(&frontend_messages).unwrap().into_bytes())); } From e673641129088b73f753453eddd957fe865bc229 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 18 Aug 2025 20:47:06 +0000 Subject: [PATCH 26/31] Move node graph execution result handling to app --- desktop/src/app.rs | 20 ++++++++++++++------ desktop/src/desktop_wrapper.rs | 2 +- desktop/src/desktop_wrapper/messages.rs | 1 - desktop/src/main.rs | 16 ++++------------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index ebf0cf75d6..928de9021c 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -2,6 +2,7 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; use crate::desktop_wrapper::DesktopWrapper; +use crate::desktop_wrapper::NodeGraphExecutionResult; use crate::desktop_wrapper::WgpuContext; use crate::desktop_wrapper::messages::DesktopFrontendMessage; use crate::desktop_wrapper::messages::DesktopWrapperMessage; @@ -110,11 +111,6 @@ impl WinitApp { } }); } - DesktopFrontendMessage::UpdateViewport(texture) => { - if let Some(graphics_state) = &mut self.graphics_state { - graphics_state.bind_viewport_texture(texture); - } - } DesktopFrontendMessage::UpdateViewportBounds { x, y, width, height } => { if let Some(graphics_state) = &mut self.graphics_state && let Some(window) = &self.window @@ -206,7 +202,19 @@ impl ApplicationHandler for WinitApp { fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { match event { CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), - CustomEvent::DesktopFrontendMessage(message) => self.handle_desktop_frontend_message(message), + CustomEvent::NodeGraphExecutionResult(result) => match result { + NodeGraphExecutionResult::HasRun(texture) => { + self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation); + if let Some(texture) = texture + && let Some(graphics_state) = self.graphics_state.as_mut() + && let Some(window) = self.window.as_ref() + { + graphics_state.bind_viewport_texture(texture); + window.request_redraw(); + } + } + NodeGraphExecutionResult::NotRun => {} + }, CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { graphics_state.resize(texture.width(), texture.height()); diff --git a/desktop/src/desktop_wrapper.rs b/desktop/src/desktop_wrapper.rs index fc2dc1d3e2..cdee90385e 100644 --- a/desktop/src/desktop_wrapper.rs +++ b/desktop/src/desktop_wrapper.rs @@ -33,7 +33,7 @@ impl DesktopWrapper { executor.execute() } - pub async fn run_node_graph() -> NodeGraphExecutionResult { + pub async fn execute_node_graph() -> NodeGraphExecutionResult { let result = graphite_editor::node_graph_executor::run_node_graph().await; match result { (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), diff --git a/desktop/src/desktop_wrapper/messages.rs b/desktop/src/desktop_wrapper/messages.rs index c852ee6b3f..ba7932e436 100644 --- a/desktop/src/desktop_wrapper/messages.rs +++ b/desktop/src/desktop_wrapper/messages.rs @@ -21,7 +21,6 @@ pub enum DesktopFrontendMessage { content: Vec, }, OpenUrl(String), - UpdateViewport(wgpu::Texture), UpdateViewportBounds { x: f32, y: f32, diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 4923c52b6f..4a1a37e331 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -16,7 +16,7 @@ use app::WinitApp; mod dirs; mod desktop_wrapper; -use desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; +use desktop_wrapper::messages::DesktopWrapperMessage; use desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; #[allow(clippy::large_enum_variant)] @@ -24,7 +24,7 @@ pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), DesktopWrapperMessage(DesktopWrapperMessage), - DesktopFrontendMessage(DesktopFrontendMessage), + NodeGraphExecutionResult(NodeGraphExecutionResult), } fn main() { @@ -64,16 +64,8 @@ fn main() { loop { let last_render = Instant::now(); - let result = futures::executor::block_on(DesktopWrapper::run_node_graph()); - match result { - NodeGraphExecutionResult::HasRun(texture) => { - let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(DesktopWrapperMessage::PollNodeGraphEvaluation)); - if let Some(texture) = texture { - let _ = rendering_loop_proxy.send_event(CustomEvent::DesktopFrontendMessage(DesktopFrontendMessage::UpdateViewport(texture))); - } - } - NodeGraphExecutionResult::NotRun => {} - } + let result = futures::executor::block_on(DesktopWrapper::execute_node_graph()); + let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result)); let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); From 49171f033e4de621ae7de9bba013867822e0bb80 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 16:30:30 +0000 Subject: [PATCH 27/31] Fix FrontendMessage RenderOverlays usage --- desktop/src/desktop_wrapper/intercept_frontend_message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/desktop_wrapper/intercept_frontend_message.rs b/desktop/src/desktop_wrapper/intercept_frontend_message.rs index 019554a25d..a8dce0c308 100644 --- a/desktop/src/desktop_wrapper/intercept_frontend_message.rs +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -7,8 +7,8 @@ use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExecutor, message: FrontendMessage) -> Option { match message { - FrontendMessage::RenderOverlays(overlay_context) => { - executor.respond(DesktopFrontendMessage::UpdateOverlays(overlay_context.take_scene())); + FrontendMessage::RenderOverlays { context } => { + executor.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); } FrontendMessage::TriggerOpenDocument => { executor.respond(DesktopFrontendMessage::OpenFileDialog { From afd74ac644bcbe85754f89a114cd4fdc50431696 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 20:07:51 +0000 Subject: [PATCH 28/31] Reimplement file drop and clean up file import and open messages --- desktop/src/app.rs | 83 ++-------- .../handle_desktop_wrapper_message.rs | 144 ++++++++++-------- desktop/src/desktop_wrapper/messages.rs | 5 + 3 files changed, 97 insertions(+), 135 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 928de9021c..a2a4a6586f 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -238,76 +238,6 @@ impl ApplicationHandler for WinitApp { let Some(event) = self.cef_context.handle_window_event(event) else { return }; match event { - // Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881 - // WindowEvent::DroppedFile(path) => { - // let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()); - // let Some(extension) = path.extension().and_then(|s| s.to_str()) else { - // tracing::warn!("Unsupported file dropped: {}", path.display()); - // // Fine to early return since we don't need to do cef work in this case - // return; - // }; - // let load_string = |path: &std::path::PathBuf| { - // let Ok(content) = fs::read_to_string(path) else { - // tracing::error!("Failed to read file: {}", path.display()); - // return None; - // }; - - // if content.is_empty() { - // tracing::warn!("Dropped file is empty: {}", path.display()); - // return None; - // } - // Some(content) - // }; - // // TODO: Consider moving this logic to the editor so we have one message to load data which is then demultiplexed in the portfolio message handler - // match extension { - // "graphite" => { - // let Some(content) = load_string(&path) else { return }; - - // let message = PortfolioMessage::OpenDocumentFile { - // document_name: None, - // document_path: Some(path), - // document_serialized_content: content, - // }; - // self.dispatch_message(message.into()); - // } - // "svg" => { - // let Some(content) = load_string(&path) else { return }; - - // let message = PortfolioMessage::PasteSvg { - // name: path.file_stem().map(|s| s.to_string_lossy().to_string()), - // svg: content, - // mouse: None, - // parent_and_insert_index: None, - // }; - // self.dispatch_message(message.into()); - // } - // _ => match image::ImageReader::open(&path) { - // Ok(reader) => match reader.decode() { - // Ok(image) => { - // let width = image.width(); - // let height = image.height(); - // // TODO: support loading images with more than 8 bits per channel - // let image_data = image.to_rgba8(); - // let image = Image::::from_image_data(image_data.as_raw(), width, height); - - // let message = PortfolioMessage::PasteImage { - // name, - // image, - // mouse: None, - // parent_and_insert_index: None, - // }; - // self.dispatch_message(message.into()); - // } - // Err(e) => { - // tracing::error!("Failed to decode image: {}: {}", path.display(), e); - // } - // }, - // Err(e) => { - // tracing::error!("Failed to open image file: {}: {}", path.display(), e); - // } - // }, - // } - // } WindowEvent::CloseRequested => { tracing::info!("The close button was pressed; stopping"); event_loop.exit(); @@ -332,6 +262,19 @@ impl ApplicationHandler for WinitApp { Err(e) => tracing::error!("{:?}", e), } } + // Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881 + WindowEvent::DroppedFile(path) => { + match std::fs::read(&path) { + Ok(content) => { + let message = DesktopWrapperMessage::OpenFile { path, content }; + let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message)); + } + Err(e) => { + tracing::error!("Failed to read dropped file {}: {}", path.display(), e); + return; + } + }; + } _ => {} } diff --git a/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs index dd69b84f17..9b2ca86167 100644 --- a/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs +++ b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs @@ -1,3 +1,5 @@ +use graphene_std::Color; +use graphene_std::raster::Image; use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; use super::DesktopWrapperMessageExecutor; @@ -17,72 +19,11 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag } } DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { - OpenFileDialogContext::Document => match String::from_utf8(content) { - Ok(content) => { - executor.queue_message( - PortfolioMessage::OpenDocumentFile { - document_name: None, - document_path: Some(path), - document_serialized_content: content, - } - .into(), - ); - } - Err(e) => { - tracing::error!("Failed to deserialize document content: {:?}", e); - } - }, + OpenFileDialogContext::Document => { + executor.queue(DesktopWrapperMessage::OpenDocument { path, content }); + } OpenFileDialogContext::Import => { - let extension = path.extension().and_then(|s| s.to_str()); - let name = path.file_stem().map(|s| s.to_string_lossy().to_string()); - match extension { - Some("svg") => match String::from_utf8(content) { - Ok(content) if !content.is_empty() => { - executor.queue_message( - PortfolioMessage::PasteSvg { - name, - svg: content, - mouse: None, - parent_and_insert_index: None, - } - .into(), - ); - } - Ok(_) => { - tracing::warn!("Svg file is empty: {}", path.display()); - } - Err(e) => { - tracing::error!("Failed to deserialize document content: {:?}", e); - } - }, - Some(_) => { - let reader = image::ImageReader::new(std::io::Cursor::new(content)); - match reader.decode() { - Ok(image) => { - let width = image.width(); - let height = image.height(); - let image_data = image.to_rgba8(); - let image = graphene_std::raster::Image::::from_image_data(image_data.as_raw(), width, height); - - executor.queue_message( - PortfolioMessage::PasteImage { - name, - image, - mouse: None, - parent_and_insert_index: None, - } - .into(), - ); - } - Err(e) => { - tracing::error!("Failed to decode image: {}: {}", path.display(), e); - } - } - } - _ => { - tracing::warn!("Unsupported file type: {}", path.display()); - } - } + executor.queue(DesktopWrapperMessage::ImportFile { path, content }); } }, DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context { @@ -97,6 +38,79 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag executor.respond(DesktopFrontendMessage::WriteFile { path, content }); } }, + DesktopWrapperMessage::OpenFile { path, content } => { + let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase(); + match extension.as_str() { + "graphite" => { + executor.queue(DesktopWrapperMessage::OpenDocument { path, content }); + } + _ => { + executor.queue(DesktopWrapperMessage::ImportFile { path, content }); + } + } + } + DesktopWrapperMessage::OpenDocument { path, content } => { + let Ok(content) = String::from_utf8(content) else { + tracing::warn!("Document file is invalid: {}", path.display()); + return; + }; + + let message = PortfolioMessage::OpenDocumentFile { + document_name: None, + document_path: Some(path), + document_serialized_content: content, + }; + executor.queue_message(message.into()); + } + DesktopWrapperMessage::ImportFile { path, content } => { + let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase(); + match extension.as_str() { + "svg" => { + executor.queue(DesktopWrapperMessage::ImportSvg { path, content }); + } + _ => { + executor.queue(DesktopWrapperMessage::ImportImage { path, content }); + } + } + } + DesktopWrapperMessage::ImportSvg { path, content } => { + let Ok(content) = String::from_utf8(content) else { + tracing::warn!("Svg file is invalid: {}", path.display()); + return; + }; + + let message = PortfolioMessage::PasteSvg { + name: path.file_stem().map(|s| s.to_string_lossy().to_string()), + svg: content, + mouse: None, + parent_and_insert_index: None, + }; + executor.queue_message(message.into()); + } + DesktopWrapperMessage::ImportImage { path, content } => { + let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()); + let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase(); + let Some(image_format) = image::ImageFormat::from_extension(&extension) else { + tracing::warn!("Unsupported file type: {}", path.display()); + return; + }; + let reader = image::ImageReader::with_format(std::io::Cursor::new(content), image_format); + let Ok(image) = reader.decode() else { + tracing::error!("Failed to decode image: {}", path.display()); + return; + }; + let width = image.width(); + let height = image.height(); + let image_data = image.to_rgba8(); + let image = Image::::from_image_data(image_data.as_raw(), width, height); + let message = PortfolioMessage::PasteImage { + name, + image, + mouse: None, + parent_and_insert_index: None, + }; + executor.queue_message(message.into()); + } DesktopWrapperMessage::PollNodeGraphEvaluation => executor.poll_node_graph_evaluation(), } } diff --git a/desktop/src/desktop_wrapper/messages.rs b/desktop/src/desktop_wrapper/messages.rs index ba7932e436..b414f87e12 100644 --- a/desktop/src/desktop_wrapper/messages.rs +++ b/desktop/src/desktop_wrapper/messages.rs @@ -39,6 +39,11 @@ pub enum DesktopWrapperMessage { FromWeb(Vec), OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, + OpenDocument { path: PathBuf, content: Vec }, + OpenFile { path: PathBuf, content: Vec }, + ImportFile { path: PathBuf, content: Vec }, + ImportSvg { path: PathBuf, content: Vec }, + ImportImage { path: PathBuf, content: Vec }, PollNodeGraphEvaluation, } From 2851a49c26ffacb735cac43115e8e0eec8875149 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 19 Aug 2025 22:32:13 +0000 Subject: [PATCH 29/31] Remove dbg --- .../messages/portfolio/document/document_message_handler.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 18915fffc5..fe6be81741 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1026,8 +1026,6 @@ impl MessageHandler> for DocumentMes }); } DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => { - dbg!(&self.path); - if let DocumentMessage::SaveDocumentAs = message { self.path = None; } @@ -1047,8 +1045,6 @@ impl MessageHandler> for DocumentMes DocumentMessage::SavedDocument { path } => { self.path = path; - dbg!(&self.path); - // Update the name to match the file stem let document_name_from_path = self.path.as_ref().and_then(|path| { if path.extension().is_some_and(|e| e == FILE_EXTENSION) { From a3db334c49c32853fa987a5dd22ab80322ea535c Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 20 Aug 2025 10:37:36 +0000 Subject: [PATCH 30/31] Post merge fix --- .../src/messages/portfolio/document/document_message_handler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 4178402ebf..0fa4d70386 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1577,7 +1577,6 @@ impl MessageHandler> for DocumentMes Noop, Redo, SaveDocument, - SaveDocumentAs, SelectAllLayers, SetSnapping, ToggleGridVisibility, From 2b9ef6e3193ae1b6f7655c1dbc02b4aa938da830 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 20 Aug 2025 13:04:42 +0000 Subject: [PATCH 31/31] Review changes --- desktop/src/app.rs | 7 +- desktop/src/cef.rs | 17 +++-- desktop/src/desktop_wrapper.rs | 31 ++++++-- .../handle_desktop_wrapper_message.rs | 50 ++++++------ .../intercept_editor_message.rs | 23 ++++++ .../intercept_frontend_message.rs | 20 ++--- .../src/desktop_wrapper/intercept_message.rs | 23 ------ .../src/desktop_wrapper/message_dispatcher.rs | 76 +++++++++++++++++++ .../src/desktop_wrapper/message_executor.rs | 73 ------------------ desktop/src/desktop_wrapper/messages.rs | 10 ++- desktop/src/main.rs | 1 - 11 files changed, 178 insertions(+), 153 deletions(-) create mode 100644 desktop/src/desktop_wrapper/intercept_editor_message.rs delete mode 100644 desktop/src/desktop_wrapper/intercept_message.rs create mode 100644 desktop/src/desktop_wrapper/message_dispatcher.rs delete mode 100644 desktop/src/desktop_wrapper/message_executor.rs diff --git a/desktop/src/app.rs b/desktop/src/app.rs index a2a4a6586f..8ca0fccc06 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -6,6 +6,7 @@ use crate::desktop_wrapper::NodeGraphExecutionResult; use crate::desktop_wrapper::WgpuContext; use crate::desktop_wrapper::messages::DesktopFrontendMessage; use crate::desktop_wrapper::messages::DesktopWrapperMessage; +use crate::desktop_wrapper::serialize_frontend_messages; use crate::render::GraphicsState; use rfd::AsyncFileDialog; use std::sync::Arc; @@ -53,7 +54,11 @@ impl WinitApp { fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) { match message { - DesktopFrontendMessage::ToWeb(bytes) => { + DesktopFrontendMessage::ToWeb(messages) => { + let Some(bytes) = serialize_frontend_messages(messages) else { + tracing::error!("Failed to serialize frontend messages"); + return; + }; self.cef_context.send_web_message(bytes.as_slice()); } DesktopFrontendMessage::OpenFileDialog { title, filters, context } => { diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index f93391353f..26a8e0f241 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -1,11 +1,9 @@ -use crate::CustomEvent; use crate::desktop_wrapper::WgpuContext; -use crate::desktop_wrapper::messages::DesktopWrapperMessage; use crate::render::FrameBufferRef; -use std::{ - sync::{Arc, Mutex, mpsc::Receiver}, - time::Instant, -}; +use crate::{CustomEvent, desktop_wrapper::deserialize_editor_message}; +use std::sync::mpsc::Receiver; +use std::sync::{Arc, Mutex}; +use std::time::Instant; mod context; mod dirs; @@ -124,7 +122,10 @@ impl CefEventHandler for CefHandler { } fn receive_web_message(&self, message: &[u8]) { - let editor_message = DesktopWrapperMessage::FromWeb(message.to_vec()); - let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(editor_message)); + let Some(desktop_wrapper_message) = deserialize_editor_message(message) else { + tracing::error!("Failed to deserialize web message"); + return; + }; + let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message)); } } diff --git a/desktop/src/desktop_wrapper.rs b/desktop/src/desktop_wrapper.rs index cdee90385e..8832f6db99 100644 --- a/desktop/src/desktop_wrapper.rs +++ b/desktop/src/desktop_wrapper.rs @@ -1,17 +1,18 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::application::Editor; +use graphite_editor::messages::prelude::{FrontendMessage, Message}; pub use wgpu_executor::Context as WgpuContext; pub mod messages; use messages::{DesktopFrontendMessage, DesktopWrapperMessage}; -mod message_executor; -use message_executor::DesktopWrapperMessageExecutor; +mod message_dispatcher; +use message_dispatcher::DesktopWrapperMessageDispatcher; mod handle_desktop_wrapper_message; +mod intercept_editor_message; mod intercept_frontend_message; -mod intercept_message; pub struct DesktopWrapper { editor: Editor, @@ -28,8 +29,8 @@ impl DesktopWrapper { } pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec { - let mut executor = DesktopWrapperMessageExecutor::new(&mut self.editor); - executor.queue(message); + let mut executor = DesktopWrapperMessageDispatcher::new(&mut self.editor); + executor.queue_desktop_wrapper_message(message); executor.execute() } @@ -46,3 +47,23 @@ pub enum NodeGraphExecutionResult { HasRun(Option), NotRun, } + +pub fn deserialize_editor_message(data: &[u8]) -> Option { + if let Ok(string) = std::str::from_utf8(data) { + if let Ok(message) = ron::de::from_str::(string) { + Some(DesktopWrapperMessage::FromWeb(message.into())) + } else { + None + } + } else { + None + } +} + +pub fn serialize_frontend_messages(messages: Vec) -> Option> { + if let Ok(serialized) = ron::ser::to_string(&messages) { + Some(serialized.into_bytes()) + } else { + None + } +} diff --git a/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs index 9b2ca86167..2cc11464b7 100644 --- a/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs +++ b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs @@ -1,51 +1,43 @@ use graphene_std::Color; use graphene_std::raster::Image; -use graphite_editor::messages::prelude::{DocumentMessage, Message, PortfolioMessage}; +use graphite_editor::messages::prelude::{DocumentMessage, PortfolioMessage}; -use super::DesktopWrapperMessageExecutor; -use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, OpenFileDialogContext, SaveFileDialogContext}; +use super::DesktopWrapperMessageDispatcher; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; -pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessageExecutor, message: DesktopWrapperMessage) { +pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) { match message { - DesktopWrapperMessage::FromWeb(data) => { - let string = std::str::from_utf8(&data).unwrap(); - match ron::from_str::(string) { - Ok(message) => { - executor.queue_message(message); - } - Err(e) => { - tracing::error!("Failed to deserialize message {:?}", e) - } - } + DesktopWrapperMessage::FromWeb(message) => { + dispatcher.queue_editor_message(*message); } DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { OpenFileDialogContext::Document => { - executor.queue(DesktopWrapperMessage::OpenDocument { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); } OpenFileDialogContext::Import => { - executor.queue(DesktopWrapperMessage::ImportFile { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content }); } }, DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context { SaveFileDialogContext::Document { document_id, content } => { - executor.respond(DesktopFrontendMessage::WriteFile { path: path.clone(), content }); - executor.queue_message(Message::Portfolio(PortfolioMessage::DocumentPassMessage { + dispatcher.respond(DesktopFrontendMessage::WriteFile { path: path.clone(), content }); + dispatcher.queue_editor_message(EditorMessage::Portfolio(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::SavedDocument { path: Some(path) }, })); } - SaveFileDialogContext::Export { content } => { - executor.respond(DesktopFrontendMessage::WriteFile { path, content }); + SaveFileDialogContext::File { content } => { + dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content }); } }, DesktopWrapperMessage::OpenFile { path, content } => { let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase(); match extension.as_str() { "graphite" => { - executor.queue(DesktopWrapperMessage::OpenDocument { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); } _ => { - executor.queue(DesktopWrapperMessage::ImportFile { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content }); } } } @@ -60,16 +52,16 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag document_path: Some(path), document_serialized_content: content, }; - executor.queue_message(message.into()); + dispatcher.queue_editor_message(message.into()); } DesktopWrapperMessage::ImportFile { path, content } => { let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase(); match extension.as_str() { "svg" => { - executor.queue(DesktopWrapperMessage::ImportSvg { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content }); } _ => { - executor.queue(DesktopWrapperMessage::ImportImage { path, content }); + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportImage { path, content }); } } } @@ -85,7 +77,7 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag mouse: None, parent_and_insert_index: None, }; - executor.queue_message(message.into()); + dispatcher.queue_editor_message(message.into()); } DesktopWrapperMessage::ImportImage { path, content } => { let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string()); @@ -101,6 +93,8 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag }; let width = image.width(); let height = image.height(); + + // TODO: Handle Image formats with more than 8 bits per channel let image_data = image.to_rgba8(); let image = Image::::from_image_data(image_data.as_raw(), width, height); let message = PortfolioMessage::PasteImage { @@ -109,8 +103,8 @@ pub(super) fn handle_desktop_wrapper_message(executor: &mut DesktopWrapperMessag mouse: None, parent_and_insert_index: None, }; - executor.queue_message(message.into()); + dispatcher.queue_editor_message(message.into()); } - DesktopWrapperMessage::PollNodeGraphEvaluation => executor.poll_node_graph_evaluation(), + DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(), } } diff --git a/desktop/src/desktop_wrapper/intercept_editor_message.rs b/desktop/src/desktop_wrapper/intercept_editor_message.rs new file mode 100644 index 0000000000..4fc869f8c5 --- /dev/null +++ b/desktop/src/desktop_wrapper/intercept_editor_message.rs @@ -0,0 +1,23 @@ +use graphite_editor::messages::prelude::InputPreprocessorMessage; + +use super::DesktopWrapperMessageDispatcher; +use super::messages::{DesktopFrontendMessage, EditorMessage}; + +pub(super) fn intercept_editor_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: EditorMessage) -> Option { + match message { + EditorMessage::InputPreprocessor(message) => { + if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { + let top_left = bounds_of_viewports[0].top_left; + let bottom_right = bounds_of_viewports[0].bottom_right; + dispatcher.respond(DesktopFrontendMessage::UpdateViewportBounds { + x: top_left.x as f32, + y: top_left.y as f32, + width: (bottom_right.x - top_left.x) as f32, + height: (bottom_right.y - top_left.y) as f32, + }); + } + Some(EditorMessage::InputPreprocessor(message)) + } + m => Some(m), + } +} diff --git a/desktop/src/desktop_wrapper/intercept_frontend_message.rs b/desktop/src/desktop_wrapper/intercept_frontend_message.rs index a8dce0c308..a8f1ef0b06 100644 --- a/desktop/src/desktop_wrapper/intercept_frontend_message.rs +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -2,16 +2,16 @@ use std::path::PathBuf; use graphite_editor::messages::prelude::FrontendMessage; -use super::DesktopWrapperMessageExecutor; +use super::DesktopWrapperMessageDispatcher; use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; -pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExecutor, message: FrontendMessage) -> Option { +pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { FrontendMessage::RenderOverlays { context } => { - executor.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); + dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); } FrontendMessage::TriggerOpenDocument => { - executor.respond(DesktopFrontendMessage::OpenFileDialog { + dispatcher.respond(DesktopFrontendMessage::OpenFileDialog { title: "Open Document".to_string(), filters: vec![FileFilter { name: "Graphite".to_string(), @@ -21,7 +21,7 @@ pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExe }); } FrontendMessage::TriggerImport => { - executor.respond(DesktopFrontendMessage::OpenFileDialog { + dispatcher.respond(DesktopFrontendMessage::OpenFileDialog { title: "Import File".to_string(), filters: vec![ FileFilter { @@ -38,9 +38,9 @@ pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExe } FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { if let Some(path) = path { - executor.respond(DesktopFrontendMessage::WriteFile { path, content }); + dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content }); } else { - executor.respond(DesktopFrontendMessage::SaveFileDialog { + dispatcher.respond(DesktopFrontendMessage::SaveFileDialog { title: "Save Document".to_string(), default_filename: name, default_folder: path.and_then(|p| p.parent().map(PathBuf::from)), @@ -53,16 +53,16 @@ pub(super) fn intercept_frontend_message(executor: &mut DesktopWrapperMessageExe } } FrontendMessage::TriggerSaveFile { name, content } => { - executor.respond(DesktopFrontendMessage::SaveFileDialog { + dispatcher.respond(DesktopFrontendMessage::SaveFileDialog { title: "Save File".to_string(), default_filename: name, default_folder: None, filters: Vec::new(), - context: SaveFileDialogContext::Export { content }, + context: SaveFileDialogContext::File { content }, }); } FrontendMessage::TriggerVisitLink { url } => { - executor.respond(DesktopFrontendMessage::OpenUrl(url)); + dispatcher.respond(DesktopFrontendMessage::OpenUrl(url)); } m => return Some(m), } diff --git a/desktop/src/desktop_wrapper/intercept_message.rs b/desktop/src/desktop_wrapper/intercept_message.rs deleted file mode 100644 index b698368289..0000000000 --- a/desktop/src/desktop_wrapper/intercept_message.rs +++ /dev/null @@ -1,23 +0,0 @@ -use graphite_editor::messages::prelude::{InputPreprocessorMessage, Message}; - -use super::DesktopWrapperMessageExecutor; -use super::messages::DesktopFrontendMessage; - -pub(super) fn intercept_message(executor: &mut DesktopWrapperMessageExecutor, message: Message) -> Option { - match message { - Message::InputPreprocessor(message) => { - if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message { - let top_left = bounds_of_viewports[0].top_left; - let bottom_right = bounds_of_viewports[0].bottom_right; - executor.respond(DesktopFrontendMessage::UpdateViewportBounds { - x: top_left.x as f32, - y: top_left.y as f32, - width: (bottom_right.x - top_left.x) as f32, - height: (bottom_right.y - top_left.y) as f32, - }); - } - Some(Message::InputPreprocessor(message)) - } - m => Some(m), - } -} diff --git a/desktop/src/desktop_wrapper/message_dispatcher.rs b/desktop/src/desktop_wrapper/message_dispatcher.rs new file mode 100644 index 0000000000..3c3852cf12 --- /dev/null +++ b/desktop/src/desktop_wrapper/message_dispatcher.rs @@ -0,0 +1,76 @@ +use graphite_editor::application::Editor; +use std::collections::VecDeque; + +use super::handle_desktop_wrapper_message::handle_desktop_wrapper_message; +use super::intercept_editor_message::intercept_editor_message; +use super::intercept_frontend_message::intercept_frontend_message; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage}; + +pub(crate) struct DesktopWrapperMessageDispatcher<'a> { + editor: &'a mut Editor, + desktop_wrapper_message_queue: VecDeque, + editor_message_queue: Vec, + responses: Vec, +} + +impl<'a> DesktopWrapperMessageDispatcher<'a> { + pub(crate) fn new(editor: &'a mut Editor) -> Self { + Self { + editor, + desktop_wrapper_message_queue: VecDeque::new(), + editor_message_queue: Vec::new(), + responses: Vec::new(), + } + } + + pub(crate) fn execute(mut self) -> Vec { + self.process_queue(); + self.responses + } + + pub(crate) fn queue_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) { + self.desktop_wrapper_message_queue.push_back(message); + } + + pub(super) fn queue_editor_message(&mut self, message: EditorMessage) { + if let Some(message) = intercept_editor_message(self, message) { + self.editor_message_queue.push(message); + } + } + + pub(super) fn respond(&mut self, response: DesktopFrontendMessage) { + self.responses.push(response); + } + + pub(super) fn poll_node_graph_evaluation(&mut self) { + let mut responses = VecDeque::new(); + if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) { + if e != "No active document" { + tracing::error!("Error poling node graph: {}", e); + } + } + while let Some(message) = responses.pop_front() { + self.queue_editor_message(message); + } + } + + fn process_queue(&mut self) { + let mut frontend_messages = Vec::new(); + + while !self.desktop_wrapper_message_queue.is_empty() || !self.editor_message_queue.is_empty() { + while let Some(message) = self.desktop_wrapper_message_queue.pop_front() { + handle_desktop_wrapper_message(self, message); + } + let current_frontend_messages = self + .editor + .handle_message(EditorMessage::Batched { + messages: std::mem::take(&mut self.editor_message_queue).into_boxed_slice(), + }) + .into_iter() + .filter_map(|m| intercept_frontend_message(self, m)); + frontend_messages.extend(current_frontend_messages); + } + + self.respond(DesktopFrontendMessage::ToWeb(frontend_messages)); + } +} diff --git a/desktop/src/desktop_wrapper/message_executor.rs b/desktop/src/desktop_wrapper/message_executor.rs deleted file mode 100644 index aca49079f2..0000000000 --- a/desktop/src/desktop_wrapper/message_executor.rs +++ /dev/null @@ -1,73 +0,0 @@ -use graphite_editor::{application::Editor, messages::prelude::Message}; -use std::collections::VecDeque; - -use super::handle_desktop_wrapper_message::handle_desktop_wrapper_message; -use super::intercept_frontend_message::intercept_frontend_message; -use super::intercept_message::intercept_message; -use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage}; - -pub(crate) struct DesktopWrapperMessageExecutor<'a> { - editor: &'a mut Editor, - queue: VecDeque, - messages: Vec, - responses: Vec, -} - -impl<'a> DesktopWrapperMessageExecutor<'a> { - pub(crate) fn new(editor: &'a mut Editor) -> Self { - Self { - editor, - queue: VecDeque::new(), - messages: Vec::new(), - responses: Vec::new(), - } - } - - pub(crate) fn execute(mut self) -> Vec { - self.process_queue(); - self.responses - } - - pub(crate) fn queue(&mut self, message: DesktopWrapperMessage) { - self.queue.push_back(message); - } - - pub(super) fn queue_message(&mut self, message: Message) { - if let Some(message) = intercept_message(self, message) { - self.messages.push(message); - } - } - - pub(super) fn respond(&mut self, response: DesktopFrontendMessage) { - self.responses.push(response); - } - - pub(super) fn poll_node_graph_evaluation(&mut self) { - let mut responses = VecDeque::new(); - if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) { - if e != "No active document" { - tracing::error!("Error poling node graph: {}", e); - } - } - while let Some(message) = responses.pop_front() { - self.queue_message(message); - } - } - - fn process_queue(&mut self) { - while !self.queue.is_empty() || !self.messages.is_empty() { - while let Some(message) = self.queue.pop_front() { - handle_desktop_wrapper_message(self, message); - } - let frontend_messages = self - .editor - .handle_message(Message::Batched { - messages: std::mem::take(&mut self.messages).into_boxed_slice(), - }) - .into_iter() - .filter_map(|m| intercept_frontend_message(self, m)) - .collect::>(); - self.respond(DesktopFrontendMessage::ToWeb(ron::to_string(&frontend_messages).unwrap().into_bytes())); - } - } -} diff --git a/desktop/src/desktop_wrapper/messages.rs b/desktop/src/desktop_wrapper/messages.rs index b414f87e12..0b5258cafd 100644 --- a/desktop/src/desktop_wrapper/messages.rs +++ b/desktop/src/desktop_wrapper/messages.rs @@ -1,9 +1,11 @@ use std::path::PathBuf; -use graphite_editor::messages::prelude::DocumentId; +use graphite_editor::messages::prelude::{DocumentId, FrontendMessage}; + +pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; pub enum DesktopFrontendMessage { - ToWeb(Vec), + ToWeb(Vec), OpenFileDialog { title: String, filters: Vec, @@ -36,7 +38,7 @@ pub struct FileFilter { } pub enum DesktopWrapperMessage { - FromWeb(Vec), + FromWeb(Box), OpenFileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext }, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, OpenDocument { path: PathBuf, content: Vec }, @@ -54,5 +56,5 @@ pub enum OpenFileDialogContext { pub enum SaveFileDialogContext { Document { document_id: DocumentId, content: Vec }, - Export { content: Vec }, + File { content: Vec }, } diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 4a1a37e331..653b559d70 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -19,7 +19,6 @@ mod desktop_wrapper; use desktop_wrapper::messages::DesktopWrapperMessage; use desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; -#[allow(clippy::large_enum_variant)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant),