mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 23:38:06 +08:00
WIP: Defer message
This commit is contained in:
@@ -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.;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Message> },
|
||||
TriggerNavigationReady,
|
||||
AfterNavigationReady { messages: Vec<Message> },
|
||||
RequestDeferredMessage { timeout: Duration, message: Box<Message> },
|
||||
CheckDeferredMessages,
|
||||
}
|
||||
|
||||
@@ -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<DocumentId, Vec<(u64, Message)>>,
|
||||
after_viewport_resize: Vec<Message>,
|
||||
current_graph_submission_id: u64,
|
||||
after_time_elapsed: BTreeMap<Instant, Message>,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
@@ -49,6 +57,15 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -305,6 +305,10 @@ pub enum FrontendMessage {
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateTooltip {
|
||||
position: Option<FrontendXY>,
|
||||
text: String,
|
||||
},
|
||||
UpdateWirePathInProgress {
|
||||
#[serde(rename = "wirePathInProgress")]
|
||||
wire_path_in_progress: Option<WirePathInProgress>,
|
||||
|
||||
@@ -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<InputPreprocessorMessage, InputPreprocessorMessageContext> 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 } => {
|
||||
|
||||
@@ -215,6 +215,7 @@ pub enum NodeGraphMessage {
|
||||
SetLockedOrVisibilitySideEffects {
|
||||
node_ids: Vec<NodeId>,
|
||||
},
|
||||
TryDisplayTooltip,
|
||||
UpdateBoxSelection,
|
||||
UpdateImportsExports,
|
||||
UpdateLayerPanel,
|
||||
|
||||
@@ -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<usize>,
|
||||
// The rendered string for each thumbnail
|
||||
pub thumbnails: HashMap<NodeId, Graphic>,
|
||||
// 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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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);
|
||||
}
|
||||
|
||||
@@ -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<FrontendGraphOutput> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ struct PathToolData {
|
||||
saved_selection_before_handle_drag: HashMap<LayerNodeIdentifier, (HashSet<ManipulatorPointId>, HashSet<SegmentId>)>,
|
||||
handle_drag_toggle: bool,
|
||||
saved_points_before_anchor_convert_smooth_sharp: HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>,
|
||||
last_click_time: u64,
|
||||
last_click_time: Instant,
|
||||
dragging_state: DraggingState,
|
||||
angle: f64,
|
||||
pivot_gizmo: PivotGizmo,
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<title>{outputTooltip(frontendOutput)}</title>
|
||||
{#if frontendOutput.connectedTo.length > 0}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
@@ -407,7 +379,6 @@
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
|
||||
>
|
||||
<title>{inputTooltip(frontendInput)}</title>
|
||||
{#if frontendInput.connectedTo !== "nothing"}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
@@ -511,6 +482,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input/Output tooltip widget -->
|
||||
{#if $nodeGraph.tooltipPosition}
|
||||
<div class="connector-tooltip">
|
||||
<div class="connector-tooltip-div" style:--offset-left={`${$nodeGraph.tooltipPosition.x + 10}px`} style:--offset-top={`${$nodeGraph.tooltipPosition.y + 10}px`}>
|
||||
{$nodeGraph.tooltipText}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Box selection widget -->
|
||||
{#if $nodeGraph.selectionBox}
|
||||
<div
|
||||
@@ -523,33 +503,6 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss" global>
|
||||
.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 `<clipPath><path d="layerBorderMask(...)" /></clipPath>` 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;
|
||||
|
||||
@@ -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<string, MessageMaker> = {
|
||||
TriggerFetchAndOpenDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerImport,
|
||||
TriggerPersistenceRemoveDocument,
|
||||
TriggerPersistenceWriteDocument,
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerLoadFirstAutoSaveDocument,
|
||||
TriggerLoadPreferences,
|
||||
TriggerLoadRestAutoSaveDocuments,
|
||||
@@ -1754,6 +1743,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
UpdateLayersPanelState,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateTooltip,
|
||||
UpdateViewportHolePunch,
|
||||
UpdateWirePathInProgress,
|
||||
UpdateWorkingColorsLayout,
|
||||
|
||||
@@ -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<T: Into<Message>>(&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<T: Into<Message>>(&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<T: Into<Message>>(&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<T: Into<Message>>(&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<Item = Message>, 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::<Vec<_>>() {
|
||||
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:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/2562#discussion_r2041102786>
|
||||
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<dyn FnMut()>, delay: Duration) {
|
||||
|
||||
/// Provides access to the `Editor` by calling the given closure with it as an argument.
|
||||
#[cfg(not(feature = "native"))]
|
||||
fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
|
||||
fn editor<T>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> Result<T, ()> {
|
||||
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<T>(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> Result<T, ()> {
|
||||
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) {
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::{
|
||||
vector::{
|
||||
Vector,
|
||||
style::{Fill, Stroke},
|
||||
style::{Fill, Stroke, StrokeAlign},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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<NodeId>,
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
||||
pub struct FrontendExport {
|
||||
pub port: FrontendGraphInput,
|
||||
pub wire: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user