diff --git a/editor/src/consts.rs b/editor/src/consts.rs index b17a8621cc..f2044abaea 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -4,6 +4,7 @@ pub const EXPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72; pub const EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP: u32 = 120; pub const IMPORTS_TO_TOP_EDGE_PIXEL_GAP: u32 = 72; pub const IMPORTS_TO_LEFT_EDGE_PIXEL_GAP: u32 = 120; +pub const TOOLTIP_DELAY: u32 = 800; // VIEWPORT pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = (1. / 600.) * 3.; diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index dea15382f8..2389012a2c 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -140,6 +140,7 @@ impl Dispatcher { Message::Defer(message) => { let context = DeferMessageContext { portfolio: &self.message_handlers.portfolio_message_handler, + time: self.message_handlers.input_preprocessor_message_handler.time, }; self.message_handlers.defer_message_handler.process_message(message, &mut queue, context); } diff --git a/editor/src/messages/defer/defer_message.rs b/editor/src/messages/defer/defer_message.rs index c1b699a70a..f80b19c17b 100644 --- a/editor/src/messages/defer/defer_message.rs +++ b/editor/src/messages/defer/defer_message.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::messages::prelude::*; #[impl_message(Message, Defer)] @@ -8,4 +10,6 @@ pub enum DeferMessage { AfterGraphRun { messages: Vec }, TriggerNavigationReady, AfterNavigationReady { messages: Vec }, + RequestDeferredMessage { timeout: Duration, message: Box }, + CheckDeferredMessages, } diff --git a/editor/src/messages/defer/defer_message_handler.rs b/editor/src/messages/defer/defer_message_handler.rs index a1c4907ecd..9b6a1f8f9c 100644 --- a/editor/src/messages/defer/defer_message_handler.rs +++ b/editor/src/messages/defer/defer_message_handler.rs @@ -1,8 +1,15 @@ +use std::{ + collections::BTreeMap, + ops::Bound, + time::{Duration, Instant}, +}; + use crate::messages::prelude::*; #[derive(ExtractField)] pub struct DeferMessageContext<'a> { pub portfolio: &'a PortfolioMessageHandler, + pub time: Instant, } #[derive(Debug, Default, ExtractField)] @@ -10,6 +17,7 @@ pub struct DeferMessageHandler { after_graph_run: HashMap>, after_viewport_resize: Vec, current_graph_submission_id: u64, + after_time_elapsed: BTreeMap, } #[message_handler_data] @@ -49,6 +57,15 @@ impl MessageHandler> for DeferMessageHandl responses.add_front(message); } } + DeferMessage::RequestDeferredMessage { timeout, message } => { + self.after_time_elapsed.insert(context.time + timeout, *message); + } + DeferMessage::CheckDeferredMessages => { + let after_current_time = self.after_time_elapsed.split_off((Bound::Unbounded, Bound::Excluded(context.time))); + for (_, message) in std::mem::replace(self.after_time_elapsed, after_current_time) { + responses.add(message); + } + } } } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 1f8aafcf44..7b8a24dfed 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -305,6 +305,10 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, + UpdateTooltip { + position: Option, + text: String, + }, UpdateWirePathInProgress { #[serde(rename = "wirePathInProgress")] wire_path_in_progress: Option, diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index a7bcfa37b9..a5ccec838a 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -4,7 +4,7 @@ use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo; use crate::messages::portfolio::utility_types::KeyboardPlatformLayout; use crate::messages::prelude::*; use glam::DVec2; -use std::time::Duration; +use std::time::{Duration, Instant}; #[derive(ExtractField)] pub struct InputPreprocessorMessageContext { @@ -14,7 +14,7 @@ pub struct InputPreprocessorMessageContext { #[derive(Debug, Default, ExtractField)] pub struct InputPreprocessorMessageHandler { pub frame_time: FrameTimeInfo, - pub time: u64, + pub time: Instant, pub keyboard: KeyStates, pub mouse: MouseState, pub viewport_bounds: ViewportBounds, @@ -114,7 +114,7 @@ impl MessageHandler f } InputPreprocessorMessage::CurrentTime { timestamp } => { responses.add(AnimationMessage::SetTime { time: timestamp as f64 }); - self.time = timestamp; + self.time = Instant::from(timestamp); self.frame_time.advance_timestamp(Duration::from_millis(timestamp)); } InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 06c2e8d2bd..9fa473a569 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -215,6 +215,7 @@ pub enum NodeGraphMessage { SetLockedOrVisibilitySideEffects { node_ids: Vec, }, + TryDisplayTooltip, UpdateBoxSelection, UpdateImportsExports, UpdateLayerPanel, 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 5007646aa7..f07c503dd3 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 @@ -1,6 +1,6 @@ use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart}; use super::{document_node_definitions, node_properties}; -use crate::consts::GRID_SIZE; +use crate::consts::{GRID_SIZE, TOOLTIP_DELAY}; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; @@ -94,6 +94,12 @@ pub struct NodeGraphMessageHandler { end_index: Option, // The rendered string for each thumbnail pub thumbnails: HashMap, + // If an input is being hovered. Used for tooltip + hovering_input: bool, + // If an output is being hovered. Used for tooltip + hovering_output: bool, + // If a node is being hovered. Used for tooltip + hovering_node: bool, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1143,6 +1149,44 @@ impl<'a> MessageHandler> for NodeG .unwrap_or_else(|| modify_import_export.reorder_imports_exports.input_ports().count() + 1), ); responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index }); + } else if !self.hovering_input && !self.hovering_output { + if network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_input = true; + responses.add(DeferMessage::RequestDeferredMessage { + message: Box::new(NodeGraphMessage::TryDisplayTooltip.into()), + timeout: Duration::from_millis(TOOLTIP_DELAY), + }); + } + if network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_output = true; + responses.add(DeferMessage::RequestDeferredMessage { + message: Box::new(NodeGraphMessage::TryDisplayTooltip.into()), + timeout: Duration::from_millis(TOOLTIP_DELAY), + }) + } + } else if self.hovering_input { + if !network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_input = false; + responses.add(FrontendMessage::UpdateTooltip { position: None, text: String::new() }); + } + } else if self.hovering_output { + if !network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_output = false; + responses.add(FrontendMessage::UpdateTooltip { position: None, text: String::new() }); + } + } else if !self.hovering_node { + if network_interface.node_from_click(ipp.mouse.position, breadcrumb_network_path) { + self.hovering_node = true; + responses.add(DeferMessage::RequestDeferredMessage { + message: Box::new(NodeGraphMessage::TryDisplayTooltip.into()), + timeout: Duration::from_millis(TOOLTIP_DELAY), + }) + } + } else if self.hovering_node { + if !network_interface.node_from_click(ipp.mouse.position, breadcrumb_network_path) { + self.hovering_node = false; + responses.add(FrontendMessage::UpdateTooltip { position: None, text: String::new() }); + } } } NodeGraphMessage::PointerUp => { @@ -1595,8 +1639,6 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - - responses.add(FrontendMessage::RequestNativeNodeGraphRender); responses.add(NodeGraphMessage::UpdateImportsExports); self.update_node_graph_hints(responses); } @@ -1807,6 +1849,54 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SetVisibility { node_id, visible }); responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }); } + NodeGraphMessage::TryDisplayTooltip => { + if let Some(input) = network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path) { + let text = network_interface.input_tooltip_text(&input, breadcrumb_network_path); + if let Some(position) = network_interface.input_position(&input, breadcrumb_network_path) { + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + return; + }; + let position = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.transform_point2(position); + let xy = FrontendXY { + x: position.x as i32, + y: position.y as i32, + }; + responses.add(FrontendMessage::UpdateTooltip { position: Some(xy), text }); + } + } else if let Some(output) = network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path) { + let text = network_interface.output_tooltip_text(&output, breadcrumb_network_path); + if let Some(position) = network_interface.output_position(&output, breadcrumb_network_path) { + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + return; + }; + let position = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.transform_point2(position); + + let xy = FrontendXY { + x: position.x as i32, + y: position.y as i32, + }; + responses.add(FrontendMessage::UpdateTooltip { position: Some(xy), text }); + } + } else if let Some(node_id) = network_interface.node_from_click(ipp.mouse.position, breadcrumb_network_path) { + let text = network_interface.node_tooltip_text(&node_id, breadcrumb_network_path); + if let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) { + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + return; + }; + let position = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .transform_point2(DVec2::new(position.x as f64 * 24., position.y as f64 * 24.)); + + let xy = FrontendXY { + x: position.x as i32, + y: position.y as i32, + }; + responses.add(FrontendMessage::UpdateTooltip { position: Some(xy), text }); + } + } + } NodeGraphMessage::SetPinned { node_id, pinned } => { network_interface.set_pinned(&node_id, selection_network_path, pinned); } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index 5d459fcf8a..67e3073786 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -144,24 +144,9 @@ impl NodeNetworkInterface { } let input_type = self.input_type(input_connector, network_path); let data_type = input_type.displayed_type(); - let resolved_type = input_type.resolved_type_name(); - let connected_to = self - .upstream_output_connector(input_connector, network_path) - .map(|output_connector| match output_connector { - OutputConnector::Node { node_id, output_index } => { - let mut name = self.display_name(&node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} output {output_index}") - } - OutputConnector::Import(import_index) => format!("Import index {import_index}"), - }) - .unwrap_or("nothing".to_string()); - - let (name, description) = match input_connector { - InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), + let name = match input_connector { + InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path).0, InputConnector::Export(export_index) => { // Get export name from parent node metadata input, which must match the number of exports. // Empty string means to use type, or "Export + index" if type is empty determined @@ -173,44 +158,26 @@ impl NodeNetworkInterface { .unwrap_or_default() }; - let export_name = if !export_name.is_empty() { + if !export_name.is_empty() { export_name } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { export_type_name } else { format!("Export index {}", export_index) - }; - - (export_name, String::new()) + } } }; - // TODO: Move in separate Tooltip overlay - // let valid_types = match self.valid_input_types(&input_connector, network_path) { - // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), - // Err(e) => { - // log::error!("Error getting valid types for input {input_connector:?}: {e}"); - // Vec::new() - // } - // }; - let connected_to_node = self.upstream_output_connector(input_connector, network_path).and_then(|output_connector| output_connector.node_id()); - Some(FrontendGraphInput { - data_type, - resolved_type, - name, - description, - connected_to, - connected_to_node, - }) + Some(FrontendGraphInput { data_type, name, connected_to_node }) } /// Returns None if there is an error, it is the document network, a hidden primary output or import pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { let output_type = self.output_type(output_connector, network_path); - let (name, description) = match output_connector { + let name = match output_connector { OutputConnector::Node { node_id, output_index } => { // Do not display the primary output port for a node if it is a network node with a hidden primary export if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { @@ -220,8 +187,7 @@ impl NodeNetworkInterface { let node_metadata = self.node_metadata(node_id, network_path)?; let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); - let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; - (output_name, String::new()) + if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() } } OutputConnector::Import(import_index) => { // Get the import name from the encapsulating node input metadata @@ -233,53 +199,19 @@ impl NodeNetworkInterface { if *import_index == 0 && self.hidden_primary_import(network_path) { return None; }; - let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); + let import_name = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path).0; - let import_name = if !import_name.is_empty() { + if !import_name.is_empty() { import_name } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { import_type_name } else { format!("Import index {}", *import_index) - }; - - (import_name, description) + } } }; let data_type = output_type.displayed_type(); - let resolved_type = output_type.resolved_type_name(); - let mut connected_to = self - .outward_wires(network_path) - .and_then(|outward_wires| outward_wires.get(output_connector)) - .cloned() - .unwrap_or_else(|| { - log::error!("Could not get {output_connector:?} in outward wires"); - Vec::new() - }) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => { - let mut name = self.display_name(node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} input {input_index}") - } - InputConnector::Export(export_index) => format!("Export index {export_index}"), - }) - .collect::>(); - - if connected_to.is_empty() { - connected_to.push("nothing".to_string()); - } - - Some(FrontendGraphOutput { - data_type, - resolved_type, - name, - description, - connected_to, - }) + Some(FrontendGraphOutput { data_type, name }) } pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { @@ -545,4 +477,90 @@ impl NodeNetworkInterface { Some(vector_wire) } + + pub fn input_tooltip_text(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> String { + let input_type = self.input_type(input_connector, network_path); + let data_type_str = format!("Data Type: {input_type:?}"); + + let connected_to = self + .upstream_output_connector(input_connector, network_path) + .map(|output_connector| match output_connector { + OutputConnector::Node { node_id, output_index } => { + let mut name = self.display_name(&node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} output {output_index}") + } + OutputConnector::Import(import_index) => format!("Import index {import_index}"), + }) + .unwrap_or("nothing".to_string()); + let connected_to_str = format!("Connected to: {connected_to}"); + + let valid_types = match self.valid_input_types(input_connector, network_path) { + Ok(valid) => valid, + Err(e) => { + log::error!("Could not get valid types in input tooltip text: {e}"); + return String::new(); + } + }; + + let valid_types_str = if !valid_types.is_empty() { + let mut strings = valid_types.iter().map(|x| format!("• {x}")).collect::>(); + strings.sort(); + strings.join("\n") + } else { + "None".to_string() + }; + + let valid_types_str = format!("Valid Types:\n{}", valid_types_str); + + format!("{data_type_str}\n\n{connected_to_str}\n\n{valid_types_str}") + } + + pub fn output_tooltip_text(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> String { + let output_type = self.output_type(output_connector, network_path); + let data_type_str = format!("Data Type: {output_type:?}"); + + let mut connected_to = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(output_connector)) + .cloned() + .unwrap_or_else(|| { + log::error!("Could not get {output_connector:?} in outward wires"); + Vec::new() + }) + .iter() + .map(|input| match input { + InputConnector::Node { node_id, input_index } => { + let mut name = self.display_name(node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} input {input_index}") + } + InputConnector::Export(export_index) => format!("Export index {export_index}"), + }) + .collect::>(); + + connected_to.sort(); + if connected_to.is_empty() { + connected_to.push("nothing".to_string()); + } + let connected_to = connected_to.join("\n"); + let connected_to_str = format!("Connected to:\n{connected_to}"); + + format!("{data_type_str}\n\n{connected_to_str}") + } + + pub fn node_tooltip_text(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> String { + let display_name = self.display_name(node_id, network_path); + let description = self.description(node_id, network_path); + let Some(reference) = self.reference(node_id, network_path) else { + log::error!("Could not get referende in node_tooltip_text for {node_id}"); + return String::new(); + }; + + format!("{display_name}\nReference: {reference}\n\n{description}") + } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 3ae2f57f67..66a27b84c3 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -562,7 +562,7 @@ struct PathToolData { saved_selection_before_handle_drag: HashMap, HashSet)>, handle_drag_toggle: bool, saved_points_before_anchor_convert_smooth_sharp: HashMap>, - last_click_time: u64, + last_click_time: Instant, dragging_state: DraggingState, angle: f64, pivot_gizmo: PivotGizmo, diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 73da5c4f48..985f317d6c 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -180,33 +180,6 @@ return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; } - function inputTooltip(value: FrontendGraphInput): string { - return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n"; - } - - function outputTooltip(value: FrontendGraphOutput): string { - return dataTypeTooltip(value) + "\n\n" + outputConnectedToText(value); - } - - function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { - return `Data Type: ${value.resolvedType}`; - } - - // function validTypesText(value: FrontendGraphInput): string { - // const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; - // return `Valid Types:\n${validTypes}`; - // } - - function outputConnectedToText(output: FrontendGraphOutput): string { - if (output.connectedTo.length === 0) return "Connected to nothing"; - - return `Connected to:\n${output.connectedTo.join("\n")}`; - } - - function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedToString}`; - } - function collectExposedInputsOutputs( inputs: (FrontendGraphInput | undefined)[], outputs: (FrontendGraphOutput | undefined)[], @@ -326,7 +299,6 @@ style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} > - {outputTooltip(frontendOutput)} {#if frontendOutput.connectedTo.length > 0} {:else} @@ -407,7 +379,6 @@ style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} > - {inputTooltip(frontendInput)} {#if frontendInput.connectedTo !== "nothing"} {:else} @@ -511,6 +482,15 @@ + +{#if $nodeGraph.tooltipPosition} +
+
+ {$nodeGraph.tooltipText} +
+
+{/if} + {#if $nodeGraph.selectionBox}
- .graph-background { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - background: var(--color-2-mildblack); - opacity: var(--fade-artwork); - - // We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements - &::before { - content: ""; - position: absolute; - width: 100%; - height: 100%; - - pointer-events: none; - background-size: var(--grid-spacing) var(--grid-spacing); - background-position: calc(var(--grid-offset-x) - var(--grid-dot-radius)) calc(var(--grid-offset-y) - var(--grid-dot-radius)); - background-image: radial-gradient(circle at var(--grid-dot-radius) var(--grid-dot-radius), var(--color-3-darkgray) var(--grid-dot-radius), transparent 0); - background-repeat: repeat; - image-rendering: pixelated; - mix-blend-mode: screen; - opacity: var(--fade-artwork); - } - } - .native-node-graph-ui { position: absolute; top: 0; @@ -745,750 +698,6 @@ } } - .breadcrumb-trail-buttons { - margin-top: 8px; - margin-left: 8px; - } - - .context-menu { - width: max-content; - position: absolute; - box-sizing: border-box; - padding: 5px; - z-index: 3; - background-color: var(--color-3-darkgray); - border-radius: 4px; - - .toggle-layer-or-node .text-label { - line-height: 24px; - margin-right: 8px; - } - - .merge-selected-nodes { - justify-content: center; - } - } - - .click-targets { - position: absolute; - pointer-events: none; - width: 100%; - height: 100%; - z-index: 10; - - svg { - overflow: visible; - width: 100%; - height: 100%; - stroke-width: 1; - fill: none; - - .layer { - stroke: yellow; - } - - .node { - stroke: blue; - } - - .connector { - stroke: green; - } - - .visibility { - stroke: red; - } - - .all-nodes-bounding-box { - stroke: purple; - } - - .modify-import-export { - stroke: orange; - } - } - } - - .wires { - pointer-events: none; - position: absolute; - width: 100%; - height: 100%; - - svg { - width: 100%; - height: 100%; - overflow: visible; - - path { - fill: none; - stroke: var(--data-color-dim); - stroke-width: var(--data-line-width); - stroke-dasharray: var(--data-dasharray); - } - } - } - - .imports-and-exports { - width: 100%; - height: 100%; - position: absolute; - // Keeps the connectors above the wires - z-index: 1; - - .connector { - position: absolute; - width: 8px; - height: 8px; - margin-top: 4px; - margin-left: 5px; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - } - - .reorder-bar { - position: absolute; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - width: 50px; - height: 2px; - background: white; - } - - .plus { - position: absolute; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - } - - .edit-import-export { - position: absolute; - display: flex; - align-items: center; - top: calc(var(--offset-top) * 24px); - margin-top: -5px; - height: 24px; - - &.separator-bottom::after, - &.separator-top::before { - content: ""; - position: absolute; - background: var(--color-8-uppergray); - height: 1px; - left: -4px; - right: -4px; - } - - &.separator-bottom::after { - bottom: -1px; - } - - &.separator-top::before { - top: 0; - } - - &.import { - right: calc(100% - var(--offset-left) * 24px); - } - - &.export { - left: calc(var(--offset-left) * 24px + 17px); - } - - .import-text { - text-align: right; - text-wrap: nowrap; - } - - .export-text { - text-wrap: nowrap; - } - - .import-text-input { - text-align: right; - } - - .remove-button-import { - margin-left: 3px; - } - - .remove-button-export { - margin-right: 3px; - } - - .reorder-drag-grip { - width: 8px; - height: 24px; - background-position: 2px 8px; - border-radius: 2px; - margin: -6px 0; - background-image: var(--icon-drag-grip-hover); - } - } - } - } - } - - .layers-and-nodes { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - .layer, - .node { - position: absolute; - display: flex; - left: calc(var(--offset-left) * 24px); - top: calc(var(--offset-top) * 24px); - // TODO: Reenable the `transition` property below after dealing with all edge cases where the wires need to be updated until the transition is complete - // transition: top 0.1s cubic-bezier(0, 0, 0.2, 1), left 0.1s cubic-bezier(0, 0, 0.2, 1); // Update `DRAG_SMOOTHING_TIME` in the JS above. - // TODO: Reenable the `backdrop-filter` property once a solution can be found for the black whole-page flickering problems it causes in Chrome. - // TODO: Additionally, find a solution for this having no effect in Firefox due to a browser bug caused when the two - // ancestor elements, `.graph` and `.panel`, each have the simultaneous pairing of `overflow: hidden` and `border-radius`. - // See: https://stackoverflow.com/questions/75137879/bug-with-backdrop-filter-in-firefox - // backdrop-filter: blur(4px); - background: rgba(var(--color-0-black-rgb), 0.33); - - .node-error { - position: absolute; - width: max-content; - white-space: pre-wrap; - max-width: 600px; - line-height: 18px; - color: var(--color-2-mildblack); - background: var(--color-error-red); - padding: 8px; - border-radius: 4px; - bottom: calc(100% + 12px); - z-index: -1; - transition: opacity 0.2s; - opacity: 0.5; - - // Tail - &::after { - content: ""; - position: absolute; - left: 6px; - bottom: -8px; - width: 0; - height: 0; - border-style: solid; - border-width: 8px 6px 0 6px; - border-color: var(--color-error-red) transparent transparent transparent; - } - - &.hover { - opacity: 0; - z-index: 1; - pointer-events: none; - } - - &.faded:hover + .hover { - opacity: 1; - } - - &.faded:hover { - z-index: 2; - opacity: 1; - -webkit-user-select: text; - user-select: text; - transition: - opacity 0.2s, - z-index 0s 0.2s; - - &::selection { - background-color: var(--color-e-nearwhite); - - // Target only Safari - @supports (background: -webkit-named-image(i)) { - & { - // Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering - // https://stackoverflow.com/a/71753552/775283 - background-color: rgba(var(--color-e-nearwhite-rgb), calc(254 / 255)); - } - } - } - } - } - - &::after { - content: ""; - position: absolute; - box-sizing: border-box; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - clip-path: var(--clip-path-id); - } - - .border-mask { - position: absolute; - top: 0; - } - - &.disabled { - background: rgba(var(--color-4-dimgray-rgb), 0.33); - color: var(--color-a-softgray); - - .icon-label { - fill: var(--color-a-softgray); - } - } - - &.previewed::after { - border: 1px dashed var(--data-color); - } - - .connectors { - position: absolute; - // Keeps the connectors above the wires - z-index: 1; - - margin-top: -24px; - - &.input { - left: -3px; - } - - &.output { - right: -5px; - } - } - - .connector { - // Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended - margin: calc(24px - 8px) 0; - width: 8px; - height: 8px; - } - - .text-label { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .layer { - border-radius: 8px; - --extra-width-to-reach-grid-multiple: 8px; - --node-chain-area-left-extension: 0; - // Keep this equation in sync with the equivalent one in the Svelte template `` above, as well as the `left` connector offset CSS rule above in `.connectors.input` above. - width: calc((var(--layer-area-width) - 0.5) * 24px); - padding-left: calc(var(--node-chain-area-left-extension) * 24px); - margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px); - - &::after { - border: 1px solid var(--color-5-dullgray); - border-radius: 8px; - } - - &.selected { - background: rgba(var(--color-5-dullgray-rgb), 0.33); - - &.in-selected-network { - background: rgba(var(--color-6-lowergray-rgb), 0.33); - } - } - - .thumbnail { - background: var(--color-2-mildblack); - border: 1px solid var(--data-color-dim); - border-radius: 2px; - position: relative; - box-sizing: border-box; - height: 48px; - // We shorten the width by 1px on the left and right so the inner thumbnail graphic maintains a perfect 3:2 aspect ratio - width: calc(72px - 2px); - margin: 0 1px; - - &::before { - content: ""; - background-image: var(--color-transparent-checkered-background); - background-size: var(--color-transparent-checkered-background-size); - background-position: var(--color-transparent-checkered-background-position); - background-repeat: var(--color-transparent-checkered-background-repeat); - } - - &::before, - svg:not(.connector) { - pointer-events: none; - position: absolute; - margin: auto; - top: 1px; - left: 1px; - width: calc(100% - 2px); - height: calc(100% - 2px); - } - - .connector { - position: absolute; - margin: 0 auto; - left: 0; - right: 0; - height: 12px; - - &.top { - top: -13px; - } - - &.bottom { - bottom: -13px; - } - } - } - - .details { - margin: 0 8px; - - .text-label { - white-space: nowrap; - line-height: 48px; - } - } - - .solo-drag-grip { - width: 8px; - height: 24px; - background-position: 2px 8px; - right: calc(-12px + 24px); - border-radius: 2px; - } - - .solo-drag-grip:hover, - &.selected .solo-drag-grip { - background-image: var(--icon-drag-grip); - - &:hover { - background-image: var(--icon-drag-grip-hover); - } - } - - .visibility { - position: absolute; - right: -12px; - } - - .input.connectors { - left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px); - } - - .solo-drag-grip, - .visibility, - .input.connectors, - .input.connectors .connector { - position: absolute; - margin: auto 0; - top: 0; - bottom: 0; - } - - .input.connectors .connector { - left: 24px; - } - } - - .node { - flex-direction: column; - border-radius: 2px; - width: 120px; - top: calc((var(--offset-top) + 0.5) * 24px); - - &::after { - border: 1px solid var(--data-color-dim); - border-radius: 2px; - } - - &.selected { - .primary { - background: rgba(var(--color-f-white-rgb), 0.15); - - &.in-selected-network { - background: rgba(var(--color-f-white-rgb), 0.2); - } - } - - .secondary { - background: rgba(var(--color-f-white-rgb), 0.1); - - &.in-selected-network { - background: rgba(var(--color-f-white-rgb), 0.15); - } - } - } - - .connector { - &:first-of-type { - margin-top: calc((24px - 8px) / 2); - - &:not(.primary-connector) { - margin-top: calc((24px - 8px) / 2 + 24px); - } - } - - &:last-of-type { - margin-bottom: calc((24px - 8px) / 2); - } - } - - .primary { - display: flex; - align-items: center; - position: relative; - width: 100%; - height: 24px; - border-radius: 2px 2px 0 0; - background: rgba(var(--color-f-white-rgb), 0.05); - - &.no-secondary-section { - border-radius: 2px; - } - - .icon-label { - display: none; // Remove after we have unique icons for the nodes - margin: 0 8px; - } - - .text-label { - // margin-right: 4px; // Restore after reenabling icon-label - margin: 0 8px; - } - } - - .secondary { - display: flex; - flex-direction: column; - width: 100%; - position: relative; - - .secondary-row { - position: relative; - display: flex; - align-items: center; - margin: 0 8px; - width: calc(100% - 8px - 8px); - height: 24px; - - &:last-of-type { - border-radius: 0 0 2px 2px; - } - - .text-label { - width: 100%; - } - - &.output { - flex-direction: row-reverse; - text-align: right; - - svg { - width: 30px; - height: 20px; - } - } - } - - &::before { - left: 0; - } - - &::after { - right: 0; - } - } - } - } - - .wire { - position: absolute; - overflow: visible; - top: 0; - left: 0; - path { - fill: none; - stroke: var(--data-color-dim); - stroke-width: var(--data-line-width); - stroke-dasharray: var(--data-dasharray); - } - } - - .graph { - position: relative; - overflow: hidden; - display: flex; - flex-direction: row; - flex-grow: 1; - > img { - position: absolute; - bottom: 0; - } - - .breadcrumb-trail-buttons { - margin-top: 8px; - margin-left: 8px; - } - - .context-menu { - width: max-content; - position: absolute; - box-sizing: border-box; - padding: 5px; - z-index: 3; - background-color: var(--color-3-darkgray); - border-radius: 4px; - - .toggle-layer-or-node .text-label { - line-height: 24px; - margin-right: 8px; - } - - .merge-selected-nodes { - justify-content: center; - } - } - - .click-targets { - position: absolute; - pointer-events: none; - width: 100%; - height: 100%; - z-index: 10; - - svg { - overflow: visible; - width: 100%; - height: 100%; - stroke-width: 1; - fill: none; - - .layer { - stroke: yellow; - } - - .node { - stroke: blue; - } - - .connector { - stroke: green; - } - - .visibility { - stroke: red; - } - - .all-nodes-bounding-box { - stroke: purple; - } - - .modify-import-export { - stroke: orange; - } - } - } - - .imports-and-exports { - width: 100%; - height: 100%; - position: absolute; - // Keeps the connectors above the wires - z-index: 1; - - .connector { - position: absolute; - width: 8px; - height: 8px; - margin-top: 4px; - margin-left: 5px; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - } - - .reorder-bar { - position: absolute; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - width: 50px; - height: 2px; - background: white; - } - - .plus { - position: absolute; - top: calc(var(--offset-top) * 24px); - left: calc(var(--offset-left) * 24px); - } - - .edit-import-export { - position: absolute; - display: flex; - align-items: center; - top: calc(var(--offset-top) * 24px); - margin-top: -5px; - height: 24px; - - &.separator-bottom::after, - &.separator-top::before { - content: ""; - position: absolute; - background: var(--color-8-uppergray); - height: 1px; - left: -4px; - right: -4px; - } - - &.separator-bottom::after { - bottom: -1px; - } - - &.separator-top::before { - top: 0; - } - - &.import { - right: calc(100% - var(--offset-left) * 24px); - } - - &.export { - left: calc(var(--offset-left) * 24px + 17px); - } - - .import-text { - text-align: right; - text-wrap: nowrap; - } - - .export-text { - text-wrap: nowrap; - } - - .import-text-input { - text-align: right; - } - - .remove-button-import { - margin-left: 3px; - } - - .remove-button-export { - margin-right: 3px; - } - - .reorder-drag-grip { - width: 8px; - height: 24px; - background-position: 2px 8px; - border-radius: 2px; - margin: -6px 0; - background-image: var(--icon-drag-grip-hover); - } - } - } - } - .box-selection { position: absolute; pointer-events: none; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 656cb9863c..b4d76fd870 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -128,9 +128,14 @@ export class UpdateNodeThumbnail extends JsMessage { readonly value!: string; } +export class UpdateTooltip extends JsMessage { + readonly position!: XY | undefined; + readonly text!: string; +} + export class UpdateOpenDocumentsList extends JsMessage { - @Type(() => OpenDocument) - readonly openDocuments!: OpenDocument[]; + @Type(() => FrontendDocumentDetails) + readonly openDocuments!: FrontendDocumentDetails[]; } export class WirePathInProgress { @@ -143,54 +148,29 @@ export class UpdateWirePathInProgress extends JsMessage { readonly wirePathInProgress!: WirePathInProgress | undefined; } -export class OpenDocument { - readonly id!: bigint; - @Type(() => DocumentDetails) - readonly details!: DocumentDetails; - - get displayName(): string { - return this.details.displayName; - } -} - -export class DocumentDetails { +// Allows the auto save system to use a string for the id rather than a BigInt. +// IndexedDb does not allow for BigInts as primary keys. +// TypeScript does not allow subclasses to change the type of class variables in subclasses. +// It is an abstract class to point out that it should not be instantiated directly. +export abstract class DocumentDetails { readonly name!: string; readonly isAutoSaved!: boolean; readonly isSaved!: boolean; + // This field must be provided by the subclass implementation + // readonly id!: bigint | string; + get displayName(): string { return `${this.name}${this.isSaved ? "" : "*"}`; } } -<<<<<<< HEAD -export class Box { - readonly startX!: number; - - readonly startY!: number; - - readonly endX!: number; - - readonly endY!: number; +export class FrontendDocumentDetails extends DocumentDetails { + readonly id!: bigint; } -export type FrontendClickTargets = { - readonly nodeClickTargets: string[]; - readonly layerClickTargets: string[]; - readonly connectorClickTargets: string[]; - readonly iconClickTargets: string[]; - readonly allNodesBoundingBox: string; - readonly importExportsBoundingBox: string; - readonly modifyImportExport: string[]; -}; - -export type ContextMenuInformation = { - contextMenuCoordinates: XY; - contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; -}; - ======= export class FrontendDocumentDetails extends DocumentDetails { readonly id!: bigint; @@ -342,20 +322,21 @@ export class WireUpdate { readonly wirePathUpdate!: WirePath | undefined; } -export class TriggerPersistenceWriteDocument extends JsMessage { - // Use a string since IndexedDB can not use BigInts for keys +export class IndexedDbDocumentDetails extends DocumentDetails { @Transform(({ value }: { value: bigint }) => value.toString()) - documentId!: string; + id!: string; +} +export class TriggerIndexedDbWriteDocument extends JsMessage { document!: string; - @Type(() => DocumentDetails) - details!: DocumentDetails; + @Type(() => IndexedDbDocumentDetails) + details!: IndexedDbDocumentDetails; version!: string; } -export class TriggerPersistenceRemoveDocument extends JsMessage { +export class TriggerIndexedDbRemoveDocument extends JsMessage { // Use a string since IndexedDB can not use BigInts for keys @Transform(({ value }: { value: bigint }) => value.toString()) documentId!: string; @@ -1488,6 +1469,7 @@ export class WidgetDiffUpdate extends JsMessage { layoutTarget!: unknown; // TODO: Replace `any` with correct typing + @Transform(({ value }: { value: any }) => createWidgetDiff(value)) diff!: WidgetDiff[]; } @@ -1522,6 +1504,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg return targetLayout; } // This is a path traversal so we can assume from the backend that it exists + if (targetLayout && "action" in targetLayout) return targetLayout.children![index]; return targetLayout?.[index]; @@ -1542,6 +1525,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg diffObject.length = 0; } // Remove all of the keys from the old object + Object.keys(diffObject).forEach((key) => delete (diffObject as any)[key]); // Assign keys to the new object @@ -1574,6 +1558,7 @@ export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSect } // Unpacking rust types to more usable type in the frontend + function createWidgetDiff(diffs: any[]): WidgetDiff[] { return diffs.map((diff) => { const { widgetPath, newValue } = diff; @@ -1592,6 +1577,7 @@ function createWidgetDiff(diffs: any[]): WidgetDiff[] { } // Unpacking a layout group + function createLayoutGroup(layoutGroup: any): LayoutGroup { if (layoutGroup.column) { const columnWidgets = hoistWidgetHolders(layoutGroup.column.columnWidgets); @@ -1649,6 +1635,7 @@ export class UpdateMenuBarLayout extends JsMessage { layoutTarget!: unknown; // TODO: Replace `any` with correct typing + @Transform(({ value }: { value: any }) => createMenuLayout(value)) layout!: MenuBarEntry[]; } @@ -1671,6 +1658,7 @@ function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] { children: createMenuLayoutRecursive(entry.children), })); } + function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] { return children.map((groups) => groups.map((entry) => ({ @@ -1683,6 +1671,7 @@ function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] { } // `any` is used since the type of the object should be known from the Rust side + type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage; type MessageMaker = typeof JsMessage | JSMessageFactory; @@ -1702,8 +1691,8 @@ export const messageMakers: Record = { TriggerFetchAndOpenDocument, TriggerFontLoad, TriggerImport, - TriggerPersistenceRemoveDocument, - TriggerPersistenceWriteDocument, + TriggerIndexedDbRemoveDocument, + TriggerIndexedDbWriteDocument, TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, TriggerLoadRestAutoSaveDocuments, @@ -1754,6 +1743,7 @@ export const messageMakers: Record = { UpdateLayersPanelState, UpdateToolOptionsLayout, UpdateToolShelfLayout, + UpdateTooltip, UpdateViewportHolePunch, UpdateWirePathInProgress, UpdateWorkingColorsLayout, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 30e18ba831..761d87f97a 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -5,8 +5,11 @@ // on the dispatcher messaging system and more complex Rust data types. // use crate::helpers::translate_key; -use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; +#[cfg(not(feature = "native"))] +use crate::wasm_node_graph_ui_executor::WasmNodeGraphUIExecutor; +use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER, WASM_NODE_GRAPH_EXECUTOR}; use editor::consts::FILE_EXTENSION; +use editor::dispatcher::EditorOutput; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -134,80 +137,9 @@ pub struct EditorHandle { frontend_message_handler_callback: js_sys::Function, } -// Defined separately from the `impl` block below since this `impl` block lacks the `#[wasm_bindgen]` attribute. -// Quirks in wasm-bindgen prevent functions in `#[wasm_bindgen]` `impl` blocks from being made publicly accessible from Rust. impl EditorHandle { - pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) { - self.send_frontend_message_to_js(message); - } -} - -#[wasm_bindgen] -impl EditorHandle { - #[cfg(not(feature = "native"))] - #[wasm_bindgen(constructor)] - pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self { - let editor = Editor::new(); - let editor_handle = EditorHandle { frontend_message_handler_callback }; - if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { - log::error!("Attempted to initialize the editor more than once"); - } - if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { - log::error!("Attempted to initialize the editor handle more than once"); - } - editor_handle - } - - #[cfg(feature = "native")] - #[wasm_bindgen(constructor)] - pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self { - let editor_handle = EditorHandle { frontend_message_handler_callback }; - if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { - log::error!("Attempted to initialize the editor handle more than once"); - } - editor_handle - } - - // Sends a message to the dispatcher in the Editor Backend - #[cfg(not(feature = "native"))] - fn dispatch>(&self, message: T) { - // Process no further messages after a crash to avoid spamming the console - - use crate::MESSAGE_BUFFER; - if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { - return; - } - - // Get the editor, dispatch the message, and store the `FrontendMessage` queue response - let frontend_messages = EDITOR.with(|editor| { - let mut guard = editor.try_lock(); - let Ok(Some(editor)) = guard.as_deref_mut() else { - // Enqueue messages which can't be procssed currently - MESSAGE_BUFFER.with_borrow_mut(|buffer| buffer.push(message.into())); - return vec![]; - }; - - editor.handle_message(message) - }); - - // Send each `FrontendMessage` to the JavaScript frontend - for message in frontend_messages.into_iter() { - self.send_frontend_message_to_js(message); - } - } - - #[cfg(feature = "native")] - fn dispatch>(&self, message: T) { - let message: Message = message.into(); - let Ok(serialized_message) = ron::to_string(&message) else { - log::error!("Failed to serialize message"); - return; - }; - crate::native_communcation::send_message_to_cef(serialized_message) - } - // Sends a FrontendMessage to JavaScript - fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { + pub fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { let new_hash = calculate_hash(image_data); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); @@ -234,6 +166,90 @@ impl EditorHandle { error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,) } } +} + +#[wasm_bindgen] +impl EditorHandle { + #[cfg(not(feature = "native"))] + #[wasm_bindgen(constructor)] + pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self { + let editor = Editor::new(); + let editor_handle = EditorHandle { frontend_message_handler_callback }; + let node_graph_executor = WasmNodeGraphUIExecutor::new(); + if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { + log::error!("Attempted to initialize the editor more than once"); + } + if WASM_NODE_GRAPH_EXECUTOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(node_graph_executor))).is_none() { + log::error!("Attempted to initialize the editor more than once"); + } + if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { + log::error!("Attempted to initialize the editor handle more than once"); + } + editor_handle + } + + #[cfg(feature = "native")] + #[wasm_bindgen(constructor)] + pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self { + let editor_handle = EditorHandle { frontend_message_handler_callback }; + if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { + log::error!("Attempted to initialize the editor handle more than once"); + } + editor_handle + } + + // Sends a message to the dispatcher in the Editor Backend + #[cfg(not(feature = "native"))] + fn dispatch>(&self, message: T) { + // Process no further messages after a crash to avoid spamming the console + if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { + return; + } + let _ = editor(|editor| { + self.process_messages(std::iter::once(message.into()), editor); + }); + } + + #[cfg(feature = "native")] + fn dispatch>(&self, message: T) { + let message: Message = message.into(); + let Ok(serialized_message) = ron::to_string(&message) else { + log::error!("Failed to serialize message"); + return; + }; + crate::native_communcation::send_message_to_cef(serialized_message) + } + + // Messages can come from the runtime, browser, or a timed callback. This processes them in the editor and does all the side effects + // Like updating the frontend and node graph ui network. Some side effects are deduplicated and produce other side effects. + fn process_messages(&self, messages: impl IntoIterator, editor_param: &mut Editor) { + // Get the editor, dispatch the message, and store the `FrontendMessage` queue response + for output in messages.into_iter().flat_map(|message| editor_param.handle_message(message)).collect::>() { + match output { + EditorOutput::RequestNativeNodeGraphRender { compilation_request } => { + let res = executor(|executor| executor.compilation_request(compilation_request)); + if let Err(_) = res { + log::error!("Could not borrow executor in process_messages_in_editor"); + } + } + EditorOutput::RequestDeferredMessage { message, timeout } => { + let callback = Closure::once_into_js(move || { + editor_and_handle(|editor, handle| { + handle.process_messages(std::iter::once(*message), editor); + }); + }); + + window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(callback.as_ref().unchecked_ref(), timeout.as_millis() as i32) + .unwrap(); + } + EditorOutput::FrontendMessage { frontend_message } => { + self.send_frontend_message_to_js(frontend_message); + } + } + } + } // ======================================================================== // Add additional JS -> Rust wrapper functions below as needed for calling @@ -264,6 +280,20 @@ impl EditorHandle { #[cfg(not(feature = "native"))] wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); + // Poll the UI node graph + #[cfg(not(feature = "native"))] + let result = editor(|editor| { + let node_graph_response = executor(|executor| executor.poll_node_graph_ui_evaluation(editor)); + + match node_graph_response { + Ok(node_graph_ui_messages) => handle(|handle| handle.process_messages(node_graph_ui_messages, editor)), + Err(_) => log::error!("Could not get executor in frame loop"), + } + }); + + if let Err(_) = result { + log::error!("Could not get editor in frame loop"); + } if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { handle(|handle| { // Process all messages that have been queued up @@ -277,7 +307,7 @@ impl EditorHandle { timestamp: js_sys::Date::now() as u64, }); handle.dispatch(AnimationMessage::IncrementFrameCounter); - + handle.dispatch(DeferMessage::CheckDeferredMessages); // Used by auto-panning, but this could possibly be refactored in the future, see: // handle.dispatch(BroadcastMessage::TriggerEvent(EventMessage::AnimationFrame)); @@ -466,7 +496,6 @@ impl EditorHandle { document_is_saved, document_serialized_content, to_front, - select_after_open: false, }; self.dispatch(message); } @@ -971,39 +1000,49 @@ fn set_timeout(f: &Closure, delay: Duration) { /// Provides access to the `Editor` by calling the given closure with it as an argument. #[cfg(not(feature = "native"))] -fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T { +fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> Result { EDITOR.with(|editor| { let mut guard = editor.try_lock(); let Ok(Some(editor)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor"); - return T::default(); + return Err(()); + }; + Ok(callback(editor)) + }) +} + +#[cfg(not(feature = "native"))] +fn executor(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> Result { + WASM_NODE_GRAPH_EXECUTOR.with(|executor| { + let mut guard = executor.try_lock(); + let Ok(Some(executor)) = guard.as_deref_mut() else { + return Err(()); }; - callback(editor) + Ok(callback(executor)) + }) +} + +/// Provides access to the `EditorHandle` by calling the given closure with them as arguments. +pub(crate) fn handle(callback: impl FnOnce(&mut EditorHandle)) { + EDITOR_HANDLE.with(|editor_handle| { + let mut guard = editor_handle.try_lock(); + let Ok(Some(editor_handle)) = guard.as_deref_mut() else { + return log::error!("Failed to borrow handle"); + }; + + // Call the closure with the editor and its handle + callback(editor_handle) }) } /// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments. #[cfg(not(feature = "native"))] pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) { - handle(|editor_handle| { - editor(|editor| { + let _ = handle(|editor_handle| { + let _ = editor(|editor| { // Call the closure with the editor and its handle callback(editor, editor_handle); - }) - }); -} -/// Provides access to the `EditorHandle` by calling the given closure with them as arguments. -pub(crate) fn handle(callback: impl FnOnce(&mut EditorHandle)) { - EDITOR_HANDLE.with(|editor_handle| { - let mut guard = editor_handle.try_lock(); - let Ok(Some(editor_handle)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor handle"); - return; - }; - - // Call the closure with the editor and its handle - callback(editor_handle); + }); }); } @@ -1032,15 +1071,11 @@ async fn poll_node_graph_evaluation() { crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst); } - // Send each `FrontendMessage` to the JavaScript frontend - for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) { - handle.send_frontend_message_to_js(response); - } + handle.process_messages(messages, editor); // If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches }); } - fn auto_save_all_documents() { // Process no further messages after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 1e25b8a5cb..b54e2e329a 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -16,7 +16,6 @@ use crate::{ vector::{ Vector, style::{Fill, Stroke}, - style::{Fill, Stroke, StrokeAlign}, }, }; diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index 8df7c0ace6..31fcd71d72 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -50,7 +50,6 @@ impl Hash for NodeGraphOverlayData { entries.sort_by(|a, b| a.0.cmp(b.0)); let mut hasher = std::collections::hash_map::DefaultHasher::new(); entries.hash(&mut hasher); - hasher.finish(); } } @@ -171,13 +170,7 @@ pub enum NodeOrLayer { pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, - #[serde(rename = "resolvedType")] - pub resolved_type: String, pub name: String, - pub description: String, - /// Either "nothing", "import index {index}", or "{node name} output {output_index}". - #[serde(rename = "connectedToString")] - pub connected_to: String, /// Used to render the upstream node once this node is rendered #[serde(rename = "connectedToNode")] pub connected_to_node: Option, @@ -188,17 +181,9 @@ pub struct FrontendGraphOutput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, pub name: String, - #[serde(rename = "resolvedType")] - pub resolved_type: String, - pub description: String, - /// If connected to an export, it is "export index {index}". - /// If connected to a node, it is "{node name} input {input_index}". - #[serde(rename = "connectedTo")] - pub connected_to: Vec, } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] - pub struct FrontendExport { pub port: FrontendGraphInput, pub wire: Option,