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:
parent
8d0c9d7b81
commit
fa2167dd7f
|
|
@ -33,7 +33,7 @@
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system overlays;
|
inherit system overlays;
|
||||||
};
|
};
|
||||||
|
|
||||||
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
|
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
||||||
|
|
@ -75,6 +75,12 @@
|
||||||
vulkan-loader
|
vulkan-loader
|
||||||
libraw
|
libraw
|
||||||
libGL
|
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
|
# Development tools that don't need to be in LD_LIBRARY_PATH
|
||||||
|
|
|
||||||
|
|
@ -2113,6 +2113,7 @@ dependencies = [
|
||||||
"graph-craft",
|
"graph-craft",
|
||||||
"graphene-std",
|
"graphene-std",
|
||||||
"graphite-editor",
|
"graphite-editor",
|
||||||
|
"image",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"open",
|
"open",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,4 @@ vello = { workspace = true }
|
||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ use crate::dialogs::dialog_save_graphite_file;
|
||||||
use crate::render::GraphicsState;
|
use crate::render::GraphicsState;
|
||||||
use crate::render::WgpuContext;
|
use crate::render::WgpuContext;
|
||||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||||
|
use graphene_std::Color;
|
||||||
|
use graphene_std::raster::Image;
|
||||||
use graphite_editor::application::Editor;
|
use graphite_editor::application::Editor;
|
||||||
|
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
|
||||||
use graphite_editor::messages::prelude::*;
|
use graphite_editor::messages::prelude::*;
|
||||||
|
use std::fs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
@ -75,7 +79,7 @@ impl WinitApp {
|
||||||
String::new()
|
String::new()
|
||||||
});
|
});
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
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,
|
document_serialized_content: content,
|
||||||
};
|
};
|
||||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
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 };
|
let Some(event) = self.cef_context.handle_window_event(event) else { return };
|
||||||
|
|
||||||
match event {
|
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 => {
|
WindowEvent::CloseRequested => {
|
||||||
tracing::info!("The close button was pressed; stopping");
|
tracing::info!("The close button was pressed; stopping");
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
|
|
|
||||||
|
|
@ -781,7 +781,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
|
|
||||||
if create_document {
|
if create_document {
|
||||||
responses.add(PortfolioMessage::NewDocumentWithName {
|
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 {
|
if create_document {
|
||||||
responses.add(PortfolioMessage::NewDocumentWithName {
|
responses.add(PortfolioMessage::NewDocumentWithName {
|
||||||
name: name.clone().unwrap_or("Untitled Document".into()),
|
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue