diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0c08ce9564..8ca0fccc06 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,17 +1,14 @@ 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::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; +use crate::desktop_wrapper::serialize_frontend_messages; 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 +34,12 @@ pub(crate) struct WinitApp { graphics_state: Option, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy, - editor: Editor, + 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 desktop_wrapper = DesktopWrapper::new(); Self { cef_context, window: None, @@ -50,97 +48,106 @@ impl WinitApp { window_size_sender, wgpu_context, event_loop_proxy, - editor: Editor::new(), + desktop_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 { context: 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_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) { + match message { + 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()); } - } - - 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())); - } - }); - } - - 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 { + DesktopFrontendMessage::OpenFileDialog { title, filters, 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 { - 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)); - } + let mut dialog = AsyncFileDialog::new().set_title(title); + for filter in filters { + dialog = dialog.add_filter(filter.name, &filter.extensions); + } + + let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) }; + + if let Some(path) = futures::executor::block_on(show_dialog) + && let Ok(content) = std::fs::read(&path) + { + let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context }; + let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message)); } }); } - } + DesktopFrontendMessage::SaveFileDialog { + title, + default_filename, + default_folder, + filters, + context, + } => { + let event_loop_proxy = self.event_loop_proxy.clone(); + let _ = thread::spawn(move || { + 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()) }; - 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); + if let Some(path) = futures::executor::block_on(show_dialog) { + let message = DesktopWrapperMessage::SaveFileDialogResult { path, context }; + let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message)); } + }); + } + DesktopFrontendMessage::WriteFile { path, content } => { + if let Err(e) = std::fs::write(&path, content) { + tracing::error!("Failed to write file {}: {}", path.display(), e); } - }); - } + } + DesktopFrontendMessage::OpenUrl(url) => { + let _ = thread::spawn(move || { + if let Err(e) = open::that(&url) { + tracing::error!("Failed to open URL: {}: {}", url, e); + } + }); + } + DesktopFrontendMessage::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(); - 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_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]); + + 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]); + } + } + DesktopFrontendMessage::UpdateOverlays(scene) => { + if let Some(graphics_state) = &mut self.graphics_state { + graphics_state.set_overlays_scene(scene); } - }); + } } + } - if responses.is_empty() { - return; + fn handle_desktop_frontend_messages(&mut self, messages: Vec) { + for message in messages { + self.handle_desktop_frontend_message(message); } - 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_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) { + let responses = self.desktop_wrapper.dispatch(message); + self.handle_desktop_frontend_messages(responses); } } @@ -194,13 +201,25 @@ impl ApplicationHandler for WinitApp { 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)); + 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::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()); @@ -217,50 +236,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); - } - } } } @@ -268,76 +243,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(); @@ -362,6 +267,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/cef.rs b/desktop/src/cef.rs index af1e7ec73d..26a8e0f241 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -1,8 +1,9 @@ -use crate::{CustomEvent, WgpuContext, render::FrameBufferRef}; -use std::{ - sync::{Arc, Mutex, mpsc::Receiver}, - time::Instant, -}; +use crate::desktop_wrapper::WgpuContext; +use crate::render::FrameBufferRef; +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; @@ -121,14 +122,10 @@ 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 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 new file mode 100644 index 0000000000..8832f6db99 --- /dev/null +++ b/desktop/src/desktop_wrapper.rs @@ -0,0 +1,69 @@ +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_dispatcher; +use message_dispatcher::DesktopWrapperMessageDispatcher; + +mod handle_desktop_wrapper_message; +mod intercept_editor_message; +mod intercept_frontend_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 = DesktopWrapperMessageDispatcher::new(&mut self.editor); + executor.queue_desktop_wrapper_message(message); + executor.execute() + } + + 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)), + (false, _) => NodeGraphExecutionResult::NotRun, + } + } +} + +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 new file mode 100644 index 0000000000..2cc11464b7 --- /dev/null +++ b/desktop/src/desktop_wrapper/handle_desktop_wrapper_message.rs @@ -0,0 +1,110 @@ +use graphene_std::Color; +use graphene_std::raster::Image; +use graphite_editor::messages::prelude::{DocumentMessage, PortfolioMessage}; + +use super::DesktopWrapperMessageDispatcher; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; + +pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) { + match message { + DesktopWrapperMessage::FromWeb(message) => { + dispatcher.queue_editor_message(*message); + } + DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { + OpenFileDialogContext::Document => { + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); + } + OpenFileDialogContext::Import => { + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content }); + } + }, + DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context { + SaveFileDialogContext::Document { document_id, content } => { + 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::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" => { + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); + } + _ => { + dispatcher.queue_desktop_wrapper_message(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, + }; + 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" => { + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content }); + } + _ => { + dispatcher.queue_desktop_wrapper_message(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, + }; + 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()); + 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(); + + // 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 { + name, + image, + mouse: None, + parent_and_insert_index: None, + }; + dispatcher.queue_editor_message(message.into()); + } + 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 new file mode 100644 index 0000000000..a8f1ef0b06 --- /dev/null +++ b/desktop/src/desktop_wrapper/intercept_frontend_message.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; + +use graphite_editor::messages::prelude::FrontendMessage; + +use super::DesktopWrapperMessageDispatcher; +use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; + +pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { + match message { + FrontendMessage::RenderOverlays { context } => { + dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); + } + FrontendMessage::TriggerOpenDocument => { + dispatcher.respond(DesktopFrontendMessage::OpenFileDialog { + title: "Open Document".to_string(), + filters: vec![FileFilter { + name: "Graphite".to_string(), + extensions: vec!["graphite".to_string()], + }], + context: OpenFileDialogContext::Document, + }); + } + FrontendMessage::TriggerImport => { + dispatcher.respond(DesktopFrontendMessage::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 } => { + if let Some(path) = path { + dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content }); + } else { + dispatcher.respond(DesktopFrontendMessage::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 } => { + dispatcher.respond(DesktopFrontendMessage::SaveFileDialog { + title: "Save File".to_string(), + default_filename: name, + default_folder: None, + filters: Vec::new(), + context: SaveFileDialogContext::File { content }, + }); + } + FrontendMessage::TriggerVisitLink { url } => { + dispatcher.respond(DesktopFrontendMessage::OpenUrl(url)); + } + m => return Some(m), + } + None +} 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/messages.rs b/desktop/src/desktop_wrapper/messages.rs new file mode 100644 index 0000000000..0b5258cafd --- /dev/null +++ b/desktop/src/desktop_wrapper/messages.rs @@ -0,0 +1,60 @@ +use std::path::PathBuf; + +use graphite_editor::messages::prelude::{DocumentId, FrontendMessage}; + +pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; + +pub enum DesktopFrontendMessage { + ToWeb(Vec), + OpenFileDialog { + title: String, + filters: Vec, + context: OpenFileDialogContext, + }, + SaveFileDialog { + title: String, + default_filename: String, + default_folder: Option, + filters: Vec, + context: SaveFileDialogContext, + }, + WriteFile { + path: PathBuf, + content: Vec, + }, + OpenUrl(String), + UpdateViewportBounds { + x: f32, + y: f32, + width: f32, + height: f32, + }, + UpdateOverlays(vello::Scene), +} + +pub struct FileFilter { + pub name: String, + pub extensions: Vec, +} + +pub enum DesktopWrapperMessage { + FromWeb(Box), + 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, +} + +pub enum OpenFileDialogContext { + Document, + Import, +} + +pub enum SaveFileDialogContext { + Document { document_id: DocumentId, content: Vec }, + File { content: Vec }, +} 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 36fe41db82..653b559d70 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,22 +9,21 @@ mod cef; use cef::{Setup, WindowSize}; mod render; -use render::WgpuContext; mod app; use app::WinitApp; mod dirs; -mod dialogs; +mod desktop_wrapper; +use desktop_wrapper::messages::DesktopWrapperMessage; +use desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult}; -#[derive(Debug)] pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), - DispatchMessage(Message), - MessageReceived(Message), - NodeGraphRan(Option), + DesktopWrapperMessage(DesktopWrapperMessage), + NodeGraphExecutionResult(NodeGraphExecutionResult), } fn main() { @@ -46,7 +42,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(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) => { @@ -66,10 +62,10 @@ 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 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(); 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..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; -pub(crate) use wgpu_executor::Context as WgpuContext; +use crate::desktop_wrapper::WgpuContext; #[derive(derivative::Derivative)] #[derivative(Debug)] 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 }) } }