From 9421aaec7f1a865f8f4f6110da991bf1ac278a57 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 18:38:35 +0100 Subject: [PATCH 01/13] wip --- desktop/src/app.rs | 2 +- .../src/handle_desktop_wrapper_message.rs | 79 +------ .../wrapper/src/intercept_frontend_message.rs | 20 +- desktop/wrapper/src/messages.rs | 16 +- editor/src/dispatcher.rs | 2 +- .../src/messages/frontend/frontend_message.rs | 4 +- .../messages/input_mapper/input_mappings.rs | 2 +- .../menu_bar/menu_bar_message_handler.rs | 7 +- .../messages/portfolio/portfolio_message.rs | 36 +++- .../portfolio/portfolio_message_handler.rs | 194 +++++++++++++----- .../src/messages/portfolio/utility_types.rs | 13 +- frontend/src/editor.ts | 2 +- frontend/src/io-managers/input.ts | 2 +- frontend/src/messages.ts | 6 +- frontend/src/state-providers/portfolio.ts | 43 +--- frontend/wasm/src/editor_api.rs | 17 +- 16 files changed, 224 insertions(+), 221 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index bafec9c1d2..82d0b9fd6c 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -180,7 +180,7 @@ impl App { if let Some(path) = futures::executor::block_on(show_dialog) && let Ok(content) = fs::read(&path) { - let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context }; + let message = DesktopWrapperMessage::FileDialogResult { path, content, context }; app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } }); diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 5330e67977..e4efc915c2 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -1,5 +1,3 @@ -use graphene_std::Color; -use graphene_std::raster::Image; use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw; use graphite_editor::messages::prelude::*; @@ -14,9 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess DesktopWrapperMessage::Input(message) => { dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message)); } - DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context { - OpenFileDialogContext::Document => { - dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content }); + DesktopWrapperMessage::FileDialogResult { path, content, context } => match context { + OpenFileDialogContext::Open => { + dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenFile { path, content }); } OpenFileDialogContext::Import => { dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content }); @@ -35,78 +33,11 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess } }, 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, - }; + let message = PortfolioMessage::OpenFile { path, content }; dispatcher.queue_editor_message(message); } 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); - } - 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, - }; + let message = PortfolioMessage::ImportFile { path, content }; dispatcher.queue_editor_message(message); } DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(), diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 9b64020eb5..e4ab2aeb2b 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -10,29 +10,17 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::RenderOverlays { context } => { dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); } - FrontendMessage::TriggerOpenDocument => { + FrontendMessage::TriggerOpen => { dispatcher.respond(DesktopFrontendMessage::OpenFileDialog { title: "Open Document".to_string(), - filters: vec![FileFilter { - name: "Graphite".to_string(), - extensions: vec!["graphite".to_string()], - }], - context: OpenFileDialogContext::Document, + filters: vec![], + context: OpenFileDialogContext::Open, }); } 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()], - }, - ], + filters: vec![], context: OpenFileDialogContext::Import, }); } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 23a9247547..84aab2b2aa 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -79,7 +79,7 @@ pub enum DesktopFrontendMessage { pub enum DesktopWrapperMessage { FromWeb(Box), Input(InputMessage), - OpenFileDialogResult { + FileDialogResult { path: PathBuf, content: Vec, context: OpenFileDialogContext, @@ -88,10 +88,6 @@ pub enum DesktopWrapperMessage { path: PathBuf, context: SaveFileDialogContext, }, - OpenDocument { - path: PathBuf, - content: Vec, - }, OpenFile { path: PathBuf, content: Vec, @@ -100,14 +96,6 @@ pub enum DesktopWrapperMessage { path: PathBuf, content: Vec, }, - ImportSvg { - path: PathBuf, - content: Vec, - }, - ImportImage { - path: PathBuf, - content: Vec, - }, PollNodeGraphEvaluation, UpdateMaximized { maximized: bool, @@ -153,7 +141,7 @@ pub struct FileFilter { } pub enum OpenFileDialogContext { - Document, + Open, Import, } diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 296084c835..f1299b0e26 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -571,7 +571,7 @@ mod test { "Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)", ); - let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile { + let responses = editor.editor.handle_message(PortfolioMessage::OpenFile { document_name: Some(document_name.to_string()), document_path: None, document_serialized_content, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e5e64720ed..188456d5b2 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -107,7 +107,6 @@ pub enum FrontendMessage { font: Font, url: String, }, - TriggerImport, TriggerPersistenceRemoveDocument { #[serde(rename = "documentId")] document_id: DocumentId, @@ -122,7 +121,8 @@ pub enum FrontendMessage { TriggerLoadRestAutoSaveDocuments, TriggerOpenLaunchDocuments, TriggerLoadPreferences, - TriggerOpenDocument, + TriggerOpen, + TriggerImport, TriggerSavePreferences { preferences: PreferencesMessageHandler, }, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 8daf93ca9f..cc94c276d2 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -442,7 +442,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument), entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation), entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation), - entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument), + entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::Open), entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import), entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index ec785db165..c867b509fe 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -120,8 +120,8 @@ impl LayoutHolder for MenuBarMessageHandler { MenuListEntry::new("Open…") .label("Open…") .icon("Folder") - .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::OpenDocument)) - .on_commit(|_| PortfolioMessage::OpenDocument.into()), + .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Open)) + .on_commit(|_| PortfolioMessage::Open.into()), MenuListEntry::new("Open Demo Artwork…") .label("Open Demo Artwork…") .icon("Image") @@ -161,7 +161,8 @@ impl LayoutHolder for MenuBarMessageHandler { .label("Import…") .icon("FileImport") .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Import)) - .on_commit(|_| PortfolioMessage::Import.into()), + .on_commit(|_| PortfolioMessage::Import.into()) + .disabled(no_active_document), MenuListEntry::new("Export…") .label("Export…") .icon("FileExport") diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index de11acd363..03b5fb2ab8 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -58,7 +58,6 @@ pub enum PortfolioMessage { font_style: String, data: Vec, }, - Import, LoadDocumentResources { document_id: DocumentId, }, @@ -66,7 +65,16 @@ pub enum PortfolioMessage { name: String, }, NextDocument, - OpenDocument, + Open, + Import, + OpenFile { + path: PathBuf, + content: Vec, + }, + ImportFile { + path: PathBuf, + content: Vec, + }, OpenDocumentFile { document_name: Option, document_path: Option, @@ -82,11 +90,13 @@ pub enum PortfolioMessage { to_front: bool, select_after_open: bool, }, - ToggleResetNodesToDefinitionsOnOpen, - PasteIntoFolder { - clipboard: Clipboard, - parent: LayerNodeIdentifier, - insert_index: usize, + OpenImage { + name: Option, + image: Image, + }, + OpenSvg { + name: Option, + svg: String, }, PasteSerializedData { data: String, @@ -94,9 +104,6 @@ pub enum PortfolioMessage { PasteSerializedVector { data: String, }, - CenterPastedLayers { - layers: Vec, - }, PasteImage { name: Option, image: Image, @@ -109,6 +116,14 @@ pub enum PortfolioMessage { mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, }, + PasteIntoFolder { + clipboard: Clipboard, + parent: LayerNodeIdentifier, + insert_index: usize, + }, + CenterPastedLayers { + layers: Vec, + }, PrevDocument, RequestWelcomeScreenButtonsLayout, RequestStatusBarInfoLayout, @@ -132,6 +147,7 @@ pub enum PortfolioMessage { document_id: DocumentId, ignore_hash: bool, }, + ToggleResetNodesToDefinitionsOnOpen, ToggleDataPanelOpen, TogglePropertiesPanelOpen, ToggleLayersPanelOpen, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 6e8ba98842..7648af860a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -17,6 +17,7 @@ use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document_migration::*; +use crate::messages::portfolio::utility_types::FileContent; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -27,11 +28,13 @@ use derivative::*; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; use graphene_std::Color; +use graphene_std::raster_types::Image; use graphene_std::renderer::Quad; use graphene_std::subpath::BezierHandles; use graphene_std::text::Font; use graphene_std::vector::misc::HandleId; use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; +use std::path::PathBuf; use std::vec; #[derive(ExtractField)] @@ -426,10 +429,6 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), - PortfolioMessage::Import => { - // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages - responses.add(FrontendMessage::TriggerImport); - } PortfolioMessage::LoadDocumentResources { document_id } => { let catalog = &self.persistent_data.font_catalog; @@ -465,10 +464,60 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::SelectDocument { document_id: next_id }); } } - PortfolioMessage::OpenDocument => { + PortfolioMessage::Open => { + // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages + responses.add(FrontendMessage::TriggerOpen); + } + PortfolioMessage::Import => { // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages - responses.add(FrontendMessage::TriggerOpenDocument); + responses.add(FrontendMessage::TriggerImport); + } + PortfolioMessage::OpenFile { path, content } => { + let name = path.file_stem().map(|n| n.to_string_lossy().to_string()); + match Self::read_file(&path, content) { + FileContent::Document(content) => { + responses.add(PortfolioMessage::OpenDocumentFile { + document_name: name, + document_path: Some(path), + document_serialized_content: content, + }); + } + FileContent::Svg(svg) => { + responses.add(PortfolioMessage::OpenSvg { name, svg }); + } + FileContent::Image(image) => { + responses.add(PortfolioMessage::OpenImage { name, image }); + } + FileContent::Unsupported => { + // TODO: Show some form of error message to the user + } + } + } + PortfolioMessage::ImportFile { path, content } => { + let name = path.file_stem().map(|n| n.to_string_lossy().to_string()); + match Self::read_file(&path, content) { + FileContent::Image(image) => { + responses.add(PortfolioMessage::PasteImage { + name, + image, + mouse: None, + parent_and_insert_index: None, + }); + } + FileContent::Svg(svg) => { + responses.add(PortfolioMessage::PasteSvg { + name, + svg, + mouse: None, + parent_and_insert_index: None, + }); + } + _ => { + // TODO: Show some form of error message to the user + } + } } + PortfolioMessage::OpenDocumentFile { document_name, document_path, @@ -596,6 +645,46 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::SelectDocument { document_id }); } } + PortfolioMessage::OpenImage { name, image } => { + responses.add(PortfolioMessage::NewDocumentWithName { + name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), + }); + + responses.add(DocumentMessage::PasteImage { + name, + image, + mouse: None, + parent_and_insert_index: None, + }); + + // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image + responses.add(DeferMessage::AfterGraphRun { + messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], + }); + responses.add(DeferMessage::AfterNavigationReady { + messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], + }); + } + PortfolioMessage::OpenSvg { name, svg } => { + responses.add(PortfolioMessage::NewDocumentWithName { + name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), + }); + + responses.add(DocumentMessage::PasteSvg { + name, + svg, + mouse: None, + parent_and_insert_index: None, + }); + + // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted SVG + responses.add(DeferMessage::AfterGraphRun { + messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], + }); + responses.add(DeferMessage::AfterNavigationReady { + messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], + }); + } PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let mut all_new_ids = Vec::new(); let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec| { @@ -856,28 +945,14 @@ impl MessageHandler> for Portfolio mouse, parent_and_insert_index, } => { - let create_document = self.documents.is_empty(); - - if create_document { - responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), - }); - } - - responses.add(DocumentMessage::PasteImage { - name, - image, - mouse, - parent_and_insert_index, - }); - - if create_document { - // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image - responses.add(DeferMessage::AfterGraphRun { - messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], - }); - responses.add(DeferMessage::AfterNavigationReady { - messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], + if self.documents.is_empty() { + responses.add(PortfolioMessage::OpenImage { name, image }); + } else { + responses.add(DocumentMessage::PasteImage { + name, + image, + mouse, + parent_and_insert_index, }); } } @@ -887,29 +962,14 @@ impl MessageHandler> for Portfolio mouse, parent_and_insert_index, } => { - let create_document = self.documents.is_empty(); - - if create_document { - responses.add(PortfolioMessage::NewDocumentWithName { - name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()), - }); - } - - responses.add(DocumentMessage::PasteSvg { - name, - svg, - mouse, - parent_and_insert_index, - }); - - if create_document { - // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image - responses.add(DeferMessage::AfterGraphRun { - messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], - }); - - responses.add(DeferMessage::AfterNavigationReady { - messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], + if self.documents.is_empty() { + responses.add(PortfolioMessage::OpenSvg { name, svg }); + } else { + responses.add(DocumentMessage::PasteSvg { + name, + svg, + mouse, + parent_and_insert_index, }); } } @@ -940,9 +1000,9 @@ impl MessageHandler> for Portfolio TextButton::new("Open Document") .icon(Some("Folder".into())) .flush(true) - .on_commit(|_| PortfolioMessage::OpenDocument.into()) + .on_commit(|_| PortfolioMessage::Open.into()) .widget_instance(), - ShortcutLabel::new(action_shortcut!(PortfolioMessageDiscriminant::OpenDocument)).widget_instance(), + ShortcutLabel::new(action_shortcut!(PortfolioMessageDiscriminant::Open)).widget_instance(), ], vec![ TextButton::new("Open Demo Artwork") @@ -1207,7 +1267,7 @@ impl MessageHandler> for Portfolio CloseAllDocumentsWithConfirmation, Import, NextDocument, - OpenDocument, + Open, PasteIntoFolder, PrevDocument, ToggleRulers, @@ -1281,6 +1341,32 @@ impl PortfolioMessageHandler { } } + fn read_file(path: &PathBuf, content: Vec) -> FileContent { + let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or_default().to_lowercase(); + match extension.as_str() { + FILE_EXTENSION => match String::from_utf8(content) { + Ok(content) => FileContent::Document(content), + Err(_) => FileContent::Unsupported, + }, + "svg" => match String::from_utf8(content) { + Ok(content) => FileContent::Svg(content), + Err(_) => FileContent::Unsupported, + }, + _ => { + let format = image::guess_format(&content).unwrap_or_else(|_| image::ImageFormat::from_path(path).unwrap_or(image::ImageFormat::Png)); + match image::load_from_memory_with_format(&content, format) { + Ok(image) => { + // 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(), image.width(), image.height()); + FileContent::Image(image) + } + Err(_) => FileContent::Unsupported, + } + } + } + } + fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque, to_front: bool) { if to_front { self.document_ids.push_front(document_id); diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index a43fced4c1..0d22876eab 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,4 +1,8 @@ -use graphene_std::text::{Font, FontCache}; +use graphene_std::{ + Color, + raster::Image, + text::{Font, FontCache}, +}; #[derive(Debug, Default)] pub struct PersistentData { @@ -104,3 +108,10 @@ impl From for PanelType { } } } + +pub enum FileContent { + Document(String), + Image(Image), + Svg(String), + Unsupported, +} diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 1661b78920..847eddf914 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -66,7 +66,7 @@ export function createEditor(): Editor { const filename = url.pathname.split("/").pop() || "Untitled"; const content = await data.text(); - handle.openDocumentFile(filename, content); + handle.openFile(filename, content); // Remove the hash fragment from the URL history.replaceState("", "", `${window.location.pathname}${window.location.search}`); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index c606bbe998..3fd7ebc155 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -336,7 +336,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (file.name.endsWith(graphiteFileSuffix)) { const content = await file.text(); const documentName = file.name.slice(0, -graphiteFileSuffix.length); - editor.handle.openDocumentFile(documentName, content); + editor.handle.openFile(documentName, content); } }); } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 5be4a1eea7..1f232768a3 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -745,7 +745,7 @@ export class TriggerFetchAndOpenDocument extends JsMessage { readonly filename!: string; } -export class TriggerOpenDocument extends JsMessage {} +export class TriggerOpen extends JsMessage {} export class TriggerImport extends JsMessage {} @@ -1679,11 +1679,11 @@ export const messageMakers: Record = { TriggerFetchAndOpenDocument, TriggerFontCatalogLoad, TriggerFontDataLoad, - TriggerImport, TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, TriggerLoadRestAutoSaveDocuments, - TriggerOpenDocument, + TriggerOpen, + TriggerImport, TriggerOpenLaunchDocuments, TriggerPersistenceRemoveDocument, TriggerPersistenceWriteDocument, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index e7573cbdca..b665ff0b3b 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -8,7 +8,7 @@ import { TriggerExportImage, TriggerSaveFile, TriggerImport, - TriggerOpenDocument, + TriggerOpen, UpdateActiveDocument, UpdateOpenDocumentsList, UpdateDataPanelState, @@ -16,7 +16,7 @@ import { UpdateLayersPanelState, } from "@graphite/messages"; import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files"; -import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization"; +import { rasterizeSVG } from "@graphite/utility-functions/rasterization"; export function createPortfolioState(editor: Editor) { const { subscribe, update } = writable({ @@ -48,9 +48,9 @@ export function createPortfolioState(editor: Editor) { const { name, filename } = data; const url = new URL(`demo-artwork/${filename}`, document.location.href); const response = await fetch(url); - const content = await response.text(); + const content = await response.bytes(); - editor.handle.openDocumentFile(name, content); + editor.handle.openFile(name, content); } catch { // Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show setTimeout(() => { @@ -58,37 +58,16 @@ export function createPortfolioState(editor: Editor) { }, 0); } }); - editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => { + editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => { + console.error("Opening file"); const suffix = "." + editor.handle.fileExtension(); - const data = await upload(suffix, "text"); - - // Use filename as document name, removing the extension if it exists - let documentName = data.filename; - if (documentName.endsWith(suffix)) { - documentName = documentName.slice(0, -suffix.length); - } - - editor.handle.openDocumentFile(documentName, data.content); + const data = await upload(suffix + "image/*", "data"); + editor.handle.openFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { - const data = await upload("image/*", "both"); - - if (data.type.includes("svg")) { - const svg = new TextDecoder().decode(data.content.data); - editor.handle.pasteSvg(data.filename, svg); - return; - } - - // In case the user accidentally uploads a Graphite file, open it instead of failing to import it - const graphiteFileSuffix = "." + editor.handle.fileExtension(); - if (data.filename.endsWith(graphiteFileSuffix)) { - const documentName = data.filename.slice(0, -graphiteFileSuffix.length); - editor.handle.openDocumentFile(documentName, data.content.text); - return; - } - - const imageData = await extractPixelData(new Blob([new Uint8Array(data.content.data)], { type: data.type })); - editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height); + console.error("Importing file"); + const data = await upload("image/*", "data"); + editor.handle.importFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (data) => { downloadFile(data.name, data.content); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 192687feac..6877874007 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -22,6 +22,7 @@ use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; use std::cell::RefCell; +use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use wasm_bindgen::JsCast; @@ -414,13 +415,15 @@ impl EditorHandle { self.dispatch(message); } - #[wasm_bindgen(js_name = openDocumentFile)] - pub fn open_document_file(&self, document_name: String, document_serialized_content: String) { - let message = PortfolioMessage::OpenDocumentFile { - document_name: Some(document_name), - document_path: None, - document_serialized_content, - }; + #[wasm_bindgen(js_name = openFile)] + pub fn open_file(&self, path: String, content: Vec) { + let message = PortfolioMessage::OpenFile { path: PathBuf::from(path), content }; + self.dispatch(message); + } + + #[wasm_bindgen(js_name = importFile)] + pub fn import_file(&self, path: String, content: Vec) { + let message = PortfolioMessage::ImportFile { path: PathBuf::from(path), content }; self.dispatch(message); } From a4d3d22557cd6e740b6b3e5d6ba9a263f9abea8b Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 18:38:35 +0100 Subject: [PATCH 02/13] fix drag and drop --- desktop/src/app.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 82d0b9fd6c..251bac1111 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -550,7 +550,7 @@ impl ApplicationHandler for App { for path in paths { match fs::read(&path) { Ok(content) => { - let message = DesktopWrapperMessage::OpenFile { path, content }; + let message = DesktopWrapperMessage::ImportFile { path, content }; self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); } Err(e) => { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 7648af860a..af6b546bf9 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -496,22 +496,31 @@ impl MessageHandler> for Portfolio PortfolioMessage::ImportFile { path, content } => { let name = path.file_stem().map(|n| n.to_string_lossy().to_string()); match Self::read_file(&path, content) { - FileContent::Image(image) => { - responses.add(PortfolioMessage::PasteImage { + FileContent::Svg(svg) => { + responses.add(PortfolioMessage::PasteSvg { name, - image, + svg, mouse: None, parent_and_insert_index: None, }); } - FileContent::Svg(svg) => { - responses.add(PortfolioMessage::PasteSvg { + FileContent::Image(image) => { + responses.add(PortfolioMessage::PasteImage { name, - svg, + image, mouse: None, parent_and_insert_index: None, }); } + FileContent::Document(content) => { + // TODO: Consider importing a document as a Node into the current document + // For now treat importing a document as opening it + responses.add(PortfolioMessage::OpenDocumentFile { + document_name: name, + document_path: Some(path), + document_serialized_content: content, + }); + } _ => { // TODO: Show some form of error message to the user } From 52643b68a788e82619810677d77c55efcb810456 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 18:35:48 +0000 Subject: [PATCH 03/13] fix --- frontend/src/editor.ts | 2 +- frontend/src/io-managers/input.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 847eddf914..dfe63c5d35 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -65,7 +65,7 @@ export function createEditor(): Editor { if (!data.ok) throw new Error(); const filename = url.pathname.split("/").pop() || "Untitled"; - const content = await data.text(); + const content = await data.bytes(); handle.openFile(filename, content); // Remove the hash fragment from the URL diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 3fd7ebc155..19f5cab742 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -334,9 +334,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const graphiteFileSuffix = "." + editor.handle.fileExtension(); if (file.name.endsWith(graphiteFileSuffix)) { - const content = await file.text(); - const documentName = file.name.slice(0, -graphiteFileSuffix.length); - editor.handle.openFile(documentName, content); + const content = await file.bytes(); + editor.handle.openFile(file.name, content); } }); } From 36743131c1d6a89eef00905b3226e952765df444 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 19:04:04 +0000 Subject: [PATCH 04/13] fix tests --- editor/src/dispatcher.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index f1299b0e26..d32b73fc1b 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -572,9 +572,8 @@ mod test { ); let responses = editor.editor.handle_message(PortfolioMessage::OpenFile { - document_name: Some(document_name.to_string()), - document_path: None, - document_serialized_content, + path: document_name.into(), + content: document_serialized_content.bytes().collect(), }); // Check if the graph renders From 3b71d53d5aea9f7414073762bd1269f357140f7e Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 19:30:34 +0000 Subject: [PATCH 05/13] fix tests --- editor/src/dispatcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index d32b73fc1b..dedd70a044 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -572,7 +572,7 @@ mod test { ); let responses = editor.editor.handle_message(PortfolioMessage::OpenFile { - path: document_name.into(), + path: file_name.into(), content: document_serialized_content.bytes().collect(), }); From b72b1bf15f4e270851db045492a2fdb10eb13fa8 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 20 Jan 2026 20:16:53 +0000 Subject: [PATCH 06/13] fix warning --- frontend/src/state-providers/portfolio.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index b665ff0b3b..6688f0957f 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -59,13 +59,11 @@ export function createPortfolioState(editor: Editor) { } }); editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => { - console.error("Opening file"); const suffix = "." + editor.handle.fileExtension(); const data = await upload(suffix + "image/*", "data"); editor.handle.openFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { - console.error("Importing file"); const data = await upload("image/*", "data"); editor.handle.importFile(data.filename, data.content); }); From 854cddcf0593014ef68292551385dd5147d7512e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 21 Jan 2026 04:03:03 -0800 Subject: [PATCH 07/13] Partial code review --- .../portfolio/portfolio_message_handler.rs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index af6b546bf9..a4d0de0a78 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -489,13 +489,26 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::OpenImage { name, image }); } FileContent::Unsupported => { - // TODO: Show some form of error message to the user + // TODO: Show a more thoughtfully designed error message to the user + responses.add(DialogMessage::DisplayDialogError { + title: "Unsupported format".into(), + description: "This file cannot be opened because it is not a supported image file type.".into(), + }) } } } PortfolioMessage::ImportFile { path, content } => { let name = path.file_stem().map(|n| n.to_string_lossy().to_string()); match Self::read_file(&path, content) { + FileContent::Document(content) => { + // TODO: Consider importing a document as a node into the current document + // For now treat importing a document as opening it + responses.add(PortfolioMessage::OpenDocumentFile { + document_name: name, + document_path: Some(path), + document_serialized_content: content, + }); + } FileContent::Svg(svg) => { responses.add(PortfolioMessage::PasteSvg { name, @@ -512,15 +525,6 @@ impl MessageHandler> for Portfolio parent_and_insert_index: None, }); } - FileContent::Document(content) => { - // TODO: Consider importing a document as a Node into the current document - // For now treat importing a document as opening it - responses.add(PortfolioMessage::OpenDocumentFile { - document_name: name, - document_path: Some(path), - document_serialized_content: content, - }); - } _ => { // TODO: Show some form of error message to the user } From a3f0af246cecece217a02797441f2c5218b7c0c7 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 21 Jan 2026 14:25:52 +0000 Subject: [PATCH 08/13] add dialog --- .../src/messages/portfolio/portfolio_message_handler.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index a4d0de0a78..27e302f435 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -525,8 +525,12 @@ impl MessageHandler> for Portfolio parent_and_insert_index: None, }); } - _ => { - // TODO: Show some form of error message to the user + FileContent::Unsupported => { + // TODO: Show a more thoughtfully designed error message to the user + responses.add(DialogMessage::DisplayDialogError { + title: "Unsupported format".into(), + description: "This file cannot be imported because it is not a supported image file type.".into(), + }) } } } From 40974df18e0f30ad87894a4b7418682a48468643 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 21 Jan 2026 15:06:49 +0000 Subject: [PATCH 09/13] fix web --- frontend/src/components/panels/Document.svelte | 4 +--- frontend/src/components/panels/Layers.svelte | 4 +--- frontend/src/components/panels/Welcome.svelte | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 79a2d6739b..73855ab216 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -154,9 +154,7 @@ const graphiteFileSuffix = "." + editor.handle.fileExtension(); if (file.name.endsWith(graphiteFileSuffix)) { - const content = await file.text(); - const documentName = file.name.slice(0, -graphiteFileSuffix.length); - editor.handle.openDocumentFile(documentName, content); + editor.handle.openFile(file.name, await file.bytes()); return; } }); diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index c65691fd67..c961203cd4 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -527,9 +527,7 @@ // When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab const graphiteFileSuffix = "." + editor.handle.fileExtension(); if (file.name.endsWith(graphiteFileSuffix)) { - const content = await file.text(); - const documentName = file.name.slice(0, -graphiteFileSuffix.length); - editor.handle.openDocumentFile(documentName, content); + editor.handle.openFile(file.name, await file.bytes()); return; } }); diff --git a/frontend/src/components/panels/Welcome.svelte b/frontend/src/components/panels/Welcome.svelte index f1cfec862e..7c11cc41a0 100644 --- a/frontend/src/components/panels/Welcome.svelte +++ b/frontend/src/components/panels/Welcome.svelte @@ -51,9 +51,7 @@ const graphiteFileSuffix = "." + editor.handle.fileExtension(); if (file.name.endsWith(graphiteFileSuffix)) { - const content = await file.text(); - const documentName = file.name.slice(0, -graphiteFileSuffix.length); - editor.handle.openDocumentFile(documentName, content); + editor.handle.openFile(file.name, await file.bytes()); return; } }); From de2bb9c2809f78a6a55decc8cf5664746184aa80 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 21 Jan 2026 15:28:39 +0000 Subject: [PATCH 10/13] fix web --- frontend/src/editor.ts | 2 +- frontend/src/io-managers/input.ts | 3 +-- frontend/src/state-providers/portfolio.ts | 6 ++---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index dfe63c5d35..27a3acc598 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -66,7 +66,7 @@ export function createEditor(): Editor { const filename = url.pathname.split("/").pop() || "Untitled"; const content = await data.bytes(); - handle.openFile(filename, content); + handle.openFile(filename + ".graphite", content); // Remove the hash fragment from the URL history.replaceState("", "", `${window.location.pathname}${window.location.search}`); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 19f5cab742..28eb01746d 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -334,8 +334,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const graphiteFileSuffix = "." + editor.handle.fileExtension(); if (file.name.endsWith(graphiteFileSuffix)) { - const content = await file.bytes(); - editor.handle.openFile(file.name, content); + editor.handle.openFile(file.name, await file.bytes()); } }); } diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 6688f0957f..e3b5a7e0c7 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -45,12 +45,10 @@ export function createPortfolioState(editor: Editor) { }); editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => { try { - const { name, filename } = data; + const { filename } = data; const url = new URL(`demo-artwork/${filename}`, document.location.href); const response = await fetch(url); - const content = await response.bytes(); - - editor.handle.openFile(name, content); + editor.handle.openFile(filename, await response.bytes()); } catch { // Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show setTimeout(() => { From 66919b876fb7d8937aa43e511df2491dfef34abd Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 21 Jan 2026 15:36:47 +0000 Subject: [PATCH 11/13] push back release candidate expiry --- frontend/src/components/window/MainWindow.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/window/MainWindow.svelte b/frontend/src/components/window/MainWindow.svelte index c316909fe0..bb056d74f7 100644 --- a/frontend/src/components/window/MainWindow.svelte +++ b/frontend/src/components/window/MainWindow.svelte @@ -31,7 +31,7 @@ {#if $tooltip.visible} {/if} - {#if isDesktop() && new Date() > new Date("2026-01-31")} + {#if isDesktop() && new Date() > new Date("2026-03-15")}

From c404f1d4747f18a4e4514ac67d4a04f2f2c899db Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 21 Jan 2026 22:26:25 -0800 Subject: [PATCH 12/13] Code review --- desktop/src/persist.rs | 2 +- desktop/wrapper/src/lib.rs | 1 + .../portfolio/portfolio_message_handler.rs | 5 ++-- .../src/messages/portfolio/utility_types.rs | 12 ++++++---- .../src/components/panels/Document.svelte | 14 +++-------- frontend/src/components/panels/Layers.svelte | 16 ++++--------- frontend/src/components/panels/Welcome.svelte | 14 +++-------- frontend/src/editor.ts | 4 ++-- frontend/src/io-managers/input.ts | 12 +++------- frontend/src/messages.ts | 24 +++++++++---------- frontend/src/state-providers/portfolio.ts | 5 ++-- 11 files changed, 40 insertions(+), 69 deletions(-) diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index a5baa211b1..97d329f66e 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -190,7 +190,7 @@ impl DocumentStore { fn document_path(id: &DocumentId) -> std::path::PathBuf { let mut path = crate::dirs::app_autosave_documents_dir(); - path.push(format!("{:x}.graphite", id.0)); + path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION)); path } } diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 1080b83df6..5c2a096681 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -2,6 +2,7 @@ use graph_craft::wasm_application_io::WasmApplicationIo; use graphite_editor::application::{Editor, Environment, Host, Platform}; use graphite_editor::messages::prelude::{FrontendMessage, Message}; +pub use graphite_editor::consts::FILE_EXTENSION; // TODO: Remove usage of this reexport in desktop create and remove this line pub use graphene_std::Color; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 27e302f435..9d88711a02 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -534,7 +534,6 @@ impl MessageHandler> for Portfolio } } } - PortfolioMessage::OpenDocumentFile { document_name, document_path, @@ -1282,11 +1281,11 @@ impl MessageHandler> for Portfolio CloseActiveDocumentWithConfirmation, CloseAllDocuments, CloseAllDocumentsWithConfirmation, - Import, NextDocument, + PrevDocument, + Import, Open, PasteIntoFolder, - PrevDocument, ToggleRulers, ToggleDataPanelOpen, ); diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index 0d22876eab..c58e762ef1 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,8 +1,6 @@ -use graphene_std::{ - Color, - raster::Image, - text::{Font, FontCache}, -}; +use graphene_std::Color; +use graphene_std::raster::Image; +use graphene_std::text::{Font, FontCache}; #[derive(Debug, Default)] pub struct PersistentData { @@ -110,8 +108,12 @@ impl From for PanelType { } pub enum FileContent { + /// A Graphite document. Document(String), + /// A bitmap image. Image(Image), + /// An SVG file string. Svg(String), + /// Any other unsupported/unrecognized file type. Unsupported, } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 73855ab216..e3a5844e5e 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -140,22 +140,14 @@ const file = item.getAsFile(); if (!file) return; - if (file.type.includes("svg")) { + if (file.type.startsWith("image/svg")) { const svgData = await file.text(); editor.handle.pasteSvg(file.name, svgData, x, y); - return; - } - - if (file.type.startsWith("image")) { + } else if (file.type.startsWith("image/")) { const imageData = await extractPixelData(file); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, x, y); - return; - } - - const graphiteFileSuffix = "." + editor.handle.fileExtension(); - if (file.name.endsWith(graphiteFileSuffix)) { + } else if (file.name.endsWith("." + editor.handle.fileExtension())) { editor.handle.openFile(file.name, await file.bytes()); - return; } }); } diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index c961203cd4..d04e510c14 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -512,23 +512,15 @@ const file = item.getAsFile(); if (!file) return; - if (file.type.includes("svg")) { + if (file.type.startsWith("image/svg")) { const svgData = await file.text(); editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex); - return; - } - - if (file.type.startsWith("image")) { + } else if (file.type.startsWith("image/")) { const imageData = await extractPixelData(file); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex); - return; - } - - // When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab - const graphiteFileSuffix = "." + editor.handle.fileExtension(); - if (file.name.endsWith(graphiteFileSuffix)) { + } else if (file.name.endsWith("." + editor.handle.fileExtension())) { + // TODO: When we eventually have sub-documents, this should be changed to import the document as a node instead of opening it in a separate tab editor.handle.openFile(file.name, await file.bytes()); - return; } }); diff --git a/frontend/src/components/panels/Welcome.svelte b/frontend/src/components/panels/Welcome.svelte index 7c11cc41a0..c1aea444c0 100644 --- a/frontend/src/components/panels/Welcome.svelte +++ b/frontend/src/components/panels/Welcome.svelte @@ -37,22 +37,14 @@ const file = item.getAsFile(); if (!file) return; - if (file.type.includes("svg")) { + if (file.type.startsWith("image/svg")) { const svgData = await file.text(); editor.handle.pasteSvg(file.name, svgData); - return; - } - - if (file.type.startsWith("image")) { + } else if (file.type.startsWith("image/")) { const imageData = await extractPixelData(file); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height); - return; - } - - const graphiteFileSuffix = "." + editor.handle.fileExtension(); - if (file.name.endsWith(graphiteFileSuffix)) { + } else if (file.name.endsWith("." + editor.handle.fileExtension())) { editor.handle.openFile(file.name, await file.bytes()); - return; } }); } diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 27a3acc598..d8c018102e 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -60,13 +60,13 @@ export function createEditor(): Editor { if (!demoArtwork) return; try { - const url = new URL(`/demo-artwork/${demoArtwork}.graphite`, document.location.href); + const url = new URL(`/demo-artwork/${demoArtwork}.${handle.fileExtension()}`, document.location.href); const data = await fetch(url); if (!data.ok) throw new Error(); const filename = url.pathname.split("/").pop() || "Untitled"; const content = await data.bytes(); - handle.openFile(filename + ".graphite", content); + handle.openFile(`${filename}.${handle.fileExtension()}`, content); // Remove the hash fragment from the URL history.replaceState("", "", `${window.location.pathname}${window.location.search}`); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 28eb01746d..b6ae1cd5f6 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -321,19 +321,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const file = item.getAsFile(); if (!file) return; - if (file.type.includes("svg")) { + if (file.type.startsWith("image/svg")) { const text = await file.text(); editor.handle.pasteSvg(file.name, text); - return; - } - - if (file.type.startsWith("image")) { + } else if (file.type.startsWith("image/")) { const imageData = await extractPixelData(file); editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height); - } - - const graphiteFileSuffix = "." + editor.handle.fileExtension(); - if (file.name.endsWith(graphiteFileSuffix)) { + } else if (file.name.endsWith("." + editor.handle.fileExtension())) { editor.handle.openFile(file.name, await file.bytes()); } }); diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 1f232768a3..a137b4755c 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -1666,24 +1666,26 @@ export const messageMakers: Record = { DisplayDialogDismiss, DisplayDialogPanic, DisplayEditableTextbox, - DisplayEditableTextboxUpdateFontData, DisplayEditableTextboxTransform, + DisplayEditableTextboxUpdateFontData, DisplayRemoveEditableTextbox, - SendUIMetadata, - SendShortcutFullscreen, SendShortcutAltClick, + SendShortcutFullscreen, SendShortcutShiftClick, + SendUIMetadata, TriggerAboutGraphiteLocalizedCommitDate, + TriggerClipboardRead, + TriggerClipboardWrite, TriggerDisplayThirdPartyLicensesDialog, TriggerExportImage, TriggerFetchAndOpenDocument, TriggerFontCatalogLoad, TriggerFontDataLoad, + TriggerImport, TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, TriggerLoadRestAutoSaveDocuments, TriggerOpen, - TriggerImport, TriggerOpenLaunchDocuments, TriggerPersistenceRemoveDocument, TriggerPersistenceWriteDocument, @@ -1691,11 +1693,9 @@ export const messageMakers: Record = { TriggerSaveDocument, TriggerSaveFile, TriggerSavePreferences, - TriggerTextCommit, - TriggerClipboardRead, - TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite, + TriggerTextCommit, TriggerVisitLink, UpdateActiveDocument, UpdateBox, @@ -1714,6 +1714,7 @@ export const messageMakers: Record = { UpdateDocumentScrollbars, UpdateExportReorderIndex, UpdateEyedropperSamplingState, + UpdateFullscreen, UpdateGraphFadeArtwork, UpdateGraphViewOverlay, UpdateImportReorderIndex, @@ -1724,6 +1725,7 @@ export const messageMakers: Record = { UpdateLayersPanelControlBarRightLayout, UpdateLayersPanelState, UpdateLayerWidths, + UpdateMaximized, UpdateMenuBarLayout, UpdateMouseCursor, UpdateNodeGraphControlBarLayout, @@ -1735,22 +1737,20 @@ export const messageMakers: Record = { UpdateNodeThumbnail, UpdateOpenDocumentsList, UpdatePlatform, - UpdateMaximized, - UpdateFullscreen, - WindowPointerLockMove, - WindowFullscreen, UpdatePropertiesPanelLayout, UpdatePropertiesPanelState, UpdateStatusBarHintsLayout, UpdateStatusBarInfoLayout, UpdateToolOptionsLayout, UpdateToolShelfLayout, + UpdateUIScale, UpdateViewportHolePunch, UpdateViewportPhysicalBounds, - UpdateUIScale, UpdateVisibleNodes, UpdateWelcomeScreenButtonsLayout, UpdateWirePathInProgress, UpdateWorkingColorsLayout, + WindowFullscreen, + WindowPointerLockMove, } as const; export type JsMessageType = keyof typeof messageMakers; diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index e3b5a7e0c7..0019d952ee 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -45,10 +45,9 @@ export function createPortfolioState(editor: Editor) { }); editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => { try { - const { filename } = data; - const url = new URL(`demo-artwork/${filename}`, document.location.href); + const url = new URL(`demo-artwork/${data.filename}`, document.location.href); const response = await fetch(url); - editor.handle.openFile(filename, await response.bytes()); + editor.handle.openFile(data.filename, await response.bytes()); } catch { // Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show setTimeout(() => { From e8b68b39355e2156fde08cc3b59ecee6be0f1156 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 22 Jan 2026 00:38:10 -0800 Subject: [PATCH 13/13] Reduce code duplication for pasting files in frontend --- .../messages/portfolio/portfolio_message.rs | 1 + .../portfolio/portfolio_message_handler.rs | 18 +++++++------ .../src/components/panels/Document.svelte | 25 ++++++------------- frontend/src/components/panels/Layers.svelte | 18 ++----------- frontend/src/components/panels/Welcome.svelte | 17 ++----------- frontend/src/io-managers/input.ts | 21 +++------------- frontend/src/state-providers/portfolio.ts | 4 +-- frontend/src/utility-functions/files.ts | 19 ++++++++++++++ 8 files changed, 46 insertions(+), 77 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 03b5fb2ab8..30edbd6c3b 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -116,6 +116,7 @@ pub enum PortfolioMessage { mouse: Option<(f64, f64)>, parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>, }, + // TODO: Unused except by tests, remove? 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 9d88711a02..a43dcb7ffd 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -701,6 +701,7 @@ impl MessageHandler> for Portfolio messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], }); } + // TODO: Unused except by tests, remove? PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let mut all_new_ids = Vec::new(); let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>, all_new_ids: &mut Vec| { @@ -1278,21 +1279,22 @@ impl MessageHandler> for Portfolio fn actions(&self) -> ActionList { let mut common = actions!(PortfolioMessageDiscriminant; - CloseActiveDocumentWithConfirmation, - CloseAllDocuments, - CloseAllDocumentsWithConfirmation, - NextDocument, - PrevDocument, - Import, Open, - PasteIntoFolder, - ToggleRulers, ToggleDataPanelOpen, ); // Extend with actions that require an active document if let Some(document) = self.active_document() { common.extend(document.actions()); + common.extend(actions!(PortfolioMessageDiscriminant; + CloseActiveDocumentWithConfirmation, + CloseAllDocuments, + CloseAllDocumentsWithConfirmation, + ToggleRulers, + NextDocument, + PrevDocument, + Import, + )); // Extend with actions that must have a selected layer if document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some() { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index e3a5844e5e..2ad4679769 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -19,8 +19,9 @@ } from "@graphite/messages"; import type { AppWindowState } from "@graphite/state-providers/app-window"; import type { DocumentState } from "@graphite/state-providers/document"; + import { pasteFile } from "@graphite/utility-functions/files"; import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; - import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; + import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports"; import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte"; @@ -130,26 +131,14 @@ })($document.toolShelfLayout[0]); function dropFile(e: DragEvent) { - const { dataTransfer } = e; - const [x, y] = e.target instanceof Element && e.target.closest("[data-viewport]") ? [e.clientX, e.clientY] : [undefined, undefined]; - if (!dataTransfer) return; + if (!e.dataTransfer) return; + + let mouse: [number, number] | undefined = undefined; + if (e.target instanceof Element && e.target.closest("[data-viewport]")) mouse = [e.clientX, e.clientY]; e.preventDefault(); - Array.from(dataTransfer.items).forEach(async (item) => { - const file = item.getAsFile(); - if (!file) return; - - if (file.type.startsWith("image/svg")) { - const svgData = await file.text(); - editor.handle.pasteSvg(file.name, svgData, x, y); - } else if (file.type.startsWith("image/")) { - const imageData = await extractPixelData(file); - editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, x, y); - } else if (file.name.endsWith("." + editor.handle.fileExtension())) { - editor.handle.openFile(file.name, await file.bytes()); - } - }); + Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor, mouse)); } function panCanvasX(newValue: number) { diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index d04e510c14..aba662ef31 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -14,8 +14,8 @@ import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { TooltipState } from "@graphite/state-providers/tooltip"; + import { pasteFile } from "@graphite/utility-functions/files"; import { operatingSystem } from "@graphite/utility-functions/platform"; - import { extractPixelData } from "@graphite/utility-functions/rasterization"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; @@ -508,21 +508,7 @@ e.preventDefault(); - Array.from(e.dataTransfer.items).forEach(async (item) => { - const file = item.getAsFile(); - if (!file) return; - - if (file.type.startsWith("image/svg")) { - const svgData = await file.text(); - editor.handle.pasteSvg(file.name, svgData, undefined, undefined, insertParentId, insertIndex); - } else if (file.type.startsWith("image/")) { - const imageData = await extractPixelData(file); - editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, undefined, undefined, insertParentId, insertIndex); - } else if (file.name.endsWith("." + editor.handle.fileExtension())) { - // TODO: When we eventually have sub-documents, this should be changed to import the document as a node instead of opening it in a separate tab - editor.handle.openFile(file.name, await file.bytes()); - } - }); + Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor, undefined, insertParentId, insertIndex)); draggingData = undefined; fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined; diff --git a/frontend/src/components/panels/Welcome.svelte b/frontend/src/components/panels/Welcome.svelte index c1aea444c0..0c9ae65d48 100644 --- a/frontend/src/components/panels/Welcome.svelte +++ b/frontend/src/components/panels/Welcome.svelte @@ -4,8 +4,8 @@ import type { Editor } from "@graphite/editor"; import type { Layout } from "@graphite/messages"; import { patchLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages"; + import { pasteFile } from "@graphite/utility-functions/files"; import { isDesktop } from "@graphite/utility-functions/platform"; - import { extractPixelData } from "@graphite/utility-functions/rasterization"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; @@ -33,20 +33,7 @@ e.preventDefault(); - Array.from(e.dataTransfer.items).forEach(async (item) => { - const file = item.getAsFile(); - if (!file) return; - - if (file.type.startsWith("image/svg")) { - const svgData = await file.text(); - editor.handle.pasteSvg(file.name, svgData); - } else if (file.type.startsWith("image/")) { - const imageData = await extractPixelData(file); - editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height); - } else if (file.name.endsWith("." + editor.handle.fileExtension())) { - editor.handle.openFile(file.name, await file.bytes()); - } - }); + Array.from(e.dataTransfer.items).forEach(async (item) => await pasteFile(item, editor)); } diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index b6ae1cd5f6..b2a73c0b26 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -6,6 +6,7 @@ import { type DialogState } from "@graphite/state-providers/dialog"; import { type DocumentState } from "@graphite/state-providers/document"; import { type FullscreenState } from "@graphite/state-providers/fullscreen"; import { type PortfolioState } from "@graphite/state-providers/portfolio"; +import { pasteFile } from "@graphite/utility-functions/files"; import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry"; import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform"; import { extractPixelData } from "@graphite/utility-functions/rasterization"; @@ -312,24 +313,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli e.preventDefault(); Array.from(dataTransfer.items).forEach(async (item) => { - if (item.type === "text/plain") { - item.getAsString((text) => { - editor.handle.pasteText(text); - }); - } - - const file = item.getAsFile(); - if (!file) return; - - if (file.type.startsWith("image/svg")) { - const text = await file.text(); - editor.handle.pasteSvg(file.name, text); - } else if (file.type.startsWith("image/")) { - const imageData = await extractPixelData(file); - editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height); - } else if (file.name.endsWith("." + editor.handle.fileExtension())) { - editor.handle.openFile(file.name, await file.bytes()); - } + if (item.type === "text/plain") item.getAsString((text) => editor.handle.pasteText(text)); + await pasteFile(item, editor); }); } diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 0019d952ee..cf6487d0e4 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -56,11 +56,11 @@ export function createPortfolioState(editor: Editor) { } }); editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => { - const suffix = "." + editor.handle.fileExtension(); - const data = await upload(suffix + "image/*", "data"); + const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data"); editor.handle.openFile(data.filename, data.content); }); editor.subscriptions.subscribeJsMessage(TriggerImport, async () => { + // TODO: Use the same `accept` string as in the `TriggerOpen` handler once importing Graphite documents as nodes is supported const data = await upload("image/*", "data"); editor.handle.importFile(data.filename, data.content); }); diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index 08be7ccdf2..2efb3366d1 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -1,3 +1,6 @@ +import { type Editor } from "@graphite/editor"; +import { extractPixelData } from "@graphite/utility-functions/rasterization"; + export function downloadFileURL(filename: string, url: string) { const element = document.createElement("a"); @@ -60,3 +63,19 @@ export async function upload(accept: string, } export type UploadResult = { filename: string; type: string; content: UploadResultType }; type UploadResultType = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never; + +export async function pasteFile(item: DataTransferItem, editor: Editor, mouse?: [number, number], insertParentId?: bigint, insertIndex?: number) { + const file = item.getAsFile(); + if (!file) return; + + if (file.type.startsWith("image/svg")) { + const svg = await file.text(); + editor.handle.pasteSvg(file.name, svg, mouse?.[0], mouse?.[1], insertParentId, insertIndex); + } else if (file.type.startsWith("image/")) { + const imageData = await extractPixelData(file); + editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, mouse?.[0], mouse?.[1], insertParentId, insertIndex); + } else if (file.name.endsWith("." + editor.handle.fileExtension())) { + // TODO: When we eventually have sub-documents, this should be changed to import the document as a node instead of opening it in a separate tab + editor.handle.openFile(file.name, await file.bytes()); + } +}