Desktop: Drag and drop file to open/import functionality (#3035)

* Desktop app add drop file functionality

* Add x11 libs to flake

* Restructure extension matching to remove nesting

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Timon 2025-08-11 11:48:10 +00:00 committed by GitHub
parent 8d0c9d7b81
commit fa2167dd7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 4 deletions

View File

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

1
Cargo.lock generated
View File

@ -2113,6 +2113,7 @@ dependencies = [
"graph-craft",
"graphene-std",
"graphite-editor",
"image",
"include_dir",
"open",
"rfd",

View File

@ -39,3 +39,4 @@ vello = { workspace = true }
derivative = { workspace = true }
rfd = { workspace = true }
open = { workspace = true }
image = { workspace = true }

View File

@ -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,75 @@ impl ApplicationHandler<CustomEvent> 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: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
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::<Color>::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();

View File

@ -781,7 +781,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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()),
});
}
@ -812,7 +812,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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()),
});
}