diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 1ad8cb6ff8..df3f276dd9 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -1,7 +1,7 @@ use super::transform_utils; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input}; use glam::{DAffine2, DVec2, IVec2}; @@ -58,72 +58,6 @@ impl<'a> ModifyInputsContext<'a> { Some(document) } - /// Starts at any folder, or the output, and skips layer nodes based on insert_index. Non layer nodes are always skipped. Returns the post node InputConnector and pre node OutputConnector - /// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram - /// -----> Post node - /// | if insert_index == 0, return (Post node, Some(Layer1)) - /// -> Layer1 - /// ↑ if insert_index == 1, return (Layer1, Some(Layer2)) - /// -> Layer2 - /// ↑ - /// -> NonLayerNode - /// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3)) - /// -> Layer3 - /// if insert_index == 3, return (Layer3, None) - pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector { - let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT { - InputConnector::Export(0) - } else { - InputConnector::node(parent.to_node(), 1) - }; - // Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack. - let mut current_index = 0; - - // Set the post node to the layer node at insert_index - loop { - if current_index == insert_index { - break; - } - let next_node_in_stack_id = network_interface - .input_from_connector(&post_node_input_connector, &[]) - .and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None }); - - if let Some(next_node_in_stack_id) = next_node_in_stack_id { - // Only increment index for layer nodes - if network_interface.is_layer(next_node_in_stack_id, &[]) { - current_index += 1; - } - // Input as a sibling to the Layer node above - post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0); - } else { - log::error!("Error getting post node: insert_index out of bounds"); - break; - }; - } - - let layer_input_connector = post_node_input_connector; - - // Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer - loop { - let pre_node_output_connector = network_interface.upstream_output_connector(&post_node_input_connector, &[]); - - match pre_node_output_connector { - Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !network_interface.is_layer(&pre_node_id, &[]) => { - // Update post_node_input_connector for the next iteration - post_node_input_connector = InputConnector::node(pre_node_id, 0); - // Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input - let primary_is_exposed = network_interface.input_from_connector(&post_node_input_connector, &[]).is_some_and(|input| input.is_exposed()); - if !primary_is_exposed { - return layer_input_connector; - } - } - _ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer - } - } - - post_node_input_connector - } - /// Creates a new layer and adds it to the document network. network_interface.move_layer_to_stack should be called after pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier { let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template(); 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 430787802e..970dc4faa5 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 @@ -530,19 +530,8 @@ impl<'a> MessageHandler> for NodeG network_interface.set_input(&encapsulating_connector, input, network_path); - let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else { - log::error!("Could not get outward wires in remove_import"); - return; - }; - let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else { - log::error!("Could not get outward wires for import in remove_import"); - return; - }; - - // Disconnect all connections in the encapsulating network - for downstream_connection in &downstream_connections { - network_interface.disconnect_input(downstream_connection, breadcrumb_network_path); - } + // Disconnect all connections fed by the hidden import in the nested network + network_interface.disconnect_import_wires(0, breadcrumb_network_path); responses.add(NodeGraphMessage::UpdateImportsExports); responses.add(NodeGraphMessage::SendWires); @@ -565,18 +554,7 @@ impl<'a> MessageHandler> for NodeG // Disconnect all connections in the encapsulating network if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() { - let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else { - log::error!("Could not get outward wires in remove_import"); - return; - }; - let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else { - log::error!("Could not get outward wires for import in remove_import"); - return; - }; - - for downstream_connection in &downstream_connections { - network_interface.disconnect_input(downstream_connection, encapsulating_path); - } + network_interface.disconnect_output_wires(&OutputConnector::node(*encapsulating_node, 0), encapsulating_path); } responses.add(NodeGraphMessage::UpdateImportsExports); @@ -1352,7 +1330,7 @@ impl<'a> MessageHandler> for NodeG self.shift_without_push = false; // Reset all offsets to end the rubber banding while dragging - network_interface.unload_stack_dependents_y_offset(selection_network_path); + network_interface.clear_drag_offsets(selection_network_path); let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in PointerUp"); return; @@ -1812,7 +1790,7 @@ impl<'a> MessageHandler> for NodeG network_interface.shift_selected_nodes(direction, self.shift_without_push, selection_network_path); if !rubber_band { - network_interface.unload_stack_dependents_y_offset(selection_network_path); + network_interface.clear_drag_offsets(selection_network_path); } if graph_view_overlay_open { diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 57315f58d3..17ea05b22a 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -2,16 +2,14 @@ use super::utility_types::{DrawHandles, OverlayContext}; use crate::consts::HIDE_HANDLE_DISTANCE; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; -use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE; +pub use crate::messages::portfolio::document::utility_types::text_metrics::text_width; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler; use glam::{DAffine2, DVec2}; use graphene_std::subpath::{Bezier, BezierHandles}; -use graphene_std::text::{TextAlign, TextContext, TypesettingConfig}; use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::{PointId, SegmentId, Vector}; use std::collections::HashMap; -use std::sync::{LazyLock, Mutex}; #[cfg(target_family = "wasm")] use wasm_bindgen::JsCast; @@ -222,24 +220,6 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: & } } -pub static GLOBAL_TEXT_CONTEXT: LazyLock> = LazyLock::new(|| Mutex::new(TextContext::default())); - -pub fn text_width(text: &str, font_size: f64) -> f64 { - let typesetting = TypesettingConfig { - font_size, - line_height_ratio: 1.2, - letter_spacing: 0., - letter_tilt: 0., - max_width: None, - max_height: None, - align: TextAlign::AlignLeft, - }; - - let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context"); - let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false); - bounds.x -} - pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] { let hex = hex.trim().trim_start_matches('#'); if hex.len() != 6 && hex.len() != 8 { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 1cf6d0ac4a..1781aa9105 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -3,7 +3,7 @@ use crate::consts::{ COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, }; -use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8}; +use crate::messages::portfolio::document::overlays::utility_functions::hex_to_rgba_u8; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE; use crate::messages::prelude::Message; @@ -16,7 +16,7 @@ use graphene_std::ATTR_TRANSFORM; use graphene_std::list::List; use graphene_std::math::quad::Quad; use graphene_std::subpath::{self, Subpath}; -use graphene_std::text::{TextAlign, TypesettingConfig}; +use graphene_std::text::{TextAlign, TextContext, TypesettingConfig}; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::point_to_dvec2; use graphene_std::vector::{PointId, SegmentId, Vector}; @@ -1117,17 +1117,17 @@ impl OverlayContextInternal { align: TextAlign::AlignLeft, }; - // Get text dimensions directly from layout - let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context"); - let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false); + // Lay out the text once, taking its dimensions and vector paths from the same thread-local context pass + let (text_size, text_list) = TextContext::with_thread_local(|text_context| { + let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false); + let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false); + (text_size, text_list) + }); let text_width = text_size.x; let text_height = text_size.y; // Create a rect from the size (assuming text starts at origin) let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height); - // Convert text to vector paths for rendering - let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false); - // Calculate position based on pivot let mut position = DVec2::ZERO; match pivot[0] { diff --git a/editor/src/messages/portfolio/document/utility_types/mod.rs b/editor/src/messages/portfolio/document/utility_types/mod.rs index 1fcf5c60ca..a38ca2c545 100644 --- a/editor/src/messages/portfolio/document/utility_types/mod.rs +++ b/editor/src/messages/portfolio/document/utility_types/mod.rs @@ -3,5 +3,6 @@ pub mod error; pub mod misc; pub mod network_interface; pub mod nodes; +pub mod text_metrics; pub mod transformation; pub mod wires; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index e7d2a7bba3..a223222911 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -27,11 +27,10 @@ use crate::consts::{ EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, HALF_GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, STACK_VERTICAL_GAP, }; -use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type}; use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput}; -use crate::messages::portfolio::document::overlays::utility_functions::text_width; use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes; +use crate::messages::portfolio::document::utility_types::text_metrics::text_width; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_thick_wire_center_line, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs index eb85e21561..d8d606c541 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs @@ -113,8 +113,12 @@ impl NodeNetworkInterface { log::error!("Could not get selected nodes in load_stack_dependents"); return; }; + self.load_stack_dependents_for_nodes(selected_nodes.selected_nodes().cloned().collect(), network_path); + } - let mut selected_layers = selected_nodes.selected_nodes().filter(|node_id| self.is_layer(node_id, network_path)).cloned().collect::>(); + /// Builds the stack dependents as if `seed_nodes` were the selection, for shifts driven by a node set other than the selection. + pub(crate) fn load_stack_dependents_for_nodes(&self, seed_nodes: Vec, network_path: &[NodeId]) { + let mut selected_layers = seed_nodes.iter().filter(|node_id| self.is_layer(node_id, network_path)).copied().collect::>(); // Deselect all layers that are upstream of other selected layers let mut removed_layers = Vec::new(); @@ -222,7 +226,7 @@ impl NodeNetworkInterface { for sole_dependent in sole_dependents { if !owned_sole_dependents.contains(&sole_dependent) { - stack_dependents.insert(sole_dependent, LayerOwner::None(0)); + stack_dependents.insert(sole_dependent, LayerOwner::None); } } } @@ -241,22 +245,32 @@ impl NodeNetworkInterface { return; }; network_metadata.transient_metadata.stack_dependents.unload(); + + // Drag offsets are only meaningful relative to the stack dependents snapshot they were accumulated against, so they must not outlive it + network_metadata.transient_metadata.drag_offsets.borrow_mut().clear(); } - /// Resets all the offsets for nodes with no LayerOwner when the drag ends - pub fn unload_stack_dependents_y_offset(&mut self, network_path: &[NodeId]) { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network_metadata in unload_stack_dependents_y_offset"); + /// The vertical distance the node has been pushed from its resting position during the current drag. + pub(crate) fn drag_offset(&self, node_id: &NodeId, network_path: &[NodeId]) -> i32 { + self.network_metadata(network_path) + .map_or(0, |network_metadata| network_metadata.transient_metadata.drag_offsets.borrow().get(node_id).copied().unwrap_or(0)) + } + + pub(crate) fn add_drag_offset(&self, node_id: &NodeId, delta: i32, network_path: &[NodeId]) { + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in add_drag_offset"); return; }; + *network_metadata.transient_metadata.drag_offsets.borrow_mut().entry(*node_id).or_insert(0) += delta; + } - if let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut() { - for layer_owner in stack_dependents.values_mut() { - if let LayerOwner::None(offset) = layer_owner { - *offset = 0; - } - } - } + /// Discards all drag offsets when the drag ends. + pub fn clear_drag_offsets(&self, network_path: &[NodeId]) { + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in clear_drag_offsets"); + return; + }; + network_metadata.transient_metadata.drag_offsets.borrow_mut().clear(); } pub fn import_export_ports(&mut self, network_path: &[NodeId]) -> Option<&Ports> { @@ -1081,7 +1095,7 @@ impl NodeNetworkInterface { let name_left = node_top_left.x + NAME_LEFT_OFFSET; let icons_reserve = VISIBILITY_INSET_FROM_LAYER_RIGHT + icons_width + GRIP_WIDTH; let name_right_max = node_top_left.x + width as f64 - icons_reserve; - let text_w = crate::messages::portfolio::document::overlays::utility_functions::text_width(&display_name, FONT_SIZE); + let text_w = text_width(&display_name, FONT_SIZE); let name_right = (name_left + text_w).min(name_right_max); if name_right > name_left { // The 1-grid-tall name strip is centered vertically in the 2-grid-tall layer. diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs index 9f62742b7f..29c4775004 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs @@ -1,4 +1,5 @@ use super::{InputConnector, OutputConnector, Previewing, RootNode, TransactionStatus}; +use crate::messages::portfolio::document::node_graph::utility_types::Direction; use crate::test_utils::test_prelude::*; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; @@ -187,26 +188,13 @@ fn merge_definition() -> DefinitionIdentifier { DefinitionIdentifier::Network("Merge".to_string()) } -async fn create_node_at(editor: &mut EditorTestUtils, node_type: DefinitionIdentifier, x: i32, y: i32) -> NodeId { - let node_id = NodeId::new(); - editor - .handle_message(NodeGraphMessage::CreateNodeFromContextMenu { - node_id: Some(node_id), - node_type, - xy: Some((x, y)), - add_transaction: true, - }) - .await; - node_id -} - #[tokio::test] async fn layer_stacking_follows_wiring() { let mut editor = EditorTestUtils::create(); editor.new_document().await; - let upper = create_node_at(&mut editor, merge_definition(), 20, 10).await; - let lower = create_node_at(&mut editor, merge_definition(), 20, 16).await; + let upper = editor.create_node_by_name_at(merge_definition(), 20, 10).await; + let lower = editor.create_node_by_name_at(merge_definition(), 20, 16).await; let network_interface = &mut editor.active_document_mut().network_interface; network_interface.set_to_node_or_layer(&upper, &[], true); @@ -234,8 +222,8 @@ async fn chain_membership_follows_wiring() { let mut editor = EditorTestUtils::create(); editor.new_document().await; - let layer = create_node_at(&mut editor, merge_definition(), 20, 10).await; - let node = create_node_at(&mut editor, rectangle_definition(), 15, 10).await; + let layer = editor.create_node_by_name_at(merge_definition(), 20, 10).await; + let node = editor.create_node_by_name_at(rectangle_definition(), 15, 10).await; let network_interface = &mut editor.active_document_mut().network_interface; network_interface.set_to_node_or_layer(&layer, &[], true); @@ -265,8 +253,8 @@ async fn move_layer_to_stack_builds_the_layer_stack() { let artboard = NodeId::new(); editor.handle_message(new_artboard_message(artboard)).await; - let first = create_node_at(&mut editor, merge_definition(), 0, 30).await; - let second = create_node_at(&mut editor, merge_definition(), 0, 40).await; + let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await; + let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await; let network_interface = &mut editor.active_document_mut().network_interface; network_interface.set_to_node_or_layer(&first, &[], true); @@ -288,12 +276,60 @@ async fn move_layer_to_stack_builds_the_layer_stack() { assert_invariants(&editor, "after moving two layers into an artboard"); } +#[tokio::test] +async fn drag_offsets_do_not_outlive_programmatic_shifts() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + let artboard = NodeId::new(); + editor.handle_message(new_artboard_message(artboard)).await; + + let first = editor.create_node_by_name_at(merge_definition(), 0, 30).await; + let second = editor.create_node_by_name_at(merge_definition(), 0, 40).await; + let third = editor.create_node_by_name_at(merge_definition(), 0, 50).await; + + let network_interface = &mut editor.active_document_mut().network_interface; + network_interface.set_to_node_or_layer(&first, &[], true); + network_interface.set_to_node_or_layer(&second, &[], true); + network_interface.set_to_node_or_layer(&third, &[], true); + + let artboard_layer = LayerNodeIdentifier::new(artboard, network_interface); + network_interface.move_layer_to_stack(LayerNodeIdentifier::new(first, network_interface), artboard_layer, 0, &[]); + network_interface.move_layer_to_stack(LayerNodeIdentifier::new(second, network_interface), artboard_layer, 1, &[]); + network_interface.move_layer_to_stack(LayerNodeIdentifier::new(third, network_interface), artboard_layer, 2, &[]); + + // Making room while inserting into the stack pushes neighbors with drag-offset bookkeeping that must not outlive the operation + for node_id in [first, second, third] { + assert_eq!(network_interface.drag_offset(&node_id, &[]), 0, "Inserting into a stack should not leave a drag offset on {node_id}"); + } + + // Deleting the middle layer collapses the stack upward, which must also leave no offsets behind + network_interface.delete_nodes(vec![second], false, &[]); + assert_eq!(network_interface.drag_offset(&third, &[]), 0, "Collapsing the deleted layer's space should not leave a drag offset"); + + // A later nudge must move the survivor by exactly one unit, without replaying any restore toward its pre-collapse position + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![third] }).await; + let network_interface = &editor.active_document().network_interface; + let before = network_interface.position(&third, &[]).expect("Surviving layer should have a position"); + editor + .handle_message(NodeGraphMessage::ShiftSelectedNodes { + direction: Direction::Down, + rubber_band: false, + }) + .await; + let network_interface = &editor.active_document().network_interface; + let after = network_interface.position(&third, &[]).expect("Surviving layer should have a position"); + assert_eq!(after.y - before.y, 1, "A single nudge should move the layer exactly one unit"); + + assert_invariants(&editor, "after stack pushes, a delete collapse, and a nudge"); +} + #[tokio::test] async fn signature_edits_keep_parallel_metadata_in_sync() { let mut editor = EditorTestUtils::create(); editor.new_document().await; - let merge = create_node_at(&mut editor, merge_definition(), 0, 0).await; + let merge = editor.create_node_by_name_at(merge_definition(), 0, 0).await; let path = vec![merge]; let network_interface = &mut editor.active_document_mut().network_interface; @@ -331,7 +367,7 @@ async fn toggle_preview_transitions_with_a_connected_export() { let artboard = NodeId::new(); editor.handle_message(new_artboard_message(artboard)).await; - let node = create_node_at(&mut editor, rectangle_definition(), 0, 20).await; + let node = editor.create_node_by_name_at(rectangle_definition(), 0, 20).await; let network_interface = &mut editor.active_document_mut().network_interface; let export_node = |network_interface: &super::NodeNetworkInterface| network_interface.input_from_connector(&InputConnector::Export(0), &[]).and_then(|input| input.as_node()); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/layout.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/layout.rs index 53eed72eff..c7c9c655a2 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/layout.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/layout.rs @@ -297,13 +297,18 @@ impl NodeNetworkInterface { } pub fn shift_selected_nodes(&mut self, direction: Direction, shift_without_push: bool, network_path: &[NodeId]) { - let Some(mut node_ids) = self + let Some(node_ids) = self .selected_nodes_in_nested_network(network_path) .map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect::>()) else { log::error!("Could not get selected nodes in shift_selected_nodes"); return; }; + self.shift_nodes(node_ids, direction, shift_without_push, network_path); + } + + pub(crate) fn shift_nodes(&mut self, mut node_ids: HashSet, direction: Direction, shift_without_push: bool, network_path: &[NodeId]) { + let seed_nodes = node_ids.clone(); if !shift_without_push { // The owned nodes of each layer are populated by the stack dependents load, which otherwise may not run until after this filter self.try_load_stack_dependents(network_path); @@ -432,16 +437,9 @@ impl NodeNetworkInterface { shifted_nodes.insert(*node_id); self.shift_node(node_id, IVec2::new(0, shift_sign), network_path); - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network_metadata in export_ports"); - continue; - }; - if let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut() - && let Some(LayerOwner::None(offset)) = stack_dependents.get_mut(node_id) - { - *offset += shift_sign; - self.transaction_modified(); - }; + if self.with_stack_dependents(network_path, |stack_dependents| matches!(stack_dependents.get(node_id), Some(LayerOwner::None))) == Some(true) { + self.add_drag_offset(node_id, shift_sign, network_path); + } // Shift the upstream layer so that it stays in the same place if self.is_layer(node_id, network_path) { @@ -470,24 +468,24 @@ impl NodeNetworkInterface { let mut stack_dependents_with_position = stack_dependents .iter() .filter_map(|(node_id, owner)| { - let LayerOwner::None(offset) = owner else { + let LayerOwner::None = owner else { return None; }; - if *offset == 0 { + let offset = self.drag_offset(node_id, network_path); + if offset == 0 { return None; } - if self.selected_nodes_in_nested_network(network_path).is_some_and(|selected_nodes| { - selected_nodes - .selected_nodes() - .any(|selected_node| selected_node == node_id || self.with_owned_nodes(node_id, network_path, |owned_nodes| owned_nodes.contains(selected_node)) == Some(true)) - }) { + if seed_nodes + .iter() + .any(|seed_node| seed_node == node_id || self.with_owned_nodes(node_id, network_path, |owned_nodes| owned_nodes.contains(seed_node)) == Some(true)) + { return None; }; let Some(position) = self.position(node_id, network_path) else { log::error!("Could not get position for node {node_id} in shift_selected_nodes"); return None; }; - Some((*node_id, *offset, position.y)) + Some((*node_id, offset, position.y)) }) .collect::>(); @@ -533,29 +531,11 @@ impl NodeNetworkInterface { self.shift_node(node_id, IVec2::new(0, shift_sign), network_path); - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - log::error!("Could not get nested network_metadata in export_ports"); - return; - }; - let Some(stack_dependents) = network_metadata.transient_metadata.stack_dependents.get_loaded_mut() else { - log::error!("Stack dependents should be loaded in vertical_shift_with_push"); - return; - }; - - let mut default_layer_owner = LayerOwner::None(0); - let layer_owner = stack_dependents.get_mut(node_id).unwrap_or_else(|| { - log::error!("Could not get layer owner in vertical_shift_with_push for node {node_id}"); - &mut default_layer_owner - }); - - match layer_owner { - LayerOwner::None(offset) => { - *offset += shift_sign; - self.transaction_modified(); - } - LayerOwner::Layer(_) => { - log::error!("Node being shifted with a push should not be owned"); - } + match self.with_stack_dependents(network_path, |stack_dependents| stack_dependents.get(node_id).cloned()) { + Some(Some(LayerOwner::None)) => self.add_drag_offset(node_id, shift_sign, network_path), + Some(Some(LayerOwner::Layer(_))) => log::error!("Node being shifted with a push should not be owned"), + Some(None) => log::error!("Could not get layer owner in vertical_shift_with_push for node {node_id}"), + None => log::error!("Stack dependents should be loaded in vertical_shift_with_push"), } // Shift the upstream layer so that it stays in the same place @@ -652,7 +632,7 @@ impl NodeNetworkInterface { let layer_owner = *layer_owner; self.shift_node_or_parent(&layer_owner, shift_sign, shifted_nodes, network_path) } - LayerOwner::None(_) => self.vertical_shift_with_push(node_id, shift_sign, shifted_nodes, network_path), + LayerOwner::None => self.vertical_shift_with_push(node_id, shift_sign, shifted_nodes, network_path), } } @@ -723,7 +703,7 @@ impl NodeNetworkInterface { insert_index = 0; } - let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index); + let post_node = self.post_node_with_index(parent, insert_index, network_path); let Some(post_node_input) = self.input_from_connector(&post_node, network_path).cloned() else { log::error!("Could not get previous input in move_layer_to_stack_for_import"); return; @@ -811,7 +791,7 @@ impl NodeNetworkInterface { // Disconnect layer to move self.remove_references_from_network(&layer.to_node(), network_path); - let post_node = ModifyInputsContext::get_post_node_with_index(self, parent, insert_index); + let post_node = self.post_node_with_index(parent, insert_index, network_path); // Get the previous input to the post node before inserting the layer let Some(post_node_input) = self.input_from_connector(&post_node, network_path).cloned() else { @@ -872,12 +852,9 @@ impl NodeNetworkInterface { // If there is an upstream node in the new location for the layer, create space for the moved layer by shifting the upstream node down if let Some(upstream_node_id) = post_node_input.as_node() { - // Select the layer to move to ensure the shifting works correctly - let Some(selected_nodes) = self.selected_nodes_mut(network_path) else { - log::error!("Could not get selected nodes in move_layer_to_stack"); - return; - }; - let old_selected_nodes = selected_nodes.replace_with(vec![upstream_node_id]); + // Build the stack dependents from the upstream node rather than the selection so the shifting works correctly + self.unload_stack_dependents(network_path); + self.load_stack_dependents_for_nodes(vec![upstream_node_id], network_path); // Create the minimum amount space for the moved layer for _ in 0..STACK_VERTICAL_GAP { @@ -886,6 +863,7 @@ impl NodeNetworkInterface { let Some(stack_position) = self.position(&upstream_node_id, network_path) else { log::error!("Could not get stack position in move_layer_to_stack"); + self.unload_stack_dependents(network_path); return; }; @@ -896,7 +874,7 @@ impl NodeNetworkInterface { self.vertical_shift_with_push(&upstream_node_id, 1, &mut HashSet::new(), network_path); } - let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes); + self.unload_stack_dependents(network_path); } // If true, this node should be inserted before the post node (toward root from the layer), and all outward wires from the pre node should be moved to its output. diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs index 46458359f2..116f2903bc 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs @@ -226,6 +226,55 @@ impl NodeNetworkInterface { self.unload_modify_import_export(network_path); } + /// Disconnects every wire fed by the given import within the network. + pub(crate) fn disconnect_import_wires(&mut self, import_index: usize, network_path: &[NodeId]) { + let Some(wires_for_import) = self.with_outward_wires(network_path, |outward_wires| outward_wires.get(&OutputConnector::Import(import_index)).cloned()) else { + log::error!("Could not get outward wires in disconnect_import_wires"); + return; + }; + let Some(wires_for_import) = wires_for_import else { + log::error!("Could not get outward wires for import in disconnect_import_wires"); + return; + }; + for downstream_connection in wires_for_import { + self.disconnect_input(&downstream_connection, network_path); + } + } + + /// Disconnects every wire fed by the given output within the network. + pub(crate) fn disconnect_output_wires(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) { + let Some(downstream_connections) = self.with_outward_wires(network_path, |outward_wires| outward_wires.get(output_connector).cloned()) else { + log::error!("Could not get outward wires in disconnect_output_wires"); + return; + }; + let Some(downstream_connections) = downstream_connections else { + log::error!("Could not get downstream connections in disconnect_output_wires"); + return; + }; + for downstream_connection in downstream_connections { + self.disconnect_input(&downstream_connection, network_path); + } + } + + /// Refreshes the metadata invalidated when the encapsulating node's signature changes, demoting it from a layer if it is no longer eligible. + fn finish_signature_edit(&mut self, parent_id: NodeId, encapsulating_network_path: &[NodeId], network_path: &[NodeId]) { + // Update the metadata for the encapsulating node + self.unload_outward_wires(encapsulating_network_path); + self.unload_node_click_targets(&parent_id, encapsulating_network_path); + self.unload_all_nodes_bounding_box(encapsulating_network_path); + if !self.is_eligible_to_be_layer(&parent_id, encapsulating_network_path) && self.is_layer(&parent_id, encapsulating_network_path) { + self.set_to_node_or_layer(&parent_id, encapsulating_network_path, false); + } + if encapsulating_network_path.is_empty() { + self.load_structure(); + } + + // Unload the metadata for the nested network + self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); + } + // First disconnects the export, then removes it pub fn remove_export(&mut self, export_index: usize, network_path: &[NodeId]) { let mut encapsulating_network_path = network_path.to_vec(); @@ -271,21 +320,7 @@ impl NodeNetworkInterface { network_metadata.persistent_metadata.reference = None; } - // Update the metadata for the encapsulating node - self.unload_outward_wires(&encapsulating_network_path); - self.unload_node_click_targets(&parent_id, &encapsulating_network_path); - self.unload_all_nodes_bounding_box(&encapsulating_network_path); - if !self.is_eligible_to_be_layer(&parent_id, &encapsulating_network_path) && self.is_layer(&parent_id, &encapsulating_network_path) { - self.set_to_node_or_layer(&parent_id, &encapsulating_network_path, false); - } - if encapsulating_network_path.is_empty() { - self.load_structure(); - } - - // Unload the metadata for the nested network - self.unload_outward_wires(network_path); - self.unload_import_export_ports(network_path); - self.unload_modify_import_export(network_path); + self.finish_signature_edit(parent_id, &encapsulating_network_path, network_path); } // First disconnects the import, then removes it @@ -300,10 +335,6 @@ impl NodeNetworkInterface { log::error!("Could not get outward wires in remove_import"); return; }; - let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(import_index)).cloned() else { - log::error!("Could not get outward wires for import in remove_import"); - return; - }; let mut new_import_mapping = Vec::new(); for i in (import_index + 1)..number_of_inputs { let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(i)).cloned() else { @@ -316,9 +347,7 @@ impl NodeNetworkInterface { } // Disconnect all upstream connections - for outward_wire in outward_wires_for_import { - self.disconnect_input(&outward_wire, network_path); - } + self.disconnect_import_wires(import_index, network_path); // Shift inputs connected to to imports at a higher index down one for (output_connector, input_wire) in new_import_mapping { self.create_wire(&output_connector, &input_wire, network_path); @@ -347,21 +376,7 @@ impl NodeNetworkInterface { network_metadata.persistent_metadata.reference = None; } - // Update the metadata for the encapsulating node - self.unload_outward_wires(encapsulating_network_path); - self.unload_node_click_targets(parent_id, encapsulating_network_path); - self.unload_all_nodes_bounding_box(encapsulating_network_path); - if !self.is_eligible_to_be_layer(parent_id, encapsulating_network_path) && self.is_layer(parent_id, encapsulating_network_path) { - self.set_to_node_or_layer(parent_id, encapsulating_network_path, false); - } - if encapsulating_network_path.is_empty() { - self.load_structure(); - } - - // Unload the metadata for the nested network - self.unload_outward_wires(network_path); - self.unload_import_export_ports(network_path); - self.unload_modify_import_export(network_path); + self.finish_signature_edit(*parent_id, encapsulating_network_path, network_path); } /// The end index is before the export is removed, so moving to the end is the length of the current exports @@ -1252,24 +1267,20 @@ impl NodeNetworkInterface { }; let max_shift_distance = reconnected_node_position.y - disconnected_node_position.y; - let upstream_nodes = self.upstream_flow_back_from_nodes(vec![*reconnect_node], network_path, FlowType::PrimaryFlow).collect::>(); - - // Select the reconnect node to move to ensure the shifting works correctly - let Some(selected_nodes) = self.selected_nodes_mut(network_path) else { - log::error!("Could not get selected nodes in remove_references_from_network"); - return false; - }; + let upstream_nodes = self.upstream_flow_back_from_nodes(vec![*reconnect_node], network_path, FlowType::PrimaryFlow).collect::>(); - let old_selected_nodes = selected_nodes.replace_with(upstream_nodes); + // Build the stack dependents from the reconnected flow rather than the selection so the shifting works correctly + self.unload_stack_dependents(network_path); + self.load_stack_dependents_for_nodes(upstream_nodes.iter().copied().collect(), network_path); // Shift up until there is either a collision or the disconnected node position is reached let mut current_shift_distance = 0; while self.check_collision_with_stack_dependents(reconnect_node, -1, network_path).is_empty() && max_shift_distance > current_shift_distance { - self.shift_selected_nodes(Direction::Up, false, network_path); + self.shift_nodes(upstream_nodes.clone(), Direction::Up, false, network_path); current_shift_distance += 1; } - let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes); + self.unload_stack_dependents(network_path); } true diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs index 112b036312..0ad94edf99 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs @@ -1,5 +1,8 @@ use super::*; +/// Index of the Artboard definition's Clip input, which must match the input order authored in document_node_definitions.rs. +pub(crate) const ARTBOARD_CLIP_INPUT_INDEX: usize = 5; + impl NodeNetworkInterface { /// Runs a query against a resolved [`NetworkView`], logging any error at this message-boundary wrapper and mapping it to None. pub(crate) fn query<'a, 'p, T>(&'a self, network_path: &'p [NodeId], caller: &str, query: impl FnOnce(NetworkView<'a, 'p>) -> Result) -> Option { @@ -619,6 +622,61 @@ impl NodeNetworkInterface { true } + /// The input connector into which a layer should be inserted for the given parent and stack index. + pub(crate) fn post_node_with_index(&self, parent: LayerNodeIdentifier, insert_index: usize, network_path: &[NodeId]) -> InputConnector { + let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT { + InputConnector::Export(0) + } else { + InputConnector::node(parent.to_node(), 1) + }; + // Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack. + let mut current_index = 0; + + // Set the post node to the layer node at insert_index + loop { + if current_index == insert_index { + break; + } + let next_node_in_stack_id = self + .input_from_connector(&post_node_input_connector, network_path) + .and_then(|input_from_connector| if let NodeInput::Node { node_id, .. } = input_from_connector { Some(node_id) } else { None }); + + if let Some(next_node_in_stack_id) = next_node_in_stack_id { + // Only increment index for layer nodes + if self.is_layer(next_node_in_stack_id, network_path) { + current_index += 1; + } + // Input as a sibling to the Layer node above + post_node_input_connector = InputConnector::node(*next_node_in_stack_id, 0); + } else { + log::error!("Error getting post node: insert_index out of bounds"); + break; + }; + } + + let layer_input_connector = post_node_input_connector; + + // Sink post_node down to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at insert_index + 1, or None if insert_index is the last layer + loop { + let pre_node_output_connector = self.upstream_output_connector(&post_node_input_connector, network_path); + + match pre_node_output_connector { + Some(OutputConnector::Node { node_id: pre_node_id, .. }) if !self.is_layer(&pre_node_id, network_path) => { + // Update post_node_input_connector for the next iteration + post_node_input_connector = InputConnector::node(pre_node_id, 0); + // Insert directly under layer if moving to the end of a layer stack that ends with a non layer node that does not have an exposed primary input + let primary_is_exposed = self.input_from_connector(&post_node_input_connector, network_path).is_some_and(|input| input.is_exposed()); + if !primary_is_exposed { + return layer_input_connector; + } + } + _ => break, // Break if pre_node_output_connector is None or if pre_node_id is a layer + } + } + + post_node_input_connector + } + // All chain nodes and branches from the chain which are sole dependents of the layer pub fn upstream_nodes_below_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> HashSet { // Every upstream node below layer must be a sole dependent @@ -885,27 +943,30 @@ impl NodeNetworkInterface { /// Calculates the document bounds in document space pub fn document_bounds_document_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> { + self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document(layer)) + } + + fn combined_document_bounds(&self, include_artboards: bool, layer_bounds: impl Fn(&DocumentMetadata, LayerNodeIdentifier) -> Option<[DVec2; 2]>) -> Option<[DVec2; 2]> { self.document_metadata .all_layers() .filter(|layer| include_artboards || !self.is_artboard(&layer.to_node(), &[])) .filter_map(|layer| { + // A layer clipped by its artboard contributes only the intersection of the two bounds if !self.is_artboard(&layer.to_node(), &[]) && let Some(artboard_node_identifier) = layer .ancestors(self.document_metadata()) .find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[])) + && let Some(artboard) = self.document_node(&artboard_node_identifier.to_node(), &[]) + && let Some(clip_input) = artboard.inputs.get(ARTBOARD_CLIP_INPUT_INDEX) + && let NodeInput::Value { tagged_value, .. } = clip_input + && tagged_value.clone().deref() == &TaggedValue::Bool(true) { - let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]); - let clip_input = artboard.unwrap().inputs.get(5).unwrap(); - if let NodeInput::Value { tagged_value, .. } = clip_input - && tagged_value.clone().deref() == &TaggedValue::Bool(true) - { - return Some(Quad::clip( - self.document_metadata.bounding_box_document(layer).unwrap_or_default(), - self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(), - )); - } + return Some(Quad::clip( + layer_bounds(&self.document_metadata, layer).unwrap_or_default(), + self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(), + )); } - self.document_metadata.bounding_box_document(layer) + layer_bounds(&self.document_metadata, layer) }) // Skip any layer bounds containing NaN to avoid poisoning the combined result .filter(|[min, max]| min.is_finite() && max.is_finite()) @@ -922,29 +983,7 @@ impl NodeNetworkInterface { /// Calculates the document bounds in document space, expanding vector layer bounds to include the rendered /// stroke width. Used for export so the output canvas captures strokes that overflow the path geometry. pub fn document_bounds_document_space_with_stroke(&self, include_artboards: bool) -> Option<[DVec2; 2]> { - self.document_metadata - .all_layers() - .filter(|layer| include_artboards || !self.is_artboard(&layer.to_node(), &[])) - .filter_map(|layer| { - if !self.is_artboard(&layer.to_node(), &[]) - && let Some(artboard_node_identifier) = layer - .ancestors(self.document_metadata()) - .find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[])) - && let Some(artboard) = self.document_node(&artboard_node_identifier.to_node(), &[]) - && let Some(clip_input) = artboard.inputs.get(5) - && let NodeInput::Value { tagged_value, .. } = clip_input - && tagged_value.clone().deref() == &TaggedValue::Bool(true) - { - return Some(Quad::clip( - self.document_metadata.bounding_box_document_with_stroke(layer).unwrap_or_default(), - self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(), - )); - } - self.document_metadata.bounding_box_document_with_stroke(layer) - }) - // Skip any layer bounds containing NaN to avoid poisoning the combined result - .filter(|[min, max]| min.is_finite() && max.is_finite()) - .reduce(Quad::combine_bounds) + self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document_with_stroke(layer)) } /// Calculates the selected layer bounds in document space diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs index 9ae8ff3bdf..e162d98999 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs @@ -437,6 +437,8 @@ pub struct NodeNetworkTransientMetadata { /// Cached wire SVG paths per input connector, where an entry's presence means that wire is loaded. pub(crate) wires: std::cell::RefCell>, + /// Drag-session state: how far each unowned stack dependent has been pushed vertically from its resting position. Cleared when the drag ends. + pub(crate) drag_offsets: std::cell::RefCell>, } #[derive(Debug, Clone)] @@ -459,8 +461,7 @@ pub struct NetworkEdgeDistance { pub enum LayerOwner { // Used to get the layer that should be shifted when there is a collision. Layer(NodeId), - // The vertical offset of a node from the start of its shift. Should be reset when the drag ends. - None(i32), + None, } #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] diff --git a/editor/src/messages/portfolio/document/utility_types/text_metrics.rs b/editor/src/messages/portfolio/document/utility_types/text_metrics.rs new file mode 100644 index 0000000000..8449988edb --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/text_metrics.rs @@ -0,0 +1,16 @@ +use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE; +use graphene_std::text::{TextAlign, TextContext, TypesettingConfig}; + +pub fn text_width(text: &str, font_size: f64) -> f64 { + let typesetting = TypesettingConfig { + font_size, + line_height_ratio: 1.2, + letter_spacing: 0., + letter_tilt: 0., + max_width: None, + max_height: None, + align: TextAlign::AlignLeft, + }; + + TextContext::with_thread_local(|text_context| text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false).x) +} diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 12bcfe45bd..946276c0e0 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -324,11 +324,19 @@ impl EditorTestUtils { } pub async fn create_node_by_name(&mut self, node_type: DefinitionIdentifier) -> NodeId { + self.create_node(node_type, None).await + } + + pub async fn create_node_by_name_at(&mut self, node_type: DefinitionIdentifier, x: i32, y: i32) -> NodeId { + self.create_node(node_type, Some((x, y))).await + } + + async fn create_node(&mut self, node_type: DefinitionIdentifier, xy: Option<(i32, i32)>) -> NodeId { let node_id = NodeId::new(); self.handle_message(NodeGraphMessage::CreateNodeFromContextMenu { node_id: Some(node_id), node_type, - xy: None, + xy, add_transaction: true, }) .await;