Unify file opening, importing, and pasting into a file ingest handler (#4548)

* Unify file and data ingest

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2026-09-18 22:47:02 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent f90bef159c
commit 7d1303f695
47 changed files with 855 additions and 980 deletions
+31 -86
View File
@@ -20,12 +20,10 @@ mod editor_commands {
use editor::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::resource_upload::utility_types::UploadTarget;
use editor::messages::portfolio::utility_types::PanelGroupId;
use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
use graph_craft::document::NodeId;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::FillChoice;
use std::path::PathBuf;
@@ -213,14 +211,6 @@ mod editor_commands {
DialogMessage::RequestNewDocumentDialog.into()
}
fn open_file(path: String, content: Vec<u8>) -> Message {
PortfolioMessage::OpenFile { path: PathBuf::from(path), content }.into()
}
fn import_file(path: String, content: Vec<u8>) -> Message {
PortfolioMessage::ImportFile { path: PathBuf::from(path), content }.into()
}
fn trigger_auto_save(document_id: u64) -> Message {
PortfolioMessage::AutoSaveDocument { document_id: DocumentId(document_id) }.into()
}
@@ -607,6 +597,36 @@ mod editor_commands {
ClipboardMessage::ReadSelection { content, cut }.into()
}
/// A file picked in the dialog that `TriggerBrowse` opened
fn ingest_picked(name: String, mime_type: String, data: Vec<u8>, action: IngestAction) -> Message {
IngestMessage::Ingest {
data,
action,
mime_type,
path: Some(PathBuf::from(name)),
}
.into()
}
/// A file dropped on a panel or pasted, placed by the drop position or the layer slot it landed in
fn ingest_file(name: Option<String>, mime_type: String, data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<u32>) -> Message {
let action = match (insert_parent_id.zip(insert_index), mouse_x.zip(mouse_y)) {
(Some((parent, insert_index)), _) => IngestAction::DropOnLayers {
parent: LayerNodeIdentifier::new_unchecked(NodeId(parent)),
insert_index,
},
(None, Some(mouse)) => IngestAction::DropOnCanvas { mouse },
(None, None) => IngestAction::Paste,
};
IngestMessage::Ingest {
data,
action,
mime_type,
path: name.map(PathBuf::from),
}
.into()
}
/// Paste from a serialized JSON representation
fn paste_text(data: String) -> Message {
ClipboardMessage::ReadClipboard {
@@ -615,82 +635,6 @@ mod editor_commands {
.into()
}
/// Pastes decoded RGBA8 pixels as an image layer, encoded as PNG for storage
fn paste_image(
name: Option<String>,
image_data: Vec<u8>,
width: u32,
height: u32,
mouse_x: Option<f64>,
mouse_y: Option<f64>,
insert_parent_id: Option<u64>,
insert_index: Option<usize>,
) -> Message {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let data = Image::from_image_data(&image_data, width, height).to_png();
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
Some((parent, insert_index))
} else {
None
};
ResourceUploadMessage::Upload {
name,
data: data.into(),
target: UploadTarget::Layer { mouse, parent_and_insert_index },
}
.into()
}
/// Pastes an image file as an image layer, keeping its original encoding
fn paste_image_file(name: Option<String>, data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<usize>) -> Message {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
Some((parent, insert_index))
} else {
None
};
ResourceUploadMessage::Upload {
name,
data: data.into(),
target: UploadTarget::Layer { mouse, parent_and_insert_index },
}
.into()
}
/// Hands the file picked for a requested resource upload to the editor
fn upload_resource(name: String, data: Vec<u8>) -> Message {
ResourceUploadMessage::ReceiveUpload { name: Some(name), data: data.into() }.into()
}
/// Pastes an SVG given its string representation
fn paste_svg(name: Option<String>, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<usize>) -> Message {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id);
Some((parent, insert_index))
} else {
None
};
PortfolioMessage::InsertSvg {
name,
svg,
mouse,
parent_and_insert_index,
}
.into()
}
/// Toggle visibility of a layer or node given its node ID
fn toggle_node_visibility_layer_panel(id: u64) -> Message {
NodeGraphMessage::ToggleVisibility {
@@ -792,6 +736,7 @@ macro_rules! editor_proxy_types {
}
editor_proxy_types! {
IngestAction = editor::messages::portfolio::ingest::utility_types::IngestAction;
LayoutTarget = editor::messages::layout::utility_types::layout_widget::LayoutTarget;
DockingSplitDirection = editor::messages::portfolio::utility_types::DockingSplitDirection;
PanelTypes = Vec<editor::messages::portfolio::utility_types::PanelType>;
-16
View File
@@ -240,22 +240,6 @@ impl EditorWrapper {
cfg!(debug_assertions)
}
/// The file extensions of raster images the editor decodes itself (web only; on desktop, dropped files are imported natively and this is never called)
#[cfg(all(feature = "web", not(feature = "native")))]
#[wasm_bindgen(js_name = rasterImageExtensions)]
pub fn raster_image_extensions(&self) -> Vec<String> {
editor::messages::portfolio::resource_upload::utility_types::RASTER_IMAGE_EXTENSIONS
.iter()
.map(|extension| extension.to_string())
.collect()
}
#[cfg(feature = "native")]
#[wasm_bindgen(js_name = rasterImageExtensions)]
pub fn raster_image_extensions(&self) -> Vec<String> {
log::error!("rasterImageExtensions is unavailable on desktop, where dropped files are imported natively");
Vec::new()
}
/// Load persisted browser storage state (web only; on desktop, persistence is handled natively and this is never triggered)
#[cfg(all(feature = "web", not(feature = "native")))]
#[wasm_bindgen(js_name = loadPersistedState)]