From 2bb45096479d30be3303c9d79aa23cb9b59e7015 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 10 Aug 2025 07:46:42 -0700 Subject: [PATCH] Replace the Spreadsheet panel with an improved Data panel (#3037) * Improve the table data panel * Add the "Window" menu bar section and polish everything --- Cargo.lock | 1 + editor/Cargo.toml | 1 + editor/src/dispatcher.rs | 4 - .../messages/defer/defer_message_handler.rs | 4 +- .../src/messages/frontend/frontend_message.rs | 13 +- .../messages/input_mapper/input_mappings.rs | 1 + .../messages/layout/layout_message_handler.rs | 7 +- .../layout/utility_types/layout_widget.rs | 7 +- .../utility_types/widgets/button_widgets.rs | 10 +- .../utility_types/widgets/input_widgets.rs | 3 + .../utility_types/widgets/label_widgets.rs | 13 + editor/src/messages/message.rs | 2 - editor/src/messages/mod.rs | 1 - .../data_panel/data_panel_message.rs} | 16 +- .../data_panel/data_panel_message_handler.rs | 633 ++++++++++++++++++ .../portfolio/document/data_panel/mod.rs | 7 + .../portfolio/document/document_message.rs | 3 + .../document/document_message_handler.rs | 53 +- editor/src/messages/portfolio/document/mod.rs | 1 + .../node_graph/node_graph_message_handler.rs | 21 +- .../properties_panel_message_handler.rs | 11 +- .../menu_bar/menu_bar_message_handler.rs | 36 +- editor/src/messages/portfolio/mod.rs | 1 - .../messages/portfolio/portfolio_message.rs | 5 +- .../portfolio/portfolio_message_handler.rs | 112 +++- .../src/messages/portfolio/spreadsheet/mod.rs | 7 - .../spreadsheet_message_handler.rs | 333 --------- .../src/messages/portfolio/utility_types.rs | 4 +- editor/src/messages/prelude.rs | 3 +- editor/src/messages/workspace/mod.rs | 7 - .../messages/workspace/workspace_message.rs | 8 - .../workspace/workspace_message_handler.rs | 24 - editor/src/node_graph_executor.rs | 30 +- editor/src/node_graph_executor/runtime.rs | 17 +- .../floating-menus/ColorPicker.svelte | 47 +- frontend/src/components/panels/Data.svelte | 50 ++ .../src/components/panels/Document.svelte | 4 +- frontend/src/components/panels/Layers.svelte | 18 +- .../src/components/panels/Properties.svelte | 18 +- .../src/components/panels/Spreadsheet.svelte | 50 -- frontend/src/components/views/Graph.svelte | 4 +- .../src/components/widgets/WidgetSpan.svelte | 5 + .../widgets/buttons/ImageButton.svelte | 7 +- .../widgets/inputs/ColorInput.svelte | 20 +- .../widgets/inputs/SpectrumInput.svelte | 97 ++- .../widgets/inputs/TextInput.svelte | 6 +- .../widgets/labels/ImageLabel.svelte | 32 + .../components/window/workspace/Panel.svelte | 4 +- .../window/workspace/Workspace.svelte | 36 +- frontend/src/messages.ts | 67 +- frontend/src/state-providers/portfolio.ts | 28 +- frontend/src/subscription-router.ts | 5 + frontend/wasm/src/editor_api.rs | 6 +- .../interpreted-executor/src/node_registry.rs | 42 +- 54 files changed, 1300 insertions(+), 645 deletions(-) rename editor/src/messages/portfolio/{spreadsheet/spreadsheet_message.rs => document/data_panel/data_panel_message.rs} (62%) create mode 100644 editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs create mode 100644 editor/src/messages/portfolio/document/data_panel/mod.rs delete mode 100644 editor/src/messages/portfolio/spreadsheet/mod.rs delete mode 100644 editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs delete mode 100644 editor/src/messages/workspace/mod.rs delete mode 100644 editor/src/messages/workspace/workspace_message.rs delete mode 100644 editor/src/messages/workspace/workspace_message_handler.rs create mode 100644 frontend/src/components/panels/Data.svelte delete mode 100644 frontend/src/components/panels/Spreadsheet.svelte create mode 100644 frontend/src/components/widgets/labels/ImageLabel.svelte diff --git a/Cargo.lock b/Cargo.lock index b9a131eb..b34cf496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2130,6 +2130,7 @@ dependencies = [ name = "graphite-editor" version = "0.0.0" dependencies = [ + "base64 0.22.1", "bezier-rs", "bitflags 2.9.1", "bytemuck", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index ed020ef9..9287333e 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -47,6 +47,7 @@ web-sys = { workspace = true } bytemuck = { workspace = true } vello = { workspace = true } tracing = { workspace = true } +base64 = { workspace = true } # Required dependencies spin = "0.9.8" diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 0c098833..a80c9404 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -26,7 +26,6 @@ pub struct DispatcherMessageHandlers { pub portfolio_message_handler: PortfolioMessageHandler, preferences_message_handler: PreferencesMessageHandler, tool_message_handler: ToolMessageHandler, - workspace_message_handler: WorkspaceMessageHandler, } impl DispatcherMessageHandlers { @@ -231,9 +230,6 @@ impl Dispatcher { self.message_handlers.tool_message_handler.process_message(message, &mut queue, context); } - Message::Workspace(message) => { - self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ()); - } Message::NoOp => {} Message::Batched { messages } => { messages.iter().for_each(|message| self.handle_message(message.to_owned(), false)); diff --git a/editor/src/messages/defer/defer_message_handler.rs b/editor/src/messages/defer/defer_message_handler.rs index d4738ff7..d4775ded 100644 --- a/editor/src/messages/defer/defer_message_handler.rs +++ b/editor/src/messages/defer/defer_message_handler.rs @@ -38,9 +38,9 @@ impl MessageHandler> for DeferMessageHandl for (_, message) in elements.rev() { responses.add_front(message); } - for (id, messages) in self.after_graph_run.iter() { + for (&document_id, messages) in self.after_graph_run.iter() { if !messages.is_empty() { - responses.add(PortfolioMessage::SubmitGraphRender { document_id: *id, ignore_hash: false }); + responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false }); } } } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c6e48d3e..6d6ddd72 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -149,11 +149,16 @@ pub enum FrontendMessage { UpdateGraphViewOverlay { open: bool, }, - UpdateSpreadsheetState { + UpdateDataPanelState { open: bool, - node: Option, }, - UpdateSpreadsheetLayout { + UpdatePropertiesPanelState { + open: bool, + }, + UpdateLayersPanelState { + open: bool, + }, + UpdateDataPanelLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, @@ -296,7 +301,7 @@ pub enum FrontendMessage { #[serde(rename = "openDocuments")] open_documents: Vec, }, - UpdatePropertyPanelSectionsLayout { + UpdatePropertiesPanelLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 1b722301..255193a4 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -427,6 +427,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers), + entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen), // // FrontendMessage entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste), diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 9a02f9a3..a7e0ab96 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -308,6 +308,7 @@ impl LayoutMessageHandler { responses.add(callback_message); } + Widget::ImageLabel(_) => {} Widget::IconLabel(_) => {} Widget::InvisibleStandinInput(invisible) => { let callback_message = match action { @@ -481,18 +482,18 @@ impl LayoutMessageHandler { diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping)); let message = match layout_target { + LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff }, LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff }, LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, + LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff }, LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff }, LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff }, LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff }, - LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), + LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff }, LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff }, - LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff }, - LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff }, LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff }, LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff }, LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff }, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index f2602277..fa2eebf0 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -42,9 +42,9 @@ pub enum LayoutTarget { /// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons. NodeGraphControlBar, /// The body of the Properties panel containing many collapsable sections. - PropertiesSections, + PropertiesPanel, /// The spredsheet panel allows for the visualisation of data in the graph. - Spreadsheet, + DataPanel, /// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown. ToolOptions, /// The vertical buttons for all of the tools on the left of the canvas. @@ -369,6 +369,7 @@ impl LayoutGroup { Widget::IconButton(x) => &mut x.tooltip, Widget::IconLabel(x) => &mut x.tooltip, Widget::ImageButton(x) => &mut x.tooltip, + Widget::ImageLabel(x) => &mut x.tooltip, Widget::NumberInput(x) => &mut x.tooltip, Widget::ParameterExposeButton(x) => &mut x.tooltip, Widget::PopoverButton(x) => &mut x.tooltip, @@ -546,6 +547,7 @@ pub enum Widget { IconButton(IconButton), IconLabel(IconLabel), ImageButton(ImageButton), + ImageLabel(ImageLabel), InvisibleStandinInput(InvisibleStandinInput), NodeCatalog(NodeCatalog), NumberInput(NumberInput), @@ -622,6 +624,7 @@ impl DiffUpdate { Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::IconLabel(_) + | Widget::ImageLabel(_) | Widget::CurveInput(_) | Widget::InvisibleStandinInput(_) | Widget::NodeCatalog(_) diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 54fbc920..3698bf3b 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -168,8 +168,6 @@ pub struct ColorInput { #[widget_builder(constructor)] pub value: FillChoice, - pub disabled: bool, - // TODO: Implement // #[serde(rename = "allowTransparency")] // #[derivative(Default(value = "false"))] @@ -179,9 +177,11 @@ pub struct ColorInput { #[derivative(Default(value = "true"))] pub allow_none: bool, - // TODO: Implement - // pub disabled: bool, - // + pub disabled: bool, + + #[serde(rename = "menuDirection")] + pub menu_direction: Option, + pub tooltip: String, #[serde(skip)] diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 02d1d3ee..11b3b46f 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -416,6 +416,9 @@ pub struct TextInput { #[serde(rename = "minWidth")] pub min_width: u32, + #[serde(rename = "maxWidth")] + pub max_width: u32, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] diff --git a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs index f89b6dbe..cb569730 100644 --- a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs @@ -65,4 +65,17 @@ pub struct TextLabel { pub value: String, } +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[derivative(Debug, PartialEq)] +pub struct ImageLabel { + #[widget_builder(constructor)] + pub url: String, + + pub width: Option, + + pub height: Option, + + pub tooltip: String, +} + // TODO: Add UserInputLabel diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index ee441f6b..d8f59ca7 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -33,8 +33,6 @@ pub enum Message { Preferences(PreferencesMessage), #[child] Tool(ToolMessage), - #[child] - Workspace(WorkspaceMessage), // Messages NoOp, diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index 407864c4..6b2656df 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -16,4 +16,3 @@ pub mod portfolio; pub mod preferences; pub mod prelude; pub mod tool; -pub mod workspace; diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs similarity index 62% rename from editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs rename to editor/src/messages/portfolio/document/data_panel/data_panel_message.rs index 1af04ab6..a9067c84 100644 --- a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs @@ -1,16 +1,15 @@ use crate::messages::prelude::*; use crate::node_graph_executor::InspectResult; -/// The spreadsheet UI allows for graph data to be previewed. -#[impl_message(Message, PortfolioMessage, Spreadsheet)] +/// The Data panel UI allows the user to visualize the output data of the selected node. +#[impl_message(Message, DocumentMessage, DataPanel)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum SpreadsheetMessage { - ToggleOpen, - +pub enum DataPanelMessage { UpdateLayout { #[serde(skip)] inspect_result: InspectResult, }, + ClearLayout, PushToElementPath { index: usize, @@ -19,14 +18,15 @@ pub enum SpreadsheetMessage { len: usize, }, - ViewVectorDomain { - domain: VectorDomain, + ViewVectorTableTab { + tab: VectorTableTab, }, } #[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] -pub enum VectorDomain { +pub enum VectorTableTab { #[default] + Properties, Points, Segments, Regions, diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs new file mode 100644 index 00000000..d1c578c5 --- /dev/null +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -0,0 +1,633 @@ +use super::VectorTableTab; +use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout}; +use crate::messages::portfolio::document::data_panel::DataPanelMessage; +use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; +use crate::messages::prelude::*; +use crate::messages::tool::tool_messages::tool_prelude::*; +use graph_craft::document::NodeId; +use graphene_std::Color; +use graphene_std::Context; +use graphene_std::memo::IORecord; +use graphene_std::raster_types::{CPU, GPU, Raster}; +use graphene_std::table::Table; +use graphene_std::vector::Vector; +use graphene_std::vector::style::{Fill, FillChoice}; +use graphene_std::{Artboard, Graphic}; +use std::any::Any; +use std::sync::Arc; + +#[derive(ExtractField)] +pub struct DataPanelMessageContext<'a> { + pub network_interface: &'a mut NodeNetworkInterface, + pub data_panel_open: bool, +} + +/// The data panel allows for graph data to be previewed. +#[derive(Default, Debug, Clone, ExtractField)] +pub struct DataPanelMessageHandler { + introspected_node: Option, + introspected_data: Option>, + element_path: Vec, + active_vector_table_tab: VectorTableTab, +} + +#[message_handler_data] +impl MessageHandler> for DataPanelMessageHandler { + fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque, context: DataPanelMessageContext) { + match message { + DataPanelMessage::UpdateLayout { mut inspect_result } => { + self.introspected_node = Some(inspect_result.inspect_node); + self.introspected_data = inspect_result.take_data(); + self.update_layout(responses, context); + } + DataPanelMessage::ClearLayout => { + self.introspected_node = None; + self.introspected_data = None; + self.element_path.clear(); + self.active_vector_table_tab = VectorTableTab::default(); + self.update_layout(responses, context); + } + + DataPanelMessage::PushToElementPath { index } => { + self.element_path.push(index); + self.update_layout(responses, context); + } + DataPanelMessage::TruncateElementPath { len } => { + self.element_path.truncate(len); + self.update_layout(responses, context); + } + + DataPanelMessage::ViewVectorTableTab { tab } => { + self.active_vector_table_tab = tab; + self.update_layout(responses, context); + } + } + } + + fn actions(&self) -> ActionList { + actions!(DataPanelMessage;) + } +} + +impl DataPanelMessageHandler { + fn update_layout(&mut self, responses: &mut VecDeque, context: DataPanelMessageContext<'_>) { + let DataPanelMessageContext { network_interface, .. } = context; + + let mut layout_data = LayoutData { + current_depth: 0, + desired_path: &mut self.element_path, + breadcrumbs: Vec::new(), + vector_table_tab: self.active_vector_table_tab, + }; + + // Main data visualization + let mut layout = self + .introspected_data + .as_ref() + .map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data).unwrap_or_else(|| label("Visualization of this data type is not yet supported"))) + .unwrap_or_default(); + + let mut widgets = Vec::new(); + + // Selected layer/node name + if let Some(node_id) = self.introspected_node { + let is_layer = network_interface.is_layer(&node_id, &[]); + + widgets.extend([ + if is_layer { + IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder() + } else { + IconLabel::new("Node").tooltip("Name of the selected node").widget_holder() + }, + Separator::new(SeparatorType::Related).widget_holder(), + TextInput::new(network_interface.display_name(&node_id, &[])) + .tooltip(if is_layer { "Name of the selected layer" } else { "Name of the selected node" }) + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id, + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .max_width(200) + .widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + ]); + } + + // Element path breadcrumbs + if !layout_data.breadcrumbs.is_empty() { + let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs) + .on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into()) + .widget_holder(); + widgets.push(breadcrumb); + } + + if !widgets.is_empty() { + layout.insert(0, LayoutGroup::Row { widgets }); + } + + responses.add(LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(WidgetLayout { layout }), + layout_target: LayoutTarget::DataPanel, + }); + } +} + +struct LayoutData<'a> { + current_depth: usize, + desired_path: &'a mut Vec, + breadcrumbs: Vec, + vector_table_tab: VectorTableTab, +} + +macro_rules! generate_layout_downcast { + ($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => { + if false { None } + $( + else if let Some(io) = $introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb($data)) + } + )* + else { None } + } +} + +// TODO: We simply try all these types sequentially. Find a better strategy. +fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { + generate_layout_downcast!(introspected_data, data, [ + Table, + Table, + Table, + Table>, + Table>, + Table, + Color, + Option, + f64, + u32, + u64, + bool, + String, + Option, + DVec2, + DAffine2, + ]) +} + +fn column_headings(value: &[&str]) -> Vec { + value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect() +} + +fn label(x: impl Into) -> Vec { + let error = vec![TextLabel::new(x).widget_holder()]; + vec![LayoutGroup::Row { widgets: error }] +} + +trait TableRowLayout { + fn type_name() -> &'static str; + fn identifier(&self) -> String; + fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { + data.breadcrumbs.push(self.identifier()); + self.element_page(data) + } + fn element_widget(&self, index: usize) -> WidgetHolder { + TextButton::new(self.identifier()) + .on_update(move |_| DataPanelMessage::PushToElementPath { index }.into()) + .widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + vec![] + } +} + +impl TableRowLayout for Table { + fn type_name() -> &'static str { + "Table" + } + fn identifier(&self) -> String { + format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" }) + } + fn element_page(&self, data: &mut LayoutData) -> Vec { + if let Some(index) = data.desired_path.get(data.current_depth).copied() { + if let Some(row) = self.get(index) { + data.current_depth += 1; + let result = row.element.layout_with_breadcrumb(data); + data.current_depth -= 1; + return result; + } else { + warn!("Desired path truncated"); + data.desired_path.truncate(data.current_depth); + } + } + + let mut rows = self + .iter() + .enumerate() + .map(|(index, row)| { + vec![ + TextLabel::new(format!("{index}")).widget_holder(), + row.element.element_widget(index), + TextLabel::new(format_transform_matrix(row.transform)).widget_holder(), + TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(), + TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(), + ] + }) + .collect::>(); + + rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"])); + + vec![LayoutGroup::Table { rows }] + } +} + +impl TableRowLayout for Artboard { + fn type_name() -> &'static str { + "Artboard" + } + fn identifier(&self) -> String { + self.label.clone() + } + fn element_page(&self, data: &mut LayoutData) -> Vec { + self.content.element_page(data) + } +} + +impl TableRowLayout for Graphic { + fn type_name() -> &'static str { + "Graphic" + } + fn identifier(&self) -> String { + match self { + Self::Graphic(table) => table.identifier(), + Self::Vector(table) => table.identifier(), + Self::RasterCPU(table) => table.identifier(), + Self::RasterGPU(table) => table.identifier(), + Self::Color(table) => table.identifier(), + } + } + // Don't put a breadcrumb for Graphic + fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { + self.element_page(data) + } + fn element_page(&self, data: &mut LayoutData) -> Vec { + match self { + Self::Graphic(table) => table.layout_with_breadcrumb(data), + Self::Vector(table) => table.layout_with_breadcrumb(data), + Self::RasterCPU(table) => table.layout_with_breadcrumb(data), + Self::RasterGPU(table) => table.layout_with_breadcrumb(data), + Self::Color(table) => table.layout_with_breadcrumb(data), + } + } +} + +impl TableRowLayout for Vector { + fn type_name() -> &'static str { + "Vector" + } + fn identifier(&self) -> String { + format!( + "Vector ({} point{}, {} segment{})", + self.point_domain.ids().len(), + if self.point_domain.ids().len() == 1 { "" } else { "s" }, + self.segment_domain.ids().len(), + if self.segment_domain.ids().len() == 1 { "" } else { "s" } + ) + } + fn element_page(&self, data: &mut LayoutData) -> Vec { + let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions] + .into_iter() + .map(|tab| { + RadioEntryData::new(format!("{tab:?}")) + .label(format!("{tab:?}")) + .on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into()) + }) + .collect(); + let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_holder()]; + + let mut table_rows = Vec::new(); + match data.vector_table_tab { + VectorTableTab::Properties => { + table_rows.push(column_headings(&["property", "value"])); + + match self.style.fill.clone() { + Fill::None => table_rows.push(vec![ + TextLabel::new("Fill").widget_holder(), + ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), + ]), + Fill::Solid(color) => table_rows.push(vec![ + TextLabel::new("Fill").widget_holder(), + ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), + ]), + Fill::Gradient(gradient) => { + table_rows.push(vec![ + TextLabel::new("Fill").widget_holder(), + ColorInput::new(FillChoice::Gradient(gradient.stops)) + .disabled(true) + .menu_direction(Some(MenuDirection::Top)) + .widget_holder(), + ]); + table_rows.push(vec![ + TextLabel::new("Fill Gradient Type").widget_holder(), + TextLabel::new(gradient.gradient_type.to_string()).widget_holder(), + ]); + table_rows.push(vec![ + TextLabel::new("Fill Gradient Start").widget_holder(), + TextLabel::new(format_dvec2(gradient.start)).widget_holder(), + ]); + table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]); + table_rows.push(vec![ + TextLabel::new("Fill Gradient Transform").widget_holder(), + TextLabel::new(format_transform_matrix(&gradient.transform)).widget_holder(), + ]); + } + } + + if let Some(stroke) = self.style.stroke.clone() { + let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None }; + table_rows.push(vec![ + TextLabel::new("Stroke").widget_holder(), + ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), + ]); + table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]); + table_rows.push(vec![ + TextLabel::new("Stroke Dash Lengths").widget_holder(), + TextLabel::new(if stroke.dash_lengths.is_empty() { + "-".to_string() + } else { + format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::>().join(", ")) + }) + .widget_holder(), + ]); + table_rows.push(vec![ + TextLabel::new("Stroke Dash Offset").widget_holder(), + TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(), + ]); + table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]); + table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]); + table_rows.push(vec![ + TextLabel::new("Stroke Join Miter Limit").widget_holder(), + TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(), + ]); + table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]); + table_rows.push(vec![ + TextLabel::new("Stroke Transform").widget_holder(), + TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(), + ]); + table_rows.push(vec![ + TextLabel::new("Stroke Non-Scaling").widget_holder(), + TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(), + ]); + table_rows.push(vec![ + TextLabel::new("Stroke Paint Order").widget_holder(), + TextLabel::new(stroke.paint_order.to_string()).widget_holder(), + ]); + } + + let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::>().join(", "); + let colinear = if colinear.is_empty() { "-".to_string() } else { colinear }; + table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]); + + table_rows.push(vec![ + TextLabel::new("Upstream Nested Layers").widget_holder(), + TextLabel::new(if self.upstream_nested_layers.is_some() { + "Yes (this preserves references to its upstream nested layers for editing by tools)" + } else { + "No (this doesn't preserve references to its upstream nested layers for editing by tools)" + }) + .widget_holder(), + ]); + } + VectorTableTab::Points => { + table_rows.push(column_headings(&["", "position"])); + table_rows.extend( + self.point_domain + .iter() + .map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]), + ); + } + VectorTableTab::Segments => { + table_rows.push(column_headings(&["", "start_index", "end_index", "handles"])); + table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { + vec![ + TextLabel::new(format!("{}", id.inner())).widget_holder(), + TextLabel::new(format!("{start}")).widget_holder(), + TextLabel::new(format!("{end}")).widget_holder(), + TextLabel::new(format!("{handles:?}")).widget_holder(), + ] + })); + } + VectorTableTab::Regions => { + table_rows.push(column_headings(&["", "segment_range", "fill"])); + table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| { + vec![ + TextLabel::new(format!("{}", id.inner())).widget_holder(), + TextLabel::new(format!("{segment_range:?}")).widget_holder(), + TextLabel::new(format!("{}", fill.inner())).widget_holder(), + ] + })); + } + } + + vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }] + } +} + +impl TableRowLayout for Raster { + fn type_name() -> &'static str { + "Raster" + } + fn identifier(&self) -> String { + format!("Raster ({}x{})", self.width, self.height) + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let base64_string = self.data().base64_string.clone().unwrap_or_else(|| { + use base64::Engine; + + let output = self.data().to_png(); + let preamble = "data:image/png;base64,"; + let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4); + base64_string.push_str(preamble); + base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string); + base64_string + }); + + let widgets = vec![ImageLabel::new(base64_string).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for Raster { + fn type_name() -> &'static str { + "Raster" + } + fn identifier(&self) -> String { + format!("Raster ({}x{})", self.data().width(), self.data().height()) + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for Color { + fn type_name() -> &'static str { + "Color" + } + fn identifier(&self) -> String { + format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb()) + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![self.element_widget(0)]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for Option { + fn type_name() -> &'static str { + "Option" + } + fn identifier(&self) -> String { + format!( + "Option (#{})", + if let Some(color) = self { color.to_linear_srgb().to_rgba_hex_srgb() } else { "None".to_string() } + ) + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + ColorInput::new(if let Some(color) = self { FillChoice::Solid(*color) } else { FillChoice::None }) + .disabled(true) + .menu_direction(Some(MenuDirection::Top)) + .widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![self.element_widget(0)]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for f64 { + fn type_name() -> &'static str { + "Number (f64)" + } + fn identifier(&self) -> String { + "Number (f64)".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(self.to_string()).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for u32 { + fn type_name() -> &'static str { + "Number (u32)" + } + fn identifier(&self) -> String { + "Number (u32)".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(self.to_string()).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for u64 { + fn type_name() -> &'static str { + "Number (u64)" + } + fn identifier(&self) -> String { + "Number (u64)".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(self.to_string()).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for bool { + fn type_name() -> &'static str { + "Bool" + } + fn identifier(&self) -> String { + "Bool".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(self.to_string()).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for String { + fn type_name() -> &'static str { + "String" + } + fn identifier(&self) -> String { + "String".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(self.to_string()).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for Option { + fn type_name() -> &'static str { + "Option" + } + fn identifier(&self) -> String { + "Option".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for DVec2 { + fn type_name() -> &'static str { + "Vec2" + } + fn identifier(&self) -> String { + "Vec2".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +impl TableRowLayout for DAffine2 { + fn type_name() -> &'static str { + "Transform" + } + fn identifier(&self) -> String { + "Transform".to_string() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + +fn format_transform_matrix(transform: &DAffine2) -> String { + let (scale, angle, translation) = transform.to_scale_angle_translation(); + let rotation = if angle == -0. { 0. } else { angle.to_degrees() }; + let round = |x: f64| (x * 1e3).round() / 1e3; + + format!( + "Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)", + round(translation.x), + round(translation.y), + round(scale.x), + round(scale.y) + ) +} + +fn format_dvec2(value: DVec2) -> String { + let round = |x: f64| (x * 1e3).round() / 1e3; + format!("({} px, {} px)", round(value.x), round(value.y)) +} diff --git a/editor/src/messages/portfolio/document/data_panel/mod.rs b/editor/src/messages/portfolio/document/data_panel/mod.rs new file mode 100644 index 00000000..d1a28217 --- /dev/null +++ b/editor/src/messages/portfolio/document/data_panel/mod.rs @@ -0,0 +1,7 @@ +mod data_panel_message; +mod data_panel_message_handler; + +#[doc(inline)] +pub use data_panel_message::*; +#[doc(inline)] +pub use data_panel_message_handler::*; diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 89db87c9..9a195261 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use super::utility_types::misc::{GroupFolderType, SnappingState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; +use crate::messages::portfolio::document::data_panel::DataPanelMessage; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlaysType; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -33,6 +34,8 @@ pub enum DocumentMessage { Overlays(OverlaysMessage), #[child] PropertiesPanel(PropertiesPanelMessage), + #[child] + DataPanel(DataPanelMessage), // Messages AlignSelectedLayers { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5bfa0af7..c36d29da 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -9,6 +9,7 @@ use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext; use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options}; @@ -18,6 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; +use crate::messages::portfolio::utility_types::PanelType; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; @@ -49,6 +51,9 @@ pub struct DocumentMessageContext<'a> { pub current_tool: &'a ToolType, pub preferences: &'a PreferencesMessageHandler, pub device_pixel_ratio: f64, + pub data_panel_open: bool, + pub layers_panel_open: bool, + pub properties_panel_open: bool, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] @@ -63,9 +68,11 @@ pub struct DocumentMessageHandler { #[serde(skip)] pub node_graph_handler: NodeGraphMessageHandler, #[serde(skip)] - overlays_message_handler: OverlaysMessageHandler, + pub overlays_message_handler: OverlaysMessageHandler, #[serde(skip)] - properties_panel_message_handler: PropertiesPanelMessageHandler, + pub properties_panel_message_handler: PropertiesPanelMessageHandler, + #[serde(skip)] + pub data_panel_message_handler: DataPanelMessageHandler, // ============================================ // Fields that are saved in the document format @@ -144,6 +151,7 @@ impl Default for DocumentMessageHandler { node_graph_handler: NodeGraphMessageHandler::default(), overlays_message_handler: OverlaysMessageHandler::default(), properties_panel_message_handler: PropertiesPanelMessageHandler::default(), + data_panel_message_handler: DataPanelMessageHandler::default(), // ============================================ // Fields that are saved in the document format // ============================================ @@ -186,6 +194,9 @@ impl MessageHandler> for DocumentMes current_tool, preferences, device_pixel_ratio, + data_panel_open, + layers_panel_open, + properties_panel_open, } = context; match message { @@ -223,9 +234,20 @@ impl MessageHandler> for DocumentMes document_name: self.name.as_str(), executor, persistent_data, + properties_panel_open, }; self.properties_panel_message_handler.process_message(message, responses, context); } + DocumentMessage::DataPanel(message) => { + self.data_panel_message_handler.process_message( + message, + responses, + DataPanelMessageContext { + network_interface: &mut self.network_interface, + data_panel_open, + }, + ); + } DocumentMessage::NodeGraph(message) => { self.node_graph_handler.process_message( message, @@ -241,6 +263,7 @@ impl MessageHandler> for DocumentMes graph_fade_artwork_percentage: self.graph_fade_artwork_percentage, navigation_handler: &self.navigation_handler, preferences, + layers_panel_open, }, ); } @@ -356,12 +379,15 @@ impl MessageHandler> for DocumentMes DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses), DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses), DocumentMessage::DocumentStructureChanged => { - self.update_layers_panel_control_bar_widgets(responses); - self.update_layers_panel_bottom_bar_widgets(responses); + if layers_panel_open { + self.network_interface.load_structure(); + let data_buffer: RawBuffer = self.serialize_root(); - self.network_interface.load_structure(); - let data_buffer: RawBuffer = self.serialize_root(); - responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer }); + self.update_layers_panel_control_bar_widgets(layers_panel_open, responses); + self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses); + + responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer }); + } } DocumentMessage::DrawArtboardOverlays(overlay_context) => { if !overlay_context.visibility_settings.artboard_name() { @@ -1128,7 +1154,6 @@ impl MessageHandler> for DocumentMes } } DocumentMessage::SetActivePanel { active_panel: panel } => { - use crate::messages::portfolio::utility_types::PanelType; match panel { PanelType::Document => { if self.graph_view_overlay_open { @@ -2549,7 +2574,11 @@ impl DocumentMessageHandler { responses.add(NodeGraphMessage::ForceRunDocumentGraph); } - pub fn update_layers_panel_control_bar_widgets(&self, responses: &mut VecDeque) { + pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque) { + if !layers_panel_open { + return; + } + // Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode). let selected_nodes = self.network_interface.selected_nodes(); let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface); @@ -2707,7 +2736,11 @@ impl DocumentMessageHandler { }); } - pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque) { + pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque) { + if !layers_panel_open { + return; + } + let selected_nodes = self.network_interface.selected_nodes(); let mut selected_layers = selected_nodes.selected_layers(self.metadata()); let selected_layer = selected_layers.next(); diff --git a/editor/src/messages/portfolio/document/mod.rs b/editor/src/messages/portfolio/document/mod.rs index 4099a584..767126b2 100644 --- a/editor/src/messages/portfolio/document/mod.rs +++ b/editor/src/messages/portfolio/document/mod.rs @@ -1,6 +1,7 @@ mod document_message; mod document_message_handler; +pub mod data_panel; pub mod graph_operation; pub mod navigation; pub mod node_graph; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 234f04c3..5207a90a 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -42,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> { pub graph_fade_artwork_percentage: f64, pub navigation_handler: &'a NavigationMessageHandler, pub preferences: &'a PreferencesMessageHandler, + pub layers_panel_open: bool, } #[derive(Debug, Clone, ExtractField)] @@ -111,6 +112,7 @@ impl<'a> MessageHandler> for NodeG graph_fade_artwork_percentage, navigation_handler, preferences, + layers_panel_open, } = context; match message { @@ -155,11 +157,13 @@ impl<'a> MessageHandler> for NodeG } responses.add(MenuBarMessage::SendLayout); responses.add(NodeGraphMessage::UpdateLayerPanel); + responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::SendSelectedNodes); responses.add(ArtboardToolMessage::UpdateSelectedArtboard); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(OverlaysMessage::Draw); responses.add(NodeGraphMessage::SendGraph); + responses.add(PortfolioMessage::SubmitActiveGraphRender); } NodeGraphMessage::CreateWire { output_connector, input_connector } => { // TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762 @@ -1215,7 +1219,7 @@ impl<'a> MessageHandler> for NodeG { return None; } - log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style); + let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY); @@ -1485,7 +1489,6 @@ impl<'a> MessageHandler> for NodeG }; selected_nodes.set_selected_nodes(nodes); responses.add(BroadcastEvent::SelectionChanged); - responses.add(PropertiesPanelMessage::Refresh); } NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets { click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)), @@ -1873,7 +1876,7 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::UpdateLayerPanel => { - Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses); + Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses); } NodeGraphMessage::UpdateEdges => { // Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes @@ -2329,9 +2332,9 @@ impl NodeGraphMessageHandler { .icon(Some("Node".to_string())) .tooltip("Add an operation to the end of this layer's chain of nodes") .popover_layout({ - let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface); + let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface); let compatible_type = { - let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, &context.network_interface); + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface); let node_type = graph_layer.horizontal_layer_flow().nth(1); if let Some(node_id) = node_type { let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]); @@ -2585,7 +2588,11 @@ impl NodeGraphMessageHandler { Some(subgraph_names) } - fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, responses: &mut VecDeque) { + fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque) { + if !layers_panel_open { + return; + } + let selected_layers = network_interface .selected_nodes() .selected_layers(network_interface.document_metadata()) @@ -2668,7 +2675,7 @@ impl NodeGraphMessageHandler { } } - pub fn update_node_graph_hints(&self, responses: &mut VecDeque) { + fn update_node_graph_hints(&self, responses: &mut VecDeque) { // A wire is in progress and its start and end connectors are set let wiring = self.wire_in_progress_from_connector.is_some(); diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 4c659f75..6e7aea73 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> { pub document_name: &'a str, pub executor: &'a mut NodeGraphExecutor, pub persistent_data: &'a PersistentData, + pub properties_panel_open: bool, } #[derive(Debug, Clone, Default, ExtractField)] @@ -28,16 +29,22 @@ impl MessageHandler> f document_name, executor, persistent_data, + properties_panel_open, } = context; match message { PropertiesPanelMessage::Clear => { responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(WidgetLayout::new(vec![])), - layout_target: LayoutTarget::PropertiesSections, + layout_target: LayoutTarget::PropertiesPanel, }); } PropertiesPanelMessage::Refresh => { + if !properties_panel_open { + responses.add(PropertiesPanelMessage::Clear); + return; + } + let mut node_properties_context = NodePropertiesContext { persistent_data, responses, @@ -50,7 +57,7 @@ impl MessageHandler> f node_properties_context.responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)), - layout_target: LayoutTarget::PropertiesSections, + layout_target: LayoutTarget::PropertiesPanel, }); } } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 0f4b574d..025b4b6b 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -16,10 +16,12 @@ pub struct MenuBarMessageHandler { pub has_selected_nodes: bool, pub has_selected_layers: bool, pub has_selection_history: (bool, bool), - pub spreadsheet_view_open: bool, pub message_logging_verbosity: MessageLoggingVerbosity, pub reset_node_definitions_on_open: bool, pub make_path_editable_is_allowed: bool, + pub data_panel_open: bool, + pub layers_panel_open: bool, + pub properties_panel_open: bool, } #[message_handler_data] @@ -585,18 +587,40 @@ impl LayoutHolder for MenuBarMessageHandler { disabled: no_active_document, ..MenuBarEntry::default() }], + ]), + ), + MenuBarEntry::new_root( + "Window".into(), + false, + MenuBarEntryChildren(vec![ + vec![ + MenuBarEntry { + label: "Properties".into(), + icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), + shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen), + action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()), + ..MenuBarEntry::default() + }, + MenuBarEntry { + label: "Layers".into(), + icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), + shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen), + action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()), + ..MenuBarEntry::default() + }, + ], vec![MenuBarEntry { - label: "Window: Spreadsheet".into(), - icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()), - disabled: no_active_document, + label: "Data".into(), + icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), + shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen), + action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()), ..MenuBarEntry::default() }], ]), ), MenuBarEntry::new_root( "Help".into(), - true, + false, MenuBarEntryChildren(vec![ vec![MenuBarEntry { label: "About Graphite…".into(), diff --git a/editor/src/messages/portfolio/mod.rs b/editor/src/messages/portfolio/mod.rs index 364dcdfd..e9673220 100644 --- a/editor/src/messages/portfolio/mod.rs +++ b/editor/src/messages/portfolio/mod.rs @@ -4,7 +4,6 @@ mod portfolio_message_handler; pub mod document; pub mod document_migration; pub mod menu_bar; -pub mod spreadsheet; pub mod utility_types; #[doc(inline)] diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 153ad6c0..ac12eafb 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -15,8 +15,6 @@ pub enum PortfolioMessage { MenuBar(MenuBarMessage), #[child] Document(DocumentMessage), - #[child] - Spreadsheet(SpreadsheetMessage), // Messages Init, @@ -128,6 +126,9 @@ pub enum PortfolioMessage { document_id: DocumentId, ignore_hash: bool, }, + ToggleDataPanelOpen, + TogglePropertiesPanelOpen, + ToggleLayersPanelOpen, ToggleRulers, UpdateDocumentWidgets, UpdateOpenDocumentsList, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54..9ec886fc 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,6 +1,5 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::network_interface; -use super::spreadsheet::SpreadsheetMessageHandler; use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH}; @@ -24,6 +23,7 @@ use crate::messages::tool::common_functionality::utility_functions::make_path_ed use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use bezier_rs::BezierHandles; +use derivative::*; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; use graphene_std::Color; @@ -37,14 +37,15 @@ use std::vec; pub struct PortfolioMessageContext<'a> { pub ipp: &'a InputPreprocessorMessageHandler, pub preferences: &'a PreferencesMessageHandler, + pub animation: &'a AnimationMessageHandler, pub current_tool: &'a ToolType, pub message_logging_verbosity: MessageLoggingVerbosity, pub reset_node_definitions_on_open: bool, pub timing_information: TimingInformation, - pub animation: &'a AnimationMessageHandler, } -#[derive(Debug, Default, ExtractField)] +#[derive(Debug, Derivative, ExtractField)] +#[derivative(Default)] pub struct PortfolioMessageHandler { menu_bar_message_handler: MenuBarMessageHandler, pub documents: HashMap, @@ -55,10 +56,13 @@ pub struct PortfolioMessageHandler { pub persistent_data: PersistentData, pub executor: NodeGraphExecutor, pub selection_mode: SelectionMode, - /// The spreadsheet UI allows for graph data to be previewed. - pub spreadsheet: SpreadsheetMessageHandler, device_pixel_ratio: Option, pub reset_node_definitions_on_open: bool, + pub data_panel_open: bool, + #[derivative(Default(value = "true"))] + pub layers_panel_open: bool, + #[derivative(Default(value = "true"))] + pub properties_panel_open: bool, } #[message_handler_data] @@ -67,11 +71,11 @@ impl MessageHandler> for Portfolio let PortfolioMessageContext { ipp, preferences, + animation, current_tool, message_logging_verbosity, reset_node_definitions_on_open, timing_information, - animation, } = context; match message { @@ -86,7 +90,9 @@ impl MessageHandler> for Portfolio self.menu_bar_message_handler.has_selected_layers = false; self.menu_bar_message_handler.has_selection_history = (false, false); self.menu_bar_message_handler.make_path_editable_is_allowed = false; - self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open; + self.menu_bar_message_handler.data_panel_open = self.data_panel_open; + self.menu_bar_message_handler.layers_panel_open = self.layers_panel_open; + self.menu_bar_message_handler.properties_panel_open = self.properties_panel_open; self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity; self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open; @@ -108,9 +114,6 @@ impl MessageHandler> for Portfolio self.menu_bar_message_handler.process_message(message, responses, ()); } - PortfolioMessage::Spreadsheet(message) => { - self.spreadsheet.process_message(message, responses, ()); - } PortfolioMessage::Document(message) => { if let Some(document_id) = self.active_document_id { if let Some(document) = self.documents.get_mut(&document_id) { @@ -122,6 +125,9 @@ impl MessageHandler> for Portfolio current_tool, preferences, device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.), + data_panel_open: self.data_panel_open, + layers_panel_open: self.layers_panel_open, + properties_panel_open: self.properties_panel_open, }; document.process_message(message, responses, document_inputs) } @@ -156,6 +162,9 @@ impl MessageHandler> for Portfolio current_tool, preferences, device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.), + data_panel_open: self.data_panel_open, + layers_panel_open: self.layers_panel_open, + properties_panel_open: self.properties_panel_open, }; document.process_message(message, responses, document_inputs) } @@ -201,6 +210,7 @@ impl MessageHandler> for Portfolio // Clear relevant UI layouts if there are no documents responses.add(PropertiesPanelMessage::Clear); responses.add(DocumentMessage::ClearLayersPanel); + responses.add(DataPanelMessage::ClearLayout); let hint_data = HintData(vec![HintGroup(vec![])]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } @@ -225,6 +235,7 @@ impl MessageHandler> for Portfolio // Clear UI layouts that assume the existence of a document responses.add(PropertiesPanelMessage::Clear); responses.add(DocumentMessage::ClearLayersPanel); + responses.add(DataPanelMessage::ClearLayout); let hint_data = HintData(vec![HintGroup(vec![])]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } @@ -344,13 +355,13 @@ impl MessageHandler> for Portfolio self.persistent_data.font_cache.insert(font, preview_url, data); self.executor.update_font_cache(self.persistent_data.font_cache.clone()); for document_id in self.document_ids.iter() { - let inspect_node = self.inspect_node_id(); + let node_to_inspect = self.node_to_inspect(); if let Ok(message) = self.executor.submit_node_graph_evaluation( self.documents.get_mut(document_id).expect("Tried to render non-existent document"), *document_id, ipp.viewport_bounds.size().as_uvec2(), timing_information, - inspect_node, + node_to_inspect, true, ) { responses.add_front(message); @@ -388,7 +399,7 @@ impl MessageHandler> for Portfolio new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() }); } - self.load_document(new_document, document_id, &mut new_responses, false); + self.load_document(new_document, document_id, self.layers_panel_open, &mut new_responses, false); new_responses.add(PortfolioMessage::SelectDocument { document_id }); new_responses.extend(responses.drain(..)); *responses = new_responses; @@ -504,7 +515,7 @@ impl MessageHandler> for Portfolio document.set_save_state(document_is_saved); // Load the document into the portfolio so it opens in the editor - self.load_document(document, document_id, responses, to_front); + self.load_document(document, document_id, self.layers_panel_open, responses, to_front); } PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { let mut all_new_ids = Vec::new(); @@ -918,13 +929,13 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => { - let inspect_node = self.inspect_node_id(); + let node_to_inspect = self.node_to_inspect(); let result = self.executor.submit_node_graph_evaluation( self.documents.get_mut(&document_id).expect("Tried to render non-existent document"), document_id, ipp.viewport_bounds.size().as_uvec2(), timing_information, - inspect_node, + node_to_inspect, ignore_hash, ); @@ -938,6 +949,58 @@ impl MessageHandler> for Portfolio Ok(message) => responses.add_front(message), } } + PortfolioMessage::ToggleDataPanelOpen => { + self.data_panel_open = !self.data_panel_open; + responses.add(MenuBarMessage::SendLayout); + + // Run the graph to grab the data + if self.data_panel_open { + // When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display + responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open }); + + responses.add(NodeGraphMessage::RunDocumentGraph); + } else { + // If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the data panel next time it is opened + responses.add(DataPanelMessage::ClearLayout); + + // When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed + responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open }); + } + } + PortfolioMessage::TogglePropertiesPanelOpen => { + self.properties_panel_open = !self.properties_panel_open; + responses.add(MenuBarMessage::SendLayout); + + responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open }); + + // Run the graph to grab the data + if self.properties_panel_open { + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + responses.add(PropertiesPanelMessage::Refresh); + } + PortfolioMessage::ToggleLayersPanelOpen => { + self.layers_panel_open = !self.layers_panel_open; + responses.add(MenuBarMessage::SendLayout); + + // Run the graph to grab the data + if self.layers_panel_open { + // When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display + responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open }); + + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(DeferMessage::AfterGraphRun { + messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()], + }); + } else { + // If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened + responses.add(DocumentMessage::ClearLayersPanel); + + // When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed + responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open }); + } + } PortfolioMessage::ToggleRulers => { if let Some(document) = self.active_document_mut() { document.rulers_visible = !document.rulers_visible; @@ -987,6 +1050,7 @@ impl MessageHandler> for Portfolio PasteIntoFolder, PrevDocument, ToggleRulers, + ToggleDataPanelOpen, ); // Extend with actions that require an active document @@ -1056,14 +1120,14 @@ impl PortfolioMessageHandler { } } - fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque, to_front: bool) { + fn load_document(&mut self, 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); } else { self.document_ids.push_back(document_id); } - new_document.update_layers_panel_control_bar_widgets(responses); - new_document.update_layers_panel_bottom_bar_widgets(responses); + new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses); + new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses); self.documents.insert(document_id, new_document); @@ -1111,17 +1175,17 @@ impl PortfolioMessageHandler { result } - /// Get the id of the node that should be used as the target for the spreadsheet - pub fn inspect_node_id(&self) -> Option { - // Spreadsheet not open, skipping - if !self.spreadsheet.spreadsheet_view_open { + /// Get the ID of the selected node that should be used as the current source for the Data panel. + pub fn node_to_inspect(&self) -> Option { + // Skip if the Data panel is not open + if !self.data_panel_open { return None; } let document = self.documents.get(&self.active_document_id?)?; let selected_nodes = document.network_interface.selected_nodes().0; - // Selected nodes != 1, skipping + // Skip if there is not exactly one selected node if selected_nodes.len() != 1 { return None; } diff --git a/editor/src/messages/portfolio/spreadsheet/mod.rs b/editor/src/messages/portfolio/spreadsheet/mod.rs deleted file mode 100644 index 1523ea0d..00000000 --- a/editor/src/messages/portfolio/spreadsheet/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod spreadsheet_message; -mod spreadsheet_message_handler; - -#[doc(inline)] -pub use spreadsheet_message::*; -#[doc(inline)] -pub use spreadsheet_message_handler::*; diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs deleted file mode 100644 index 7a218be9..00000000 --- a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs +++ /dev/null @@ -1,333 +0,0 @@ -use super::VectorDomain; -use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout}; -use crate::messages::prelude::*; -use crate::messages::tool::tool_messages::tool_prelude::*; -use graph_craft::document::NodeId; -use graphene_std::Color; -use graphene_std::Context; -use graphene_std::memo::IORecord; -use graphene_std::raster::Image; -use graphene_std::table::Table; -use graphene_std::vector::Vector; -use graphene_std::{Artboard, Graphic}; -use std::any::Any; -use std::sync::Arc; - -/// The spreadsheet UI allows for graph data to be previewed. -#[derive(Default, Debug, Clone, ExtractField)] -pub struct SpreadsheetMessageHandler { - /// Sets whether or not the spreadsheet is drawn. - pub spreadsheet_view_open: bool, - inspect_node: Option, - introspected_data: Option>, - element_path: Vec, - viewing_vector_domain: VectorDomain, -} - -#[message_handler_data] -impl MessageHandler for SpreadsheetMessageHandler { - fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque, _: ()) { - match message { - SpreadsheetMessage::ToggleOpen => { - self.spreadsheet_view_open = !self.spreadsheet_view_open; - // Run the graph to grab the data - if self.spreadsheet_view_open { - responses.add(NodeGraphMessage::RunDocumentGraph); - } - // Update checked UI state for open - responses.add(MenuBarMessage::SendLayout); - self.update_layout(responses); - } - - SpreadsheetMessage::UpdateLayout { mut inspect_result } => { - self.inspect_node = Some(inspect_result.inspect_node); - self.introspected_data = inspect_result.take_data(); - self.update_layout(responses) - } - - SpreadsheetMessage::PushToElementPath { index } => { - self.element_path.push(index); - self.update_layout(responses); - } - SpreadsheetMessage::TruncateElementPath { len } => { - self.element_path.truncate(len); - self.update_layout(responses); - } - - SpreadsheetMessage::ViewVectorDomain { domain } => { - self.viewing_vector_domain = domain; - self.update_layout(responses); - } - } - } - - fn actions(&self) -> ActionList { - actions!(SpreadsheetMessage;) - } -} - -impl SpreadsheetMessageHandler { - fn update_layout(&mut self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateSpreadsheetState { - node: self.inspect_node, - open: self.spreadsheet_view_open, - }); - if !self.spreadsheet_view_open { - return; - } - let mut layout_data = LayoutData { - current_depth: 0, - desired_path: &mut self.element_path, - breadcrumbs: Vec::new(), - vector_domain: self.viewing_vector_domain, - }; - let mut layout = self - .introspected_data - .as_ref() - .map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data)) - .unwrap_or_else(|| Some(label("No data"))) - .unwrap_or_else(|| label("Failed to downcast data")); - - if layout_data.breadcrumbs.len() > 1 { - let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs) - .on_update(|&len| SpreadsheetMessage::TruncateElementPath { len: len as usize }.into()) - .widget_holder(); - layout.insert(0, LayoutGroup::Row { widgets: vec![breadcrumb] }); - } - - responses.add(LayoutMessage::SendLayout { - layout: Layout::WidgetLayout(WidgetLayout { layout }), - layout_target: LayoutTarget::Spreadsheet, - }); - } -} - -struct LayoutData<'a> { - current_depth: usize, - desired_path: &'a mut Vec, - breadcrumbs: Vec, - vector_domain: VectorDomain, -} - -fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { - // We simply try random types. TODO: better strategy. - #[allow(clippy::manual_map)] - if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else if let Some(io) = introspected_data.downcast_ref::>>() { - Some(io.output.layout_with_breadcrumb(data)) - } else { - None - } -} - -fn column_headings(value: &[&str]) -> Vec { - value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect() -} - -fn label(x: impl Into) -> Vec { - let error = vec![TextLabel::new(x).widget_holder()]; - vec![LayoutGroup::Row { widgets: error }] -} - -trait TableRowLayout { - fn type_name() -> &'static str; - fn identifier(&self) -> String; - fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { - data.breadcrumbs.push(self.identifier()); - self.compute_layout(data) - } - fn compute_layout(&self, data: &mut LayoutData) -> Vec; -} - -impl TableRowLayout for Graphic { - fn type_name() -> &'static str { - "Graphic" - } - fn identifier(&self) -> String { - match self { - Self::Graphic(graphic) => graphic.identifier(), - Self::Vector(vector) => vector.identifier(), - Self::RasterCPU(_) => "Raster (on CPU)".to_string(), - Self::RasterGPU(_) => "Raster (on GPU)".to_string(), - Self::Color(_) => "Color".to_string(), - } - } - // Don't put a breadcrumb for Graphic - fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { - self.compute_layout(data) - } - fn compute_layout(&self, data: &mut LayoutData) -> Vec { - match self { - Self::Graphic(table) => table.layout_with_breadcrumb(data), - Self::Vector(table) => table.layout_with_breadcrumb(data), - Self::RasterCPU(_) => label("Raster is not supported"), - Self::RasterGPU(_) => label("Raster is not supported"), - Self::Color(color) => { - let rows = vec![vec![ - TextLabel::new(format!("Colors:\n{}", color.iter().map(|color| color.element.to_rgba_hex_srgb()).collect::>().join("\n"))).widget_holder(), - ]]; - vec![LayoutGroup::Table { rows }] - } - } - } -} - -impl TableRowLayout for Vector { - fn type_name() -> &'static str { - "Vector" - } - fn identifier(&self) -> String { - format!( - "Vector ({} point{}, {} segment{})", - self.point_domain.ids().len(), - if self.point_domain.ids().len() == 1 { "" } else { "s" }, - self.segment_domain.ids().len(), - if self.segment_domain.ids().len() == 1 { "" } else { "s" } - ) - } - fn compute_layout(&self, data: &mut LayoutData) -> Vec { - let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::>().join(", "); - let colinear = if colinear.is_empty() { "None" } else { &colinear }; - let style = vec![ - TextLabel::new(format!( - "{}\n\nColinear Handle IDs: {}\nPreserves Reference to Upstream Nested Layers for Editing by Tools: {}", - self.style, - colinear, - if self.upstream_nested_layers.is_some() { "Yes" } else { "No" } - )) - .multiline(true) - .widget_holder(), - ]; - - let domain_entries = [VectorDomain::Points, VectorDomain::Segments, VectorDomain::Regions] - .into_iter() - .map(|domain| { - RadioEntryData::new(format!("{domain:?}")) - .label(format!("{domain:?}")) - .on_update(move |_| SpreadsheetMessage::ViewVectorDomain { domain }.into()) - }) - .collect(); - let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_domain as u32)).widget_holder()]; - - let mut table_rows = Vec::new(); - match data.vector_domain { - VectorDomain::Points => { - table_rows.push(column_headings(&["", "position"])); - table_rows.extend( - self.point_domain - .iter() - .map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]), - ); - } - VectorDomain::Segments => { - table_rows.push(column_headings(&["", "start_index", "end_index", "handles"])); - table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { - vec![ - TextLabel::new(format!("{}", id.inner())).widget_holder(), - TextLabel::new(format!("{}", start)).widget_holder(), - TextLabel::new(format!("{}", end)).widget_holder(), - TextLabel::new(format!("{:?}", handles)).widget_holder(), - ] - })); - } - VectorDomain::Regions => { - table_rows.push(column_headings(&["", "segment_range", "fill"])); - table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| { - vec![ - TextLabel::new(format!("{}", id.inner())).widget_holder(), - TextLabel::new(format!("{:?}", segment_range)).widget_holder(), - TextLabel::new(format!("{}", fill.inner())).widget_holder(), - ] - })); - } - } - - vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }] - } -} - -impl TableRowLayout for Image { - fn type_name() -> &'static str { - "Image" - } - fn identifier(&self) -> String { - format!("Image ({}x{})", self.width, self.height) - } - fn compute_layout(&self, _data: &mut LayoutData) -> Vec { - let rows = vec![vec![TextLabel::new(format!("Image ({}x{})", self.width, self.height)).widget_holder()]]; - vec![LayoutGroup::Table { rows }] - } -} - -impl TableRowLayout for Artboard { - fn type_name() -> &'static str { - "Artboard" - } - fn identifier(&self) -> String { - self.label.clone() - } - fn compute_layout(&self, data: &mut LayoutData) -> Vec { - self.content.compute_layout(data) - } -} - -impl TableRowLayout for Table { - fn type_name() -> &'static str { - "Table" - } - fn identifier(&self) -> String { - format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" }) - } - fn compute_layout(&self, data: &mut LayoutData) -> Vec { - if let Some(index) = data.desired_path.get(data.current_depth).copied() { - if let Some(row) = self.get(index) { - data.current_depth += 1; - let result = row.element.layout_with_breadcrumb(data); - data.current_depth -= 1; - return result; - } else { - warn!("Desired path truncated"); - data.desired_path.truncate(data.current_depth); - } - } - - let mut rows = self - .iter() - .enumerate() - .map(|(index, row)| { - let (scale, angle, translation) = row.transform.to_scale_angle_translation(); - let rotation = if angle == -0. { 0. } else { angle.to_degrees() }; - let round = |x: f64| (x * 1e3).round() / 1e3; - vec![ - TextLabel::new(format!("{index}")).widget_holder(), - TextButton::new(row.element.identifier()) - .on_update(move |_| SpreadsheetMessage::PushToElementPath { index }.into()) - .widget_holder(), - TextLabel::new(format!( - "Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)", - round(translation.x), - round(translation.y), - round(scale.x), - round(scale.y) - )) - .widget_holder(), - TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(), - TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(), - ] - }) - .collect::>(); - - rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"])); - - vec![LayoutGroup::Table { rows }] - } -} diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index c8cce280..3edc26d9 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -43,7 +43,7 @@ pub enum PanelType { Document, Layers, Properties, - Spreadsheet, + DataPanel, } impl From for PanelType { @@ -52,7 +52,7 @@ impl From for PanelType { "Document" => PanelType::Document, "Layers" => PanelType::Layers, "Properties" => PanelType::Properties, - "Spreadsheet" => PanelType::Spreadsheet, + "Data" => PanelType::DataPanel, _ => panic!("Unknown panel type: {value}"), } } diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 1f12aad6..8df632f9 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -17,6 +17,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; +pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant}; pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler}; @@ -24,12 +25,10 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler}; pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler}; pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; -pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler}; pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler}; -pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler}; // Message, MessageDiscriminant pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant}; diff --git a/editor/src/messages/workspace/mod.rs b/editor/src/messages/workspace/mod.rs deleted file mode 100644 index 891ac11f..00000000 --- a/editor/src/messages/workspace/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod workspace_message; -mod workspace_message_handler; - -#[doc(inline)] -pub use workspace_message::{WorkspaceMessage, WorkspaceMessageDiscriminant}; -#[doc(inline)] -pub use workspace_message_handler::WorkspaceMessageHandler; diff --git a/editor/src/messages/workspace/workspace_message.rs b/editor/src/messages/workspace/workspace_message.rs deleted file mode 100644 index 66eb0858..00000000 --- a/editor/src/messages/workspace/workspace_message.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::messages::prelude::*; - -#[impl_message(Message, Workspace)] -#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum WorkspaceMessage { - // Messages - NodeGraphToggleVisibility, -} diff --git a/editor/src/messages/workspace/workspace_message_handler.rs b/editor/src/messages/workspace/workspace_message_handler.rs deleted file mode 100644 index d71c7a42..00000000 --- a/editor/src/messages/workspace/workspace_message_handler.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::messages::prelude::*; - -#[derive(Debug, Clone, Default, ExtractField)] -pub struct WorkspaceMessageHandler { - node_graph_visible: bool, -} - -#[message_handler_data] -impl MessageHandler for WorkspaceMessageHandler { - fn process_message(&mut self, message: WorkspaceMessage, _responses: &mut VecDeque, _: ()) { - match message { - // Messages - WorkspaceMessage::NodeGraphToggleVisibility => { - self.node_graph_visible = !self.node_graph_visible; - } - } - } - - fn actions(&self) -> ActionList { - actions!(WorkspaceMessageDiscriminant; - NodeGraphToggleVisibility, - ) - } -} diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index e3aa0c40..560ff471 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -58,7 +58,7 @@ pub struct NodeGraphExecutor { current_execution_id: u64, futures: HashMap, node_graph_hash: u64, - old_inspect_node: Option, + previous_node_to_inspect: Option, } #[derive(Debug, Clone)] @@ -80,7 +80,7 @@ impl NodeGraphExecutor { runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver), node_graph_hash: 0, current_execution_id: 0, - old_inspect_node: None, + previous_node_to_inspect: None, }; (node_runtime, node_executor) } @@ -113,22 +113,22 @@ impl NodeGraphExecutor { let instrumented = Instrumented::new(&mut network); self.runtime_io - .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None })) + .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None })) .map_err(|e| e.to_string())?; Ok(instrumented) } /// Update the cached network if necessary. - fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, inspect_node: Option, ignore_hash: bool) -> Result<(), String> { + fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option, ignore_hash: bool) -> Result<(), String> { let network_hash = document.network_interface.document_network().current_hash(); // Refresh the graph when it changes or the inspect node changes - if network_hash != self.node_graph_hash || self.old_inspect_node != inspect_node || ignore_hash { + if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash { let network = document.network_interface.document_network().clone(); - self.old_inspect_node = inspect_node; + self.previous_node_to_inspect = node_to_inspect; self.node_graph_hash = network_hash; self.runtime_io - .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node })) + .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect })) .map_err(|e| e.to_string())?; } Ok(()) @@ -173,10 +173,10 @@ impl NodeGraphExecutor { document_id: DocumentId, viewport_resolution: UVec2, time: TimingInformation, - inspect_node: Option, + node_to_inspect: Option, ignore_hash: bool, ) -> Result { - self.update_node_graph(document, inspect_node, ignore_hash)?; + self.update_node_graph(document, node_to_inspect, ignore_hash)?; self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, time) } @@ -210,7 +210,7 @@ impl NodeGraphExecutor { // Execute the node graph self.runtime_io - .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, inspect_node: None })) + .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None })) .map_err(|e| e.to_string())?; let execution_id = self.queue_execution(render_config); let execution_context = ExecutionContext { @@ -293,11 +293,11 @@ impl NodeGraphExecutor { } responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id)); - // Update the spreadsheet on the frontend using the value of the inspect result. - if self.old_inspect_node.is_some() { - if let Some(inspect_result) = inspect_result { - responses.add(SpreadsheetMessage::UpdateLayout { inspect_result }); - } + // Update the Data panel on the frontend using the value of the inspect result. + if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { + responses.add(DataPanelMessage::UpdateLayout { inspect_result }); + } else { + responses.add(DataPanelMessage::ClearLayout); } } NodeGraphUpdate::CompilationResponse(execution_response) => { diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index f5c6ba04..55a262ab 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -44,7 +44,7 @@ pub struct NodeRuntime { node_graph_errors: GraphErrors, monitor_nodes: Vec>, - /// Which node is inspected and which monitor node is used (if any) for the current execution + /// Which node is inspected and which monitor node is used (if any) for the current execution. inspect_state: Option, /// Mapping of the fully-qualified node paths to their preprocessor substitutions. @@ -69,7 +69,7 @@ pub enum GraphRuntimeRequest { pub struct GraphUpdate { pub(super) network: NodeNetwork, /// The node that should be temporary inspected during execution - pub(super) inspect_node: Option, + pub(super) node_to_inspect: Option, } #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -190,18 +190,19 @@ impl NodeRuntime { let _ = self.update_network(graph).await; } } - GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, inspect_node }) => { + GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, node_to_inspect }) => { // Insert the monitor node to manage the inspection - self.inspect_state = inspect_node.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect)); + self.inspect_state = node_to_inspect.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect)); self.old_graph = Some(network.clone()); + self.node_graph_errors.clear(); let result = self.update_network(network).await; + let node_graph_errors = self.node_graph_errors.clone(); + self.update_thumbnails = true; - self.sender.send_generation_response(CompilationResponse { - result, - node_graph_errors: self.node_graph_errors.clone(), - }); + + self.sender.send_generation_response(CompilationResponse { result, node_graph_errors }); } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { let result = self.execute_network(render_config).await; diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 53a20010..7882e22c 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -39,6 +39,7 @@ export let colorOrGradient: FillChoice; export let allowNone = false; // export let allowTransparency = false; // TODO: Implement + export let disabled = false; export let direction: MenuDirection = "Bottom"; // TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed export let open: boolean; @@ -133,6 +134,8 @@ } function onPointerDown(e: PointerEvent) { + if (disabled) return; + const target = (e.target || undefined) as HTMLElement | undefined; draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined; @@ -403,7 +406,7 @@ }); - + - + {#if !isNone}
{/if} @@ -432,12 +435,12 @@ /> {/if} - + {#if !isNone}
{/if} - + {#if !isNone}
{/if} @@ -447,6 +450,7 @@ { gradient = gradient; if (gradient) dispatch("colorOrGradient", gradient); @@ -459,6 +463,7 @@ {#if gradientSpectrumInputWidget && activeIndex !== undefined} { if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100); }} @@ -478,7 +483,7 @@ styles={{ "--outline-amount": outlineFactor }} tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"} > - {#if !newColor.equals(oldColor)} + {#if !newColor.equals(oldColor) && !disabled}
{/if} @@ -500,6 +505,7 @@ { dispatch("startHistoryTransaction"); setColorCode(detail); @@ -520,6 +526,7 @@ {/if} { strength = detail; setColorRGB(channel, detail); @@ -547,6 +554,7 @@ {/if} { strength = detail; setColorHSV(channel, detail); @@ -573,6 +581,7 @@ { if (detail !== undefined) alpha = detail / 100; setColorAlphaPercent(detail); @@ -593,14 +602,14 @@ {#if allowNone && !gradient} - + {/if} - + - + - - + @@ -954,7 +963,7 @@ // For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors. // We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover. background: var(--pure-color-gray); - transition: background-color 0.2s ease; + transition: background-color 0.1s; } &:hover div { @@ -963,5 +972,21 @@ } } } + + &.disabled .pickers-and-gradient .pickers :is(.saturation-value-picker, .hue-picker, .alpha-picker), + &.disabled .details .preset-color, + &.disabled .details .choice-preview { + transition: opacity 0.1s; + + &:hover { + opacity: 0.5; + } + } + + &.disabled .details .preset-color.pure:hover div { + background: var(--pure-color-gray); + } } + + // paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding diff --git a/frontend/src/components/panels/Data.svelte b/frontend/src/components/panels/Data.svelte new file mode 100644 index 00000000..7873be68 --- /dev/null +++ b/frontend/src/components/panels/Data.svelte @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 4ab7afd6..b907b51c 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -675,7 +675,7 @@ &[title^="Coming Soon"] { opacity: 0.25; - transition: opacity 0.2s; + transition: opacity 0.1s; &:hover { opacity: 1; @@ -825,7 +825,7 @@ .graph-view { pointer-events: none; - transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s; opacity: 0; &.open { diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 092a83fb..8d097fe8 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -98,6 +98,12 @@ }); onDestroy(() => { + editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout); + editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout); + editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout); + editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs); + editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails); + removeEventListener("pointermove", clippingHover); removeEventListener("keydown", clippingKeyPress); removeEventListener("keyup", clippingKeyPress); @@ -489,7 +495,9 @@ (dragInPanel = false)}> - + {#if layersPanelControlBarLeftLayout?.layout?.length > 0 && layersPanelControlBarRightLayout?.layout?.length > 0} + + {/if} @@ -605,6 +613,10 @@ .widget-span:first-child { flex: 1 1 auto; } + + &:not(:has(*)) { + display: none; + } } // Bottom bar @@ -619,6 +631,10 @@ .widget-span > * { margin: 0; } + + &:not(:has(*)) { + display: none; + } } // Layer hierarchy diff --git a/frontend/src/components/panels/Properties.svelte b/frontend/src/components/panels/Properties.svelte index 72607849..889ccba9 100644 --- a/frontend/src/components/panels/Properties.svelte +++ b/frontend/src/components/panels/Properties.svelte @@ -1,27 +1,31 @@ - + diff --git a/frontend/src/components/panels/Spreadsheet.svelte b/frontend/src/components/panels/Spreadsheet.svelte deleted file mode 100644 index 6dd49c60..00000000 --- a/frontend/src/components/panels/Spreadsheet.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}: - - - - - - - diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index eaedc3a6..5efebc35 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -1008,7 +1008,7 @@ border-radius: 4px; bottom: calc(100% + 12px); z-index: -1; - transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s; opacity: 0.5; // Tail @@ -1040,7 +1040,7 @@ -webkit-user-select: text; user-select: text; transition: - opacity 0.2s ease-in-out, + opacity 0.2s, z-index 0s 0.2s; &::selection { diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 831e1dbb..35400eb9 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -25,6 +25,7 @@ import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; + import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte"; import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; @@ -124,6 +125,10 @@ {#if iconLabel} {/if} + {@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")} + {#if imageLabel} + + {/if} {@const imageButton = narrowWidgetProps(component.props, "ImageButton")} {#if imageButton} widgetValueCommitAndUpdate(index, undefined)} /> diff --git a/frontend/src/components/widgets/buttons/ImageButton.svelte b/frontend/src/components/widgets/buttons/ImageButton.svelte index 00e9de6a..ec5b8da8 100644 --- a/frontend/src/components/widgets/buttons/ImageButton.svelte +++ b/frontend/src/components/widgets/buttons/ImageButton.svelte @@ -17,14 +17,15 @@ .join(" "); - + diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index cabc70b4..6611614c 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -13,6 +13,7 @@ const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>(); export let gradient: Gradient; + export let disabled = false; export let activeMarkerIndex = 0 as number | undefined; // export let disabled = false; // export let tooltip: string | undefined = undefined; @@ -22,6 +23,8 @@ let deletionRestore: boolean | undefined = undefined; function markerPointerDown(e: PointerEvent, index: number) { + if (disabled) return; + // Left-click to select and begin potentially dragging if (e.button === BUTTON_LEFT) { activeMarkerIndex = index; @@ -47,6 +50,8 @@ } function insertStop(e: MouseEvent) { + if (disabled) return; + if (e.button !== BUTTON_LEFT) return; let position = markerPosition(e); @@ -79,6 +84,8 @@ } function deleteStop(e: KeyboardEvent) { + if (disabled) return; + if (e.key !== "Delete" && e.key !== "Backspace") return; if (activeMarkerIndex === undefined) return; @@ -88,6 +95,8 @@ } function deleteStopByIndex(index: number) { + if (disabled) return; + if (gradient.stops.length <= 2) return; gradient.stops.splice(index, 1); @@ -103,6 +112,8 @@ } function moveMarker(e: PointerEvent, index: number) { + if (disabled) return; + // Just in case the mouseup event is lost if (e.buttons === 0) stopDrag(); @@ -120,6 +131,8 @@ } export function setPosition(index: number, position: number) { + if (disabled) return; + const active = gradient.stops[index]; active.position = position; gradient.stops.sort((a, b) => a.position - b.position); @@ -131,6 +144,8 @@ } function abortDrag() { + if (disabled) return; + if (activeMarkerIndex === undefined) return; if (deletionRestore) { @@ -143,6 +158,8 @@ } function stopDrag() { + if (disabled) return; + removeEvents(); positionRestore = undefined; @@ -152,19 +169,27 @@ } function onPointerMove(e: PointerEvent) { + if (disabled) return; + if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex); } function onPointerUp() { + if (disabled) return; + stopDrag(); } function onMouseDown(e: MouseEvent) { + if (disabled) return; + const BUTTONS_RIGHT = 0b0000_0010; if (e.buttons & BUTTONS_RIGHT) abortDrag(); } function onKeyDown(e: KeyboardEvent) { + if (disabled) return; + if (e.key === "Escape") { const element = markerTrack?.div(); if (element) preventEscapeClosingParentFloatingMenu(element); @@ -193,25 +218,28 @@ document.removeEventListener("keydown", deleteStop); }); + // Future design notes: + // // # Backend -> Frontend // Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged. // UpdateGradient(gradient) // UpdateMarkers({ index, position, color }[]) - + // // # Frontend -> Backend // SendNewActive(index) // SendPositions({ index, position }[]) // AddMarker(position) // RemoveMarkers(index[]) // ResetMarkerToDefault(index) - - // // We need a way to encode constraints on some markers, like locking them in place or preventing reordering - // // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers - // // We need the ability to multi-select markers and move them all at once + // + // We need a way to encode constraints on some markers, like locking them in place or preventing reordering + // We need a way to encode the allowability of adding new markers between certain markers, or preventing the deletion of certain markers + // We need the ability to multi-select markers and move them all at once + {#if disabled} + + {/if} 0 ? { "min-width": `${minWidth}px` } : {}) }} + styles={{ + ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), + ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}), + }} {value} on:value on:textFocused={onTextFocused} diff --git a/frontend/src/components/widgets/labels/ImageLabel.svelte b/frontend/src/components/widgets/labels/ImageLabel.svelte new file mode 100644 index 00000000..64536c99 --- /dev/null +++ b/frontend/src/components/widgets/labels/ImageLabel.svelte @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/components/window/workspace/Panel.svelte b/frontend/src/components/window/workspace/Panel.svelte index d7352583..f53b9ff3 100644 --- a/frontend/src/components/window/workspace/Panel.svelte +++ b/frontend/src/components/window/workspace/Panel.svelte @@ -1,14 +1,14 @@ diff --git a/frontend/src/components/window/workspace/Workspace.svelte b/frontend/src/components/window/workspace/Workspace.svelte index 1f27bd75..89607844 100644 --- a/frontend/src/components/window/workspace/Workspace.svelte +++ b/frontend/src/components/window/workspace/Workspace.svelte @@ -16,7 +16,7 @@ /**/ root: 100, /* ├─ */ content: 80, /* │ ├─ */ document: 70, - /* │ └─ */ spreadsheet: 30, + /* │ └─ */ data: 30, /* └─ */ details: 20, /* ├─ */ properties: 45, /* └─ */ layers: 55, @@ -148,23 +148,31 @@ bind:this={documentPanel} /> - {#if $portfolio.spreadsheetOpen} + {#if $portfolio.dataPanelOpen} resizePanel(e)} /> - - + + {/if} - resizePanel(e)} /> - - - - - resizePanel(e)} /> - - - - + {#if $portfolio.propertiesPanelOpen || $portfolio.layersPanelOpen} + resizePanel(e)} /> + + {#if $portfolio.propertiesPanelOpen} + + + + {/if} + {#if $portfolio.propertiesPanelOpen && $portfolio.layersPanelOpen} + resizePanel(e)} /> + {/if} + {#if $portfolio.layersPanelOpen} + + + + {/if} + + {/if} {#if $dialog.visible} diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 157c46d0..53df541c 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -763,10 +763,16 @@ export class UpdateGraphFadeArtwork extends JsMessage { readonly percentage!: number; } -export class UpdateSpreadsheetState extends JsMessage { +export class UpdateDataPanelState extends JsMessage { readonly open!: boolean; +} - readonly node!: bigint | undefined; +export class UpdatePropertiesPanelState extends JsMessage { + readonly open!: boolean; +} + +export class UpdateLayersPanelState extends JsMessage { + readonly open!: boolean; } export class UpdateMouseCursor extends JsMessage { @@ -981,9 +987,11 @@ export class ColorInput extends WidgetProps { }) value!: FillChoice; + allowNone!: boolean; + disabled!: boolean; - allowNone!: boolean; + menuDirection!: MenuDirection | undefined; // allowTransparency!: boolean; // TODO: Implement @@ -1140,6 +1148,19 @@ export class ImageButton extends WidgetProps { tooltip!: string | undefined; } +export class ImageLabel extends WidgetProps { + url!: string; + + @Transform(({ value }: { value: string }) => value || undefined) + width!: string | undefined; + + @Transform(({ value }: { value: string }) => value || undefined) + height!: string | undefined; + + @Transform(({ value }: { value: string }) => value || undefined) + tooltip!: string | undefined; +} + export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None"; export type NumberInputMode = "Increment" | "Range"; @@ -1332,6 +1353,8 @@ export class TextInput extends WidgetProps { minWidth!: number; + maxWidth!: number; + @Transform(({ value }: { value: string }) => value || undefined) tooltip!: string | undefined; } @@ -1383,6 +1406,7 @@ const widgetSubTypes = [ { value: FontInput, name: "FontInput" }, { value: IconButton, name: "IconButton" }, { value: ImageButton, name: "ImageButton" }, + { value: ImageLabel, name: "ImageLabel" }, { value: IconLabel, name: "IconLabel" }, { value: NodeCatalog, name: "NodeCatalog" }, { value: NumberInput, name: "NumberInput" }, @@ -1472,11 +1496,11 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg updates.diff.forEach((update) => { // Find the object where the diff applies to - const diffObject = update.widgetPath.reduce((targetLayout, index) => { - if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; - if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; - if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; - if ("layout" in targetLayout) return targetLayout.layout[index]; + const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => { + if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; + if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; + if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; + if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index]; if (targetLayout instanceof Widget) { if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) { return targetLayout.props.popoverLayout[index]; @@ -1487,10 +1511,21 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg } // This is a path traversal so we can assume from the backend that it exists // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if ("action" in targetLayout) return targetLayout.children![index]; - return targetLayout[index]; + if (targetLayout && "action" in targetLayout) return targetLayout.children![index]; + + return targetLayout?.[index]; }, layout.layout as UIItem); + // Exit if we failed to produce a valid patch for the existing layout. + // This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for + // example, if a panel is destroyed in the frontend but was never cleared in the backend, so the next time the backend + // tries to update the layout, it attempts to insert only the changes against the old layout that no longer exists. + if (diffObject === undefined) { + // eslint-disable-next-line no-console + console.error("In `patchWidgetLayout`, the `diffObject` is undefined. The layout has not been updated. See the source code comment above this error for hints."); + return; + } + // If this is a list with a length, then set the length to 0 to clear the list if ("length" in diffObject) { diffObject.length = 0; @@ -1613,9 +1648,9 @@ export class UpdateMenuBarLayout extends JsMessage { export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} -export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} +export class UpdatePropertiesPanelLayout extends WidgetDiffUpdate {} -export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} +export class UpdateDataPanelLayout extends WidgetDiffUpdate {} export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} @@ -1712,9 +1747,11 @@ export const messageMakers: Record = { UpdateNodeThumbnail, UpdateOpenDocumentsList, UpdatePlatform, - UpdatePropertyPanelSectionsLayout, - UpdateSpreadsheetLayout, - UpdateSpreadsheetState, + UpdatePropertiesPanelLayout, + UpdateDataPanelLayout, + UpdateDataPanelState, + UpdatePropertiesPanelState, + UpdateLayersPanelState, UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateViewportHolePunch, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 4305ccc6..41ad207f 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -13,10 +13,9 @@ import { TriggerOpenDocument, UpdateActiveDocument, UpdateOpenDocumentsList, - UpdateSpreadsheetState, - defaultWidgetLayout, - patchWidgetLayout, - UpdateSpreadsheetLayout, + UpdateDataPanelState, + UpdatePropertiesPanelState, + UpdateLayersPanelState, } from "@graphite/messages"; import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files"; import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization"; @@ -27,9 +26,9 @@ export function createPortfolioState(editor: Editor) { unsaved: false, documents: [] as FrontendDocumentDetails[], activeDocumentIndex: 0, - spreadsheetOpen: false, - spreadsheetNode: BigInt(0) as bigint | undefined, - spreadsheetWidgets: defaultWidgetLayout(), + dataPanelOpen: false, + propertiesPanelOpen: true, + layersPanelOpen: true, }); // Set up message subscriptions on creation @@ -107,16 +106,21 @@ export function createPortfolioState(editor: Editor) { // Fail silently if there's an error rasterizing the SVG, such as a zero-sized image } }); - editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => { + editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (updateDataPanelState) => { update((state) => { - state.spreadsheetOpen = updateSpreadsheetState.open; - state.spreadsheetNode = updateSpreadsheetState.node; + state.dataPanelOpen = updateDataPanelState.open; return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => { + editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (updatePropertiesPanelState) => { update((state) => { - patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout); + state.propertiesPanelOpen = updatePropertiesPanelState.open; + return state; + }); + }); + editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (updateLayersPanelState) => { + update((state) => { + state.layersPanelOpen = updateLayersPanelState.open; return state; }); }); diff --git a/frontend/src/subscription-router.ts b/frontend/src/subscription-router.ts index 1234c194..c7af44ba 100644 --- a/frontend/src/subscription-router.ts +++ b/frontend/src/subscription-router.ts @@ -17,6 +17,10 @@ export function createSubscriptionRouter() { subscriptions[messageType.name] = callback; }; + const unsubscribeJsMessage = (messageType: new () => T) => { + delete subscriptions[messageType.name]; + }; + const handleJsMessage = (messageType: JsMessageType, messageData: Record, wasm: WebAssembly.Memory, handle: EditorHandle) => { // Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class const messageMaker = messageMakers[messageType]; @@ -68,6 +72,7 @@ export function createSubscriptionRouter() { return { subscribeJsMessage, + unsubscribeJsMessage, handleJsMessage, }; } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 2b81d98b..80490c8b 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -231,11 +231,7 @@ impl EditorHandle { let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); if let Err(error) = js_return_value { - error!( - "While handling FrontendMessage \"{:?}\", JavaScript threw an error: {:?}", - message.to_discriminant().local_name(), - error, - ) + error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,) } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 45d0082e..282dd8e2 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,10 +1,13 @@ use dyn_any::StaticType; use glam::{DAffine2, DVec2, IVec2}; +use graph_craft::document::DocumentNode; use graph_craft::document::value::RenderOutput; use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_core::raster::color::Color; use graphene_core::raster::*; -use graphene_core::raster_types::{CPU, GPU, Raster}; +#[cfg(feature = "gpu")] +use graphene_core::raster_types::GPU; +use graphene_core::raster_types::{CPU, Raster}; use graphene_core::{Artboard, concrete, generic}; use graphene_core::{Cow, ProtoNodeIdentifier, Type}; use graphene_core::{NodeIO, NodeIOTypes}; @@ -15,7 +18,10 @@ use graphene_std::Graphic; use graphene_std::any::DowncastBothNode; use graphene_std::any::{ComposeTypeErased, DynAnyNode, IntoTypeErasedNode}; use graphene_std::application_io::{ImageTexture, SurfaceFrame}; +use graphene_std::brush::brush_cache::BrushCache; +use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::table::Table; +use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; #[cfg(feature = "gpu")] use graphene_std::wasm_application_io::{WasmEditorApi, WasmSurfaceHandle}; @@ -84,6 +90,40 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Box]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => [f64; 4]]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::text::Font]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BrushCache]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DocumentNode]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::curve::Curve]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::transform::Footprint]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::blending::BlendMode]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::extract_xy::XY]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::animation::RealTimeMode]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::FillType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::transform::ReferencePoint]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::CentroidType]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::text::TextAlign]), // ========== // MEMO NODES // ==========