From 42109fd45c88ca21f547d95c3927a158e9dd1fbb Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 16:07:40 +0000 Subject: [PATCH 1/3] Desktop app add drop file functionality --- Cargo.lock | 1 + desktop/Cargo.toml | 1 + desktop/src/app.rs | 73 ++++++++++++++++++- .../portfolio/portfolio_message_handler.rs | 4 +- 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9a131eba5..24ed8b2c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2113,6 +2113,7 @@ dependencies = [ "graph-craft", "graphene-std", "graphite-editor", + "image", "include_dir", "open", "rfd", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 03768efd38..ee27130810 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -39,3 +39,4 @@ vello = { workspace = true } derivative = { workspace = true } rfd = { workspace = true } open = { workspace = true } +image = { workspace = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0ec9eaa81e..c7e1b5bf0b 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -7,8 +7,12 @@ use crate::dialogs::dialog_save_graphite_file; 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::consts::DEFAULT_DOCUMENT_NAME; use graphite_editor::messages::prelude::*; +use std::fs; use std::sync::Arc; use std::sync::mpsc::Sender; use std::thread; @@ -75,7 +79,7 @@ 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: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(), document_serialized_content: content, }; let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into())); @@ -264,6 +268,73 @@ 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()); + match path.extension().and_then(|s| s.to_str()) { + Some("graphite") => { + let content = fs::read_to_string(&path).unwrap_or_else(|_| { + tracing::error!("Failed to read file: {}", path.display()); + String::new() + }); + + if !content.is_empty() { + let message = PortfolioMessage::OpenDocumentFile { + document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()), + document_serialized_content: content, + }; + self.dispatch_message(message.into()); + } else { + tracing::warn!("Dropped file is empty: {}", path.display()); + } + } + Some("svg") => { + let content = fs::read_to_string(&path).unwrap_or_else(|_| { + tracing::error!("Failed to read file: {}", path.display()); + String::new() + }); + + if !content.is_empty() { + 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()); + } else { + tracing::warn!("Dropped file is empty: {}", path.display()); + } + } + Some(_) => match image::ImageReader::open(&path) { + Ok(reader) => 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.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); + } + }, + _ => { + tracing::warn!("Unsupported file dropped: {}", path.display()); + } + } + } WindowEvent::CloseRequested => { tracing::info!("The close button was pressed; stopping"); event_loop.exit(); diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54ed..de00871eda 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -770,7 +770,7 @@ impl MessageHandler> for Portfolio if create_document { responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or("Untitled Document".into()), + name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), }); } @@ -801,7 +801,7 @@ impl MessageHandler> for Portfolio if create_document { responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or("Untitled Document".into()), + name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), }); } From 88d75904514e990fee13fdcbbd8c2fc5f8e33a92 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 9 Aug 2025 16:08:11 +0000 Subject: [PATCH 2/3] Add x11 libs to flake --- .nix/flake.nix | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.nix/flake.nix b/.nix/flake.nix index d8fb88ed54..d058edc292 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -33,7 +33,7 @@ pkgs = import nixpkgs { inherit system overlays; }; - + rustc-wasm = pkgs.rust-bin.stable.latest.default.override { targets = [ "wasm32-unknown-unknown" ]; extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; @@ -75,6 +75,12 @@ vulkan-loader libraw libGL + + # X11 libraries, not needed on wayland! Remove when x11 is finally dead + libxkbcommon + xorg.libXcursor + xorg.libxcb + xorg.libX11 ]; # Development tools that don't need to be in LD_LIBRARY_PATH From bd6d2a82d69ce29e4acf75eaada68d5bc31c25ab Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 11 Aug 2025 13:04:37 +0200 Subject: [PATCH 3/3] Restructure extension matching to remove nesting --- desktop/src/app.rs | 76 ++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index c7e1b5bf0b..803a2f0a3a 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -271,46 +271,51 @@ impl ApplicationHandler for WinitApp { // 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()); - match path.extension().and_then(|s| s.to_str()) { - Some("graphite") => { - let content = fs::read_to_string(&path).unwrap_or_else(|_| { - tracing::error!("Failed to read file: {}", path.display()); - String::new() - }); - - if !content.is_empty() { - let message = PortfolioMessage::OpenDocumentFile { - document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()), - document_serialized_content: content, - }; - self.dispatch_message(message.into()); - } else { - tracing::warn!("Dropped file is empty: {}", path.display()); - } + 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("svg") => { - let content = fs::read_to_string(&path).unwrap_or_else(|_| { - tracing::error!("Failed to read file: {}", path.display()); - String::new() - }); - - if !content.is_empty() { - 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()); - } else { - tracing::warn!("Dropped file is empty: {}", path.display()); - } + 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: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()), + document_serialized_content: content, + }; + self.dispatch_message(message.into()); } - Some(_) => match image::ImageReader::open(&path) { + "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); @@ -330,9 +335,6 @@ impl ApplicationHandler for WinitApp { tracing::error!("Failed to open image file: {}: {}", path.display(), e); } }, - _ => { - tracing::warn!("Unsupported file dropped: {}", path.display()); - } } } WindowEvent::CloseRequested => {