Skip to main content

azul_layout/widgets/
node_graph.rs

1//! Interactive node graph editor widget.
2//!
3//! Provides the [`NodeGraph`] widget for building visual node-based editors
4//! (e.g. shader graphs, data-flow pipelines). Key types:
5//!
6//! - [`NodeGraph`] — top-level widget holding nodes, types, and callbacks
7//! - [`Node`] — a single node with typed input/output connections and editable fields
8//! - [`NodeTypeInfo`] / [`InputOutputInfo`] — metadata describing node types and their I/O ports
9//! - [`NodeGraphCallbacks`] — user-provided callbacks for add, remove, drag, connect, etc.
10//!
11//! **Known limitation:** Connection curves between nodes are currently not rendered
12//! (`draw_connection` returns a null image pending `RenderImageCallbackInfo` support).
13
14use alloc::vec::Vec;
15use core::fmt;
16
17use azul_core::{
18    callbacks::{CoreCallback, CoreCallbackData, Update},
19    dom::{Dom, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec},
20    geom::{LogicalPosition, LogicalRect, LogicalSize, PhysicalSizeU32},
21    gl::Texture,
22    menu::{Menu, MenuItem, StringMenuItem},
23    refany::{OptionRefAny, RefAny},
24    resources::{ImageRef, RawImageFormat},
25    svg::{SvgPath, SvgPathElement, SvgStrokeStyle, TessellatedGPUSvgNode},
26    window::CursorPosition::InWindow,
27};
28#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
29use azul_css::{
30    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
31    props::{
32        basic::*,
33        layout::*,
34        property::{CssProperty, *},
35        style::*,
36    },
37    *,
38};
39use azul_css::css::BoxOrStatic;
40
41use crate::{
42    callbacks::{Callback, CallbackInfo},
43    widgets::{
44        check_box::{CheckBox, CheckBoxOnToggleCallbackType, CheckBoxState},
45        color_input::{ColorInput, ColorInputOnValueChangeCallbackType, ColorInputState},
46        file_input::{FileInput, FileInputOnPathChangeCallbackType, FileInputState},
47        number_input::{NumberInput, NumberInputOnFocusLostCallbackType, NumberInputState},
48        text_input::{TextInput, TextInputOnFocusLostCallbackType, TextInputState},
49    },
50};
51
52/// Interactive node graph editor widget with typed input/output connections.
53#[derive(Debug, Clone)]
54#[repr(C)]
55pub struct NodeGraph {
56    pub node_types: NodeTypeIdInfoMapVec,
57    pub input_output_types: InputOutputTypeIdInfoMapVec,
58    pub nodes: NodeIdNodeMapVec,
59    pub allow_multiple_root_nodes: bool,
60    pub offset: LogicalPosition,
61    pub style: NodeGraphStyle,
62    pub callbacks: NodeGraphCallbacks,
63    pub add_node_str: AzString,
64    pub scale_factor: f32,
65}
66
67impl Default for NodeGraph {
68    fn default() -> Self {
69        Self {
70            node_types: NodeTypeIdInfoMapVec::from_const_slice(&[]),
71            input_output_types: InputOutputTypeIdInfoMapVec::from_const_slice(&[]),
72            nodes: NodeIdNodeMapVec::from_const_slice(&[]),
73            allow_multiple_root_nodes: false,
74            offset: LogicalPosition::zero(),
75            style: NodeGraphStyle::Default,
76            callbacks: NodeGraphCallbacks::default(),
77            add_node_str: AzString::from_const_str(""),
78            scale_factor: 1.0,
79        }
80    }
81}
82
83impl NodeGraph {
84    /// Generates a new `NodeId` that is unique in the graph
85    #[must_use] pub fn generate_unique_node_id(&self) -> NodeGraphNodeId {
86        NodeGraphNodeId {
87            inner: self
88                .nodes
89                .iter()
90                .map(|i| i.node_id.inner)
91                .max()
92                .unwrap_or(0)
93                .saturating_add(1),
94        }
95    }
96}
97
98/// Maps a [`NodeTypeId`] to its [`NodeTypeInfo`] metadata.
99#[derive(Debug, Clone)]
100#[repr(C)]
101pub struct NodeTypeIdInfoMap {
102    pub node_type_id: NodeTypeId,
103    pub node_type_info: NodeTypeInfo,
104}
105
106impl_option!(NodeTypeIdInfoMap, OptionNodeTypeIdInfoMap, copy = false, [Debug, Clone]);
107impl_vec!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec, NodeTypeIdInfoMapVecDestructor, NodeTypeIdInfoMapVecDestructorType, NodeTypeIdInfoMapVecSlice, OptionNodeTypeIdInfoMap);
108impl_vec_clone!(
109    NodeTypeIdInfoMap,
110    NodeTypeIdInfoMapVec,
111    NodeTypeIdInfoMapVecDestructor
112);
113impl_vec_mut!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec);
114impl_vec_debug!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec);
115
116/// Maps an [`InputOutputTypeId`] to its [`InputOutputInfo`] metadata.
117#[derive(Debug, Clone)]
118#[repr(C)]
119pub struct InputOutputTypeIdInfoMap {
120    pub io_type_id: InputOutputTypeId,
121    pub io_info: InputOutputInfo,
122}
123
124impl_option!(InputOutputTypeIdInfoMap, OptionInputOutputTypeIdInfoMap, copy = false, [Debug, Clone]);
125impl_vec!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec, InputOutputTypeIdInfoMapVecDestructor, InputOutputTypeIdInfoMapVecDestructorType, InputOutputTypeIdInfoMapVecSlice, OptionInputOutputTypeIdInfoMap);
126impl_vec_clone!(
127    InputOutputTypeIdInfoMap,
128    InputOutputTypeIdInfoMapVec,
129    InputOutputTypeIdInfoMapVecDestructor
130);
131impl_vec_mut!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec);
132impl_vec_debug!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec);
133
134/// Maps a [`NodeGraphNodeId`] to its [`Node`] data.
135#[derive(Debug, Clone)]
136#[repr(C)]
137pub struct NodeIdNodeMap {
138    pub node_id: NodeGraphNodeId,
139    pub node: Node,
140}
141
142impl_option!(NodeIdNodeMap, OptionNodeIdNodeMap, copy = false, [Debug, Clone]);
143impl_vec!(NodeIdNodeMap, NodeIdNodeMapVec, NodeIdNodeMapVecDestructor, NodeIdNodeMapVecDestructorType, NodeIdNodeMapVecSlice, OptionNodeIdNodeMap);
144impl_vec_clone!(NodeIdNodeMap, NodeIdNodeMapVec, NodeIdNodeMapVecDestructor);
145impl_vec_mut!(NodeIdNodeMap, NodeIdNodeMapVec);
146impl_vec_debug!(NodeIdNodeMap, NodeIdNodeMapVec);
147
148#[derive(Debug, Copy, Clone)]
149#[repr(C)]
150pub enum NodeGraphStyle {
151    Default,
152    // to be extended
153}
154
155/// User-provided callbacks for node graph interaction events.
156#[derive(Default, Debug, Clone)]
157#[repr(C)]
158pub struct NodeGraphCallbacks {
159    pub on_node_added: OptionOnNodeAdded,
160    pub on_node_removed: OptionOnNodeRemoved,
161    pub on_node_dragged: OptionOnNodeDragged,
162    pub on_node_graph_dragged: OptionOnNodeGraphDragged,
163    pub on_node_connected: OptionOnNodeConnected,
164    pub on_node_input_disconnected: OptionOnNodeInputDisconnected,
165    pub on_node_output_disconnected: OptionOnNodeOutputDisconnected,
166    pub on_node_field_edited: OptionOnNodeFieldEdited,
167}
168
169pub type OnNodeAddedCallbackType = extern "C" fn(
170    refany: RefAny,
171    info: CallbackInfo,
172    new_node_type: NodeTypeId,
173    new_node_id: NodeGraphNodeId,
174    new_node_position: NodeGraphNodePosition,
175) -> Update;
176impl_widget_callback!(
177    OnNodeAdded,
178    OptionOnNodeAdded,
179    OnNodeAddedCallback,
180    OnNodeAddedCallbackType
181);
182
183pub type OnNodeRemovedCallbackType =
184    extern "C" fn(refany: RefAny, info: CallbackInfo, node_id_to_remove: NodeGraphNodeId) -> Update;
185impl_widget_callback!(
186    OnNodeRemoved,
187    OptionOnNodeRemoved,
188    OnNodeRemovedCallback,
189    OnNodeRemovedCallbackType
190);
191
192pub type OnNodeGraphDraggedCallbackType =
193    extern "C" fn(refany: RefAny, info: CallbackInfo, drag_amount: GraphDragAmount) -> Update;
194impl_widget_callback!(
195    OnNodeGraphDragged,
196    OptionOnNodeGraphDragged,
197    OnNodeGraphDraggedCallback,
198    OnNodeGraphDraggedCallbackType
199);
200
201pub type OnNodeDraggedCallbackType = extern "C" fn(
202    refany: RefAny,
203    info: CallbackInfo,
204    node_dragged: NodeGraphNodeId,
205    drag_amount: NodeDragAmount,
206) -> Update;
207impl_widget_callback!(
208    OnNodeDragged,
209    OptionOnNodeDragged,
210    OnNodeDraggedCallback,
211    OnNodeDraggedCallbackType
212);
213
214pub type OnNodeConnectedCallbackType = extern "C" fn(
215    refany: RefAny,
216    info: CallbackInfo,
217    input: NodeGraphNodeId,
218    input_index: usize,
219    output: NodeGraphNodeId,
220    output_index: usize,
221) -> Update;
222impl_widget_callback!(
223    OnNodeConnected,
224    OptionOnNodeConnected,
225    OnNodeConnectedCallback,
226    OnNodeConnectedCallbackType
227);
228
229pub type OnNodeInputDisconnectedCallbackType = extern "C" fn(
230    refany: RefAny,
231    info: CallbackInfo,
232    input: NodeGraphNodeId,
233    input_index: usize,
234) -> Update;
235impl_widget_callback!(
236    OnNodeInputDisconnected,
237    OptionOnNodeInputDisconnected,
238    OnNodeInputDisconnectedCallback,
239    OnNodeInputDisconnectedCallbackType
240);
241
242pub type OnNodeOutputDisconnectedCallbackType = extern "C" fn(
243    refany: RefAny,
244    info: CallbackInfo,
245    output: NodeGraphNodeId,
246    output_index: usize,
247) -> Update;
248impl_widget_callback!(
249    OnNodeOutputDisconnected,
250    OptionOnNodeOutputDisconnected,
251    OnNodeOutputDisconnectedCallback,
252    OnNodeOutputDisconnectedCallbackType
253);
254
255pub type OnNodeFieldEditedCallbackType = extern "C" fn(
256    refany: RefAny,
257    info: CallbackInfo,
258    node_id: NodeGraphNodeId,
259    field_id: usize,
260    node_type: NodeTypeId,
261    new_value: NodeTypeFieldValue,
262) -> Update;
263impl_widget_callback!(
264    OnNodeFieldEdited,
265    OptionOnNodeFieldEdited,
266    OnNodeFieldEditedCallback,
267    OnNodeFieldEditedCallbackType
268);
269
270/// Unique identifier for an input/output port type.
271#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
272#[repr(C)]
273pub struct InputOutputTypeId {
274    pub inner: u64,
275}
276
277impl_option!(InputOutputTypeId, OptionInputOutputTypeId, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
278impl_vec!(InputOutputTypeId, InputOutputTypeIdVec, InputOutputTypeIdVecDestructor, InputOutputTypeIdVecDestructorType, InputOutputTypeIdVecSlice, OptionInputOutputTypeId);
279impl_vec_clone!(
280    InputOutputTypeId,
281    InputOutputTypeIdVec,
282    InputOutputTypeIdVecDestructor
283);
284impl_vec_mut!(InputOutputTypeId, InputOutputTypeIdVec);
285impl_vec_debug!(InputOutputTypeId, InputOutputTypeIdVec);
286
287/// Unique identifier for a node type (e.g. "Add", "Multiply").
288#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
289#[repr(C)]
290pub struct NodeTypeId {
291    pub inner: u64,
292}
293
294/// Unique identifier for a node instance within the graph.
295#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
296#[repr(C)]
297pub struct NodeGraphNodeId {
298    pub inner: u64,
299}
300
301/// A single node with typed input/output connections and editable fields.
302#[derive(Debug, Clone)]
303#[repr(C)]
304pub struct Node {
305    pub node_type: NodeTypeId,
306    pub position: NodeGraphNodePosition,
307    pub fields: NodeTypeFieldVec,
308    pub connect_in: InputConnectionVec,
309    pub connect_out: OutputConnectionVec,
310}
311
312/// A key-value field on a node (e.g. a text input labelled "Name").
313#[derive(Debug, Clone)]
314#[repr(C)]
315pub struct NodeTypeField {
316    pub key: AzString,
317    pub value: NodeTypeFieldValue,
318}
319
320impl_option!(NodeTypeField, OptionNodeTypeField, copy = false, [Debug, Clone]);
321impl_vec!(NodeTypeField, NodeTypeFieldVec, NodeTypeFieldVecDestructor, NodeTypeFieldVecDestructorType, NodeTypeFieldVecSlice, OptionNodeTypeField);
322impl_vec_clone!(NodeTypeField, NodeTypeFieldVec, NodeTypeFieldVecDestructor);
323impl_vec_debug!(NodeTypeField, NodeTypeFieldVec);
324impl_vec_mut!(NodeTypeField, NodeTypeFieldVec);
325
326/// The value of a node field, determining which widget is rendered.
327#[derive(Debug, Clone)]
328#[repr(C, u8)]
329pub enum NodeTypeFieldValue {
330    TextInput(AzString),
331    NumberInput(f32),
332    CheckBox(bool),
333    ColorInput(ColorU),
334    FileInput(OptionString),
335}
336
337/// An input port's connections to one or more output ports on other nodes.
338#[derive(Debug, Clone)]
339#[repr(C)]
340pub struct InputConnection {
341    pub input_index: usize,
342    pub connects_to: OutputNodeAndIndexVec,
343}
344
345impl_option!(InputConnection, OptionInputConnection, copy = false, [Debug, Clone]);
346impl_vec!(InputConnection, InputConnectionVec, InputConnectionVecDestructor, InputConnectionVecDestructorType, InputConnectionVecSlice, OptionInputConnection);
347impl_vec_clone!(
348    InputConnection,
349    InputConnectionVec,
350    InputConnectionVecDestructor
351);
352impl_vec_debug!(InputConnection, InputConnectionVec);
353impl_vec_mut!(InputConnection, InputConnectionVec);
354
355/// Reference to a specific output port on a node.
356#[derive(Copy, Debug, Clone)]
357#[repr(C)]
358pub struct OutputNodeAndIndex {
359    pub node_id: NodeGraphNodeId,
360    pub output_index: usize,
361}
362
363impl_option!(OutputNodeAndIndex, OptionOutputNodeAndIndex, copy = false, [Debug, Clone]);
364impl_vec!(OutputNodeAndIndex, OutputNodeAndIndexVec, OutputNodeAndIndexVecDestructor, OutputNodeAndIndexVecDestructorType, OutputNodeAndIndexVecSlice, OptionOutputNodeAndIndex);
365impl_vec_clone!(
366    OutputNodeAndIndex,
367    OutputNodeAndIndexVec,
368    OutputNodeAndIndexVecDestructor
369);
370impl_vec_debug!(OutputNodeAndIndex, OutputNodeAndIndexVec);
371impl_vec_mut!(OutputNodeAndIndex, OutputNodeAndIndexVec);
372
373/// An output port's connections to one or more input ports on other nodes.
374#[derive(Debug, Clone)]
375#[repr(C)]
376pub struct OutputConnection {
377    pub output_index: usize,
378    pub connects_to: InputNodeAndIndexVec,
379}
380
381impl_option!(OutputConnection, OptionOutputConnection, copy = false, [Debug, Clone]);
382impl_vec!(OutputConnection, OutputConnectionVec, OutputConnectionVecDestructor, OutputConnectionVecDestructorType, OutputConnectionVecSlice, OptionOutputConnection);
383impl_vec_clone!(
384    OutputConnection,
385    OutputConnectionVec,
386    OutputConnectionVecDestructor
387);
388impl_vec_debug!(OutputConnection, OutputConnectionVec);
389impl_vec_mut!(OutputConnection, OutputConnectionVec);
390
391/// Reference to a specific input port on a node.
392#[derive(Copy, Debug, Clone, PartialEq, Eq)]
393#[repr(C)]
394pub struct InputNodeAndIndex {
395    pub node_id: NodeGraphNodeId,
396    pub input_index: usize,
397}
398
399impl_option!(InputNodeAndIndex, OptionInputNodeAndIndex, copy = false, [Debug, Clone]);
400impl_vec!(InputNodeAndIndex, InputNodeAndIndexVec, InputNodeAndIndexVecDestructor, InputNodeAndIndexVecDestructorType, InputNodeAndIndexVecSlice, OptionInputNodeAndIndex);
401impl_vec_clone!(
402    InputNodeAndIndex,
403    InputNodeAndIndexVec,
404    InputNodeAndIndexVecDestructor
405);
406impl_vec_debug!(InputNodeAndIndex, InputNodeAndIndexVec);
407impl_vec_mut!(InputNodeAndIndex, InputNodeAndIndexVec);
408
409/// Metadata describing a node type and its I/O port configuration.
410#[derive(Debug, Clone)]
411#[repr(C)]
412pub struct NodeTypeInfo {
413    /// Whether this node type is a "root" type
414    pub is_root: bool,
415    /// Name of the node type
416    pub node_type_name: AzString,
417    /// List of inputs for this node
418    pub inputs: InputOutputTypeIdVec,
419    /// List of outputs for this node
420    pub outputs: InputOutputTypeIdVec,
421}
422
423/// Display metadata for an input/output port type (name and color).
424#[derive(Debug, Clone)]
425#[repr(C)]
426pub struct InputOutputInfo {
427    /// Data type of this input / output
428    pub data_type: AzString,
429    /// Which color to use for the input / output
430    pub color: ColorU,
431}
432
433/// Things only relevant to the display of the node in an interactive editor
434/// - such as x and y position in the node graph, name, etc.
435#[derive(Debug, Copy, Clone)]
436#[repr(C)]
437pub struct NodeGraphNodePosition {
438    /// X Position of the node
439    pub x: f32,
440    /// Y Position of the node
441    pub y: f32,
442}
443
444#[derive(Debug, Copy, Clone, PartialEq, Eq)]
445#[repr(C)]
446pub enum NodeGraphError {
447    /// MIME type is not the same (for example: connection "spatialdata/point"
448    /// with a node that expects "spatialdata/line")
449    NodeMimeTypeMismatch,
450    /// Invalid index when accessing a node in / output
451    NodeInvalidIndex,
452    /// The in-/ output matching encountered a non-existing hash to a node that doesn't exist
453    NodeInvalidNode,
454    /// Root node is missing from the graph tree
455    NoRootNode,
456}
457
458impl fmt::Display for NodeGraphError {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        use self::NodeGraphError::{NodeMimeTypeMismatch, NodeInvalidIndex, NodeInvalidNode, NoRootNode};
461        match self {
462            NodeMimeTypeMismatch => write!(f, "MIME type mismatch"),
463            NodeInvalidIndex => write!(f, "Invalid node index"),
464            NodeInvalidNode => write!(f, "Invalid node"),
465            NoRootNode => write!(f, "No root node found"),
466        }
467    }
468}
469
470/// Amount (in logical pixels) the entire graph was dragged.
471#[derive(Debug, Copy, Clone, PartialEq)]
472#[repr(C)]
473pub struct GraphDragAmount {
474    pub x: f32,
475    pub y: f32,
476}
477
478/// Amount (in logical pixels) a single node was dragged.
479#[derive(Debug, Copy, Clone, PartialEq)]
480#[repr(C)]
481pub struct NodeDragAmount {
482    pub x: f32,
483    pub y: f32,
484}
485
486impl NodeGraph {
487    #[must_use]
488    pub fn swap_with_default(&mut self) -> Self {
489        let mut default = Self::default();
490        ::core::mem::swap(&mut default, self);
491        default
492    }
493
494    /// Connects the current nodes input with another nodes output
495    ///
496    /// ## Inputs
497    ///
498    /// - `output_node_id`: The ID of the output node (index in the `NodeGraphs` internal `BTree`)
499    /// - `output_index`: The index of the output *on the output node*
500    /// - `input_node_id`: Same as `output_node_id`, but for the input node
501    /// - `input_index`: Same as `output_index`, but for the input node
502    ///
503    /// ## Returns
504    ///
505    /// One of:
506    ///
507    /// - `NodeGraphError::NodeInvalidNode`: One of the input nodes does not exist
508    /// - `NodeGraphError::NodeInvalidIndex`: One node has an invalid `output` or `input` index
509    /// - `NodeGraphError::NodeMimeTypeMismatch`: The types of two connected `outputs` and `inputs`
510    ///   aren't the same
511    /// - `Ok(())`: The connection was established successfully.
512    fn connect_input_output(
513        &mut self,
514        input_node_id: NodeGraphNodeId,
515        input_index: usize,
516        output_node_id: NodeGraphNodeId,
517        output_index: usize,
518    ) -> Result<(), NodeGraphError> {
519        // Verify that the node type of the connection matches
520        self.verify_nodetype_match(output_node_id, output_index, input_node_id, input_index)?;
521
522        // connect input -> output
523        if let Some(input_node) = self
524            .nodes
525            .as_mut()
526            .iter_mut()
527            .find(|i| i.node_id == input_node_id)
528        {
529            if let Some(position) = input_node
530                .node
531                .connect_in
532                .as_ref()
533                .iter()
534                .position(|i| i.input_index == input_index)
535            {
536                input_node.node.connect_in.as_mut()[position]
537                    .connects_to
538                    .push(OutputNodeAndIndex {
539                        node_id: output_node_id,
540                        output_index,
541                    });
542            } else {
543                input_node.node.connect_in.push(InputConnection {
544                    input_index,
545                    connects_to: vec![OutputNodeAndIndex {
546                        node_id: output_node_id,
547                        output_index,
548                    }]
549                    .into(),
550                });
551            }
552        } else {
553            return Err(NodeGraphError::NodeInvalidNode);
554        }
555
556        // connect output -> input
557        if let Some(output_node) = self
558            .nodes
559            .as_mut()
560            .iter_mut()
561            .find(|i| i.node_id == output_node_id)
562        {
563            if let Some(position) = output_node
564                .node
565                .connect_out
566                .as_ref()
567                .iter()
568                .position(|i| i.output_index == output_index)
569            {
570                output_node.node.connect_out.as_mut()[position]
571                    .connects_to
572                    .push(InputNodeAndIndex {
573                        node_id: input_node_id,
574                        input_index,
575                    });
576            } else {
577                output_node.node.connect_out.push(OutputConnection {
578                    output_index,
579                    connects_to: vec![InputNodeAndIndex {
580                        node_id: input_node_id,
581                        input_index,
582                    }]
583                    .into(),
584                });
585            }
586        } else {
587            return Err(NodeGraphError::NodeInvalidNode);
588        }
589
590        Ok(())
591    }
592
593    /// Disconnect an input if it is connected to an output
594    ///
595    /// # Inputs
596    ///
597    /// - `input_node_id`: The ID of the input node (index in the `NodeGraphs` internal `BTree`)
598    /// - `input_index`: The index of the input *on the input node*
599    ///
600    /// # Returns
601    ///
602    /// - `Err(NodeGraphError::NodeInvalidNode)`: The node at index `input_node_id` does not
603    ///   exist
604    /// - `Err(NodeGraphError::NodeInvalidIndex)`: One node has an invalid `input` or `output`
605    ///   index
606    /// - `Err(NodeGraphError::NodeMimeTypeMismatch)`: The types of two connected `input` and
607    ///   `output` do not match
608    /// - `Ok(())`: The disconnection completed successfully.
609    fn disconnect_input(
610        &mut self,
611        input_node_id: NodeGraphNodeId,
612        input_index: usize,
613    ) -> Result<(), NodeGraphError> {
614        let output_connections = {
615            let input_node = self
616                .nodes
617                .as_ref()
618                .iter()
619                .find(|i| i.node_id == input_node_id)
620                .ok_or(NodeGraphError::NodeInvalidNode)?;
621
622            match input_node
623                .node
624                .connect_in
625                .iter()
626                .find(|i| i.input_index == input_index)
627            {
628                None => return Ok(()),
629                Some(s) => s.connects_to.clone(),
630            }
631        };
632
633        // for every output that this input was connected to...
634        for OutputNodeAndIndex {
635            node_id,
636            output_index,
637        } in output_connections.as_ref()
638        {
639            let output_node_id = *node_id;
640            let output_index = *output_index;
641
642            // verify that the node type of the connection matches
643            self.verify_nodetype_match(
644                output_node_id,
645                output_index,
646                input_node_id,
647                input_index,
648            )?;
649
650            // disconnect input -> output
651
652            if let Some(input_node) = self
653                .nodes
654                .as_mut()
655                .iter_mut()
656                .find(|i| i.node_id == input_node_id)
657            {
658                if let Some(position) = input_node
659                    .node
660                    .connect_in
661                    .iter()
662                    .position(|i| i.input_index == input_index)
663                {
664                    input_node.node.connect_in.remove(position);
665                }
666            } else {
667                return Err(NodeGraphError::NodeInvalidNode);
668            }
669
670            if let Some(output_node) = self
671                .nodes
672                .as_mut()
673                .iter_mut()
674                .find(|i| i.node_id == output_node_id)
675            {
676                if let Some(position) = output_node
677                    .node
678                    .connect_out
679                    .iter()
680                    .position(|i| i.output_index == output_index)
681                {
682                    output_node.node.connect_out.remove(position);
683                }
684            } else {
685                return Err(NodeGraphError::NodeInvalidNode);
686            }
687        }
688
689        Ok(())
690    }
691
692    /// Disconnect an output if it is connected to an input
693    ///
694    /// # Inputs
695    ///
696    /// - `output_node_id`: The ID of the output node (index in the `NodeGraphs` internal `BTree`)
697    /// - `output_index`: The index of the output *on the output node*
698    ///
699    /// # Returns
700    ///
701    /// - `Err(NodeGraphError::NodeInvalidNode)`: The node at index `output_node_id` does not exist
702    /// - `Err(NodeGraphError::NodeInvalidIndex)`: One node has an invalid `input` or `output` index
703    /// - `Err(NodeGraphError::NodeMimeTypeMismatch)`: The types of two connected `input` and
704    ///   `output` do not match
705    /// - `Ok(())`: The disconnection completed successfully.
706    fn disconnect_output(
707        &mut self,
708        output_node_id: NodeGraphNodeId,
709        output_index: usize,
710    ) -> Result<(), NodeGraphError> {
711        let input_connections = {
712            let output_node = self
713                .nodes
714                .as_ref()
715                .iter()
716                .find(|i| i.node_id == output_node_id)
717                .ok_or(NodeGraphError::NodeInvalidNode)?;
718
719            match output_node
720                .node
721                .connect_out
722                .iter()
723                .find(|i| i.output_index == output_index)
724            {
725                None => return Ok(()),
726                Some(s) => s.connects_to.clone(),
727            }
728        };
729
730        for InputNodeAndIndex {
731            node_id,
732            input_index,
733        } in &input_connections
734        {
735            let input_node_id = *node_id;
736            let input_index = *input_index;
737
738            // verify that the node type of the connection matches
739            self.verify_nodetype_match(
740                output_node_id,
741                output_index,
742                input_node_id,
743                input_index,
744            )?;
745
746            if let Some(output_node) = self
747                .nodes
748                .as_mut()
749                .iter_mut()
750                .find(|i| i.node_id == output_node_id)
751            {
752                if let Some(position) = output_node
753                    .node
754                    .connect_out
755                    .iter()
756                    .position(|i| i.output_index == output_index)
757                {
758                    output_node.node.connect_out.remove(position);
759                }
760            } else {
761                return Err(NodeGraphError::NodeInvalidNode);
762            }
763
764            if let Some(input_node) = self
765                .nodes
766                .as_mut()
767                .iter_mut()
768                .find(|i| i.node_id == input_node_id)
769            {
770                if let Some(position) = input_node
771                    .node
772                    .connect_in
773                    .iter()
774                    .position(|i| i.input_index == input_index)
775                {
776                    input_node.node.connect_in.remove(position);
777                }
778            } else {
779                return Err(NodeGraphError::NodeInvalidNode);
780            }
781        }
782
783        Ok(())
784    }
785
786    /// Verifies that the node types of two connections match
787    fn verify_nodetype_match(
788        &self,
789        output_node_id: NodeGraphNodeId,
790        output_index: usize,
791        input_node_id: NodeGraphNodeId,
792        input_index: usize,
793    ) -> Result<(), NodeGraphError> {
794        let output_node = self
795            .nodes
796            .iter()
797            .find(|i| i.node_id == output_node_id)
798            .ok_or(NodeGraphError::NodeInvalidNode)?;
799
800        let output_node_type = self
801            .node_types
802            .iter()
803            .find(|i| i.node_type_id == output_node.node.node_type)
804            .ok_or(NodeGraphError::NodeInvalidNode)?;
805
806        let output_type = output_node_type
807            .node_type_info
808            .outputs
809            .as_ref()
810            .get(output_index)
811            .copied()
812            .ok_or(NodeGraphError::NodeInvalidIndex)?;
813
814        let input_node = self
815            .nodes
816            .iter()
817            .find(|i| i.node_id == input_node_id)
818            .ok_or(NodeGraphError::NodeInvalidNode)?;
819
820        let input_node_type = self
821            .node_types
822            .iter()
823            .find(|i| i.node_type_id == input_node.node.node_type)
824            .ok_or(NodeGraphError::NodeInvalidNode)?;
825
826        let input_type = input_node_type
827            .node_type_info
828            .inputs
829            .as_ref()
830            .get(input_index)
831            .copied()
832            .ok_or(NodeGraphError::NodeInvalidIndex)?;
833
834        // Input / Output do not have the same TypeId
835        if input_type != output_type {
836            return Err(NodeGraphError::NodeMimeTypeMismatch);
837        }
838
839        Ok(())
840    }
841
842    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
843    #[must_use] pub fn dom(self) -> Dom {
844        static NODEGRAPH_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("nodegraph"))];
845
846        static NODEGRAPH_BACKGROUND: &[StyleBackgroundContent] = &[StyleBackgroundContent::Image(
847            AzString::from_const_str("nodegraph-background"),
848        )];
849
850        static NODEGRAPH_NODES_CONTAINER_CLASS: &[IdOrClass] =
851            &[Class(AzString::from_const_str("nodegraph-nodes-container"))];
852
853        static NODEGRAPH_NODES_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
854            CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
855            CssPropertyWithConditions::simple(CssProperty::position(LayoutPosition::Absolute)),
856        ];
857
858        let nodegraph_wrapper_props = vec![
859            CssPropertyWithConditions::simple(CssProperty::overflow_x(LayoutOverflow::Hidden)),
860            CssPropertyWithConditions::simple(CssProperty::overflow_y(LayoutOverflow::Hidden)),
861            CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
862            CssPropertyWithConditions::simple(CssProperty::background_content(
863                StyleBackgroundContentVec::from_const_slice(NODEGRAPH_BACKGROUND),
864            )),
865            CssPropertyWithConditions::simple(CssProperty::background_repeat(
866                vec![StyleBackgroundRepeat::PatternRepeat].into(),
867            )),
868            CssPropertyWithConditions::simple(CssProperty::background_position(
869                vec![StyleBackgroundPosition {
870                    horizontal: BackgroundPositionHorizontal::Exact(PixelValue::const_px(0)),
871                    vertical: BackgroundPositionVertical::Exact(PixelValue::const_px(0)),
872                }]
873                .into(),
874            )),
875        ];
876
877        let nodegraph_props = vec![
878            CssPropertyWithConditions::simple(CssProperty::overflow_x(LayoutOverflow::Hidden)),
879            CssPropertyWithConditions::simple(CssProperty::overflow_y(LayoutOverflow::Hidden)),
880            CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
881            CssPropertyWithConditions::simple(CssProperty::position(LayoutPosition::Relative)),
882        ];
883
884        let node_connection_marker = RefAny::new(NodeConnectionMarkerDataset {});
885
886        let node_graph_local_dataset = RefAny::new(NodeGraphLocalDataset {
887            node_graph: self.clone(), // TODO: expensive
888            last_input_or_output_clicked: None,
889            active_node_being_dragged: None,
890            node_connection_marker: node_connection_marker.clone(),
891            callbacks: self.callbacks.clone(),
892        });
893
894        let context_menu = Menu::create(
895            vec![MenuItem::String(
896                StringMenuItem::create(self.add_node_str.clone()).with_children(
897                    self.node_types
898                        .iter()
899                        .map(
900                            |NodeTypeIdInfoMap {
901                                 node_type_id,
902                                 node_type_info,
903                             }| {
904                                let context_menu_local_dataset =
905                                    RefAny::new(ContextMenuEntryLocalDataset {
906                                        node_type: *node_type_id,
907                                        // RefAny<NodeGraphLocalDataset>
908                                        backref: node_graph_local_dataset.clone(),
909                                    });
910
911                                MenuItem::String(
912                                    StringMenuItem::create(
913                                        node_type_info.node_type_name.clone(),
914                                    )
915                                    .with_callback(
916                                        context_menu_local_dataset,
917                                        nodegraph_context_menu_click as usize,
918                                    ),
919                                )
920                            },
921                        )
922                        .collect::<Vec<_>>()
923                        .into(),
924                ),
925            )]
926            .into(),
927        );
928
929        Dom::create_div()
930            .with_css_props(nodegraph_wrapper_props.into())
931            .with_context_menu(context_menu)
932            .with_children(
933                vec![Dom::create_div()
934                    .with_ids_and_classes(IdOrClassVec::from_const_slice(NODEGRAPH_CLASS))
935                    .with_css_props(nodegraph_props.into())
936                    .with_callbacks(
937                        vec![
938                            CoreCallbackData {
939                                event: EventFilter::Hover(HoverEventFilter::MouseOver),
940                                refany: node_graph_local_dataset.clone(),
941                                callback: CoreCallback {
942                                    cb: nodegraph_drag_graph_or_nodes as usize,
943                                    ctx: OptionRefAny::None,
944                                },
945                            },
946                            CoreCallbackData {
947                                event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
948                                refany: node_graph_local_dataset.clone(),
949                                callback: CoreCallback {
950                                    cb: nodegraph_unset_active_node as usize,
951                                    ctx: OptionRefAny::None,
952                                },
953                            },
954                        ]
955                        .into(),
956                    )
957                    .with_children({
958                        vec![
959                            // connections
960                            render_connections(&self, node_connection_marker),
961                            // nodes
962                            self.nodes
963                                .iter()
964                                .filter_map(|NodeIdNodeMap { node_id, node }| {
965                                    let node_type_info = self
966                                        .node_types
967                                        .iter()
968                                        .find(|i| i.node_type_id == node.node_type)?;
969                                    let node_local_dataset = NodeLocalDataset {
970                                        node_id: *node_id,
971                                        backref: node_graph_local_dataset.clone(),
972                                    };
973
974                                    Some(render_node(
975                                        node,
976                                        (self.offset.x, self.offset.y),
977                                        &node_type_info.node_type_info,
978                                        node_local_dataset,
979                                        self.scale_factor,
980                                    ))
981                                })
982                                .collect::<Dom>()
983                                .with_ids_and_classes(IdOrClassVec::from_const_slice(
984                                    NODEGRAPH_NODES_CONTAINER_CLASS,
985                                ))
986                                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
987                                    NODEGRAPH_NODES_CONTAINER_PROPS,
988                                )),
989                        ]
990                        .into()
991                    })]
992                .into(),
993            )
994            .with_dataset(Some(node_graph_local_dataset).into())
995    }
996}
997
998// dataset set on the top-level nodegraph node,
999// containing all the state of the node graph
1000struct NodeGraphLocalDataset {
1001    node_graph: NodeGraph,
1002    last_input_or_output_clicked: Option<(NodeGraphNodeId, InputOrOutput)>,
1003    // Ref<NodeLocalDataSet> - used as a marker for getting the visual node ID
1004    active_node_being_dragged: Option<(NodeGraphNodeId, RefAny)>,
1005    node_connection_marker: RefAny, // Ref<NodeConnectionMarkerDataset>
1006    callbacks: NodeGraphCallbacks,
1007}
1008
1009struct ContextMenuEntryLocalDataset {
1010    node_type: NodeTypeId,
1011    backref: RefAny, // RefAny<NodeGraphLocalDataset>
1012}
1013
1014struct NodeConnectionMarkerDataset {}
1015
1016struct NodeLocalDataset {
1017    node_id: NodeGraphNodeId,
1018    backref: RefAny, // RefAny<NodeGraphLocalDataset>
1019}
1020
1021#[derive(Debug, Copy, Clone)]
1022enum InputOrOutput {
1023    Input(usize),
1024    Output(usize),
1025}
1026
1027struct NodeInputOutputLocalDataset {
1028    io_id: InputOrOutput,
1029    backref: RefAny, // RefAny<NodeLocalDataset>
1030}
1031
1032struct NodeFieldLocalDataset {
1033    field_idx: usize,
1034    backref: RefAny, // RefAny<NodeLocalDataset>
1035}
1036
1037#[derive(Copy, Clone)]
1038struct ConnectionLocalDataset {
1039    out_node_id: NodeGraphNodeId,
1040    out_idx: usize,
1041    in_node_id: NodeGraphNodeId,
1042    in_idx: usize,
1043    swap_vert: bool,
1044    swap_horz: bool,
1045    color: ColorU,
1046}
1047
1048#[allow(clippy::float_cmp)] // intentional exact compare: change-detection / identity fast-path / cache-key match
1049#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1050fn render_node(
1051    node: &Node,
1052    graph_offset: (f32, f32),
1053    node_info: &NodeTypeInfo,
1054    mut node_local_dataset: NodeLocalDataset,
1055    scale_factor: f32,
1056) -> Dom {
1057    use azul_core::dom::{
1058        CssPropertyWithConditions, CssPropertyWithConditionsVec, Dom, DomVec, IdOrClass,
1059        IdOrClass::Class, IdOrClassVec,
1060    };
1061    #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
1062    use azul_css::*;
1063
1064    const STRING_9416190750059025162: AzString = AzString::from_const_str("Material Icons");
1065    const STRING_16146701490593874959: AzString = AzString::from_const_str("system:ui");
1066    const STYLE_BACKGROUND_CONTENT_524016094839686509_ITEMS: &[StyleBackgroundContent] =
1067        &[StyleBackgroundContent::Color(ColorU {
1068            r: 34,
1069            g: 34,
1070            b: 34,
1071            a: 255,
1072        })];
1073    const STYLE_BACKGROUND_CONTENT_10430246856047584562_ITEMS: &[StyleBackgroundContent] =
1074        &[StyleBackgroundContent::LinearGradient(LinearGradient {
1075            direction: Direction::FromTo(DirectionCorners {
1076                dir_from: DirectionCorner::Left,
1077                dir_to: DirectionCorner::Right,
1078            }),
1079            extend_mode: ExtendMode::Clamp,
1080            stops: NormalizedLinearColorStopVec::from_const_slice(
1081                LINEAR_COLOR_STOP_4373556077110009258_ITEMS,
1082            ),
1083        })];
1084    const STYLE_BACKGROUND_CONTENT_11535310356736632656_ITEMS: &[StyleBackgroundContent] =
1085        &[StyleBackgroundContent::RadialGradient(RadialGradient {
1086            shape: Shape::Ellipse,
1087            extend_mode: ExtendMode::Clamp,
1088            position: StyleBackgroundPosition {
1089                horizontal: BackgroundPositionHorizontal::Left,
1090                vertical: BackgroundPositionVertical::Top,
1091            },
1092            size: RadialGradientSize::FarthestCorner,
1093            stops: NormalizedLinearColorStopVec::from_const_slice(
1094                LINEAR_COLOR_STOP_15596411095679453272_ITEMS,
1095            ),
1096        })];
1097    const STYLE_BACKGROUND_CONTENT_11936041127084538304_ITEMS: &[StyleBackgroundContent] =
1098        &[StyleBackgroundContent::LinearGradient(LinearGradient {
1099            direction: Direction::FromTo(DirectionCorners {
1100                dir_from: DirectionCorner::Right,
1101                dir_to: DirectionCorner::Left,
1102            }),
1103            extend_mode: ExtendMode::Clamp,
1104            stops: NormalizedLinearColorStopVec::from_const_slice(
1105                LINEAR_COLOR_STOP_4373556077110009258_ITEMS,
1106            ),
1107        })];
1108    const STYLE_BACKGROUND_CONTENT_15813232491335471489_ITEMS: &[StyleBackgroundContent] =
1109        &[StyleBackgroundContent::Color(ColorU {
1110            r: 0,
1111            g: 0,
1112            b: 0,
1113            a: 85,
1114        })];
1115    const STYLE_BACKGROUND_CONTENT_17648039690071193942_ITEMS: &[StyleBackgroundContent] =
1116        &[StyleBackgroundContent::LinearGradient(LinearGradient {
1117            direction: Direction::FromTo(DirectionCorners {
1118                dir_from: DirectionCorner::Top,
1119                dir_to: DirectionCorner::Bottom,
1120            }),
1121            extend_mode: ExtendMode::Clamp,
1122            stops: NormalizedLinearColorStopVec::from_const_slice(
1123                LINEAR_COLOR_STOP_7397113864565941600_ITEMS,
1124            ),
1125        })];
1126    const STYLE_TRANSFORM_347117342922946953_ITEMS: &[StyleTransform] =
1127        &[StyleTransform::Translate(StyleTransformTranslate2D {
1128            x: PixelValue::const_px(200),
1129            y: PixelValue::const_px(100),
1130        })];
1131    const STYLE_TRANSFORM_14683950870521466298_ITEMS: &[StyleTransform] =
1132        &[StyleTransform::Translate(StyleTransformTranslate2D {
1133            x: PixelValue::const_px(240),
1134            y: PixelValue::const_px(-10),
1135        })];
1136    const STYLE_FONT_FAMILY_8122988506401935406_ITEMS: &[StyleFontFamily] =
1137        &[StyleFontFamily::System(STRING_16146701490593874959)];
1138    const STYLE_FONT_FAMILY_11383897783350685780_ITEMS: &[StyleFontFamily] =
1139        &[StyleFontFamily::System(STRING_9416190750059025162)];
1140    const LINEAR_COLOR_STOP_4373556077110009258_ITEMS: &[NormalizedLinearColorStop] = &[
1141        NormalizedLinearColorStop {
1142            offset: PercentageValue::const_new(20),
1143            color: ColorOrSystem::color(ColorU {
1144                r: 0,
1145                g: 0,
1146                b: 0,
1147                a: 204,
1148            }),
1149        },
1150        NormalizedLinearColorStop {
1151            offset: PercentageValue::const_new(100),
1152            color: ColorOrSystem::color(ColorU {
1153                r: 0,
1154                g: 0,
1155                b: 0,
1156                a: 0,
1157            }),
1158        },
1159    ];
1160    const LINEAR_COLOR_STOP_7397113864565941600_ITEMS: &[NormalizedLinearColorStop] = &[
1161        NormalizedLinearColorStop {
1162            offset: PercentageValue::const_new(0),
1163            color: ColorOrSystem::color(ColorU {
1164                r: 229,
1165                g: 57,
1166                b: 53,
1167                a: 255,
1168            }),
1169        },
1170        NormalizedLinearColorStop {
1171            offset: PercentageValue::const_new(100),
1172            color: ColorOrSystem::color(ColorU {
1173                r: 227,
1174                g: 93,
1175                b: 91,
1176                a: 255,
1177            }),
1178        },
1179    ];
1180    const LINEAR_COLOR_STOP_15596411095679453272_ITEMS: &[NormalizedLinearColorStop] = &[
1181        NormalizedLinearColorStop {
1182            offset: PercentageValue::const_new(0),
1183            color: ColorOrSystem::color(ColorU {
1184                r: 47,
1185                g: 49,
1186                b: 54,
1187                a: 255,
1188            }),
1189        },
1190        NormalizedLinearColorStop {
1191            offset: PercentageValue::const_new(50),
1192            color: ColorOrSystem::color(ColorU {
1193                r: 47,
1194                g: 49,
1195                b: 54,
1196                a: 255,
1197            }),
1198        },
1199        NormalizedLinearColorStop {
1200            offset: PercentageValue::const_new(100),
1201            color: ColorOrSystem::color(ColorU {
1202                r: 32,
1203                g: 34,
1204                b: 37,
1205                a: 255,
1206            }),
1207        },
1208    ];
1209
1210    const CSS_MATCH_10339190304804100510_PROPERTIES: &[CssPropertyWithConditions] = &[
1211        // .node_output_wrapper
1212        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
1213            LayoutDisplay::Flex,
1214        ))),
1215        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1216            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
1217        )),
1218        CssPropertyWithConditions::simple(CssProperty::Left(LayoutLeftValue::Exact(LayoutLeft {
1219            inner: PixelValue::const_px(0),
1220        }))),
1221        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
1222            LayoutOverflow::Visible,
1223        ))),
1224        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
1225            LayoutOverflow::Visible,
1226        ))),
1227        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1228            LayoutPosition::Absolute,
1229        ))),
1230    ];
1231    const CSS_MATCH_10339190304804100510: CssPropertyWithConditionsVec =
1232        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_10339190304804100510_PROPERTIES);
1233
1234    const CSS_MATCH_11452431279102104133_PROPERTIES: &[CssPropertyWithConditions] = &[
1235        // .node_input_connection_label
1236        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
1237            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
1238        ))),
1239        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
1240            StyleFontSize {
1241                inner: PixelValue::const_px(12),
1242            },
1243        ))),
1244        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
1245            LayoutHeight::Px(PixelValue::const_px(15)),
1246        ))),
1247        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
1248            StyleTextAlign::Right,
1249        ))),
1250        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
1251            LayoutWidth::Px(PixelValue::const_px(100)),
1252        ))),
1253    ];
1254    const CSS_MATCH_11452431279102104133: CssPropertyWithConditionsVec =
1255        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_11452431279102104133_PROPERTIES);
1256
1257    const CSS_MATCH_1173826950760010563_PROPERTIES: &[CssPropertyWithConditions] = &[
1258        // .node_configuration_field_value:focus
1259        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
1260            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
1261                inner: ColorU {
1262                    r: 0,
1263                    g: 131,
1264                    b: 176,
1265                    a: 119,
1266                },
1267            }),
1268        )),
1269        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
1270            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
1271                inner: ColorU {
1272                    r: 0,
1273                    g: 131,
1274                    b: 176,
1275                    a: 119,
1276                },
1277            }),
1278        )),
1279        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
1280            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
1281                inner: ColorU {
1282                    r: 0,
1283                    g: 131,
1284                    b: 176,
1285                    a: 119,
1286                },
1287            }),
1288        )),
1289        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
1290            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
1291                inner: ColorU {
1292                    r: 0,
1293                    g: 131,
1294                    b: 176,
1295                    a: 119,
1296                },
1297            }),
1298        )),
1299        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
1300            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
1301                inner: BorderStyle::Solid,
1302            }),
1303        )),
1304        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
1305            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
1306                inner: BorderStyle::Solid,
1307            }),
1308        )),
1309        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
1310            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
1311                inner: BorderStyle::Solid,
1312            }),
1313        )),
1314        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
1315            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
1316                inner: BorderStyle::Solid,
1317            }),
1318        )),
1319        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
1320            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
1321                inner: PixelValue::const_px(1),
1322            }),
1323        )),
1324        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
1325            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
1326                inner: PixelValue::const_px(1),
1327            }),
1328        )),
1329        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
1330            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
1331                inner: PixelValue::const_px(1),
1332            }),
1333        )),
1334        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
1335            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
1336                inner: PixelValue::const_px(1),
1337            }),
1338        )),
1339        // .node_configuration_field_value
1340        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
1341            LayoutAlignItems::Center,
1342        ))),
1343        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
1344            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
1345                STYLE_BACKGROUND_CONTENT_524016094839686509_ITEMS,
1346            )),
1347        )),
1348        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
1349            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
1350                inner: ColorU {
1351                    r: 54,
1352                    g: 57,
1353                    b: 63,
1354                    a: 255,
1355                },
1356            }),
1357        )),
1358        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
1359            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
1360                inner: ColorU {
1361                    r: 54,
1362                    g: 57,
1363                    b: 63,
1364                    a: 255,
1365                },
1366            }),
1367        )),
1368        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
1369            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
1370                inner: ColorU {
1371                    r: 54,
1372                    g: 57,
1373                    b: 63,
1374                    a: 255,
1375                },
1376            }),
1377        )),
1378        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
1379            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
1380                inner: ColorU {
1381                    r: 54,
1382                    g: 57,
1383                    b: 63,
1384                    a: 255,
1385                },
1386            }),
1387        )),
1388        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
1389            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
1390                inner: BorderStyle::Solid,
1391            }),
1392        )),
1393        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
1394            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
1395                inner: BorderStyle::Solid,
1396            }),
1397        )),
1398        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
1399            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
1400                inner: BorderStyle::Solid,
1401            }),
1402        )),
1403        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
1404            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
1405                inner: BorderStyle::Solid,
1406            }),
1407        )),
1408        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
1409            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
1410                inner: PixelValue::const_px(1),
1411            }),
1412        )),
1413        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
1414            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
1415                inner: PixelValue::const_px(1),
1416            }),
1417        )),
1418        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
1419            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
1420                inner: PixelValue::const_px(1),
1421            }),
1422        )),
1423        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
1424            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
1425                inner: PixelValue::const_px(1),
1426            }),
1427        )),
1428        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1429            LayoutFlexGrow {
1430                inner: FloatValue::const_new(1),
1431            },
1432        ))),
1433        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
1434            StyleTextAlign::Left,
1435        ))),
1436    ];
1437    const CSS_MATCH_1173826950760010563: CssPropertyWithConditionsVec =
1438        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1173826950760010563_PROPERTIES);
1439
1440    const CSS_MATCH_1198521124955124418_PROPERTIES: &[CssPropertyWithConditions] = &[
1441        // .node_configuration_field_label
1442        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
1443            LayoutAlignItems::Center,
1444        ))),
1445        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1446            LayoutFlexGrow {
1447                inner: FloatValue::const_new(1),
1448            },
1449        ))),
1450        CssPropertyWithConditions::simple(CssProperty::MaxWidth(LayoutMaxWidthValue::Exact(
1451            LayoutMaxWidth {
1452                inner: PixelValue::const_px(120),
1453            },
1454        ))),
1455        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
1456            LayoutPaddingLeft {
1457                inner: PixelValue::const_px(10),
1458            },
1459        ))),
1460        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
1461            StyleTextAlign::Left,
1462        ))),
1463    ];
1464    const CSS_MATCH_1198521124955124418: CssPropertyWithConditionsVec =
1465        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1198521124955124418_PROPERTIES);
1466
1467    const CSS_MATCH_12038890904436132038_PROPERTIES: &[CssPropertyWithConditions] = &[
1468        // .node_output_connection_label_wrapper
1469        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
1470            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
1471                STYLE_BACKGROUND_CONTENT_10430246856047584562_ITEMS,
1472            )),
1473        )),
1474        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
1475            LayoutPaddingLeft {
1476                inner: PixelValue::const_px(5),
1477            },
1478        ))),
1479    ];
1480    const CSS_MATCH_12038890904436132038: CssPropertyWithConditionsVec =
1481        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_12038890904436132038_PROPERTIES);
1482
1483    const CSS_MATCH_12400244273289328300_PROPERTIES: &[CssPropertyWithConditions] = &[
1484        // .node_output_container
1485        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
1486            LayoutDisplay::Flex,
1487        ))),
1488        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1489            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
1490        )),
1491        CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
1492            LayoutMarginTop {
1493                inner: PixelValue::const_px(10),
1494            },
1495        ))),
1496    ];
1497    const CSS_MATCH_12400244273289328300: CssPropertyWithConditionsVec =
1498        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_12400244273289328300_PROPERTIES);
1499
1500    const CSS_MATCH_14906563417280941890_PROPERTIES: &[CssPropertyWithConditions] = &[
1501        // .outputs
1502        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1503            LayoutFlexGrow {
1504                inner: FloatValue::const_new(0),
1505            },
1506        ))),
1507        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
1508            LayoutOverflow::Visible,
1509        ))),
1510        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
1511            LayoutOverflow::Visible,
1512        ))),
1513        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1514            LayoutPosition::Relative,
1515        ))),
1516        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
1517            LayoutWidth::Px(PixelValue::const_px(0)),
1518        ))),
1519    ];
1520    const CSS_MATCH_14906563417280941890: CssPropertyWithConditionsVec =
1521        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_14906563417280941890_PROPERTIES);
1522
1523    const CSS_MATCH_16946967739775705757_PROPERTIES: &[CssPropertyWithConditions] = &[
1524        // .inputs
1525        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1526            LayoutFlexGrow {
1527                inner: FloatValue::const_new(0),
1528            },
1529        ))),
1530        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
1531            LayoutOverflow::Visible,
1532        ))),
1533        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
1534            LayoutOverflow::Visible,
1535        ))),
1536        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1537            LayoutPosition::Relative,
1538        ))),
1539        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
1540            LayoutWidth::Px(PixelValue::const_px(0)),
1541        ))),
1542    ];
1543    const CSS_MATCH_16946967739775705757: CssPropertyWithConditionsVec =
1544        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_16946967739775705757_PROPERTIES);
1545
1546    const CSS_MATCH_1739273067404038547_PROPERTIES: &[CssPropertyWithConditions] = &[
1547        // .node_label
1548        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
1549            StyleFontSize {
1550                inner: PixelValue::const_px(18),
1551            },
1552        ))),
1553        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
1554            LayoutHeight::Px(PixelValue::const_px(50)),
1555        ))),
1556        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
1557            LayoutPaddingLeft {
1558                inner: PixelValue::const_px(5),
1559            },
1560        ))),
1561        CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
1562            LayoutPaddingTop {
1563                inner: PixelValue::const_px(10),
1564            },
1565        ))),
1566    ];
1567    const CSS_MATCH_1739273067404038547: CssPropertyWithConditionsVec =
1568        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1739273067404038547_PROPERTIES);
1569
1570    const CSS_MATCH_2008162367868363199_PROPERTIES: &[CssPropertyWithConditions] = &[
1571        // .node_output_connection_label
1572        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
1573            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
1574        ))),
1575        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
1576            StyleFontSize {
1577                inner: PixelValue::const_px(12),
1578            },
1579        ))),
1580        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
1581            LayoutHeight::Px(PixelValue::const_px(15)),
1582        ))),
1583        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
1584            StyleTextAlign::Left,
1585        ))),
1586        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
1587            LayoutWidth::Px(PixelValue::const_px(100)),
1588        ))),
1589    ];
1590    const CSS_MATCH_2008162367868363199: CssPropertyWithConditionsVec =
1591        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_2008162367868363199_PROPERTIES);
1592
1593    const CSS_MATCH_2639191696846875011_PROPERTIES: &[CssPropertyWithConditions] = &[
1594        // .node_configuration_field_container
1595        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1596            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
1597        )),
1598        CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
1599            LayoutPaddingTop {
1600                inner: PixelValue::const_px(3),
1601            },
1602        ))),
1603        CssPropertyWithConditions::simple(CssProperty::PaddingBottom(
1604            LayoutPaddingBottomValue::Exact(LayoutPaddingBottom {
1605                inner: PixelValue::const_px(3),
1606            }),
1607        )),
1608        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
1609            LayoutPaddingLeft {
1610                inner: PixelValue::const_px(5),
1611            },
1612        ))),
1613        CssPropertyWithConditions::simple(CssProperty::PaddingRight(
1614            LayoutPaddingRightValue::Exact(LayoutPaddingRight {
1615                inner: PixelValue::const_px(5),
1616            }),
1617        )),
1618    ];
1619    const CSS_MATCH_2639191696846875011: CssPropertyWithConditionsVec =
1620        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_2639191696846875011_PROPERTIES);
1621
1622    const CSS_MATCH_3354247437065914166_PROPERTIES: &[CssPropertyWithConditions] = &[
1623        // .node_body
1624        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1625            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
1626        )),
1627        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1628            LayoutPosition::Relative,
1629        ))),
1630    ];
1631    const CSS_MATCH_3354247437065914166: CssPropertyWithConditionsVec =
1632        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_3354247437065914166_PROPERTIES);
1633
1634    const CSS_MATCH_4700400755767504372_PROPERTIES: &[CssPropertyWithConditions] = &[
1635        // .node_input_connection_label_wrapper
1636        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
1637            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
1638                STYLE_BACKGROUND_CONTENT_11936041127084538304_ITEMS,
1639            )),
1640        )),
1641        CssPropertyWithConditions::simple(CssProperty::PaddingRight(
1642            LayoutPaddingRightValue::Exact(LayoutPaddingRight {
1643                inner: PixelValue::const_px(5),
1644            }),
1645        )),
1646    ];
1647    const CSS_MATCH_4700400755767504372: CssPropertyWithConditionsVec =
1648        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_4700400755767504372_PROPERTIES);
1649
1650    const CSS_MATCH_705881630351954657_PROPERTIES: &[CssPropertyWithConditions] = &[
1651        // .node_input_wrapper
1652        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
1653            LayoutDisplay::Flex,
1654        ))),
1655        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1656            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
1657        )),
1658        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
1659            LayoutOverflow::Visible,
1660        ))),
1661        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
1662            LayoutOverflow::Visible,
1663        ))),
1664        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1665            LayoutPosition::Absolute,
1666        ))),
1667        CssPropertyWithConditions::simple(CssProperty::Right(LayoutRightValue::Exact(
1668            LayoutRight {
1669                inner: PixelValue::const_px(0),
1670            },
1671        ))),
1672    ];
1673    const CSS_MATCH_705881630351954657: CssPropertyWithConditionsVec =
1674        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_705881630351954657_PROPERTIES);
1675
1676    const CSS_MATCH_7395766480280098891_PROPERTIES: &[CssPropertyWithConditions] = &[
1677        // .node_close_button
1678        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
1679            LayoutAlignItems::Center,
1680        ))),
1681        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
1682            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
1683                STYLE_BACKGROUND_CONTENT_17648039690071193942_ITEMS,
1684            )),
1685        )),
1686        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
1687            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
1688                inner: ColorU {
1689                    r: 255,
1690                    g: 255,
1691                    b: 255,
1692                    a: 153,
1693                },
1694            }),
1695        )),
1696        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
1697            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
1698                inner: ColorU {
1699                    r: 255,
1700                    g: 255,
1701                    b: 255,
1702                    a: 153,
1703                },
1704            }),
1705        )),
1706        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
1707            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
1708                inner: ColorU {
1709                    r: 255,
1710                    g: 255,
1711                    b: 255,
1712                    a: 153,
1713                },
1714            }),
1715        )),
1716        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
1717            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
1718                inner: ColorU {
1719                    r: 255,
1720                    g: 255,
1721                    b: 255,
1722                    a: 153,
1723                },
1724            }),
1725        )),
1726        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
1727            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
1728                inner: BorderStyle::Solid,
1729            }),
1730        )),
1731        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
1732            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
1733                inner: BorderStyle::Solid,
1734            }),
1735        )),
1736        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
1737            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
1738                inner: BorderStyle::Solid,
1739            }),
1740        )),
1741        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
1742            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
1743                inner: BorderStyle::Solid,
1744            }),
1745        )),
1746        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
1747            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
1748                inner: PixelValue::const_px(1),
1749            }),
1750        )),
1751        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
1752            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
1753                inner: PixelValue::const_px(1),
1754            }),
1755        )),
1756        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
1757            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
1758                inner: PixelValue::const_px(1),
1759            }),
1760        )),
1761        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
1762            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
1763                inner: PixelValue::const_px(1),
1764            }),
1765        )),
1766        CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1767            StyleBoxShadow {
1768                offset_x: PixelValueNoPercent {
1769                    inner: PixelValue::const_px(0),
1770                },
1771                offset_y: PixelValueNoPercent {
1772                    inner: PixelValue::const_px(0),
1773                },
1774                color: ColorU {
1775                    r: 229,
1776                    g: 57,
1777                    b: 53,
1778                    a: 255,
1779                },
1780                blur_radius: PixelValueNoPercent {
1781                    inner: PixelValue::const_px(2),
1782                },
1783                spread_radius: PixelValueNoPercent {
1784                    inner: PixelValue::const_px(0),
1785                },
1786                clip_mode: BoxShadowClipMode::Outset,
1787            },
1788        )))),
1789        CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1790            StyleBoxShadow {
1791                offset_x: PixelValueNoPercent {
1792                    inner: PixelValue::const_px(0),
1793                },
1794                offset_y: PixelValueNoPercent {
1795                    inner: PixelValue::const_px(0),
1796                },
1797                color: ColorU {
1798                    r: 229,
1799                    g: 57,
1800                    b: 53,
1801                    a: 255,
1802                },
1803                blur_radius: PixelValueNoPercent {
1804                    inner: PixelValue::const_px(2),
1805                },
1806                spread_radius: PixelValueNoPercent {
1807                    inner: PixelValue::const_px(0),
1808                },
1809                clip_mode: BoxShadowClipMode::Outset,
1810            },
1811        )))),
1812        CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1813            StyleBoxShadow {
1814                offset_x: PixelValueNoPercent {
1815                    inner: PixelValue::const_px(0),
1816                },
1817                offset_y: PixelValueNoPercent {
1818                    inner: PixelValue::const_px(0),
1819                },
1820                color: ColorU {
1821                    r: 229,
1822                    g: 57,
1823                    b: 53,
1824                    a: 255,
1825                },
1826                blur_radius: PixelValueNoPercent {
1827                    inner: PixelValue::const_px(2),
1828                },
1829                spread_radius: PixelValueNoPercent {
1830                    inner: PixelValue::const_px(0),
1831                },
1832                clip_mode: BoxShadowClipMode::Outset,
1833            },
1834        )))),
1835        CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
1836            StyleBoxShadowValue::Exact(BoxOrStatic::Static(&StyleBoxShadow {
1837                offset_x: PixelValueNoPercent {
1838                    inner: PixelValue::const_px(0),
1839                },
1840                offset_y: PixelValueNoPercent {
1841                    inner: PixelValue::const_px(0),
1842                },
1843                color: ColorU {
1844                    r: 229,
1845                    g: 57,
1846                    b: 53,
1847                    a: 255,
1848                },
1849                blur_radius: PixelValueNoPercent {
1850                    inner: PixelValue::const_px(2),
1851                },
1852                spread_radius: PixelValueNoPercent {
1853                    inner: PixelValue::const_px(0),
1854                },
1855                clip_mode: BoxShadowClipMode::Outset,
1856            })),
1857        )),
1858        CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
1859            StyleCursor::Pointer,
1860        ))),
1861        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
1862            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_11383897783350685780_ITEMS),
1863        ))),
1864        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
1865            LayoutHeight::Px(PixelValue::const_px(20)),
1866        ))),
1867        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
1868            LayoutPosition::Absolute,
1869        ))),
1870        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
1871            StyleTextAlign::Center,
1872        ))),
1873        CssPropertyWithConditions::simple(CssProperty::Transform(StyleTransformVecValue::Exact(
1874            StyleTransformVec::from_const_slice(STYLE_TRANSFORM_14683950870521466298_ITEMS),
1875        ))),
1876        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
1877            LayoutWidth::Px(PixelValue::const_px(20)),
1878        ))),
1879    ];
1880    const CSS_MATCH_7395766480280098891: CssPropertyWithConditionsVec =
1881        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_7395766480280098891_PROPERTIES);
1882
1883    const CSS_MATCH_7432473243011547380_PROPERTIES: &[CssPropertyWithConditions] = &[
1884        // .node_content_wrapper
1885        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
1886            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
1887                STYLE_BACKGROUND_CONTENT_15813232491335471489_ITEMS,
1888            )),
1889        )),
1890        CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1891            StyleBoxShadow {
1892                offset_x: PixelValueNoPercent {
1893                    inner: PixelValue::const_px(0),
1894                },
1895                offset_y: PixelValueNoPercent {
1896                    inner: PixelValue::const_px(0),
1897                },
1898                color: ColorU {
1899                    r: 0,
1900                    g: 0,
1901                    b: 0,
1902                    a: 255,
1903                },
1904                blur_radius: PixelValueNoPercent {
1905                    inner: PixelValue::const_px(4),
1906                },
1907                spread_radius: PixelValueNoPercent {
1908                    inner: PixelValue::const_px(0),
1909                },
1910                clip_mode: BoxShadowClipMode::Inset,
1911            },
1912        )))),
1913        CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1914            StyleBoxShadow {
1915                offset_x: PixelValueNoPercent {
1916                    inner: PixelValue::const_px(0),
1917                },
1918                offset_y: PixelValueNoPercent {
1919                    inner: PixelValue::const_px(0),
1920                },
1921                color: ColorU {
1922                    r: 0,
1923                    g: 0,
1924                    b: 0,
1925                    a: 255,
1926                },
1927                blur_radius: PixelValueNoPercent {
1928                    inner: PixelValue::const_px(4),
1929                },
1930                spread_radius: PixelValueNoPercent {
1931                    inner: PixelValue::const_px(0),
1932                },
1933                clip_mode: BoxShadowClipMode::Inset,
1934            },
1935        )))),
1936        CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
1937            StyleBoxShadow {
1938                offset_x: PixelValueNoPercent {
1939                    inner: PixelValue::const_px(0),
1940                },
1941                offset_y: PixelValueNoPercent {
1942                    inner: PixelValue::const_px(0),
1943                },
1944                color: ColorU {
1945                    r: 0,
1946                    g: 0,
1947                    b: 0,
1948                    a: 255,
1949                },
1950                blur_radius: PixelValueNoPercent {
1951                    inner: PixelValue::const_px(4),
1952                },
1953                spread_radius: PixelValueNoPercent {
1954                    inner: PixelValue::const_px(0),
1955                },
1956                clip_mode: BoxShadowClipMode::Inset,
1957            },
1958        )))),
1959        CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
1960            StyleBoxShadowValue::Exact(BoxOrStatic::Static(&StyleBoxShadow {
1961                offset_x: PixelValueNoPercent {
1962                    inner: PixelValue::const_px(0),
1963                },
1964                offset_y: PixelValueNoPercent {
1965                    inner: PixelValue::const_px(0),
1966                },
1967                color: ColorU {
1968                    r: 0,
1969                    g: 0,
1970                    b: 0,
1971                    a: 255,
1972                },
1973                blur_radius: PixelValueNoPercent {
1974                    inner: PixelValue::const_px(4),
1975                },
1976                spread_radius: PixelValueNoPercent {
1977                    inner: PixelValue::const_px(0),
1978                },
1979                clip_mode: BoxShadowClipMode::Inset,
1980            })),
1981        )),
1982        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
1983            LayoutFlexGrow {
1984                inner: FloatValue::const_new(1),
1985            },
1986        ))),
1987    ];
1988    const CSS_MATCH_7432473243011547380: CssPropertyWithConditionsVec =
1989        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_7432473243011547380_PROPERTIES);
1990
1991    const CSS_MATCH_9863994880298313101_PROPERTIES: &[CssPropertyWithConditions] = &[
1992        // .node_input_container
1993        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
1994            LayoutDisplay::Flex,
1995        ))),
1996        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
1997            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
1998        )),
1999        CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
2000            LayoutMarginTop {
2001                inner: PixelValue::const_px(10),
2002            },
2003        ))),
2004    ];
2005    const CSS_MATCH_9863994880298313101: CssPropertyWithConditionsVec =
2006        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_9863994880298313101_PROPERTIES);
2007
2008    // NODE RENDER FUNCTION BEGIN
2009
2010    let node_transform = StyleTransformTranslate2D {
2011        x: PixelValue::px(graph_offset.0 + node.position.x),
2012        y: PixelValue::px(graph_offset.1 + node.position.y),
2013    };
2014
2015    // get names and colors for inputs / outputs
2016    let inputs = node_info
2017        .inputs
2018        .iter()
2019        .filter_map(|io_id| {
2020            let node_graph_ref = node_local_dataset
2021                .backref
2022                .downcast_ref::<NodeGraphLocalDataset>()?;
2023            let io_info = node_graph_ref
2024                .node_graph
2025                .input_output_types
2026                .iter()
2027                .find(|i| i.io_type_id == *io_id)?;
2028            Some((
2029                io_info.io_info.data_type.clone(),
2030                io_info.io_info.color,
2031            ))
2032        })
2033        .collect::<Vec<_>>();
2034
2035    let outputs = node_info
2036        .outputs
2037        .iter()
2038        .filter_map(|io_id| {
2039            let node_graph_ref = node_local_dataset
2040                .backref
2041                .downcast_ref::<NodeGraphLocalDataset>()?;
2042            let io_info = node_graph_ref
2043                .node_graph
2044                .input_output_types
2045                .iter()
2046                .find(|i| i.io_type_id == *io_id)?;
2047            Some((
2048                io_info.io_info.data_type.clone(),
2049                io_info.io_info.color,
2050            ))
2051        })
2052        .collect::<Vec<_>>();
2053
2054    let node_local_dataset = RefAny::new(node_local_dataset);
2055
2056    Dom::create_div()
2057    .with_css_props(vec![
2058        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
2059            LayoutPosition::Absolute,
2060        ))),
2061    ].into())
2062    .with_children(vec![
2063        Dom::create_div()
2064        .with_callbacks(vec![
2065           CoreCallbackData {
2066               event: EventFilter::Hover(HoverEventFilter::LeftMouseDown),
2067               refany: node_local_dataset.clone(),
2068               callback: CoreCallback { cb: nodegraph_set_active_node as usize, ctx: OptionRefAny::None },
2069           },
2070        ].into())
2071        .with_css_props(vec![
2072           // .node_graph_node
2073           CssPropertyWithConditions::simple(CssProperty::OverflowX(
2074               LayoutOverflowValue::Exact(LayoutOverflow::Visible)
2075           )),
2076           CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
2077               LayoutPosition::Relative,
2078           ))),
2079           CssPropertyWithConditions::simple(CssProperty::OverflowY(
2080               LayoutOverflowValue::Exact(LayoutOverflow::Visible)
2081           )),
2082           CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
2083               StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
2084                   STYLE_BACKGROUND_CONTENT_11535310356736632656_ITEMS,
2085               )),
2086           )),
2087           CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
2088               StyleBorderTopColorValue::Exact(StyleBorderTopColor {
2089                   inner: ColorU {
2090                       r: 0,
2091                       g: 180,
2092                       b: 219,
2093                       a: 255,
2094                   },
2095               }),
2096           )),
2097           CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
2098               StyleBorderRightColorValue::Exact(StyleBorderRightColor {
2099                   inner: ColorU {
2100                       r: 0,
2101                       g: 180,
2102                       b: 219,
2103                       a: 255,
2104                   },
2105               }),
2106           )),
2107           CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
2108               StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
2109                   inner: ColorU {
2110                       r: 0,
2111                       g: 180,
2112                       b: 219,
2113                       a: 255,
2114                   },
2115               }),
2116           )),
2117           CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
2118               StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
2119                   inner: ColorU {
2120                       r: 0,
2121                       g: 180,
2122                       b: 219,
2123                       a: 255,
2124                   },
2125               }),
2126           )),
2127           CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
2128               StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
2129                   inner: BorderStyle::Solid,
2130               }),
2131           )),
2132           CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
2133               StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
2134                   inner: BorderStyle::Solid,
2135               }),
2136           )),
2137           CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
2138               StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
2139                   inner: BorderStyle::Solid,
2140               }),
2141           )),
2142           CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
2143               StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
2144                   inner: BorderStyle::Solid,
2145               }),
2146           )),
2147           CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
2148               LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
2149                   inner: PixelValue::const_px(1),
2150               }),
2151           )),
2152           CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
2153               LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
2154                   inner: PixelValue::const_px(1),
2155               }),
2156           )),
2157           CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
2158               LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
2159                   inner: PixelValue::const_px(1),
2160               }),
2161           )),
2162           CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
2163               LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
2164                   inner: PixelValue::const_px(1),
2165               }),
2166           )),
2167           CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
2168               StyleBoxShadow {
2169                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
2170                   color: ColorU {
2171                       r: 0,
2172                       g: 131,
2173                       b: 176,
2174                       a: 119,
2175                   },
2176                   blur_radius: PixelValueNoPercent {
2177                       inner: PixelValue::const_px(3),
2178                   },
2179                   spread_radius: PixelValueNoPercent {
2180                       inner: PixelValue::const_px(0),
2181                   },
2182                   clip_mode: BoxShadowClipMode::Outset,
2183               },
2184           )))),
2185           CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
2186               StyleBoxShadow {
2187                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
2188                   color: ColorU {
2189                       r: 0,
2190                       g: 131,
2191                       b: 176,
2192                       a: 119,
2193                   },
2194                   blur_radius: PixelValueNoPercent {
2195                       inner: PixelValue::const_px(3),
2196                   },
2197                   spread_radius: PixelValueNoPercent {
2198                       inner: PixelValue::const_px(0),
2199                   },
2200                   clip_mode: BoxShadowClipMode::Outset,
2201               },
2202           )))),
2203           CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
2204               StyleBoxShadow {
2205                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
2206                   color: ColorU {
2207                       r: 0,
2208                       g: 131,
2209                       b: 176,
2210                       a: 119,
2211                   },
2212                   blur_radius: PixelValueNoPercent {
2213                       inner: PixelValue::const_px(3),
2214                   },
2215                   spread_radius: PixelValueNoPercent {
2216                       inner: PixelValue::const_px(0),
2217                   },
2218                   clip_mode: BoxShadowClipMode::Outset,
2219               },
2220           )))),
2221           CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
2222               StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
2223                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
2224                   color: ColorU {
2225                       r: 0,
2226                       g: 131,
2227                       b: 176,
2228                       a: 119,
2229                   },
2230                   blur_radius: PixelValueNoPercent {
2231                       inner: PixelValue::const_px(3),
2232                   },
2233                   spread_radius: PixelValueNoPercent {
2234                       inner: PixelValue::const_px(0),
2235                   },
2236                   clip_mode: BoxShadowClipMode::Outset,
2237               })),
2238           )),
2239           CssPropertyWithConditions::simple(CssProperty::TextColor(StyleTextColorValue::Exact(
2240               StyleTextColor {
2241                   inner: ColorU {
2242                       r: 255,
2243                       g: 255,
2244                       b: 255,
2245                       a: 255,
2246                   },
2247               },
2248           ))),
2249
2250           CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
2251               LayoutDisplay::Block
2252           ))),
2253           CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
2254               StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
2255           ))),
2256           CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
2257               LayoutPaddingTop {
2258                   inner: PixelValue::const_px(10),
2259               },
2260           ))),
2261           CssPropertyWithConditions::simple(CssProperty::PaddingBottom(
2262               LayoutPaddingBottomValue::Exact(LayoutPaddingBottom {
2263                   inner: PixelValue::const_px(10),
2264               }),
2265           )),
2266           CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
2267               LayoutPaddingLeft {
2268                   inner: PixelValue::const_px(10),
2269               },
2270           ))),
2271           CssPropertyWithConditions::simple(CssProperty::PaddingRight(
2272               LayoutPaddingRightValue::Exact(LayoutPaddingRight {
2273                   inner: PixelValue::const_px(10),
2274               }),
2275           )),
2276           CssPropertyWithConditions::simple(CssProperty::Transform(StyleTransformVecValue::Exact(
2277               if scale_factor == 1.0 {
2278                    vec![
2279                         StyleTransform::Translate(node_transform)
2280                    ]
2281               } else {
2282                    vec![
2283                         StyleTransform::Translate(node_transform),
2284                         StyleTransform::ScaleX(PercentageValue::new(scale_factor * 100.0)),
2285                         StyleTransform::ScaleY(PercentageValue::new(scale_factor * 100.0)),
2286                    ]
2287               }.into()
2288           ))),
2289           CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
2290               LayoutWidth::Px(PixelValue::const_px(250),),
2291           ))),
2292        ].into())
2293        .with_ids_and_classes({
2294           const IDS_AND_CLASSES_4480169002427296613: &[IdOrClass] =
2295               &[Class(AzString::from_const_str("node_graph_node"))];
2296           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_4480169002427296613)
2297        })
2298        .with_children(DomVec::from_vec(vec![
2299           Dom::create_text(AzString::from_const_str("X"))
2300               .with_css_props(CSS_MATCH_7395766480280098891)
2301               .with_callbacks(vec![
2302                   CoreCallbackData {
2303                       event: EventFilter::Hover(HoverEventFilter::MouseUp),
2304                       refany: node_local_dataset.clone(),
2305                       callback: CoreCallback { cb: nodegraph_delete_node as usize, ctx: OptionRefAny::None },
2306                   },
2307               ].into())
2308               .with_ids_and_classes({
2309                   const IDS_AND_CLASSES_7122017923389407516: &[IdOrClass] =
2310                       &[Class(AzString::from_const_str("node_close_button"))];
2311                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_7122017923389407516)
2312               }),
2313           Dom::create_text(node_info.node_type_name.clone())
2314               .with_css_props(CSS_MATCH_1739273067404038547)
2315               .with_ids_and_classes({
2316                   const IDS_AND_CLASSES_15777790571346582635: &[IdOrClass] =
2317                       &[Class(AzString::from_const_str("node_label"))];
2318                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_15777790571346582635)
2319               }),
2320           Dom::create_div()
2321               .with_css_props(CSS_MATCH_3354247437065914166)
2322               .with_ids_and_classes({
2323                   const IDS_AND_CLASSES_5590500152394859708: &[IdOrClass] =
2324                       &[Class(AzString::from_const_str("node_body"))];
2325                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_5590500152394859708)
2326               })
2327               .with_children(DomVec::from_vec(vec![
2328                   Dom::create_div()
2329                       .with_css_props(CSS_MATCH_16946967739775705757)
2330                       .with_ids_and_classes({
2331                           const IDS_AND_CLASSES_3626404106673061698: &[IdOrClass] =
2332                               &[Class(AzString::from_const_str("inputs"))];
2333                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_3626404106673061698)
2334                       })
2335                       .with_children(DomVec::from_vec(vec![Dom::create_div()
2336                           .with_css_props(CSS_MATCH_705881630351954657)
2337                           .with_ids_and_classes({
2338                               const IDS_AND_CLASSES_12825690349660780627: &[IdOrClass] =
2339                                   &[Class(AzString::from_const_str("node_input_wrapper"))];
2340                               IdOrClassVec::from_const_slice(
2341                                   IDS_AND_CLASSES_12825690349660780627,
2342                               )
2343                           })
2344                           .with_children(DomVec::from_vec(
2345                               inputs
2346                               .into_iter()
2347                               .enumerate()
2348                               .map(|(io_id, (input_label, input_color))| {
2349                                   use self::InputOrOutput::Input;
2350
2351                                   Dom::create_div()
2352                                       .with_css_props(CSS_MATCH_9863994880298313101)
2353                                       .with_ids_and_classes({
2354                                           const IDS_AND_CLASSES_5020681879750641508:
2355                                               &[IdOrClass] = &[Class(AzString::from_const_str(
2356                                               "node_input_container",
2357                                           ))];
2358                                           IdOrClassVec::from_const_slice(
2359                                               IDS_AND_CLASSES_5020681879750641508,
2360                                           )
2361                                       })
2362                                       .with_children(DomVec::from_vec(vec![
2363                                           Dom::create_div()
2364                                               .with_css_props(
2365                                                   CSS_MATCH_4700400755767504372,
2366                                               )
2367                                               .with_ids_and_classes({
2368                                                   const IDS_AND_CLASSES_9154857442066749879:
2369                                                       &[IdOrClass] =
2370                                                       &[Class(AzString::from_const_str(
2371                                                           "node_input_connection_label_wrapper",
2372                                                       ))];
2373                                                   IdOrClassVec::from_const_slice(
2374                                                       IDS_AND_CLASSES_9154857442066749879,
2375                                                   )
2376                                               })
2377                                               .with_children(DomVec::from_vec(vec![Dom::create_text(
2378                                                   input_label,
2379                                               )
2380                                               .with_css_props(
2381                                                   CSS_MATCH_11452431279102104133,
2382                                               )
2383                                               .with_ids_and_classes({
2384                                                   const IDS_AND_CLASSES_16291496011772407931:
2385                                                       &[IdOrClass] =
2386                                                       &[Class(AzString::from_const_str(
2387                                                           "node_input_connection_label",
2388                                                       ))];
2389                                                   IdOrClassVec::from_const_slice(
2390                                                       IDS_AND_CLASSES_16291496011772407931,
2391                                                   )
2392                                               })])),
2393                                           Dom::create_div()
2394                                               .with_callbacks(vec![
2395                                                   CoreCallbackData {
2396                                                       event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
2397                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
2398                                                           io_id: Input(io_id),
2399                                                           backref: node_local_dataset.clone(),
2400                                                       }),
2401                                                       callback: CoreCallback { cb: nodegraph_input_output_connect as usize, ctx: OptionRefAny::None },
2402                                                   },
2403                                                   CoreCallbackData {
2404                                                       event: EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
2405                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
2406                                                           io_id: Input(io_id),
2407                                                           backref: node_local_dataset.clone(),
2408                                                       }),
2409                                                       callback: CoreCallback { cb: nodegraph_input_output_disconnect as usize, ctx: OptionRefAny::None },
2410                                                   },
2411                                               ].into())
2412                                               .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
2413                                                       // .node_input
2414                                                       CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
2415                                                           StyleBackgroundContentVecValue::Exact(vec![StyleBackgroundContent::Color(input_color)].into()),
2416                                                       )),
2417                                                       CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
2418                                                           StyleCursor::Pointer,
2419                                                       ))),
2420                                                       CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
2421                                                           LayoutHeight::Px(PixelValue::const_px(15),),
2422                                                       ))),
2423                                                       CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
2424                                                           LayoutWidth::Px(PixelValue::const_px(15),),
2425                                                       ))),
2426                                                   ])
2427                                               )
2428                                               .with_ids_and_classes({
2429                                                   const IDS_AND_CLASSES_2128818677168244823:
2430                                                       &[IdOrClass] = &[Class(
2431                                                       AzString::from_const_str("node_input"),
2432                                                   )];
2433                                                   IdOrClassVec::from_const_slice(
2434                                                       IDS_AND_CLASSES_2128818677168244823,
2435                                                   )
2436                                               }),
2437                                       ]))
2438                               }).collect()
2439                           ))
2440                       ])),
2441                   Dom::create_div()
2442                       .with_css_props(CSS_MATCH_7432473243011547380)
2443                       .with_ids_and_classes({
2444                           const IDS_AND_CLASSES_746059979773622802: &[IdOrClass] =
2445                               &[Class(AzString::from_const_str("node_content_wrapper"))];
2446                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_746059979773622802)
2447                       })
2448                       .with_children({
2449
2450                           let mut fields = Vec::new();
2451
2452                           for (field_idx, field) in node.fields.iter().enumerate() {
2453
2454                               let field_local_dataset = RefAny::new(NodeFieldLocalDataset {
2455                                   field_idx,
2456                                   backref: node_local_dataset.clone(),
2457                               });
2458
2459                               let div = Dom::create_div()
2460                               .with_css_props(CSS_MATCH_2639191696846875011)
2461                               .with_ids_and_classes({
2462                                   const IDS_AND_CLASSES_4413230059125905311: &[IdOrClass] =
2463                                       &[Class(AzString::from_const_str(
2464                                           "node_configuration_field_container",
2465                                       ))];
2466                                   IdOrClassVec::from_const_slice(
2467                                       IDS_AND_CLASSES_4413230059125905311,
2468                                   )
2469                               })
2470                               .with_children(DomVec::from_vec(vec![
2471                                   Dom::create_text(field.key.clone())
2472                                   .with_css_props(CSS_MATCH_1198521124955124418)
2473                                   .with_ids_and_classes({
2474                                       const IDS_AND_CLASSES_12334207996395559585:
2475                                           &[IdOrClass] =
2476                                           &[Class(AzString::from_const_str(
2477                                               "node_configuration_field_label",
2478                                           ))];
2479                                       IdOrClassVec::from_const_slice(
2480                                           IDS_AND_CLASSES_12334207996395559585,
2481                                       )
2482                                   }),
2483
2484                                   match &field.value {
2485                                       NodeTypeFieldValue::TextInput(initial_text) => {
2486                                           let cb: TextInputOnFocusLostCallbackType = nodegraph_on_textinput_focus_lost;
2487                                           TextInput::create()
2488                                           .with_text(initial_text.clone())
2489                                           .with_on_focus_lost(field_local_dataset, cb)
2490                                           .dom()
2491                                       },
2492                                       NodeTypeFieldValue::NumberInput(initial_value) => {
2493                                           let cb: NumberInputOnFocusLostCallbackType = nodegraph_on_numberinput_focus_lost;
2494                                           NumberInput::create(*initial_value)
2495                                           .with_on_focus_lost(field_local_dataset, cb)
2496                                           .dom()
2497                                       },
2498                                       NodeTypeFieldValue::CheckBox(initial_checked) => {
2499                                           let cb: CheckBoxOnToggleCallbackType = nodegraph_on_checkbox_value_changed;
2500                                           CheckBox::create(*initial_checked)
2501                                           .with_on_toggle(field_local_dataset, cb)
2502                                           .dom()
2503                                       },
2504                                       NodeTypeFieldValue::ColorInput(initial_color) => {
2505                                           let cb: ColorInputOnValueChangeCallbackType = nodegraph_on_colorinput_value_changed;
2506                                           ColorInput::create(*initial_color)
2507                                           .with_on_value_change(field_local_dataset, cb)
2508                                           .dom()
2509                                       },
2510                                       NodeTypeFieldValue::FileInput(file_path) => {
2511                                           let cb: FileInputOnPathChangeCallbackType = nodegraph_on_fileinput_button_clicked;
2512                                           FileInput::create(file_path.clone())
2513                                           .with_on_path_change(field_local_dataset, cb)
2514                                           .dom()
2515                                       },
2516                                   }
2517                               ]));
2518
2519                               fields.push(div);
2520                           }
2521
2522                           DomVec::from_vec(fields)
2523                       }),
2524                   Dom::create_div()
2525                       .with_css_props(CSS_MATCH_14906563417280941890)
2526                       .with_ids_and_classes({
2527                           const IDS_AND_CLASSES_4737474624251936466: &[IdOrClass] =
2528                               &[Class(AzString::from_const_str("outputs"))];
2529                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_4737474624251936466)
2530                       })
2531                       .with_children(DomVec::from_vec(vec![Dom::create_div()
2532                           .with_css_props(CSS_MATCH_10339190304804100510)
2533                           .with_ids_and_classes({
2534                               const IDS_AND_CLASSES_12883576328110161157: &[IdOrClass] =
2535                                   &[Class(AzString::from_const_str("node_output_wrapper"))];
2536                               IdOrClassVec::from_const_slice(
2537                                   IDS_AND_CLASSES_12883576328110161157,
2538                               )
2539                           })
2540                           .with_children(DomVec::from_vec(
2541                               outputs
2542                               .into_iter()
2543                               .enumerate()
2544                               .map(|(io_id, (output_label, output_color))| {
2545                                   use self::InputOrOutput::Output;
2546                                   Dom::create_div()
2547                                       .with_css_props(CSS_MATCH_12400244273289328300)
2548                                       .with_ids_and_classes({
2549                                           const IDS_AND_CLASSES_10917819668096233812:
2550                                               &[IdOrClass] = &[Class(AzString::from_const_str(
2551                                               "node_output_container",
2552                                           ))];
2553                                           IdOrClassVec::from_const_slice(
2554                                               IDS_AND_CLASSES_10917819668096233812,
2555                                           )
2556                                       })
2557                                       .with_children(DomVec::from_vec(vec![
2558                                           Dom::create_div()
2559                                               .with_callbacks(vec![
2560                                                   CoreCallbackData {
2561                                                       event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
2562                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
2563                                                           io_id: Output(io_id),
2564                                                           backref: node_local_dataset.clone(),
2565                                                       }),
2566                                                       callback: CoreCallback { cb: nodegraph_input_output_connect as usize, ctx: OptionRefAny::None },
2567                                                   },
2568                                                   CoreCallbackData {
2569                                                       event: EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
2570                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
2571                                                           io_id: Output(io_id),
2572                                                           backref: node_local_dataset.clone(),
2573                                                       }),
2574                                                       callback: CoreCallback { cb: nodegraph_input_output_disconnect as usize, ctx: OptionRefAny::None },
2575                                                   },
2576                                               ].into())
2577                                               .with_css_props(
2578                                                   CssPropertyWithConditionsVec::from_vec(vec![
2579                                                       // .node_output
2580                                                       CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
2581                                                           StyleBackgroundContentVecValue::Exact(vec![
2582                                                               StyleBackgroundContent::Color(output_color)
2583                                                           ].into()),
2584                                                       )),
2585                                                       CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
2586                                                           StyleCursor::Pointer,
2587                                                       ))),
2588                                                       CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
2589                                                           LayoutHeight::Px(PixelValue::const_px(15),),
2590                                                       ))),
2591                                                       CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
2592                                                           LayoutWidth::Px(PixelValue::const_px(15),),
2593                                                       ))),
2594                                                   ])
2595                                               )
2596                                               .with_ids_and_classes({
2597                                                   const IDS_AND_CLASSES_17632471664405317563:
2598                                                       &[IdOrClass] = &[Class(
2599                                                       AzString::from_const_str("node_output"),
2600                                                   )];
2601                                                   IdOrClassVec::from_const_slice(
2602                                                       IDS_AND_CLASSES_17632471664405317563,
2603                                                   )
2604                                               }),
2605                                           Dom::create_div()
2606                                               .with_css_props(
2607                                                   CSS_MATCH_12038890904436132038,
2608                                               )
2609                                               .with_ids_and_classes({
2610                                                   const IDS_AND_CLASSES_1667960214206134147:
2611                                                       &[IdOrClass] =
2612                                                       &[Class(AzString::from_const_str(
2613                                                           "node_output_connection_label_wrapper",
2614                                                       ))];
2615                                                   IdOrClassVec::from_const_slice(
2616                                                       IDS_AND_CLASSES_1667960214206134147,
2617                                                   )
2618                                               })
2619                                               .with_children(DomVec::from_vec(vec![Dom::create_text(
2620                                                   output_label,
2621                                               )
2622                                               .with_css_props(
2623                                                   CSS_MATCH_2008162367868363199,
2624                                               )
2625                                               .with_ids_and_classes({
2626                                                   const IDS_AND_CLASSES_2974914452796301884:
2627                                                       &[IdOrClass] =
2628                                                       &[Class(AzString::from_const_str(
2629                                                           "node_output_connection_label",
2630                                                       ))];
2631                                                   IdOrClassVec::from_const_slice(
2632                                                       IDS_AND_CLASSES_2974914452796301884,
2633                                                   )
2634                                               })])),
2635                                       ]))
2636                               }).collect()
2637                           ))])),
2638               ])),
2639        ]))
2640        .with_dataset(Some(node_local_dataset).into())
2641    ].into())
2642}
2643
2644#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
2645fn render_connections(node_graph: &NodeGraph, root_marker_nodedata: RefAny) -> Dom {
2646    static NODEGRAPH_CONNECTIONS_CONTAINER_CLASS: &[IdOrClass] = &[Class(
2647        AzString::from_const_str("nodegraph-connections-container"),
2648    )];
2649
2650    static NODEGRAPH_CONNECTIONS_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
2651        CssPropertyWithConditions::simple(CssProperty::position(LayoutPosition::Absolute)),
2652        CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
2653    ];
2654
2655    Dom::create_div()
2656        .with_ids_and_classes(IdOrClassVec::from_const_slice(
2657            NODEGRAPH_CONNECTIONS_CONTAINER_CLASS,
2658        ))
2659        .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
2660            NODEGRAPH_CONNECTIONS_CONTAINER_PROPS,
2661        ))
2662        .with_dataset(Some(root_marker_nodedata).into())
2663        .with_children({
2664            let mut children = Vec::new();
2665
2666            for NodeIdNodeMap { node_id, node } in node_graph.nodes.as_ref() {
2667                let out_node_id = node_id;
2668                let node_type_info = match node_graph
2669                    .node_types
2670                    .iter()
2671                    .find(|i| i.node_type_id == node.node_type)
2672                {
2673                    Some(s) => &s.node_type_info,
2674                    None => continue,
2675                };
2676
2677                for OutputConnection {
2678                    output_index,
2679                    connects_to,
2680                } in node.connect_out.as_ref()
2681                {
2682                    let Some(output_type_id) = node_type_info.outputs.get(*output_index) else {
2683                        continue;
2684                    };
2685
2686                    let output_color = match node_graph
2687                        .input_output_types
2688                        .iter()
2689                        .find(|o| o.io_type_id == *output_type_id)
2690                    {
2691                        Some(s) => s.io_info.color,
2692                        None => continue,
2693                    };
2694
2695                    for InputNodeAndIndex {
2696                        node_id,
2697                        input_index,
2698                    } in connects_to.as_ref()
2699                    {
2700                        let in_node_id = node_id;
2701
2702                        let mut cld = ConnectionLocalDataset {
2703                            out_node_id: *out_node_id,
2704                            out_idx: *output_index,
2705                            in_node_id: *in_node_id,
2706                            in_idx: *input_index,
2707                            swap_vert: false,
2708                            swap_horz: false,
2709                            color: output_color,
2710                        };
2711
2712                        let Some((rect, swap_vert, swap_horz)) = get_rect(node_graph, cld) else {
2713                            continue;
2714                        };
2715
2716                        cld.swap_vert = swap_vert;
2717                        cld.swap_horz = swap_horz;
2718
2719                        let cld_refany = RefAny::new(cld);
2720                        let connection_div = Dom::create_image(ImageRef::callback(
2721                            draw_connection as usize,
2722                            cld_refany.clone(),
2723                        ))
2724                        .with_dataset(Some(cld_refany).into())
2725                        .with_css_props(
2726                            vec![
2727                                CssPropertyWithConditions::simple(CssProperty::Transform(
2728                                    StyleTransformVecValue::Exact(
2729                                        vec![
2730                                            StyleTransform::Translate(StyleTransformTranslate2D {
2731                                                x: PixelValue::px(
2732                                                    node_graph.offset.x + rect.origin.x,
2733                                                ),
2734                                                y: PixelValue::px(
2735                                                    node_graph.offset.y + rect.origin.y,
2736                                                ),
2737                                            }),
2738                                            StyleTransform::ScaleX(PercentageValue::new(
2739                                                node_graph.scale_factor * 100.0,
2740                                            )),
2741                                            StyleTransform::ScaleY(PercentageValue::new(
2742                                                node_graph.scale_factor * 100.0,
2743                                            )),
2744                                        ]
2745                                        .into(),
2746                                    ),
2747                                )),
2748                                CssPropertyWithConditions::simple(CssProperty::Width(
2749                                    LayoutWidthValue::Exact(LayoutWidth::Px(PixelValue::px(
2750                                        rect.size.width,
2751                                    ))),
2752                                )),
2753                                CssPropertyWithConditions::simple(CssProperty::Height(
2754                                    LayoutHeightValue::Exact(LayoutHeight::Px(PixelValue::px(
2755                                        rect.size.height,
2756                                    ))),
2757                                )),
2758                            ]
2759                            .into(),
2760                        );
2761
2762                        children.push(
2763                            Dom::create_div()
2764                                .with_css(
2765                                    "flex-grow: 1; position: absolute; overflow: hidden;",
2766                                )
2767                                .with_children(vec![connection_div].into()),
2768                        );
2769                    }
2770                }
2771            }
2772
2773            children.into()
2774        })
2775}
2776
2777#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded layout/render numeric cast
2778extern "C" fn draw_connection(mut refany: RefAny, _info: ()) -> ImageRef {
2779    // RenderImageCallbackInfo not available in memtest
2780    // let size = info.get_bounds().get_physical_size();
2781    let size = LogicalSize {
2782        width: 100.0,
2783        height: 100.0,
2784    };
2785    
2786
2787    // Cannot call draw_connection_inner without RenderImageCallbackInfo
2788    ImageRef::null_image(
2789        size.width as usize,
2790        size.height as usize,
2791        RawImageFormat::R8,
2792        Vec::new(),
2793    )
2794}
2795
2796const NODE_WIDTH: f32 = 250.0;
2797const V_OFFSET: f32 = 71.0;
2798const DIST_BETWEEN_NODES: f32 = 10.0;
2799const CONNECTION_DOT_HEIGHT: f32 = 15.0;
2800
2801// calculates the rect on which the connection is drawn in the UI
2802#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2803#[allow(clippy::cast_precision_loss)] // bounded layout/render numeric cast
2804fn get_rect(
2805    node_graph: &NodeGraph,
2806    connection: ConnectionLocalDataset,
2807) -> Option<(LogicalRect, bool, bool)> {
2808    let ConnectionLocalDataset {
2809        out_node_id,
2810        out_idx,
2811        in_node_id,
2812        in_idx,
2813        ..
2814    } = connection;
2815    let out_node = node_graph.nodes.iter().find(|i| i.node_id == out_node_id)?;
2816    let in_node = node_graph.nodes.iter().find(|i| i.node_id == in_node_id)?;
2817
2818    let x_out = out_node.node.position.x + NODE_WIDTH;
2819    let y_out = out_node.node.position.y
2820        + V_OFFSET
2821        + (out_idx as f32 * (DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT));
2822
2823    let x_in = in_node.node.position.x;
2824    let y_in = in_node.node.position.y
2825        + V_OFFSET
2826        + (in_idx as f32 * (DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT));
2827
2828    let should_swap_vertical = y_in > y_out;
2829    let should_swap_horizontal = x_in < x_out;
2830
2831    let width = (x_in - x_out).abs();
2832    let height = (y_in - y_out).abs() + CONNECTION_DOT_HEIGHT;
2833
2834    let x = x_in.min(x_out);
2835    let y = y_in.min(y_out);
2836
2837    Some((
2838        LogicalRect {
2839            size: LogicalSize { width, height },
2840            origin: LogicalPosition { x, y },
2841        },
2842        should_swap_vertical,
2843        should_swap_horizontal,
2844    ))
2845}
2846
2847extern "C" fn nodegraph_set_active_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
2848    let data_clone = refany.clone();
2849    if let Some(mut refany) = refany.downcast_mut::<NodeLocalDataset>() {
2850        let node_id = refany.node_id;
2851        if let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() {
2852            backref.active_node_being_dragged = Some((node_id, data_clone));
2853        }
2854    }
2855    Update::DoNothing
2856}
2857
2858extern "C" fn nodegraph_unset_active_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
2859    if let Some(mut refany) = refany.downcast_mut::<NodeGraphLocalDataset>() {
2860        refany.active_node_being_dragged = None;
2861    }
2862    Update::DoNothing
2863}
2864
2865// drag either the graph or the currently active nodes
2866#[allow(clippy::float_cmp)] // intentional exact compare: change-detection / identity fast-path / cache-key match
2867#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
2868#[allow(clippy::single_match_else)] // drag-node (Some) and drag-graph (None) are each ~135-line blocks; match labels the two modes far more clearly than if-let/else
2869extern "C" fn nodegraph_drag_graph_or_nodes(mut refany: RefAny, mut info: CallbackInfo) -> Update {
2870    let Some(mut refany) = refany.downcast_mut::<NodeGraphLocalDataset>() else {
2871        return Update::DoNothing;
2872    };
2873    let refany = &mut *refany;
2874
2875    let Some(prev) = info.get_previous_mouse_state() else {
2876        return Update::DoNothing;
2877    };
2878    let cur = info.get_current_mouse_state();
2879    if !(cur.left_down && prev.left_down) {
2880        // event is not a drag event
2881        return Update::DoNothing;
2882    }
2883
2884    let (InWindow(current_mouse_pos), InWindow(previous_mouse_pos)) =
2885        (cur.cursor_position, prev.cursor_position)
2886    else {
2887        return Update::DoNothing;
2888    };
2889
2890    let dx = (current_mouse_pos.x - previous_mouse_pos.x) * (1.0 / refany.node_graph.scale_factor);
2891    let dy = (current_mouse_pos.y - previous_mouse_pos.y) * (1.0 / refany.node_graph.scale_factor);
2892    let nodegraph_node = info.get_hit_node();
2893
2894    let should_update = match refany.active_node_being_dragged.clone() {
2895        // drag node
2896        Some((node_graph_node_id, data_marker)) => {
2897            let node_connection_marker = &mut refany.node_connection_marker;
2898
2899            let _nodegraph_node = info.get_hit_node();
2900            let result = match refany.callbacks.on_node_dragged.as_ref() {
2901                Some(OnNodeDragged { callback, refany }) => (callback.cb)(
2902                    refany.clone(),
2903                    info,
2904                    node_graph_node_id,
2905                    NodeDragAmount { x: dx, y: dy },
2906                ),
2907                None => Update::DoNothing,
2908            };
2909
2910            // update the visual transform of the node in the UI
2911            let node_position = match refany
2912                .node_graph
2913                .nodes
2914                .iter_mut()
2915                .find(|i| i.node_id == node_graph_node_id)
2916            {
2917                Some(s) => {
2918                    s.node.position.x += dx;
2919                    s.node.position.y += dy;
2920                    s.node.position
2921                }
2922                None => return Update::DoNothing,
2923            };
2924
2925            let Some(visual_node_id) = info.get_node_id_of_root_dataset(data_marker) else {
2926                return Update::DoNothing;
2927            };
2928
2929            let node_transform = StyleTransformTranslate2D {
2930                x: PixelValue::px(node_position.x + refany.node_graph.offset.x),
2931                y: PixelValue::px(node_position.y + refany.node_graph.offset.y),
2932            };
2933
2934            info.set_css_property(
2935                visual_node_id,
2936                CssProperty::transform(
2937                    if refany.node_graph.scale_factor == 1.0 {
2938                        vec![StyleTransform::Translate(node_transform)]
2939                    } else {
2940                        vec![
2941                            StyleTransform::Translate(node_transform),
2942                            StyleTransform::ScaleX(PercentageValue::new(
2943                                refany.node_graph.scale_factor * 100.0,
2944                            )),
2945                            StyleTransform::ScaleY(PercentageValue::new(
2946                                refany.node_graph.scale_factor * 100.0,
2947                            )),
2948                        ]
2949                    }
2950                    .into(),
2951                ),
2952            );
2953
2954            // get the NodeId of the node containing all the connection lines
2955            let Some(connection_container_nodeid) =
2956                info.get_node_id_of_root_dataset(node_connection_marker.clone())
2957            else {
2958                return result;
2959            };
2960
2961            // animate all the connections
2962            let mut first_connection_child = info.get_first_child(connection_container_nodeid);
2963
2964            while let Some(connection_nodeid) = first_connection_child {
2965                first_connection_child = info.get_next_sibling(connection_nodeid);
2966
2967                let Some(first_child) = info.get_first_child(connection_nodeid) else {
2968                    continue;
2969                };
2970
2971                let Some(mut dataset) = info.get_dataset(first_child) else {
2972                    continue;
2973                };
2974
2975                let Some(mut cld) = dataset.downcast_mut::<ConnectionLocalDataset>() else {
2976                    continue;
2977                };
2978
2979                if !(cld.out_node_id == node_graph_node_id || cld.in_node_id == node_graph_node_id)
2980                {
2981                    continue; // connection does not need to be modified
2982                }
2983
2984                let Some((new_rect, swap_vert, swap_horz)) = get_rect(&refany.node_graph, *cld)
2985                else {
2986                    continue;
2987                };
2988
2989                cld.swap_vert = swap_vert;
2990                cld.swap_horz = swap_horz;
2991
2992                let node_transform = StyleTransformTranslate2D {
2993                    x: PixelValue::px(refany.node_graph.offset.x + new_rect.origin.x),
2994                    y: PixelValue::px(refany.node_graph.offset.y + new_rect.origin.y),
2995                };
2996
2997                info.set_css_property(
2998                    first_child,
2999                    CssProperty::transform(
3000                        if refany.node_graph.scale_factor == 1.0 {
3001                            vec![StyleTransform::Translate(node_transform)]
3002                        } else {
3003                            vec![
3004                                StyleTransform::Translate(node_transform),
3005                                StyleTransform::ScaleX(PercentageValue::new(
3006                                    refany.node_graph.scale_factor * 100.0,
3007                                )),
3008                                StyleTransform::ScaleY(PercentageValue::new(
3009                                    refany.node_graph.scale_factor * 100.0,
3010                                )),
3011                            ]
3012                        }
3013                        .into(),
3014                    ),
3015                );
3016
3017                info.set_css_property(
3018                    first_child,
3019                    CssProperty::Width(LayoutWidthValue::Exact(LayoutWidth::Px(PixelValue::px(
3020                        new_rect.size.width,
3021                    )))),
3022                );
3023                info.set_css_property(
3024                    first_child,
3025                    CssProperty::Height(LayoutHeightValue::Exact(LayoutHeight::Px(
3026                        PixelValue::px(new_rect.size.height),
3027                    ))),
3028                );
3029            }
3030
3031            result
3032        }
3033        // drag graph
3034        None => {
3035            let result = match refany.callbacks.on_node_graph_dragged.as_ref() {
3036                Some(OnNodeGraphDragged { callback, refany }) => (callback.cb)(
3037                    refany.clone(),
3038                    info,
3039                    GraphDragAmount { x: dx, y: dy },
3040                ),
3041                None => Update::DoNothing,
3042            };
3043
3044            refany.node_graph.offset.x += dx;
3045            refany.node_graph.offset.y += dy;
3046
3047            // Update the visual node positions
3048            let Some(node_container) = info.get_first_child(nodegraph_node) else {
3049                return Update::DoNothing;
3050            };
3051
3052            let Some(node_container) = info.get_next_sibling(node_container) else {
3053                return Update::DoNothing;
3054            };
3055
3056            let Some(mut node) = info.get_first_child(node_container) else {
3057                return Update::DoNothing;
3058            };
3059
3060            loop {
3061                let Some(node_first_child) = info.get_first_child(node) else {
3062                    return Update::DoNothing;
3063                };
3064
3065                let mut node_local_dataset = match info.get_dataset(node_first_child) {
3066                    None => return Update::DoNothing,
3067                    Some(s) => s,
3068                };
3069
3070                let Some(node_graph_node_id) =
3071                    node_local_dataset.downcast_ref::<NodeLocalDataset>()
3072                else {
3073                    continue;
3074                };
3075
3076                let node_graph_node_id = node_graph_node_id.node_id;
3077
3078                let node_position = match refany
3079                    .node_graph
3080                    .nodes
3081                    .iter()
3082                    .find(|i| i.node_id == node_graph_node_id)
3083                {
3084                    Some(s) => s.node.position,
3085                    None => continue,
3086                };
3087
3088                let node_transform = StyleTransformTranslate2D {
3089                    x: PixelValue::px(node_position.x + refany.node_graph.offset.x),
3090                    y: PixelValue::px(node_position.y + refany.node_graph.offset.y),
3091                };
3092
3093                info.set_css_property(
3094                    node_first_child,
3095                    CssProperty::transform(
3096                        if refany.node_graph.scale_factor == 1.0 {
3097                            vec![StyleTransform::Translate(node_transform)]
3098                        } else {
3099                            vec![
3100                                StyleTransform::Translate(node_transform),
3101                                StyleTransform::ScaleX(PercentageValue::new(
3102                                    refany.node_graph.scale_factor * 100.0,
3103                                )),
3104                                StyleTransform::ScaleY(PercentageValue::new(
3105                                    refany.node_graph.scale_factor * 100.0,
3106                                )),
3107                            ]
3108                        }
3109                        .into(),
3110                    ),
3111                );
3112
3113                node = match info.get_next_sibling(node) {
3114                    Some(s) => s,
3115                    None => break,
3116                };
3117            }
3118
3119            let node_connection_marker = &mut refany.node_connection_marker;
3120
3121            // Update the connection positions
3122            let Some(connection_container_nodeid) =
3123                info.get_node_id_of_root_dataset(node_connection_marker.clone())
3124            else {
3125                return result;
3126            };
3127
3128            let mut first_connection_child = info.get_first_child(connection_container_nodeid);
3129
3130            while let Some(connection_nodeid) = first_connection_child {
3131                first_connection_child = info.get_next_sibling(connection_nodeid);
3132
3133                let Some(first_child) = info.get_first_child(connection_nodeid) else {
3134                    continue;
3135                };
3136
3137                let Some(mut dataset) = info.get_dataset(first_child) else {
3138                    continue;
3139                };
3140
3141                let Some(cld) = dataset.downcast_ref::<ConnectionLocalDataset>() else {
3142                    continue;
3143                };
3144
3145                let Some((new_rect, _, _)) = get_rect(&refany.node_graph, *cld) else {
3146                    continue;
3147                };
3148
3149                info.set_css_property(
3150                    first_child,
3151                    CssProperty::transform(
3152                        vec![
3153                            StyleTransform::Translate(StyleTransformTranslate2D {
3154                                x: PixelValue::px(refany.node_graph.offset.x + new_rect.origin.x),
3155                                y: PixelValue::px(refany.node_graph.offset.y + new_rect.origin.y),
3156                            }),
3157                            StyleTransform::ScaleX(PercentageValue::new(
3158                                refany.node_graph.scale_factor * 100.0,
3159                            )),
3160                            StyleTransform::ScaleY(PercentageValue::new(
3161                                refany.node_graph.scale_factor * 100.0,
3162                            )),
3163                        ]
3164                        .into(),
3165                    ),
3166                );
3167            }
3168
3169            result
3170        }
3171    };
3172
3173    info.stop_propagation();
3174
3175    should_update
3176}
3177
3178extern "C" fn nodegraph_duplicate_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
3179    let Some(_data) = refany.downcast_mut::<NodeLocalDataset>() else {
3180        return Update::DoNothing;
3181    };
3182
3183    Update::DoNothing // TODO
3184}
3185
3186extern "C" fn nodegraph_delete_node(mut refany: RefAny, mut info: CallbackInfo) -> Update {
3187    let Some(mut refany) = refany.downcast_mut::<NodeLocalDataset>() else {
3188        return Update::DoNothing;
3189    };
3190
3191    let node_id = refany.node_id;
3192
3193    let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() else {
3194        return Update::DoNothing;
3195    };
3196
3197    let result = match backref.callbacks.on_node_removed.as_ref() {
3198        Some(OnNodeRemoved { callback, refany }) => (callback.cb)(refany.clone(), info, node_id),
3199        None => Update::DoNothing,
3200    };
3201
3202    result
3203}
3204
3205#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
3206#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
3207extern "C" fn nodegraph_context_menu_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
3208    use azul_core::window::CursorPosition;
3209
3210    let Some(mut refany) = refany.downcast_mut::<ContextMenuEntryLocalDataset>() else {
3211        return Update::DoNothing;
3212    };
3213
3214    let new_node_type = refany.node_type;
3215
3216    let Some(node_graph_wrapper_id) = info.get_node_id_of_root_dataset(refany.backref.clone())
3217    else {
3218        return Update::DoNothing;
3219    };
3220
3221    let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() else {
3222        return Update::DoNothing;
3223    };
3224
3225    let node_wrapper_offset = info
3226        .get_node_position(node_graph_wrapper_id)
3227        .map_or((0.0, 0.0), |p| (p.x, p.y));
3228
3229    let cursor_in_viewport = match info.get_current_mouse_state().cursor_position {
3230        InWindow(i) => i,
3231        CursorPosition::OutOfWindow(i) => i,
3232        CursorPosition::Uninitialized => LogicalPosition::zero(),
3233    };
3234
3235    let new_node_pos = NodeGraphNodePosition {
3236        x: (cursor_in_viewport.x - node_wrapper_offset.0) * (1.0 / backref.node_graph.scale_factor)
3237            - backref.node_graph.offset.x,
3238        y: (cursor_in_viewport.y - node_wrapper_offset.1) * (1.0 / backref.node_graph.scale_factor)
3239            - backref.node_graph.offset.y,
3240    };
3241
3242    let new_node_id = backref.node_graph.generate_unique_node_id();
3243
3244    let result = match backref.callbacks.on_node_added.as_ref() {
3245        Some(OnNodeAdded { callback, refany }) => (callback.cb)(
3246            refany.clone(),
3247            info,
3248            new_node_type,
3249            new_node_id,
3250            new_node_pos,
3251        ),
3252        None => Update::DoNothing,
3253    };
3254
3255    result
3256}
3257
3258extern "C" fn nodegraph_input_output_connect(mut refany: RefAny, mut info: CallbackInfo) -> Update {
3259    use self::InputOrOutput::{Input, Output};
3260
3261    let Some(mut refany) = refany.downcast_mut::<NodeInputOutputLocalDataset>() else {
3262        return Update::DoNothing;
3263    };
3264
3265    let io_id = refany.io_id;
3266
3267    let Some(mut backref) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3268        return Update::DoNothing;
3269    };
3270
3271    let node_id = backref.node_id;
3272
3273    let Some(mut backref) = backref.backref.downcast_mut::<NodeGraphLocalDataset>() else {
3274        return Update::DoNothing;
3275    };
3276
3277    let (input_node, input_index, output_node, output_index) =
3278        match backref.last_input_or_output_clicked {
3279            None => {
3280                backref.last_input_or_output_clicked = Some((node_id, io_id));
3281                return Update::DoNothing;
3282            }
3283            Some((prev_node_id, prev_io_id)) => {
3284                match (prev_io_id, io_id) {
3285                    (Input(i), Output(o)) => (prev_node_id, i, node_id, o),
3286                    (Output(o), Input(i)) => (node_id, i, prev_node_id, o),
3287                    _ => {
3288                        // error: trying to connect input to input or output to output
3289                        backref.last_input_or_output_clicked = None;
3290                        return Update::DoNothing;
3291                    }
3292                }
3293            }
3294        };
3295
3296    // verify that the nodetype matches
3297    match backref.node_graph.connect_input_output(
3298        input_node,
3299        input_index,
3300        output_node,
3301        output_index,
3302    ) {
3303        Ok(()) => {}
3304        Err(e) => {
3305            eprintln!("{e:?}");
3306            backref.last_input_or_output_clicked = None;
3307            return Update::DoNothing;
3308        }
3309    }
3310
3311    let result = match backref.callbacks.on_node_connected.as_ref() {
3312        Some(OnNodeConnected { callback, refany }) => {
3313            let r = (callback.cb)(
3314                refany.clone(),
3315                info,
3316                input_node,
3317                input_index,
3318                output_node,
3319                output_index,
3320            );
3321            backref.last_input_or_output_clicked = None;
3322            r
3323        }
3324        None => Update::DoNothing,
3325    };
3326
3327    result
3328}
3329
3330extern "C" fn nodegraph_input_output_disconnect(mut refany: RefAny, info: CallbackInfo) -> Update {
3331    use self::InputOrOutput::{Input, Output};
3332
3333    let Some(mut refany) = refany.downcast_mut::<NodeInputOutputLocalDataset>() else {
3334        return Update::DoNothing;
3335    };
3336
3337    let io_id = refany.io_id;
3338
3339    let Some(mut backref) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3340        return Update::DoNothing;
3341    };
3342
3343    let node_id = backref.node_id;
3344
3345    let Some(mut backref) = backref.backref.downcast_mut::<NodeGraphLocalDataset>() else {
3346        return Update::DoNothing;
3347    };
3348
3349    let mut result = Update::DoNothing;
3350    match io_id {
3351        Input(i) => {
3352            result.max_self(
3353                match backref.callbacks.on_node_input_disconnected.as_ref() {
3354                    Some(OnNodeInputDisconnected { callback, refany }) => {
3355                        (callback.cb)(refany.clone(), info, node_id, i)
3356                    }
3357                    None => Update::DoNothing,
3358                },
3359            );
3360        }
3361        Output(o) => {
3362            result.max_self(
3363                match backref.callbacks.on_node_output_disconnected.as_ref() {
3364                    Some(OnNodeOutputDisconnected { callback, refany }) => {
3365                        (callback.cb)(refany.clone(), info, node_id, o)
3366                    }
3367                    None => Update::DoNothing,
3368                },
3369            );
3370        }
3371    }
3372
3373    result
3374}
3375
3376extern "C" fn nodegraph_on_textinput_focus_lost(
3377    mut refany: RefAny,
3378    info: CallbackInfo,
3379    textinputstate: TextInputState,
3380) -> Update {
3381    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
3382        return Update::DoNothing;
3383    };
3384
3385    let field_idx = refany.field_idx;
3386
3387    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3388        return Update::DoNothing;
3389    };
3390
3391    let node_id = node_local_dataset.node_id;
3392
3393    let Some(mut node_graph) = node_local_dataset
3394        .backref
3395        .downcast_mut::<NodeGraphLocalDataset>()
3396    else {
3397        return Update::DoNothing;
3398    };
3399
3400    let node_type = match node_graph
3401        .node_graph
3402        .nodes
3403        .iter()
3404        .find(|i| i.node_id == node_id)
3405    {
3406        Some(s) => s.node.node_type,
3407        None => return Update::DoNothing,
3408    };
3409
3410    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3411        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3412            refany.clone(),
3413            info,
3414            node_id,
3415            field_idx,
3416            node_type,
3417            NodeTypeFieldValue::TextInput(textinputstate.get_text().into()),
3418        ),
3419        None => Update::DoNothing,
3420    };
3421
3422    result
3423}
3424
3425extern "C" fn nodegraph_on_numberinput_focus_lost(
3426    mut refany: RefAny,
3427    info: CallbackInfo,
3428    numberinputstate: NumberInputState,
3429) -> Update {
3430    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
3431        return Update::DoNothing;
3432    };
3433
3434    let field_idx = refany.field_idx;
3435
3436    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3437        return Update::DoNothing;
3438    };
3439
3440    let node_id = node_local_dataset.node_id;
3441
3442    let Some(mut node_graph) = node_local_dataset
3443        .backref
3444        .downcast_mut::<NodeGraphLocalDataset>()
3445    else {
3446        return Update::DoNothing;
3447    };
3448
3449    let node_type = match node_graph
3450        .node_graph
3451        .nodes
3452        .iter()
3453        .find(|i| i.node_id == node_id)
3454    {
3455        Some(s) => s.node.node_type,
3456        None => return Update::DoNothing,
3457    };
3458
3459    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3460        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3461            refany.clone(),
3462            info,
3463            node_id,
3464            field_idx,
3465            node_type,
3466            NodeTypeFieldValue::NumberInput(numberinputstate.number),
3467        ),
3468        None => Update::DoNothing,
3469    };
3470
3471    result
3472}
3473
3474extern "C" fn nodegraph_on_checkbox_value_changed(
3475    mut refany: RefAny,
3476    info: CallbackInfo,
3477    checkboxinputstate: CheckBoxState,
3478) -> Update {
3479    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
3480        return Update::DoNothing;
3481    };
3482
3483    let field_idx = refany.field_idx;
3484
3485    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3486        return Update::DoNothing;
3487    };
3488
3489    let node_id = node_local_dataset.node_id;
3490
3491    let Some(mut node_graph) = node_local_dataset
3492        .backref
3493        .downcast_mut::<NodeGraphLocalDataset>()
3494    else {
3495        return Update::DoNothing;
3496    };
3497
3498    let node_type = match node_graph
3499        .node_graph
3500        .nodes
3501        .iter()
3502        .find(|i| i.node_id == node_id)
3503    {
3504        Some(s) => s.node.node_type,
3505        None => return Update::DoNothing,
3506    };
3507
3508    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3509        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3510            refany.clone(),
3511            info,
3512            node_id,
3513            field_idx,
3514            node_type,
3515            NodeTypeFieldValue::CheckBox(checkboxinputstate.checked),
3516        ),
3517        None => Update::DoNothing,
3518    };
3519
3520    result
3521}
3522
3523extern "C" fn nodegraph_on_colorinput_value_changed(
3524    mut refany: RefAny,
3525    info: CallbackInfo,
3526    colorinputstate: ColorInputState,
3527) -> Update {
3528    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
3529        return Update::DoNothing;
3530    };
3531
3532    let field_idx = refany.field_idx;
3533
3534    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3535        return Update::DoNothing;
3536    };
3537
3538    let node_id = node_local_dataset.node_id;
3539    let Some(mut node_graph) = node_local_dataset
3540        .backref
3541        .downcast_mut::<NodeGraphLocalDataset>()
3542    else {
3543        return Update::DoNothing;
3544    };
3545
3546    let node_type = match node_graph
3547        .node_graph
3548        .nodes
3549        .iter()
3550        .find(|i| i.node_id == node_id)
3551    {
3552        Some(s) => s.node.node_type,
3553        None => return Update::DoNothing,
3554    };
3555
3556    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3557        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3558            refany.clone(),
3559            info,
3560            node_id,
3561            field_idx,
3562            node_type,
3563            NodeTypeFieldValue::ColorInput(colorinputstate.color),
3564        ),
3565        None => Update::DoNothing,
3566    };
3567
3568    result
3569}
3570
3571extern "C" fn nodegraph_on_fileinput_button_clicked(
3572    mut refany: RefAny,
3573    info: CallbackInfo,
3574    file: FileInputState,
3575) -> Update {
3576    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
3577        return Update::DoNothing;
3578    };
3579
3580    let field_idx = refany.field_idx;
3581
3582    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
3583        return Update::DoNothing;
3584    };
3585
3586    let node_id = node_local_dataset.node_id;
3587    let Some(mut node_graph) = node_local_dataset
3588        .backref
3589        .downcast_mut::<NodeGraphLocalDataset>()
3590    else {
3591        return Update::DoNothing;
3592    };
3593
3594    let node_type = match node_graph
3595        .node_graph
3596        .nodes
3597        .iter()
3598        .find(|i| i.node_id == node_id)
3599    {
3600        Some(s) => s.node.node_type,
3601        None => return Update::DoNothing,
3602    };
3603
3604    // If a new file was selected, invoke callback
3605    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3606        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3607            refany.clone(),
3608            info,
3609            node_id,
3610            field_idx,
3611            node_type,
3612            NodeTypeFieldValue::FileInput(file.path),
3613        ),
3614        None => return Update::DoNothing,
3615    };
3616
3617    result
3618}
3619
3620#[cfg(all(test, feature = "std"))]
3621#[allow(clippy::float_cmp, clippy::too_many_lines)]
3622mod autotest_generated {
3623    use std::{
3624        collections::{BTreeMap, HashMap},
3625        sync::{Arc, Mutex},
3626    };
3627
3628    use azul_core::{
3629        dom::{DomId, DomNodeId},
3630        geom::{LogicalRect, OptionLogicalPosition},
3631        gl::OptionGlContextPtr,
3632        hit_test::ScrollPosition,
3633        resources::RendererResources,
3634        styled_dom::{NodeHierarchyItemId, StyledDom},
3635        window::{MonitorVec, RawWindowHandle},
3636    };
3637    use rust_fontconfig::FcFontCache;
3638
3639    use super::*;
3640    #[cfg(feature = "icu")]
3641    use crate::icu::IcuLocalizerHandle;
3642    use crate::{
3643        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
3644        solver3::{display_list::DisplayList, layout_tree::LayoutTree},
3645        window::{DomLayoutResult, LayoutWindow},
3646        window_state::FullWindowState,
3647    };
3648
3649    // ------------------------------------------------------------------
3650    // Fixtures
3651    // ------------------------------------------------------------------
3652
3653    /// Two node types that are deliberately *type-incompatible*: `TYPE_A` speaks
3654    /// `IO_INT` on both ends, `TYPE_B` speaks `IO_FLOAT`. Every "mime type mismatch"
3655    /// assertion below is A-to-B; every legal connection is A-to-A.
3656    const TYPE_A: NodeTypeId = NodeTypeId { inner: 1 };
3657    const TYPE_B: NodeTypeId = NodeTypeId { inner: 2 };
3658    /// A node type id that is *never* registered in `node_types`.
3659    const TYPE_UNREGISTERED: NodeTypeId = NodeTypeId { inner: 99 };
3660
3661    const IO_INT: InputOutputTypeId = InputOutputTypeId { inner: 10 };
3662    const IO_FLOAT: InputOutputTypeId = InputOutputTypeId { inner: 20 };
3663    /// An I/O type id that has no entry in `input_output_types` (so no color).
3664    const IO_COLORLESS: InputOutputTypeId = InputOutputTypeId { inner: 77 };
3665
3666    const N1: NodeGraphNodeId = NodeGraphNodeId { inner: 1 };
3667    const N2: NodeGraphNodeId = NodeGraphNodeId { inner: 2 };
3668    const N3: NodeGraphNodeId = NodeGraphNodeId { inner: 3 };
3669    const N4: NodeGraphNodeId = NodeGraphNodeId { inner: 4 };
3670    /// A node id that is never in the graph.
3671    const MISSING: NodeGraphNodeId = NodeGraphNodeId { inner: 999 };
3672
3673    /// The four geometry constants `get_rect` is built from, restated here so that a
3674    /// silent change to any of them fails the geometry tests loudly instead of
3675    /// silently re-deriving the "expected" value from the same source.
3676    const EXPECT_NODE_WIDTH: f32 = 250.0;
3677    const EXPECT_V_OFFSET: f32 = 71.0;
3678    const EXPECT_PORT_PITCH: f32 = 25.0; // DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT
3679    const EXPECT_DOT_HEIGHT: f32 = 15.0;
3680
3681    fn io_types() -> InputOutputTypeIdInfoMapVec {
3682        vec![
3683            InputOutputTypeIdInfoMap {
3684                io_type_id: IO_INT,
3685                io_info: InputOutputInfo {
3686                    data_type: AzString::from_const_str("int"),
3687                    color: ColorU {
3688                        r: 1,
3689                        g: 2,
3690                        b: 3,
3691                        a: 255,
3692                    },
3693                },
3694            },
3695            InputOutputTypeIdInfoMap {
3696                io_type_id: IO_FLOAT,
3697                io_info: InputOutputInfo {
3698                    data_type: AzString::from_const_str("float"),
3699                    color: ColorU {
3700                        r: 4,
3701                        g: 5,
3702                        b: 6,
3703                        a: 255,
3704                    },
3705                },
3706            },
3707        ]
3708        .into()
3709    }
3710
3711    fn node_types() -> NodeTypeIdInfoMapVec {
3712        vec![
3713            NodeTypeIdInfoMap {
3714                node_type_id: TYPE_A,
3715                node_type_info: NodeTypeInfo {
3716                    is_root: true,
3717                    node_type_name: AzString::from_const_str("A"),
3718                    inputs: vec![IO_INT].into(),
3719                    outputs: vec![IO_INT].into(),
3720                },
3721            },
3722            NodeTypeIdInfoMap {
3723                node_type_id: TYPE_B,
3724                node_type_info: NodeTypeInfo {
3725                    is_root: false,
3726                    node_type_name: AzString::from_const_str("B"),
3727                    inputs: vec![IO_FLOAT].into(),
3728                    outputs: vec![IO_FLOAT].into(),
3729                },
3730            },
3731        ]
3732        .into()
3733    }
3734
3735    fn mk_node(node_type: NodeTypeId, x: f32, y: f32) -> Node {
3736        Node {
3737            node_type,
3738            position: NodeGraphNodePosition { x, y },
3739            fields: NodeTypeFieldVec::new(),
3740            connect_in: InputConnectionVec::new(),
3741            connect_out: OutputConnectionVec::new(),
3742        }
3743    }
3744
3745    /// Four nodes: `N1`, `N3`, `N4` are `TYPE_A` (int), `N2` is `TYPE_B` (float).
3746    /// So `N1 -> N3`, `N1 -> N4` and `N3 -> N4` are legal connections and anything
3747    /// touching `N2` is a mime-type mismatch.
3748    fn graph() -> NodeGraph {
3749        NodeGraph {
3750            node_types: node_types(),
3751            input_output_types: io_types(),
3752            nodes: vec![
3753                NodeIdNodeMap {
3754                    node_id: N1,
3755                    node: mk_node(TYPE_A, 0.0, 0.0),
3756                },
3757                NodeIdNodeMap {
3758                    node_id: N2,
3759                    node: mk_node(TYPE_B, 400.0, 100.0),
3760                },
3761                NodeIdNodeMap {
3762                    node_id: N3,
3763                    node: mk_node(TYPE_A, 800.0, 50.0),
3764                },
3765                NodeIdNodeMap {
3766                    node_id: N4,
3767                    node: mk_node(TYPE_A, -100.0, 200.0),
3768                },
3769            ]
3770            .into(),
3771            ..NodeGraph::default()
3772        }
3773    }
3774
3775    /// `(input_index, [(output_node_id, output_index)])` for every input port of `id`.
3776    fn inputs_of(g: &NodeGraph, id: NodeGraphNodeId) -> Vec<(usize, Vec<(u64, usize)>)> {
3777        g.nodes
3778            .iter()
3779            .find(|n| n.node_id == id)
3780            .map_or_else(Vec::new, |n| {
3781                n.node
3782                    .connect_in
3783                    .iter()
3784                    .map(|c| {
3785                        (
3786                            c.input_index,
3787                            c.connects_to
3788                                .iter()
3789                                .map(|o| (o.node_id.inner, o.output_index))
3790                                .collect(),
3791                        )
3792                    })
3793                    .collect()
3794            })
3795    }
3796
3797    /// `(output_index, [(input_node_id, input_index)])` for every output port of `id`.
3798    fn outputs_of(g: &NodeGraph, id: NodeGraphNodeId) -> Vec<(usize, Vec<(u64, usize)>)> {
3799        g.nodes
3800            .iter()
3801            .find(|n| n.node_id == id)
3802            .map_or_else(Vec::new, |n| {
3803                n.node
3804                    .connect_out
3805                    .iter()
3806                    .map(|c| {
3807                        (
3808                            c.output_index,
3809                            c.connects_to
3810                                .iter()
3811                                .map(|i| (i.node_id.inner, i.input_index))
3812                                .collect(),
3813                        )
3814                    })
3815                    .collect()
3816            })
3817    }
3818
3819    /// The full wiring of the graph, as a comparable value — the "encoding" that the
3820    /// connect/disconnect round-trip tests compare before and after.
3821    type Wiring = Vec<(u64, Vec<(usize, Vec<(u64, usize)>)>, Vec<(usize, Vec<(u64, usize)>)>)>;
3822    fn wiring(g: &NodeGraph) -> Wiring {
3823        g.nodes
3824            .iter()
3825            .map(|n| {
3826                (
3827                    n.node_id.inner,
3828                    inputs_of(g, n.node_id),
3829                    outputs_of(g, n.node_id),
3830                )
3831            })
3832            .collect()
3833    }
3834
3835    /// Pushes an output connection *without* going through `connect_input_output`, so
3836    /// that structurally-impossible graphs (dangling target, out-of-range port, port
3837    /// with no registered color) can be handed to the renderers.
3838    fn force_out_connection(
3839        mut g: NodeGraph,
3840        from: NodeGraphNodeId,
3841        out_idx: usize,
3842        to: NodeGraphNodeId,
3843        in_idx: usize,
3844    ) -> NodeGraph {
3845        if let Some(n) = g.nodes.as_mut().iter_mut().find(|n| n.node_id == from) {
3846            n.node.connect_out.push(OutputConnection {
3847                output_index: out_idx,
3848                connects_to: vec![InputNodeAndIndex {
3849                    node_id: to,
3850                    input_index: in_idx,
3851                }]
3852                .into(),
3853            });
3854        }
3855        g
3856    }
3857
3858    fn count_nodes(dom: &Dom) -> usize {
3859        1 + dom.children.iter().map(count_nodes).sum::<usize>()
3860    }
3861
3862    /// A `RefAny<NodeGraphLocalDataset>` wrapping a snapshot of `g` — the payload every
3863    /// node-graph callback expects to find at the end of its `backref` chain.
3864    fn graph_dataset(g: &NodeGraph) -> RefAny {
3865        RefAny::new(NodeGraphLocalDataset {
3866            node_graph: g.clone(),
3867            last_input_or_output_clicked: None,
3868            active_node_being_dragged: None,
3869            node_connection_marker: RefAny::new(NodeConnectionMarkerDataset {}),
3870            callbacks: g.callbacks.clone(),
3871        })
3872    }
3873
3874    /// Reads the graph back out of a `NodeGraphLocalDataset` handle.
3875    fn dataset_graph(handle: &RefAny) -> NodeGraph {
3876        let mut handle = handle.clone();
3877        let d = handle
3878            .downcast_ref::<NodeGraphLocalDataset>()
3879            .expect("not a NodeGraphLocalDataset");
3880        d.node_graph.clone()
3881    }
3882
3883    /// `InputOrOutput` is not `PartialEq`, so flatten it into something that is.
3884    fn io_kind(io: InputOrOutput) -> (bool, usize) {
3885        match io {
3886            InputOrOutput::Input(i) => (true, i),
3887            InputOrOutput::Output(o) => (false, o),
3888        }
3889    }
3890
3891    fn pending_click(handle: &RefAny) -> Option<(u64, (bool, usize))> {
3892        let mut handle = handle.clone();
3893        let d = handle
3894            .downcast_ref::<NodeGraphLocalDataset>()
3895            .expect("not a NodeGraphLocalDataset");
3896        d.last_input_or_output_clicked
3897            .map(|(id, io)| (id.inner, io_kind(io)))
3898    }
3899
3900    // ------------------------------------------------------------------
3901    // Callback harness (mirrors the one in check_box.rs / color_input.rs)
3902    // ------------------------------------------------------------------
3903
3904    /// A `DomNodeId` whose node component is `None` — "no concrete node was hit".
3905    fn hit_none() -> DomNodeId {
3906        DomNodeId {
3907            dom: DomId::ROOT_ID,
3908            node: NodeHierarchyItemId::NONE,
3909        }
3910    }
3911
3912    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
3913        DomLayoutResult {
3914            styled_dom,
3915            layout_tree: LayoutTree {
3916                nodes: Vec::new(),
3917                warm: Vec::new(),
3918                cold: Vec::new(),
3919                root: 0,
3920                dom_to_layout: BTreeMap::new(),
3921                children_arena: Vec::new(),
3922                children_offsets: Vec::new(),
3923                subtree_needs_intrinsic: Vec::new(),
3924            },
3925            calculated_positions: Vec::new(),
3926            viewport: LogicalRect::zero(),
3927            display_list: DisplayList::default(),
3928            scroll_ids: HashMap::new(),
3929            scroll_id_to_node_id: HashMap::new(),
3930        }
3931    }
3932
3933    /// Runs `f` with a `CallbackInfo` whose window holds `styled_dom` as the root DOM.
3934    /// `previous_window_state` is deliberately `None`, which is what makes
3935    /// `get_previous_mouse_state()` return `None` in the drag tests.
3936    fn with_info<R>(
3937        styled_dom: StyledDom,
3938        hit: DomNodeId,
3939        f: impl FnOnce(&mut CallbackInfo) -> R,
3940    ) -> (R, Vec<CallbackChange>) {
3941        let mut layout_window =
3942            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
3943        layout_window
3944            .layout_results
3945            .insert(DomId::ROOT_ID, layout_result(styled_dom));
3946
3947        let renderer_resources = RendererResources::default();
3948        let previous_window_state: Option<FullWindowState> = None;
3949        let current_window_state = FullWindowState::default();
3950        let gl_context = OptionGlContextPtr::None;
3951        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
3952            BTreeMap::new();
3953        let window_handle = RawWindowHandle::Unsupported;
3954        let system_callbacks = ExternalSystemCallbacks::rust_internal();
3955
3956        let ref_data = CallbackInfoRefData {
3957            layout_window: &layout_window,
3958            renderer_resources: &renderer_resources,
3959            previous_window_state: &previous_window_state,
3960            current_window_state: &current_window_state,
3961            gl_context: &gl_context,
3962            current_scroll_manager: &scroll_states,
3963            current_window_handle: &window_handle,
3964            system_callbacks: &system_callbacks,
3965            system_style: Arc::new(azul_css::system::SystemStyle::default()),
3966            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
3967            #[cfg(feature = "icu")]
3968            icu_localizer: IcuLocalizerHandle::default(),
3969            ctx: OptionRefAny::None,
3970        };
3971
3972        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
3973
3974        let mut info = CallbackInfo::new(
3975            &ref_data,
3976            &changes,
3977            hit,
3978            OptionLogicalPosition::None,
3979            OptionLogicalPosition::None,
3980        );
3981
3982        let r = f(&mut info);
3983        let pushed = info.take_changes();
3984        (r, pushed)
3985    }
3986
3987    /// Shorthand for "deliver one event to `cb` with an otherwise-empty window".
3988    fn fire(cb: impl FnOnce(CallbackInfo) -> Update) -> Update {
3989        with_info(StyledDom::default(), hit_none(), |info| cb(*info)).0
3990    }
3991
3992    // ------------------------------------------------------------------
3993    // User-callback recorder
3994    // ------------------------------------------------------------------
3995
3996    /// Everything the widget's user-facing callbacks were handed, in call order.
3997    #[derive(Debug, Default)]
3998    struct Log {
3999        removed: Vec<u64>,
4000        added: Vec<(u64, u64, f32, f32)>,
4001        connected: Vec<(u64, usize, u64, usize)>,
4002        input_disconnected: Vec<(u64, usize)>,
4003        output_disconnected: Vec<(u64, usize)>,
4004        /// `(node_id, field_idx, node_type)` of every `on_node_field_edited` call.
4005        edited: Vec<(u64, usize, u64)>,
4006        text_values: Vec<String>,
4007        number_values: Vec<f32>,
4008        bool_values: Vec<bool>,
4009        color_values: Vec<(u8, u8, u8, u8)>,
4010        file_values: Vec<Option<String>>,
4011    }
4012
4013    fn log_of(handle: &RefAny, f: impl FnOnce(&Log)) {
4014        let mut handle = handle.clone();
4015        let l = handle.downcast_ref::<Log>().expect("not a Log");
4016        f(&l);
4017    }
4018
4019    extern "C" fn rec_removed(
4020        mut refany: RefAny,
4021        _info: CallbackInfo,
4022        node_id: NodeGraphNodeId,
4023    ) -> Update {
4024        if let Some(mut l) = refany.downcast_mut::<Log>() {
4025            l.removed.push(node_id.inner);
4026        }
4027        Update::RefreshDom
4028    }
4029
4030    extern "C" fn rec_added(
4031        mut refany: RefAny,
4032        _info: CallbackInfo,
4033        new_node_type: NodeTypeId,
4034        new_node_id: NodeGraphNodeId,
4035        new_node_position: NodeGraphNodePosition,
4036    ) -> Update {
4037        if let Some(mut l) = refany.downcast_mut::<Log>() {
4038            l.added.push((
4039                new_node_type.inner,
4040                new_node_id.inner,
4041                new_node_position.x,
4042                new_node_position.y,
4043            ));
4044        }
4045        Update::RefreshDomAllWindows
4046    }
4047
4048    extern "C" fn rec_connected(
4049        mut refany: RefAny,
4050        _info: CallbackInfo,
4051        input: NodeGraphNodeId,
4052        input_index: usize,
4053        output: NodeGraphNodeId,
4054        output_index: usize,
4055    ) -> Update {
4056        if let Some(mut l) = refany.downcast_mut::<Log>() {
4057            l.connected
4058                .push((input.inner, input_index, output.inner, output_index));
4059        }
4060        Update::RefreshDom
4061    }
4062
4063    extern "C" fn rec_input_disconnected(
4064        mut refany: RefAny,
4065        _info: CallbackInfo,
4066        input: NodeGraphNodeId,
4067        input_index: usize,
4068    ) -> Update {
4069        if let Some(mut l) = refany.downcast_mut::<Log>() {
4070            l.input_disconnected.push((input.inner, input_index));
4071        }
4072        Update::RefreshDom
4073    }
4074
4075    extern "C" fn rec_output_disconnected(
4076        mut refany: RefAny,
4077        _info: CallbackInfo,
4078        output: NodeGraphNodeId,
4079        output_index: usize,
4080    ) -> Update {
4081        if let Some(mut l) = refany.downcast_mut::<Log>() {
4082            l.output_disconnected.push((output.inner, output_index));
4083        }
4084        Update::RefreshDomAllWindows
4085    }
4086
4087    extern "C" fn rec_field_edited(
4088        mut refany: RefAny,
4089        _info: CallbackInfo,
4090        node_id: NodeGraphNodeId,
4091        field_id: usize,
4092        node_type: NodeTypeId,
4093        new_value: NodeTypeFieldValue,
4094    ) -> Update {
4095        if let Some(mut l) = refany.downcast_mut::<Log>() {
4096            l.edited.push((node_id.inner, field_id, node_type.inner));
4097            match new_value {
4098                NodeTypeFieldValue::TextInput(s) => l.text_values.push(s.as_str().to_string()),
4099                NodeTypeFieldValue::NumberInput(n) => l.number_values.push(n),
4100                NodeTypeFieldValue::CheckBox(b) => l.bool_values.push(b),
4101                NodeTypeFieldValue::ColorInput(c) => l.color_values.push((c.r, c.g, c.b, c.a)),
4102                NodeTypeFieldValue::FileInput(p) => l
4103                    .file_values
4104                    .push(p.as_ref().map(|s| s.as_str().to_string())),
4105            }
4106        }
4107        Update::RefreshDom
4108    }
4109
4110    /// A graph whose callbacks all funnel into one freshly-created `Log`.
4111    fn graph_with_log() -> (NodeGraph, RefAny) {
4112        let log = RefAny::new(Log::default());
4113        let mut g = graph();
4114        g.callbacks = NodeGraphCallbacks {
4115            on_node_removed: OptionOnNodeRemoved::Some(OnNodeRemoved {
4116                refany: log.clone(),
4117                callback: OnNodeRemovedCallback {
4118                    cb: rec_removed,
4119                    ctx: OptionRefAny::None,
4120                },
4121            }),
4122            on_node_added: OptionOnNodeAdded::Some(OnNodeAdded {
4123                refany: log.clone(),
4124                callback: OnNodeAddedCallback {
4125                    cb: rec_added,
4126                    ctx: OptionRefAny::None,
4127                },
4128            }),
4129            on_node_connected: OptionOnNodeConnected::Some(OnNodeConnected {
4130                refany: log.clone(),
4131                callback: OnNodeConnectedCallback {
4132                    cb: rec_connected,
4133                    ctx: OptionRefAny::None,
4134                },
4135            }),
4136            on_node_input_disconnected: OptionOnNodeInputDisconnected::Some(
4137                OnNodeInputDisconnected {
4138                    refany: log.clone(),
4139                    callback: OnNodeInputDisconnectedCallback {
4140                        cb: rec_input_disconnected,
4141                        ctx: OptionRefAny::None,
4142                    },
4143                },
4144            ),
4145            on_node_output_disconnected: OptionOnNodeOutputDisconnected::Some(
4146                OnNodeOutputDisconnected {
4147                    refany: log.clone(),
4148                    callback: OnNodeOutputDisconnectedCallback {
4149                        cb: rec_output_disconnected,
4150                        ctx: OptionRefAny::None,
4151                    },
4152                },
4153            ),
4154            on_node_field_edited: OptionOnNodeFieldEdited::Some(OnNodeFieldEdited {
4155                refany: log.clone(),
4156                callback: OnNodeFieldEditedCallback {
4157                    cb: rec_field_edited,
4158                    ctx: OptionRefAny::None,
4159                },
4160            }),
4161            ..NodeGraphCallbacks::default()
4162        };
4163        (g, log)
4164    }
4165
4166    // ==================================================================
4167    // 1. NodeGraph::generate_unique_node_id
4168    // ==================================================================
4169
4170    #[test]
4171    fn generate_unique_node_id_on_an_empty_graph_is_one_not_zero() {
4172        // `0` is a perfectly valid node id, so the generator must not hand it out for
4173        // the first node either — `max().unwrap_or(0) + 1`.
4174        assert_eq!(NodeGraph::default().generate_unique_node_id().inner, 1);
4175    }
4176
4177    #[test]
4178    fn generate_unique_node_id_returns_max_plus_one_and_ignores_gaps_and_order() {
4179        // Ids are deliberately unsorted and non-contiguous: the generator must take the
4180        // maximum, not the last element and not the length.
4181        let g = NodeGraph {
4182            nodes: vec![
4183                NodeIdNodeMap {
4184                    node_id: NodeGraphNodeId { inner: 7 },
4185                    node: mk_node(TYPE_A, 0.0, 0.0),
4186                },
4187                NodeIdNodeMap {
4188                    node_id: NodeGraphNodeId { inner: 0 },
4189                    node: mk_node(TYPE_A, 0.0, 0.0),
4190                },
4191                NodeIdNodeMap {
4192                    node_id: NodeGraphNodeId { inner: 3 },
4193                    node: mk_node(TYPE_A, 0.0, 0.0),
4194                },
4195            ]
4196            .into(),
4197            ..Default::default()
4198        };
4199        assert_eq!(g.generate_unique_node_id().inner, 8);
4200    }
4201
4202    #[test]
4203    fn generate_unique_node_id_tolerates_duplicate_ids_in_the_graph() {
4204        let g = NodeGraph {
4205            nodes: vec![
4206                NodeIdNodeMap {
4207                    node_id: N2,
4208                    node: mk_node(TYPE_A, 0.0, 0.0),
4209                },
4210                NodeIdNodeMap {
4211                    node_id: N2,
4212                    node: mk_node(TYPE_A, 0.0, 0.0),
4213                },
4214            ]
4215            .into(),
4216            ..Default::default()
4217        };
4218        assert_eq!(g.generate_unique_node_id().inner, 3);
4219    }
4220
4221    #[test]
4222    fn generate_unique_node_id_saturates_instead_of_overflowing_at_u64_max() {
4223        // `saturating_add(1)` means the id at the top of the range is NOT unique: it
4224        // collides with the existing node. That is a real (if unreachable in practice)
4225        // limitation — what matters here is that it saturates rather than wrapping to
4226        // 0 or panicking in a debug build.
4227        let mut g = NodeGraph {
4228            nodes: vec![NodeIdNodeMap {
4229                node_id: NodeGraphNodeId { inner: u64::MAX },
4230                node: mk_node(TYPE_A, 0.0, 0.0),
4231            }]
4232            .into(),
4233            ..Default::default()
4234        };
4235        let id = g.generate_unique_node_id();
4236        assert_eq!(id.inner, u64::MAX);
4237        assert!(
4238            g.nodes.iter().any(|n| n.node_id == id),
4239            "at u64::MAX the generated id collides — documented saturation, not wraparound",
4240        );
4241
4242        // ...and one below the top still behaves normally.
4243        g.nodes = vec![NodeIdNodeMap {
4244            node_id: NodeGraphNodeId {
4245                inner: u64::MAX - 1,
4246            },
4247            node: mk_node(TYPE_A, 0.0, 0.0),
4248        }]
4249        .into();
4250        assert_eq!(g.generate_unique_node_id().inner, u64::MAX);
4251    }
4252
4253    #[test]
4254    fn generate_unique_node_id_is_pure_and_repeats_until_the_node_is_inserted() {
4255        let g = graph();
4256        let first = g.generate_unique_node_id();
4257        assert_eq!(first, g.generate_unique_node_id());
4258        assert_eq!(first.inner, 5); // max(1,2,3,4) + 1
4259    }
4260
4261    // ==================================================================
4262    // 2. NodeGraphError: Display / Debug
4263    // ==================================================================
4264
4265    const ALL_ERRORS: [NodeGraphError; 4] = [
4266        NodeGraphError::NodeMimeTypeMismatch,
4267        NodeGraphError::NodeInvalidIndex,
4268        NodeGraphError::NodeInvalidNode,
4269        NodeGraphError::NoRootNode,
4270    ];
4271
4272    #[test]
4273    fn node_graph_error_display_is_non_empty_ascii_and_single_line() {
4274        for e in ALL_ERRORS {
4275            let s = format!("{e}");
4276            assert!(!s.is_empty(), "{e:?} formatted to the empty string");
4277            assert!(!s.contains('\n'), "{e:?} formatted to a multi-line string");
4278            assert!(s.is_ascii(), "{e:?} formatted to non-ascii: {s}");
4279        }
4280    }
4281
4282    #[test]
4283    fn node_graph_error_display_distinguishes_every_variant() {
4284        // A copy-pasted match arm that returns the same message for two variants would
4285        // make the error useless in a log; this is the assertion that catches it.
4286        let mut seen: Vec<String> = ALL_ERRORS.iter().map(|e| format!("{e}")).collect();
4287        seen.sort();
4288        seen.dedup();
4289        assert_eq!(seen.len(), ALL_ERRORS.len());
4290    }
4291
4292    #[test]
4293    fn node_graph_error_display_survives_width_precision_and_fill_specifiers() {
4294        // `write!` inside a Display impl ignores the outer format spec, but the spec
4295        // must not make the impl panic or truncate to nothing.
4296        for e in ALL_ERRORS {
4297            assert!(!format!("{e:>80}").is_empty());
4298            assert!(!format!("{e:*^3}").is_empty());
4299            assert!(!format!("{e:.1}").is_empty());
4300            assert!(!format!("{e:?}").is_empty());
4301        }
4302    }
4303
4304    #[test]
4305    fn node_graph_error_debug_and_display_are_both_usable_and_differ_in_style() {
4306        // Debug is the derived variant name; Display is prose. They should not be the
4307        // same string, otherwise one of the two impls is missing.
4308        for e in ALL_ERRORS {
4309            assert_ne!(format!("{e:?}"), format!("{e}"));
4310        }
4311    }
4312
4313    // ==================================================================
4314    // 3. NodeGraph::swap_with_default
4315    // ==================================================================
4316
4317    /// Everything about a `NodeGraph` that is cheaply comparable.
4318    fn summary(g: &NodeGraph) -> (usize, usize, usize, bool, f32, f32, f32, String) {
4319        (
4320            g.node_types.len(),
4321            g.input_output_types.len(),
4322            g.nodes.len(),
4323            g.allow_multiple_root_nodes,
4324            g.offset.x,
4325            g.offset.y,
4326            g.scale_factor,
4327            g.add_node_str.as_str().to_string(),
4328        )
4329    }
4330
4331    fn distinctive() -> NodeGraph {
4332        NodeGraph {
4333            allow_multiple_root_nodes: true,
4334            offset: LogicalPosition { x: -3.5, y: 12.25 },
4335            scale_factor: 2.5,
4336            add_node_str: AzString::from_const_str("Ajouter un nœud"),
4337            ..graph()
4338        }
4339    }
4340
4341    #[test]
4342    fn swap_with_default_hands_back_the_old_value_and_leaves_a_default_behind() {
4343        let mut g = distinctive();
4344        let expected = summary(&g);
4345
4346        let taken = g.swap_with_default();
4347
4348        assert_eq!(summary(&taken), expected);
4349        assert_eq!(summary(&g), summary(&NodeGraph::default()));
4350    }
4351
4352    #[test]
4353    fn swap_with_default_round_trips_a_graph_through_two_owners() {
4354        // encode == decode: moving a graph out and back must not lose a single field.
4355        let mut a = distinctive();
4356        let expected = summary(&a);
4357
4358        let mut b = a.swap_with_default(); // a := default, b := original
4359        let c = b.swap_with_default(); // b := default, c := original again
4360
4361        assert_eq!(summary(&c), expected);
4362        assert_eq!(summary(&b), summary(&NodeGraph::default()));
4363        assert_eq!(summary(&a), summary(&NodeGraph::default()));
4364    }
4365
4366    #[test]
4367    fn swap_with_default_on_an_already_default_graph_is_a_no_op() {
4368        let mut g = NodeGraph::default();
4369        let taken = g.swap_with_default();
4370        assert_eq!(summary(&taken), summary(&NodeGraph::default()));
4371        assert_eq!(summary(&g), summary(&NodeGraph::default()));
4372    }
4373
4374    #[test]
4375    fn swap_with_default_preserves_non_finite_offsets_and_scale_verbatim() {
4376        // The swap is a `mem::swap`, so NaN/inf must survive bit-for-bit rather than
4377        // being normalised away.
4378        let mut g = NodeGraph {
4379            offset: LogicalPosition {
4380                x: f32::INFINITY,
4381                y: f32::NEG_INFINITY,
4382            },
4383            scale_factor: f32::NAN,
4384            ..NodeGraph::default()
4385        };
4386        let taken = g.swap_with_default();
4387        assert!(taken.offset.x.is_infinite() && taken.offset.x.is_sign_positive());
4388        assert!(taken.offset.y.is_infinite() && taken.offset.y.is_sign_negative());
4389        assert!(taken.scale_factor.is_nan());
4390        assert_eq!(g.scale_factor, 1.0);
4391    }
4392
4393    #[test]
4394    fn swap_with_default_moves_the_connections_not_just_the_node_list() {
4395        let mut g = graph();
4396        g.connect_input_output(N3, 0, N1, 0).expect("legal A->A wire");
4397        let before = wiring(&g);
4398
4399        let taken = g.swap_with_default();
4400
4401        assert_eq!(wiring(&taken), before);
4402        assert!(g.nodes.is_empty());
4403    }
4404
4405    // ==================================================================
4406    // 4. NodeGraph::verify_nodetype_match
4407    // ==================================================================
4408
4409    #[test]
4410    fn verify_nodetype_match_accepts_matching_types_at_index_zero() {
4411        let g = graph();
4412        assert_eq!(g.verify_nodetype_match(N1, 0, N3, 0), Ok(()));
4413    }
4414
4415    #[test]
4416    fn verify_nodetype_match_rejects_a_type_mismatch() {
4417        let g = graph();
4418        // N1 emits `int`, N2 consumes `float`.
4419        assert_eq!(
4420            g.verify_nodetype_match(N1, 0, N2, 0),
4421            Err(NodeGraphError::NodeMimeTypeMismatch)
4422        );
4423    }
4424
4425    #[test]
4426    fn verify_nodetype_match_reports_a_missing_node_on_either_side() {
4427        let g = graph();
4428        assert_eq!(
4429            g.verify_nodetype_match(MISSING, 0, N3, 0),
4430            Err(NodeGraphError::NodeInvalidNode)
4431        );
4432        assert_eq!(
4433            g.verify_nodetype_match(N1, 0, MISSING, 0),
4434            Err(NodeGraphError::NodeInvalidNode)
4435        );
4436    }
4437
4438    #[test]
4439    fn verify_nodetype_match_reports_a_node_whose_type_is_not_registered() {
4440        let mut g = graph();
4441        g.nodes.push(NodeIdNodeMap {
4442            node_id: NodeGraphNodeId { inner: 50 },
4443            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
4444        });
4445        assert_eq!(
4446            g.verify_nodetype_match(NodeGraphNodeId { inner: 50 }, 0, N3, 0),
4447            Err(NodeGraphError::NodeInvalidNode)
4448        );
4449        assert_eq!(
4450            g.verify_nodetype_match(N1, 0, NodeGraphNodeId { inner: 50 }, 0),
4451            Err(NodeGraphError::NodeInvalidNode)
4452        );
4453    }
4454
4455    #[test]
4456    fn verify_nodetype_match_rejects_out_of_range_port_indices() {
4457        let g = graph();
4458        // Both node types declare exactly one input and one output, so index 1 is the
4459        // first out-of-range index.
4460        assert_eq!(
4461            g.verify_nodetype_match(N1, 1, N3, 0),
4462            Err(NodeGraphError::NodeInvalidIndex)
4463        );
4464        assert_eq!(
4465            g.verify_nodetype_match(N1, 0, N3, 1),
4466            Err(NodeGraphError::NodeInvalidIndex)
4467        );
4468    }
4469
4470    #[test]
4471    fn verify_nodetype_match_does_not_panic_at_usize_max_indices() {
4472        // `Vec::get(usize::MAX)` must be the thing that fails, not an unchecked index.
4473        let g = graph();
4474        assert_eq!(
4475            g.verify_nodetype_match(N1, usize::MAX, N3, 0),
4476            Err(NodeGraphError::NodeInvalidIndex)
4477        );
4478        assert_eq!(
4479            g.verify_nodetype_match(N1, 0, N3, usize::MAX),
4480            Err(NodeGraphError::NodeInvalidIndex)
4481        );
4482        assert_eq!(
4483            g.verify_nodetype_match(N1, usize::MAX, N3, usize::MAX),
4484            Err(NodeGraphError::NodeInvalidIndex)
4485        );
4486    }
4487
4488    #[test]
4489    fn verify_nodetype_match_checks_nodes_before_indices() {
4490        // Ordering matters for the error a user sees: a missing node is reported even
4491        // when the index is also nonsense.
4492        let g = graph();
4493        assert_eq!(
4494            g.verify_nodetype_match(MISSING, usize::MAX, N3, usize::MAX),
4495            Err(NodeGraphError::NodeInvalidNode)
4496        );
4497    }
4498
4499    #[test]
4500    fn verify_nodetype_match_allows_a_node_to_be_wired_to_itself() {
4501        // Documented behaviour: there is no self-loop / cycle check at this layer.
4502        let g = graph();
4503        assert_eq!(g.verify_nodetype_match(N1, 0, N1, 0), Ok(()));
4504    }
4505
4506    #[test]
4507    fn verify_nodetype_match_does_not_mutate_the_graph() {
4508        let g = graph();
4509        let before = wiring(&g);
4510        let _ = g.verify_nodetype_match(N1, 0, N3, 0);
4511        let _ = g.verify_nodetype_match(MISSING, usize::MAX, N2, 9);
4512        assert_eq!(wiring(&g), before);
4513    }
4514
4515    // ==================================================================
4516    // 5. NodeGraph::connect_input_output
4517    // ==================================================================
4518
4519    #[test]
4520    fn connect_input_output_wires_both_directions_at_index_zero() {
4521        let mut g = graph();
4522        assert_eq!(g.connect_input_output(N3, 0, N1, 0), Ok(()));
4523
4524        assert_eq!(inputs_of(&g, N3), vec![(0, vec![(N1.inner, 0)])]);
4525        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N3.inner, 0)])]);
4526        // ...and nothing else moved.
4527        assert!(outputs_of(&g, N3).is_empty());
4528        assert!(inputs_of(&g, N1).is_empty());
4529    }
4530
4531    #[test]
4532    fn connect_input_output_rejects_a_mime_type_mismatch_without_mutating() {
4533        let mut g = graph();
4534        let before = wiring(&g);
4535        assert_eq!(
4536            g.connect_input_output(N2, 0, N1, 0),
4537            Err(NodeGraphError::NodeMimeTypeMismatch)
4538        );
4539        assert_eq!(wiring(&g), before, "a rejected connect must be atomic");
4540    }
4541
4542    #[test]
4543    fn connect_input_output_rejects_missing_nodes_without_mutating() {
4544        for (input, output) in [(MISSING, N1), (N3, MISSING), (MISSING, MISSING)] {
4545            let mut g = graph();
4546            let before = wiring(&g);
4547            assert_eq!(
4548                g.connect_input_output(input, 0, output, 0),
4549                Err(NodeGraphError::NodeInvalidNode)
4550            );
4551            assert_eq!(wiring(&g), before);
4552        }
4553    }
4554
4555    #[test]
4556    fn connect_input_output_rejects_out_of_range_and_usize_max_indices() {
4557        for (in_idx, out_idx) in [
4558            (1_usize, 0_usize),
4559            (0, 1),
4560            (usize::MAX, 0),
4561            (0, usize::MAX),
4562            (usize::MAX, usize::MAX),
4563        ] {
4564            let mut g = graph();
4565            let before = wiring(&g);
4566            assert_eq!(
4567                g.connect_input_output(N3, in_idx, N1, out_idx),
4568                Err(NodeGraphError::NodeInvalidIndex),
4569                "in={in_idx} out={out_idx}",
4570            );
4571            assert_eq!(wiring(&g), before);
4572        }
4573    }
4574
4575    #[test]
4576    fn connect_input_output_appends_to_an_existing_port_rather_than_replacing_it() {
4577        // Two different sources feeding the same input port must both be recorded.
4578        let mut g = graph();
4579        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
4580        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
4581
4582        assert_eq!(
4583            inputs_of(&g, N4),
4584            vec![(0, vec![(N1.inner, 0), (N3.inner, 0)])],
4585            "the second wire must not overwrite the first",
4586        );
4587        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N4.inner, 0)])]);
4588        assert_eq!(outputs_of(&g, N3), vec![(0, vec![(N4.inner, 0)])]);
4589    }
4590
4591    #[test]
4592    fn connect_input_output_records_a_duplicate_wire_twice() {
4593        // Documented behaviour: there is no de-duplication, so connecting the same two
4594        // ports twice yields two identical entries on both sides.
4595        let mut g = graph();
4596        g.connect_input_output(N3, 0, N1, 0).expect("first");
4597        g.connect_input_output(N3, 0, N1, 0).expect("second");
4598
4599        assert_eq!(inputs_of(&g, N3), vec![(0, vec![(N1.inner, 0), (N1.inner, 0)])]);
4600        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N3.inner, 0), (N3.inner, 0)])]);
4601    }
4602
4603    #[test]
4604    fn connect_input_output_permits_a_self_loop() {
4605        // No cycle detection at this layer — the node ends up wired to itself.
4606        let mut g = graph();
4607        assert_eq!(g.connect_input_output(N1, 0, N1, 0), Ok(()));
4608        assert_eq!(inputs_of(&g, N1), vec![(0, vec![(N1.inner, 0)])]);
4609        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N1.inner, 0)])]);
4610    }
4611
4612    // ==================================================================
4613    // 6. NodeGraph::disconnect_input
4614    // ==================================================================
4615
4616    #[test]
4617    fn disconnect_input_round_trips_a_single_connection() {
4618        // encode == decode: connect then disconnect restores the exact wiring.
4619        let mut g = graph();
4620        let before = wiring(&g);
4621
4622        g.connect_input_output(N3, 0, N1, 0).expect("connect");
4623        assert_ne!(wiring(&g), before);
4624
4625        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
4626        assert_eq!(wiring(&g), before);
4627    }
4628
4629    #[test]
4630    fn disconnect_input_reports_a_missing_node() {
4631        let mut g = graph();
4632        assert_eq!(
4633            g.disconnect_input(MISSING, 0),
4634            Err(NodeGraphError::NodeInvalidNode)
4635        );
4636    }
4637
4638    #[test]
4639    fn disconnect_input_on_an_unconnected_port_is_ok_and_changes_nothing() {
4640        let mut g = graph();
4641        let before = wiring(&g);
4642        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
4643        assert_eq!(wiring(&g), before);
4644    }
4645
4646    #[test]
4647    fn disconnect_input_at_usize_max_is_ok_rather_than_invalid_index() {
4648        // Documented behaviour: an index that is not present short-circuits to `Ok(())`
4649        // *before* any range validation, so even `usize::MAX` is accepted silently.
4650        let mut g = graph();
4651        let before = wiring(&g);
4652        assert_eq!(g.disconnect_input(N3, usize::MAX), Ok(()));
4653        assert_eq!(wiring(&g), before);
4654    }
4655
4656    #[test]
4657    fn disconnect_input_clears_every_source_feeding_that_port() {
4658        let mut g = graph();
4659        let before = wiring(&g);
4660        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
4661        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
4662
4663        assert_eq!(g.disconnect_input(N4, 0), Ok(()));
4664        assert_eq!(
4665            wiring(&g),
4666            before,
4667            "both upstream ports must be released, not just the first",
4668        );
4669    }
4670
4671    #[test]
4672    fn disconnect_input_orphans_a_sibling_sharing_the_same_output_port() {
4673        // BUG (characterised, not endorsed): `disconnect_input` removes the *whole*
4674        // `OutputConnection` entry of the upstream port instead of removing just the
4675        // one `InputNodeAndIndex` that pointed back. When two inputs are fed by the
4676        // same output, disconnecting one of them silently drops the other's
4677        // forward edge while leaving its backward edge in place — the two halves of
4678        // the graph disagree afterwards.
4679        let mut g = graph();
4680        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
4681        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
4682        assert_eq!(
4683            outputs_of(&g, N1),
4684            vec![(0, vec![(N3.inner, 0), (N4.inner, 0)])]
4685        );
4686
4687        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
4688
4689        assert!(inputs_of(&g, N3).is_empty(), "the requested edge is gone");
4690        assert_eq!(
4691            inputs_of(&g, N4),
4692            vec![(0, vec![(N1.inner, 0)])],
4693            "N4 still believes it is connected to N1",
4694        );
4695        assert!(
4696            outputs_of(&g, N1).is_empty(),
4697            "...but N1 no longer lists N4 — the collateral damage this test pins down",
4698        );
4699    }
4700
4701    // ==================================================================
4702    // 7. NodeGraph::disconnect_output
4703    // ==================================================================
4704
4705    #[test]
4706    fn disconnect_output_round_trips_a_single_connection() {
4707        let mut g = graph();
4708        let before = wiring(&g);
4709
4710        g.connect_input_output(N3, 0, N1, 0).expect("connect");
4711        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
4712
4713        assert_eq!(wiring(&g), before);
4714    }
4715
4716    #[test]
4717    fn disconnect_output_reports_a_missing_node() {
4718        let mut g = graph();
4719        assert_eq!(
4720            g.disconnect_output(MISSING, 0),
4721            Err(NodeGraphError::NodeInvalidNode)
4722        );
4723    }
4724
4725    #[test]
4726    fn disconnect_output_on_an_unconnected_port_is_ok_and_changes_nothing() {
4727        let mut g = graph();
4728        let before = wiring(&g);
4729        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
4730        assert_eq!(wiring(&g), before);
4731    }
4732
4733    #[test]
4734    fn disconnect_output_at_usize_max_is_ok_rather_than_invalid_index() {
4735        let mut g = graph();
4736        let before = wiring(&g);
4737        assert_eq!(g.disconnect_output(N1, usize::MAX), Ok(()));
4738        assert_eq!(wiring(&g), before);
4739    }
4740
4741    #[test]
4742    fn disconnect_output_releases_every_downstream_input_it_fed() {
4743        // The mirror image of `disconnect_input_orphans_a_sibling...`: here the fan-out
4744        // case *is* handled correctly, because the loop walks the cloned target list.
4745        let mut g = graph();
4746        let before = wiring(&g);
4747        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
4748        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
4749
4750        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
4751        assert_eq!(wiring(&g), before, "no dangling back-reference may survive");
4752    }
4753
4754    #[test]
4755    fn disconnect_output_of_a_self_loop_leaves_no_residue() {
4756        let mut g = graph();
4757        let before = wiring(&g);
4758        g.connect_input_output(N1, 0, N1, 0).expect("self loop");
4759        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
4760        assert_eq!(wiring(&g), before);
4761    }
4762
4763    // ==================================================================
4764    // 8. get_rect
4765    // ==================================================================
4766
4767    fn connection(out: NodeGraphNodeId, out_idx: usize, inn: NodeGraphNodeId, in_idx: usize)
4768        -> ConnectionLocalDataset {
4769        ConnectionLocalDataset {
4770            out_node_id: out,
4771            out_idx,
4772            in_node_id: inn,
4773            in_idx,
4774            // Deliberately wrong: `get_rect` must recompute both flags from geometry.
4775            swap_vert: true,
4776            swap_horz: true,
4777            color: ColorU {
4778                r: 0,
4779                g: 0,
4780                b: 0,
4781                a: 0,
4782            },
4783        }
4784    }
4785
4786    #[test]
4787    fn get_rect_returns_none_for_a_dangling_endpoint() {
4788        let g = graph();
4789        assert!(get_rect(&g, connection(MISSING, 0, N3, 0)).is_none());
4790        assert!(get_rect(&g, connection(N1, 0, MISSING, 0)).is_none());
4791        assert!(get_rect(&g, connection(MISSING, 0, MISSING, 0)).is_none());
4792    }
4793
4794    #[test]
4795    fn get_rect_computes_the_bounding_box_of_the_two_ports() {
4796        // N1 sits at (0, 0), N3 at (800, 50); both use port 0.
4797        let g = graph();
4798        let (rect, swap_vert, swap_horz) =
4799            get_rect(&g, connection(N1, 0, N3, 0)).expect("both nodes exist");
4800
4801        let x_out = 0.0 + EXPECT_NODE_WIDTH;
4802        let y_out = 0.0 + EXPECT_V_OFFSET;
4803        let x_in = 800.0;
4804        let y_in = 50.0 + EXPECT_V_OFFSET;
4805
4806        assert_eq!(rect.origin.x, x_out.min(x_in));
4807        assert_eq!(rect.origin.y, y_out.min(y_in));
4808        assert_eq!(rect.size.width, (x_in - x_out).abs());
4809        assert_eq!(rect.size.height, (y_in - y_out).abs() + EXPECT_DOT_HEIGHT);
4810        assert!(swap_vert, "the input port sits below the output port");
4811        assert!(!swap_horz, "the input node is to the right of the output");
4812    }
4813
4814    #[test]
4815    fn get_rect_recomputes_the_swap_flags_and_ignores_the_ones_it_was_handed() {
4816        // The fixture passes `swap_vert: true, swap_horz: true` every time; here both
4817        // must come back `false`, proving the incoming values are not echoed.
4818        let g = graph();
4819        // N3 (800, 50) -> N1 (0, 0): input is left of, and above, the output.
4820        let (_, swap_vert, swap_horz) = get_rect(&g, connection(N3, 0, N1, 0)).expect("exists");
4821        assert!(!swap_vert);
4822        assert!(swap_horz);
4823    }
4824
4825    #[test]
4826    fn get_rect_height_is_never_below_the_connection_dot() {
4827        // Two nodes at the same height give a zero-height span; the dot height is the
4828        // floor that keeps the rect drawable.
4829        let mut g = graph();
4830        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition { x: 800.0, y: 0.0 };
4831        let (rect, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
4832        assert_eq!(rect.size.height, EXPECT_DOT_HEIGHT);
4833    }
4834
4835    #[test]
4836    fn get_rect_port_index_shifts_the_endpoint_by_a_fixed_pitch() {
4837        // N1's output port (y = 71) is the topmost point of the rect; moving N3's input
4838        // down by three port pitches must therefore grow the height by exactly three
4839        // pitches and leave the origin where it was.
4840        let g = graph();
4841        let (base, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
4842        let (shifted, _, _) = get_rect(&g, connection(N1, 0, N3, 3)).expect("exists");
4843
4844        assert_eq!(shifted.origin.y, base.origin.y);
4845        assert_eq!(
4846            shifted.size.height - base.size.height,
4847            3.0 * EXPECT_PORT_PITCH,
4848        );
4849    }
4850
4851    #[test]
4852    fn get_rect_stays_finite_at_usize_max_port_indices() {
4853        // `usize::MAX as f32` is ~1.8e19 — large, but multiplying by the 25px pitch
4854        // still lands well inside f32 range, so nothing may become inf or NaN.
4855        let g = graph();
4856        let (rect, _, _) = get_rect(&g, connection(N1, usize::MAX, N3, usize::MAX))
4857            .expect("both nodes exist");
4858        assert!(rect.origin.y.is_finite(), "y = {}", rect.origin.y);
4859        assert!(rect.size.height.is_finite(), "h = {}", rect.size.height);
4860        assert!(rect.size.width.is_finite());
4861        assert!(rect.size.height >= EXPECT_DOT_HEIGHT);
4862    }
4863
4864    #[test]
4865    fn get_rect_with_a_nan_position_yields_nan_extent_but_a_finite_origin() {
4866        // `f32::min` returns the non-NaN operand, so the origin survives even though
4867        // the extent does not. Neither may panic.
4868        let mut g = graph();
4869        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition {
4870            x: f32::NAN,
4871            y: f32::NAN,
4872        };
4873        let (rect, swap_vert, swap_horz) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
4874
4875        assert!(rect.size.width.is_nan());
4876        assert!(rect.size.height.is_nan());
4877        assert_eq!(rect.origin.x, EXPECT_NODE_WIDTH);
4878        assert_eq!(rect.origin.y, EXPECT_V_OFFSET);
4879        // NaN compares false against everything, so both flags fall to `false`.
4880        assert!(!swap_vert);
4881        assert!(!swap_horz);
4882    }
4883
4884    #[test]
4885    fn get_rect_with_infinite_positions_does_not_panic() {
4886        let mut g = graph();
4887        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition {
4888            x: f32::INFINITY,
4889            y: f32::NEG_INFINITY,
4890        };
4891        let (rect, swap_vert, swap_horz) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
4892        assert!(rect.size.width.is_infinite());
4893        assert!(rect.size.height.is_infinite());
4894        assert!(!swap_vert, "-inf is not above the output port");
4895        assert!(!swap_horz, "+inf is not left of the output port");
4896
4897        // Both endpoints infinite in the same direction => inf - inf => NaN extent.
4898        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
4899            x: f32::INFINITY,
4900            y: f32::NEG_INFINITY,
4901        };
4902        let (rect, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
4903        assert!(rect.size.width.is_nan());
4904    }
4905
4906    #[test]
4907    fn get_rect_is_a_pure_query() {
4908        let g = graph();
4909        let before = wiring(&g);
4910        let _ = get_rect(&g, connection(N1, usize::MAX, N3, 0));
4911        assert_eq!(wiring(&g), before);
4912    }
4913
4914    // ==================================================================
4915    // 9. render_node
4916    // ==================================================================
4917
4918    fn node_dataset(g: &NodeGraph, id: NodeGraphNodeId) -> NodeLocalDataset {
4919        NodeLocalDataset {
4920            node_id: id,
4921            backref: graph_dataset(g),
4922        }
4923    }
4924
4925    fn render_one(g: &NodeGraph, id: NodeGraphNodeId, offset: (f32, f32), scale: f32) -> Dom {
4926        let n = g
4927            .nodes
4928            .iter()
4929            .find(|n| n.node_id == id)
4930            .expect("node in fixture");
4931        let ty = g
4932            .node_types
4933            .iter()
4934            .find(|t| t.node_type_id == n.node.node_type)
4935            .expect("type in fixture");
4936        render_node(
4937            &n.node,
4938            offset,
4939            &ty.node_type_info,
4940            node_dataset(g, id),
4941            scale,
4942        )
4943    }
4944
4945    #[test]
4946    fn render_node_produces_a_single_wrapper_child_carrying_the_dataset() {
4947        let g = graph();
4948        let dom = render_one(&g, N1, (0.0, 0.0), 1.0);
4949        assert_eq!(dom.children.len(), 1);
4950        let inner = &dom.children.as_slice()[0];
4951        let mut ds = inner
4952            .root
4953            .get_dataset()
4954            .cloned()
4955            .expect("the node body must carry its NodeLocalDataset");
4956        assert!(ds.downcast_ref::<NodeLocalDataset>().is_some());
4957    }
4958
4959    #[test]
4960    fn render_node_survives_every_pathological_scale_factor() {
4961        // `scale_factor == 1.0` picks a shorter transform list; every other value takes
4962        // the scale branch, where the f32 is pushed through `PercentageValue::new`.
4963        let g = graph();
4964        let baseline = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
4965        for scale in [
4966            0.0,
4967            -0.0,
4968            -1.0,
4969            1e-30,
4970            f32::MAX,
4971            f32::MIN,
4972            f32::EPSILON,
4973            f32::INFINITY,
4974            f32::NEG_INFINITY,
4975            f32::NAN,
4976        ] {
4977            let dom = render_one(&g, N1, (0.0, 0.0), scale);
4978            assert_eq!(
4979                count_nodes(&dom),
4980                baseline,
4981                "scale {scale} changed the node structure",
4982            );
4983        }
4984    }
4985
4986    #[test]
4987    fn render_node_survives_every_pathological_graph_offset() {
4988        let g = graph();
4989        let baseline = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
4990        for offset in [
4991            (f32::NAN, f32::NAN),
4992            (f32::INFINITY, f32::NEG_INFINITY),
4993            (f32::MAX, f32::MIN),
4994            (-1e30, 1e30),
4995        ] {
4996            assert_eq!(count_nodes(&render_one(&g, N1, offset, 1.0)), baseline);
4997        }
4998    }
4999
5000    #[test]
5001    fn render_node_survives_a_node_positioned_at_nan_and_infinity() {
5002        let mut g = graph();
5003        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
5004            x: f32::NAN,
5005            y: f32::INFINITY,
5006        };
5007        let dom = render_one(&g, N1, (0.0, 0.0), 1.0);
5008        assert!(count_nodes(&dom) > 1);
5009    }
5010
5011    #[test]
5012    fn render_node_drops_all_ports_when_the_backref_is_not_a_node_graph_dataset() {
5013        // Both port lists are built by downcasting through `backref`; a foreign payload
5014        // must degrade to "no ports" rather than panicking.
5015        let g = graph();
5016        let n = g.nodes.iter().find(|n| n.node_id == N1).expect("fixture");
5017        let ty = g
5018            .node_types
5019            .iter()
5020            .find(|t| t.node_type_id == TYPE_A)
5021            .expect("fixture");
5022
5023        let broken = render_node(
5024            &n.node,
5025            (0.0, 0.0),
5026            &ty.node_type_info,
5027            NodeLocalDataset {
5028                node_id: N1,
5029                backref: RefAny::new(0xDEAD_BEEF_u32),
5030            },
5031            1.0,
5032        );
5033        let intact = render_one(&g, N1, (0.0, 0.0), 1.0);
5034
5035        assert!(count_nodes(&broken) > 1, "the node body still renders");
5036        assert!(
5037            count_nodes(&broken) < count_nodes(&intact),
5038            "a broken backref must cost the ports: {} vs {}",
5039            count_nodes(&broken),
5040            count_nodes(&intact),
5041        );
5042    }
5043
5044    #[test]
5045    fn render_node_drops_ports_whose_io_type_has_no_registered_info() {
5046        let mut g = graph();
5047        // Point TYPE_A's single input at an I/O id that has no `InputOutputInfo`.
5048        g.node_types.as_mut()[0].node_type_info.inputs = vec![IO_COLORLESS].into();
5049        let stripped = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
5050        let intact = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
5051        assert!(stripped < intact, "{stripped} vs {intact}");
5052    }
5053
5054    #[test]
5055    fn render_node_renders_all_five_field_widget_kinds() {
5056        let mut g = graph();
5057        g.nodes.as_mut()[0].node.fields = vec![
5058            NodeTypeField {
5059                key: AzString::from_const_str("text"),
5060                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("hello")),
5061            },
5062            NodeTypeField {
5063                key: AzString::from_const_str("number"),
5064                value: NodeTypeFieldValue::NumberInput(1.5),
5065            },
5066            NodeTypeField {
5067                key: AzString::from_const_str("check"),
5068                value: NodeTypeFieldValue::CheckBox(true),
5069            },
5070            NodeTypeField {
5071                key: AzString::from_const_str("color"),
5072                value: NodeTypeFieldValue::ColorInput(ColorU {
5073                    r: 9,
5074                    g: 8,
5075                    b: 7,
5076                    a: 6,
5077                }),
5078            },
5079            NodeTypeField {
5080                key: AzString::from_const_str("file"),
5081                value: NodeTypeFieldValue::FileInput(OptionString::None),
5082            },
5083        ]
5084        .into();
5085
5086        let with_fields = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
5087        let without = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
5088        assert!(with_fields > without, "{with_fields} vs {without}");
5089    }
5090
5091    #[test]
5092    fn render_node_field_count_is_monotonic() {
5093        let base = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
5094        let mut previous = base;
5095        for count in 1..=4_usize {
5096            let mut g = graph();
5097            g.nodes.as_mut()[0].node.fields = (0..count)
5098                .map(|_| NodeTypeField {
5099                    key: AzString::from_const_str("f"),
5100                    value: NodeTypeFieldValue::CheckBox(false),
5101                })
5102                .collect::<Vec<_>>()
5103                .into();
5104            let now = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
5105            assert!(now > previous, "{count} fields: {now} !> {previous}");
5106            previous = now;
5107        }
5108    }
5109
5110    #[test]
5111    fn render_node_accepts_pathological_field_values() {
5112        // Empty / emoji / RTL / zero-width labels, NaN and infinite numbers, a fully
5113        // transparent color and a unicode file path.
5114        let mut g = graph();
5115        g.nodes.as_mut()[0].node.fields = vec![
5116            NodeTypeField {
5117                key: AzString::from_const_str(""),
5118                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("")),
5119            },
5120            NodeTypeField {
5121                key: AzString::from_const_str("🎉\u{200b}اختبار"),
5122                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("𝕬\u{0301}\u{feff}")),
5123            },
5124            NodeTypeField {
5125                key: AzString::from_const_str("nan"),
5126                value: NodeTypeFieldValue::NumberInput(f32::NAN),
5127            },
5128            NodeTypeField {
5129                key: AzString::from_const_str("inf"),
5130                value: NodeTypeFieldValue::NumberInput(f32::NEG_INFINITY),
5131            },
5132            NodeTypeField {
5133                key: AzString::from_const_str("max"),
5134                value: NodeTypeFieldValue::NumberInput(f32::MAX),
5135            },
5136            NodeTypeField {
5137                key: AzString::from_const_str("clear"),
5138                value: NodeTypeFieldValue::ColorInput(ColorU {
5139                    r: 0,
5140                    g: 0,
5141                    b: 0,
5142                    a: 0,
5143                }),
5144            },
5145            NodeTypeField {
5146                key: AzString::from_const_str("path"),
5147                value: NodeTypeFieldValue::FileInput(OptionString::Some(
5148                    AzString::from_const_str("/tmp/日本語/🎉.txt"),
5149                )),
5150            },
5151        ]
5152        .into();
5153
5154        assert!(count_nodes(&render_one(&g, N1, (f32::NAN, f32::NAN), f32::NAN)) > 1);
5155    }
5156
5157    // ==================================================================
5158    // 10. render_connections
5159    // ==================================================================
5160
5161    fn marker() -> RefAny {
5162        RefAny::new(NodeConnectionMarkerDataset {})
5163    }
5164
5165    #[test]
5166    fn render_connections_of_an_unwired_graph_has_no_children() {
5167        let dom = render_connections(&graph(), marker());
5168        assert_eq!(dom.children.len(), 0);
5169        assert!(
5170            dom.root.get_dataset().is_some(),
5171            "the container must keep the marker dataset that drag-handling looks up",
5172        );
5173    }
5174
5175    #[test]
5176    fn render_connections_emits_exactly_one_child_per_wire() {
5177        let mut g = graph();
5178        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5179        assert_eq!(render_connections(&g, marker()).children.len(), 1);
5180
5181        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
5182        assert_eq!(render_connections(&g, marker()).children.len(), 2);
5183
5184        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
5185        assert_eq!(render_connections(&g, marker()).children.len(), 3);
5186    }
5187
5188    #[test]
5189    fn render_connections_skips_a_wire_to_a_node_that_no_longer_exists() {
5190        // `get_rect` returns `None`; the renderer must drop the wire, not unwrap it.
5191        let g = force_out_connection(graph(), N1, 0, MISSING, 0);
5192        assert_eq!(render_connections(&g, marker()).children.len(), 0);
5193    }
5194
5195    #[test]
5196    fn render_connections_skips_an_out_of_range_output_port() {
5197        for out_idx in [1_usize, 99, usize::MAX] {
5198            let g = force_out_connection(graph(), N1, out_idx, N3, 0);
5199            assert_eq!(
5200                render_connections(&g, marker()).children.len(),
5201                0,
5202                "output index {out_idx} must be skipped",
5203            );
5204        }
5205    }
5206
5207    #[test]
5208    fn render_connections_skips_a_node_whose_type_is_not_registered() {
5209        let mut g = graph();
5210        g.nodes.push(NodeIdNodeMap {
5211            node_id: NodeGraphNodeId { inner: 50 },
5212            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
5213        });
5214        let g = force_out_connection(g, NodeGraphNodeId { inner: 50 }, 0, N3, 0);
5215        assert_eq!(render_connections(&g, marker()).children.len(), 0);
5216    }
5217
5218    #[test]
5219    fn render_connections_skips_a_port_whose_io_type_has_no_color() {
5220        let mut g = graph();
5221        g.node_types.as_mut()[0].node_type_info.outputs = vec![IO_COLORLESS].into();
5222        let g = force_out_connection(g, N1, 0, N3, 0);
5223        assert_eq!(render_connections(&g, marker()).children.len(), 0);
5224    }
5225
5226    #[test]
5227    fn render_connections_survives_nan_positions_and_scale() {
5228        let mut g = graph();
5229        g.scale_factor = f32::NAN;
5230        g.offset = LogicalPosition {
5231            x: f32::INFINITY,
5232            y: f32::NAN,
5233        };
5234        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
5235            x: f32::NAN,
5236            y: f32::NAN,
5237        };
5238        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5239        assert_eq!(render_connections(&g, marker()).children.len(), 1);
5240    }
5241
5242    #[test]
5243    fn render_connections_renders_a_self_loop() {
5244        let mut g = graph();
5245        g.connect_input_output(N1, 0, N1, 0).expect("self loop");
5246        assert_eq!(render_connections(&g, marker()).children.len(), 1);
5247    }
5248
5249    // ==================================================================
5250    // 11. draw_connection
5251    // ==================================================================
5252
5253    #[test]
5254    fn draw_connection_returns_a_fixed_100x100_null_image() {
5255        // The real curve rendering is stubbed out pending `RenderImageCallbackInfo`;
5256        // until then the size is a constant and must not depend on the payload.
5257        let img = draw_connection(RefAny::new(connection(N1, 0, N3, 0)), ());
5258        assert_eq!(img.get_size().width, 100.0);
5259        assert_eq!(img.get_size().height, 100.0);
5260    }
5261
5262    #[test]
5263    fn draw_connection_ignores_a_payload_of_the_wrong_type() {
5264        for payload in [
5265            RefAny::new(NodeConnectionMarkerDataset {}),
5266            RefAny::new(0_u8),
5267            RefAny::new(String::new()),
5268        ] {
5269            let img = draw_connection(payload, ());
5270            assert_eq!(img.get_size().width, 100.0);
5271        }
5272    }
5273
5274    #[test]
5275    fn draw_connection_does_not_consume_or_corrupt_its_payload() {
5276        let cld = RefAny::new(connection(N1, 2, N3, 5));
5277        let _ = draw_connection(cld.clone(), ());
5278        let _ = draw_connection(cld.clone(), ());
5279
5280        let mut probe = cld.clone();
5281        let read_back = probe
5282            .downcast_ref::<ConnectionLocalDataset>()
5283            .expect("payload must still be downcastable after the callback ran");
5284        assert_eq!(read_back.out_idx, 2);
5285        assert_eq!(read_back.in_idx, 5);
5286    }
5287
5288    #[test]
5289    fn draw_connection_returns_distinct_image_handles_per_call() {
5290        let cld = RefAny::new(connection(N1, 0, N3, 0));
5291        let a = draw_connection(cld.clone(), ());
5292        let b = draw_connection(cld.clone(), ());
5293        assert_ne!(a, b, "each call must mint a fresh ImageRef id");
5294    }
5295
5296    // ==================================================================
5297    // 12. NodeGraph::dom
5298    // ==================================================================
5299
5300    #[test]
5301    fn dom_of_a_default_graph_has_the_expected_skeleton() {
5302        let dom = NodeGraph::default().dom();
5303
5304        assert!(
5305            dom.root.get_context_menu().is_some(),
5306            "the 'add node' context menu is the only way to create nodes",
5307        );
5308        assert!(dom.root.get_dataset().is_some());
5309        assert_eq!(dom.children.len(), 1, "wrapper holds exactly the .nodegraph");
5310
5311        let nodegraph = &dom.children.as_slice()[0];
5312        assert_eq!(
5313            nodegraph.children.len(),
5314            2,
5315            "connections container + nodes container",
5316        );
5317    }
5318
5319    #[test]
5320    fn dom_root_dataset_round_trips_back_to_a_node_graph_local_dataset() {
5321        let dom = graph().dom();
5322        let mut ds = dom.root.get_dataset().cloned().expect("root dataset");
5323        let inner = ds
5324            .downcast_ref::<NodeGraphLocalDataset>()
5325            .expect("root dataset must be the NodeGraphLocalDataset");
5326        assert_eq!(inner.node_graph.nodes.len(), 4);
5327        assert!(inner.last_input_or_output_clicked.is_none());
5328        assert!(inner.active_node_being_dragged.is_none());
5329    }
5330
5331    #[test]
5332    fn dom_renders_one_child_per_node_and_silently_drops_unregistered_types() {
5333        let mut g = graph();
5334        g.nodes.push(NodeIdNodeMap {
5335            node_id: NodeGraphNodeId { inner: 50 },
5336            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
5337        });
5338
5339        let dom = g.dom();
5340        let nodes_container = &dom.children.as_slice()[0].children.as_slice()[1];
5341        assert_eq!(
5342            nodes_container.children.len(),
5343            4,
5344            "the 5th node has no registered type and must be filtered out",
5345        );
5346    }
5347
5348    #[test]
5349    fn dom_context_menu_lists_one_submenu_entry_per_node_type() {
5350        let g = graph();
5351        let dom = g.dom();
5352        let menu = dom.root.get_context_menu().expect("context menu").clone();
5353        assert_eq!(menu.items.len(), 1, "one top-level 'add node' entry");
5354        match &menu.items.as_slice()[0] {
5355            MenuItem::String(s) => assert_eq!(s.children.len(), 2, "TYPE_A and TYPE_B"),
5356            other => panic!("expected a string menu item, got {other:?}"),
5357        }
5358    }
5359
5360    #[test]
5361    fn dom_context_menu_is_present_even_with_no_node_types_at_all() {
5362        let mut g = graph();
5363        g.node_types = NodeTypeIdInfoMapVec::new();
5364        let dom = g.dom();
5365        let menu = dom.root.get_context_menu().expect("context menu").clone();
5366        assert_eq!(menu.items.len(), 1);
5367        match &menu.items.as_slice()[0] {
5368            MenuItem::String(s) => assert_eq!(s.children.len(), 0),
5369            other => panic!("expected a string menu item, got {other:?}"),
5370        }
5371    }
5372
5373    #[test]
5374    fn dom_survives_pathological_scale_offset_and_positions() {
5375        for (scale, ox, oy) in [
5376            (f32::NAN, f32::NAN, f32::NAN),
5377            (0.0, 0.0, 0.0),
5378            (-1.0, -1e30, 1e30),
5379            (f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY),
5380            (f32::MAX, f32::MAX, f32::MIN),
5381        ] {
5382            let mut g = graph();
5383            g.scale_factor = scale;
5384            g.offset = LogicalPosition { x: ox, y: oy };
5385            g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
5386                x: f32::NAN,
5387                y: f32::INFINITY,
5388            };
5389            g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5390            let dom = g.dom();
5391            assert_eq!(dom.children.len(), 1, "scale {scale}");
5392        }
5393    }
5394
5395    #[test]
5396    fn dom_of_a_wired_graph_renders_the_connection_container_children() {
5397        let mut g = graph();
5398        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5399        let dom = g.dom();
5400        let connections = &dom.children.as_slice()[0].children.as_slice()[0];
5401        assert_eq!(connections.children.len(), 1);
5402    }
5403
5404    #[test]
5405    fn dom_can_be_converted_into_a_styled_dom() {
5406        // `StyledDom::create_from_dom` re-derives the child counters; a mismatch there
5407        // would panic while building the compact arena.
5408        let mut g = graph();
5409        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5410        g.nodes.as_mut()[0].node.fields = vec![NodeTypeField {
5411            key: AzString::from_const_str("k"),
5412            value: NodeTypeFieldValue::NumberInput(f32::NAN),
5413        }]
5414        .into();
5415        let styled = StyledDom::create_from_dom(g.dom());
5416        assert!(styled.node_data.len() > 1);
5417    }
5418
5419    // ==================================================================
5420    // 13. nodegraph_set_active_node / nodegraph_unset_active_node
5421    // ==================================================================
5422
5423    #[test]
5424    fn set_active_node_records_the_node_and_unset_clears_it() {
5425        let g = graph();
5426        let gd = graph_dataset(&g);
5427        let nd = RefAny::new(NodeLocalDataset {
5428            node_id: N2,
5429            backref: gd.clone(),
5430        });
5431
5432        assert_eq!(
5433            fire(|info| nodegraph_set_active_node(nd.clone(), info)),
5434            Update::DoNothing,
5435        );
5436        {
5437            let mut probe = gd.clone();
5438            let d = probe.downcast_ref::<NodeGraphLocalDataset>().expect("gd");
5439            assert_eq!(
5440                d.active_node_being_dragged.as_ref().map(|(id, _)| *id),
5441                Some(N2),
5442            );
5443        }
5444
5445        assert_eq!(
5446            fire(|info| nodegraph_unset_active_node(gd.clone(), info)),
5447            Update::DoNothing,
5448        );
5449        {
5450            let mut probe = gd.clone();
5451            let d = probe.downcast_ref::<NodeGraphLocalDataset>().expect("gd");
5452            assert!(d.active_node_being_dragged.is_none());
5453        }
5454    }
5455
5456    #[test]
5457    fn set_active_node_ignores_a_payload_of_the_wrong_type() {
5458        assert_eq!(
5459            fire(|info| nodegraph_set_active_node(RefAny::new(1_u64), info)),
5460            Update::DoNothing,
5461        );
5462    }
5463
5464    #[test]
5465    fn set_active_node_ignores_a_node_dataset_with_a_broken_backref() {
5466        // The outer downcast succeeds, the inner one does not — no state may change and
5467        // nothing may panic.
5468        let nd = RefAny::new(NodeLocalDataset {
5469            node_id: N2,
5470            backref: RefAny::new(0_u8),
5471        });
5472        assert_eq!(
5473            fire(|info| nodegraph_set_active_node(nd.clone(), info)),
5474            Update::DoNothing,
5475        );
5476    }
5477
5478    #[test]
5479    fn unset_active_node_is_idempotent_and_ignores_wrong_payloads() {
5480        let g = graph();
5481        let gd = graph_dataset(&g);
5482        for _ in 0..3 {
5483            assert_eq!(
5484                fire(|info| nodegraph_unset_active_node(gd.clone(), info)),
5485                Update::DoNothing,
5486            );
5487        }
5488        assert_eq!(
5489            fire(|info| nodegraph_unset_active_node(RefAny::new(0_i8), info)),
5490            Update::DoNothing,
5491        );
5492    }
5493
5494    // ==================================================================
5495    // 14. nodegraph_duplicate_node / nodegraph_delete_node
5496    // ==================================================================
5497
5498    #[test]
5499    fn duplicate_node_is_a_documented_no_op_for_valid_and_invalid_payloads() {
5500        let g = graph();
5501        let nd = RefAny::new(NodeLocalDataset {
5502            node_id: N1,
5503            backref: graph_dataset(&g),
5504        });
5505        assert_eq!(
5506            fire(|info| nodegraph_duplicate_node(nd.clone(), info)),
5507            Update::DoNothing,
5508        );
5509        assert_eq!(
5510            fire(|info| nodegraph_duplicate_node(RefAny::new(0_u16), info)),
5511            Update::DoNothing,
5512        );
5513    }
5514
5515    #[test]
5516    fn delete_node_forwards_the_node_id_to_on_node_removed() {
5517        let (g, log) = graph_with_log();
5518        let nd = RefAny::new(NodeLocalDataset {
5519            node_id: N3,
5520            backref: graph_dataset(&g),
5521        });
5522
5523        assert_eq!(
5524            fire(|info| nodegraph_delete_node(nd.clone(), info)),
5525            Update::RefreshDom,
5526            "the user callback's Update must be propagated verbatim",
5527        );
5528        log_of(&log, |l| assert_eq!(l.removed, vec![N3.inner]));
5529    }
5530
5531    #[test]
5532    fn delete_node_without_a_user_callback_does_nothing() {
5533        let g = graph();
5534        let nd = RefAny::new(NodeLocalDataset {
5535            node_id: N3,
5536            backref: graph_dataset(&g),
5537        });
5538        assert_eq!(
5539            fire(|info| nodegraph_delete_node(nd.clone(), info)),
5540            Update::DoNothing,
5541        );
5542    }
5543
5544    #[test]
5545    fn delete_node_reports_a_node_id_that_is_not_even_in_the_graph() {
5546        // Documented behaviour: the handler does not validate the id, it just forwards
5547        // it — removal is entirely the user callback's job.
5548        let (g, log) = graph_with_log();
5549        let nd = RefAny::new(NodeLocalDataset {
5550            node_id: MISSING,
5551            backref: graph_dataset(&g),
5552        });
5553        let _ = fire(|info| nodegraph_delete_node(nd.clone(), info));
5554        log_of(&log, |l| assert_eq!(l.removed, vec![MISSING.inner]));
5555    }
5556
5557    #[test]
5558    fn delete_node_ignores_broken_payloads() {
5559        assert_eq!(
5560            fire(|info| nodegraph_delete_node(RefAny::new(0_u32), info)),
5561            Update::DoNothing,
5562        );
5563        let nd = RefAny::new(NodeLocalDataset {
5564            node_id: N1,
5565            backref: RefAny::new(0_u8),
5566        });
5567        assert_eq!(
5568            fire(|info| nodegraph_delete_node(nd.clone(), info)),
5569            Update::DoNothing,
5570        );
5571    }
5572
5573    // ==================================================================
5574    // 15. nodegraph_drag_graph_or_nodes
5575    // ==================================================================
5576
5577    #[test]
5578    fn drag_without_a_previous_window_state_does_nothing() {
5579        // The harness leaves `previous_window_state` as `None`, which is exactly the
5580        // very-first-event case: no delta can be computed, so nothing may move.
5581        let (g, _log) = graph_with_log();
5582        let gd = graph_dataset(&g);
5583        let before = wiring(&dataset_graph(&gd));
5584
5585        assert_eq!(
5586            fire(|info| nodegraph_drag_graph_or_nodes(gd.clone(), info)),
5587            Update::DoNothing,
5588        );
5589        assert_eq!(wiring(&dataset_graph(&gd)), before);
5590        let after = dataset_graph(&gd);
5591        assert_eq!(after.offset.x, 0.0);
5592        assert_eq!(after.offset.y, 0.0);
5593    }
5594
5595    #[test]
5596    fn drag_ignores_a_payload_of_the_wrong_type() {
5597        assert_eq!(
5598            fire(|info| nodegraph_drag_graph_or_nodes(RefAny::new(0_u64), info)),
5599            Update::DoNothing,
5600        );
5601    }
5602
5603    #[test]
5604    fn drag_does_not_dereference_the_active_node_before_checking_the_mouse() {
5605        // An "active node" that is not in the graph would be an unwrap hazard if the
5606        // mouse-state guard were ever reordered after the lookup.
5607        let g = graph();
5608        let gd = graph_dataset(&g);
5609        {
5610            let mut probe = gd.clone();
5611            let mut d = probe.downcast_mut::<NodeGraphLocalDataset>().expect("gd");
5612            d.active_node_being_dragged = Some((MISSING, RefAny::new(0_u8)));
5613        }
5614        assert_eq!(
5615            fire(|info| nodegraph_drag_graph_or_nodes(gd.clone(), info)),
5616            Update::DoNothing,
5617        );
5618    }
5619
5620    // ==================================================================
5621    // 16. nodegraph_input_output_connect / _disconnect
5622    // ==================================================================
5623
5624    fn io_dataset(gd: &RefAny, node_id: NodeGraphNodeId, io: InputOrOutput) -> RefAny {
5625        RefAny::new(NodeInputOutputLocalDataset {
5626            io_id: io,
5627            backref: RefAny::new(NodeLocalDataset {
5628                node_id,
5629                backref: gd.clone(),
5630            }),
5631        })
5632    }
5633
5634    #[test]
5635    fn connect_click_one_only_arms_the_pending_port() {
5636        let g = graph();
5637        let gd = graph_dataset(&g);
5638        let first = io_dataset(&gd, N1, InputOrOutput::Output(0));
5639
5640        assert_eq!(
5641            fire(|info| nodegraph_input_output_connect(first.clone(), info)),
5642            Update::DoNothing,
5643        );
5644        assert_eq!(pending_click(&gd), Some((N1.inner, (false, 0))));
5645        assert_eq!(
5646            wiring(&dataset_graph(&gd)),
5647            wiring(&graph()),
5648            "arming a port must not wire anything yet",
5649        );
5650    }
5651
5652    #[test]
5653    fn connect_output_then_input_wires_the_graph_inside_the_dataset() {
5654        let (g, log) = graph_with_log();
5655        let gd = graph_dataset(&g);
5656        let out = io_dataset(&gd, N1, InputOrOutput::Output(0));
5657        let inn = io_dataset(&gd, N3, InputOrOutput::Input(0));
5658
5659        let _ = fire(|info| nodegraph_input_output_connect(out.clone(), info));
5660        assert_eq!(
5661            fire(|info| nodegraph_input_output_connect(inn.clone(), info)),
5662            Update::RefreshDom,
5663        );
5664
5665        let wired = dataset_graph(&gd);
5666        assert_eq!(inputs_of(&wired, N3), vec![(0, vec![(N1.inner, 0)])]);
5667        assert_eq!(outputs_of(&wired, N1), vec![(0, vec![(N3.inner, 0)])]);
5668        log_of(&log, |l| {
5669            assert_eq!(l.connected, vec![(N3.inner, 0, N1.inner, 0)]);
5670        });
5671        assert_eq!(
5672            pending_click(&gd),
5673            None,
5674            "a completed connection must disarm the pending port",
5675        );
5676    }
5677
5678    #[test]
5679    fn connect_input_then_output_wires_the_same_edge_in_the_same_direction() {
5680        // Clicking input-first and output-first must produce identical graphs — the
5681        // handler swaps the roles itself.
5682        let (g, _log) = graph_with_log();
5683
5684        let gd_a = graph_dataset(&g);
5685        let _ = fire(|info| {
5686            nodegraph_input_output_connect(io_dataset(&gd_a, N1, InputOrOutput::Output(0)), info)
5687        });
5688        let _ = fire(|info| {
5689            nodegraph_input_output_connect(io_dataset(&gd_a, N3, InputOrOutput::Input(0)), info)
5690        });
5691
5692        let gd_b = graph_dataset(&g);
5693        let _ = fire(|info| {
5694            nodegraph_input_output_connect(io_dataset(&gd_b, N3, InputOrOutput::Input(0)), info)
5695        });
5696        let _ = fire(|info| {
5697            nodegraph_input_output_connect(io_dataset(&gd_b, N1, InputOrOutput::Output(0)), info)
5698        });
5699
5700        assert_eq!(wiring(&dataset_graph(&gd_a)), wiring(&dataset_graph(&gd_b)));
5701    }
5702
5703    #[test]
5704    fn connect_output_to_output_disarms_instead_of_wiring() {
5705        let (g, log) = graph_with_log();
5706        let gd = graph_dataset(&g);
5707
5708        let _ = fire(|info| {
5709            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
5710        });
5711        assert_eq!(
5712            fire(|info| {
5713                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Output(0)), info)
5714            }),
5715            Update::DoNothing,
5716        );
5717
5718        assert_eq!(pending_click(&gd), None);
5719        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
5720        log_of(&log, |l| assert!(l.connected.is_empty()));
5721    }
5722
5723    #[test]
5724    fn connect_input_to_input_disarms_instead_of_wiring() {
5725        let (g, _log) = graph_with_log();
5726        let gd = graph_dataset(&g);
5727
5728        let _ = fire(|info| {
5729            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Input(0)), info)
5730        });
5731        assert_eq!(
5732            fire(|info| {
5733                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
5734            }),
5735            Update::DoNothing,
5736        );
5737        assert_eq!(pending_click(&gd), None);
5738        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
5739    }
5740
5741    #[test]
5742    fn connect_across_incompatible_types_disarms_and_leaves_the_graph_alone() {
5743        let (g, log) = graph_with_log();
5744        let gd = graph_dataset(&g);
5745
5746        // N1 emits `int`, N2 consumes `float`.
5747        let _ = fire(|info| {
5748            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
5749        });
5750        assert_eq!(
5751            fire(|info| {
5752                nodegraph_input_output_connect(io_dataset(&gd, N2, InputOrOutput::Input(0)), info)
5753            }),
5754            Update::DoNothing,
5755        );
5756
5757        assert_eq!(pending_click(&gd), None);
5758        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
5759        log_of(&log, |l| assert!(l.connected.is_empty()));
5760    }
5761
5762    #[test]
5763    fn connect_with_an_out_of_range_port_index_is_rejected() {
5764        let (g, _log) = graph_with_log();
5765        let gd = graph_dataset(&g);
5766
5767        let _ = fire(|info| {
5768            nodegraph_input_output_connect(
5769                io_dataset(&gd, N1, InputOrOutput::Output(usize::MAX)),
5770                info,
5771            )
5772        });
5773        assert_eq!(
5774            fire(|info| {
5775                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
5776            }),
5777            Update::DoNothing,
5778        );
5779        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
5780    }
5781
5782    #[test]
5783    fn connect_leaves_the_pending_port_armed_when_no_user_callback_is_installed() {
5784        // BUG (characterised): `last_input_or_output_clicked` is only cleared inside the
5785        // `Some(OnNodeConnected)` arm. A graph with no `on_node_connected` callback
5786        // therefore keeps the *first* click armed after a successful wire, so the next
5787        // port click re-uses the stale port and wires the wrong edge.
5788        let g = graph(); // no callbacks
5789        let gd = graph_dataset(&g);
5790
5791        let _ = fire(|info| {
5792            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
5793        });
5794        let _ = fire(|info| {
5795            nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
5796        });
5797
5798        let wired = dataset_graph(&gd);
5799        assert_eq!(inputs_of(&wired, N3), vec![(0, vec![(N1.inner, 0)])]);
5800        assert_eq!(
5801            pending_click(&gd),
5802            Some((N1.inner, (false, 0))),
5803            "N1's output stays armed after the wire was already made",
5804        );
5805
5806        // ...and the very next input click silently wires a second edge from it.
5807        let _ = fire(|info| {
5808            nodegraph_input_output_connect(io_dataset(&gd, N4, InputOrOutput::Input(0)), info)
5809        });
5810        assert_eq!(
5811            inputs_of(&dataset_graph(&gd), N4),
5812            vec![(0, vec![(N1.inner, 0)])],
5813            "the stale port produced an edge the user never armed",
5814        );
5815    }
5816
5817    #[test]
5818    fn connect_ignores_broken_payloads_at_every_level_of_the_backref_chain() {
5819        assert_eq!(
5820            fire(|info| nodegraph_input_output_connect(RefAny::new(0_u32), info)),
5821            Update::DoNothing,
5822        );
5823        let bad_mid = RefAny::new(NodeInputOutputLocalDataset {
5824            io_id: InputOrOutput::Input(0),
5825            backref: RefAny::new(0_u8),
5826        });
5827        assert_eq!(
5828            fire(|info| nodegraph_input_output_connect(bad_mid.clone(), info)),
5829            Update::DoNothing,
5830        );
5831        let bad_tail = RefAny::new(NodeInputOutputLocalDataset {
5832            io_id: InputOrOutput::Input(0),
5833            backref: RefAny::new(NodeLocalDataset {
5834                node_id: N1,
5835                backref: RefAny::new(0_u16),
5836            }),
5837        });
5838        assert_eq!(
5839            fire(|info| nodegraph_input_output_connect(bad_tail.clone(), info)),
5840            Update::DoNothing,
5841        );
5842    }
5843
5844    #[test]
5845    fn disconnect_notifies_the_input_or_the_output_callback_but_never_both() {
5846        let (g, log) = graph_with_log();
5847        let gd = graph_dataset(&g);
5848
5849        assert_eq!(
5850            fire(|info| {
5851                nodegraph_input_output_disconnect(io_dataset(&gd, N3, InputOrOutput::Input(4)), info)
5852            }),
5853            Update::RefreshDom,
5854        );
5855        log_of(&log, |l| {
5856            assert_eq!(l.input_disconnected, vec![(N3.inner, 4)]);
5857            assert!(l.output_disconnected.is_empty());
5858        });
5859
5860        assert_eq!(
5861            fire(|info| {
5862                nodegraph_input_output_disconnect(
5863                    io_dataset(&gd, N1, InputOrOutput::Output(7)),
5864                    info,
5865                )
5866            }),
5867            Update::RefreshDomAllWindows,
5868        );
5869        log_of(&log, |l| {
5870            assert_eq!(l.input_disconnected, vec![(N3.inner, 4)]);
5871            assert_eq!(l.output_disconnected, vec![(N1.inner, 7)]);
5872        });
5873    }
5874
5875    #[test]
5876    fn disconnect_notifies_but_does_not_actually_unwire_the_graph() {
5877        // BUG (characterised): the handler calls neither `disconnect_input` nor
5878        // `disconnect_output`, so the middle-click gesture fires the user callback while
5879        // the widget's own copy of the graph keeps the edge. Unless the user callback
5880        // rebuilds the graph, the connection stays on screen.
5881        let (mut g, _log) = graph_with_log();
5882        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
5883        let gd = graph_dataset(&g);
5884        let before = wiring(&dataset_graph(&gd));
5885
5886        let _ = fire(|info| {
5887            nodegraph_input_output_disconnect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
5888        });
5889
5890        assert_eq!(
5891            wiring(&dataset_graph(&gd)),
5892            before,
5893            "the model is untouched by the disconnect gesture",
5894        );
5895    }
5896
5897    #[test]
5898    fn disconnect_without_user_callbacks_does_nothing() {
5899        let g = graph();
5900        let gd = graph_dataset(&g);
5901        assert_eq!(
5902            fire(|info| {
5903                nodegraph_input_output_disconnect(io_dataset(&gd, N1, InputOrOutput::Input(0)), info)
5904            }),
5905            Update::DoNothing,
5906        );
5907        assert_eq!(
5908            fire(|info| nodegraph_input_output_disconnect(RefAny::new(0_u32), info)),
5909            Update::DoNothing,
5910        );
5911    }
5912
5913    #[test]
5914    fn disconnect_forwards_usize_max_port_indices_unclamped() {
5915        let (g, log) = graph_with_log();
5916        let gd = graph_dataset(&g);
5917        let _ = fire(|info| {
5918            nodegraph_input_output_disconnect(
5919                io_dataset(&gd, N1, InputOrOutput::Output(usize::MAX)),
5920                info,
5921            )
5922        });
5923        log_of(&log, |l| {
5924            assert_eq!(l.output_disconnected, vec![(N1.inner, usize::MAX)]);
5925        });
5926    }
5927
5928    // ==================================================================
5929    // 17. nodegraph_context_menu_click
5930    // ==================================================================
5931
5932    #[test]
5933    fn context_menu_click_does_nothing_when_the_graph_is_not_in_the_dom() {
5934        // `get_node_id_of_root_dataset` finds nothing in an empty window, so the handler
5935        // must bail out before touching the (still valid) backref.
5936        let (g, log) = graph_with_log();
5937        let cm = RefAny::new(ContextMenuEntryLocalDataset {
5938            node_type: TYPE_A,
5939            backref: graph_dataset(&g),
5940        });
5941        assert_eq!(
5942            fire(|info| nodegraph_context_menu_click(cm.clone(), info)),
5943            Update::DoNothing,
5944        );
5945        log_of(&log, |l| assert!(l.added.is_empty()));
5946    }
5947
5948    #[test]
5949    fn context_menu_click_reports_a_fresh_node_id_at_the_cursor() {
5950        let (g, log) = graph_with_log();
5951        let dom = g.dom();
5952        let gd = dom.root.get_dataset().cloned().expect("root dataset");
5953        let styled = StyledDom::create_from_dom(dom);
5954
5955        let cm = RefAny::new(ContextMenuEntryLocalDataset {
5956            node_type: TYPE_B,
5957            backref: gd,
5958        });
5959
5960        let (update, _) = with_info(styled, hit_none(), |info| {
5961            nodegraph_context_menu_click(cm.clone(), *info)
5962        });
5963
5964        assert_eq!(update, Update::RefreshDomAllWindows);
5965        log_of(&log, |l| {
5966            // Cursor is `Uninitialized` and there is no layout, so both the cursor and
5967            // the wrapper offset are (0, 0) — the position must be exactly zero, not NaN.
5968            assert_eq!(l.added, vec![(TYPE_B.inner, 5, 0.0, 0.0)]);
5969        });
5970    }
5971
5972    #[test]
5973    fn context_menu_click_position_degrades_to_nan_at_a_zero_scale_factor() {
5974        // `1.0 / scale_factor` is `inf` at zero; `0 * inf` is NaN. This pins down what
5975        // the widget actually hands the user callback in that case.
5976        let (mut g, log) = graph_with_log();
5977        g.scale_factor = 0.0;
5978        let dom = g.dom();
5979        let gd = dom.root.get_dataset().cloned().expect("root dataset");
5980        let styled = StyledDom::create_from_dom(dom);
5981
5982        let cm = RefAny::new(ContextMenuEntryLocalDataset {
5983            node_type: TYPE_A,
5984            backref: gd,
5985        });
5986        let (_, _) = with_info(styled, hit_none(), |info| {
5987            nodegraph_context_menu_click(cm.clone(), *info)
5988        });
5989
5990        log_of(&log, |l| {
5991            assert_eq!(l.added.len(), 1);
5992            let (_, id, x, y) = l.added[0];
5993            assert_eq!(id, 5, "the id must still be generated normally");
5994            assert!(x.is_nan() && y.is_nan(), "0 * inf == NaN, got ({x}, {y})");
5995        });
5996    }
5997
5998    #[test]
5999    fn context_menu_click_ignores_a_payload_of_the_wrong_type() {
6000        assert_eq!(
6001            fire(|info| nodegraph_context_menu_click(RefAny::new(0_u32), info)),
6002            Update::DoNothing,
6003        );
6004    }
6005
6006    // ==================================================================
6007    // 18. field-edit callbacks
6008    // ==================================================================
6009
6010    #[test]
6011    fn textinput_focus_lost_forwards_the_decoded_text() {
6012        let (g, log) = graph_with_log();
6013        let fd = RefAny::new(NodeFieldLocalDataset {
6014            field_idx: 2,
6015            backref: RefAny::new(NodeLocalDataset {
6016                node_id: N2,
6017                backref: graph_dataset(&g),
6018            }),
6019        });
6020
6021        // 'H', a lone surrogate (never a valid char), then U+1F389 — `get_text` drops
6022        // the surrogate, so the callback must see exactly "H🎉".
6023        let state = TextInputState {
6024            text: vec![0x48_u32, 0xD800, 0x1F389].into(),
6025            ..TextInputState::default()
6026        };
6027
6028        assert_eq!(
6029            fire(|info| nodegraph_on_textinput_focus_lost(fd.clone(), info, state.clone())),
6030            Update::RefreshDom,
6031        );
6032        log_of(&log, |l| {
6033            assert_eq!(l.edited, vec![(N2.inner, 2, TYPE_B.inner)]);
6034            assert_eq!(l.text_values, vec!["H\u{1F389}".to_string()]);
6035        });
6036    }
6037
6038    #[test]
6039    fn textinput_focus_lost_handles_an_empty_and_an_out_of_range_scalar() {
6040        let (g, log) = graph_with_log();
6041        let fd = RefAny::new(NodeFieldLocalDataset {
6042            field_idx: 0,
6043            backref: RefAny::new(NodeLocalDataset {
6044                node_id: N1,
6045                backref: graph_dataset(&g),
6046            }),
6047        });
6048
6049        for (raw, expected) in [
6050            (vec![], ""),
6051            (vec![0x11_0000_u32, 0xFFFF_FFFF], ""), // both above the Unicode range
6052            (vec![0x0041, 0x0301], "A\u{0301}"),    // combining mark survives
6053        ] {
6054            let state = TextInputState {
6055                text: raw.into(),
6056                ..TextInputState::default()
6057            };
6058            let _ = fire(|info| nodegraph_on_textinput_focus_lost(fd.clone(), info, state.clone()));
6059            log_of(&log, |l| {
6060                assert_eq!(l.text_values.last().map(String::as_str), Some(expected));
6061            });
6062        }
6063    }
6064
6065    #[test]
6066    fn numberinput_focus_lost_forwards_nan_and_infinities_verbatim() {
6067        let (g, log) = graph_with_log();
6068        let fd = RefAny::new(NodeFieldLocalDataset {
6069            field_idx: 1,
6070            backref: RefAny::new(NodeLocalDataset {
6071                node_id: N1,
6072                backref: graph_dataset(&g),
6073            }),
6074        });
6075
6076        for n in [
6077            0.0_f32,
6078            -0.0,
6079            f32::MAX,
6080            f32::MIN,
6081            f32::MIN_POSITIVE,
6082            f32::EPSILON,
6083            f32::INFINITY,
6084            f32::NEG_INFINITY,
6085            f32::NAN,
6086        ] {
6087            let state = NumberInputState {
6088                number: n,
6089                ..NumberInputState::default()
6090            };
6091            assert_eq!(
6092                fire(|info| nodegraph_on_numberinput_focus_lost(fd.clone(), info, state)),
6093                Update::RefreshDom,
6094            );
6095        }
6096
6097        log_of(&log, |l| {
6098            assert_eq!(l.number_values.len(), 9);
6099            assert_eq!(l.number_values[0], 0.0);
6100            assert_eq!(l.number_values[2], f32::MAX);
6101            assert!(l.number_values[6].is_infinite());
6102            assert!(l.number_values[8].is_nan(), "NaN must not be normalised");
6103            assert!(l.edited.iter().all(|e| *e == (N1.inner, 1, TYPE_A.inner)));
6104        });
6105    }
6106
6107    #[test]
6108    fn checkbox_change_forwards_both_states() {
6109        let (g, log) = graph_with_log();
6110        let fd = RefAny::new(NodeFieldLocalDataset {
6111            field_idx: 0,
6112            backref: RefAny::new(NodeLocalDataset {
6113                node_id: N1,
6114                backref: graph_dataset(&g),
6115            }),
6116        });
6117        for checked in [true, false, true] {
6118            let _ = fire(|info| {
6119                nodegraph_on_checkbox_value_changed(fd.clone(), info, CheckBoxState { checked })
6120            });
6121        }
6122        log_of(&log, |l| assert_eq!(l.bool_values, vec![true, false, true]));
6123    }
6124
6125    #[test]
6126    fn colorinput_change_forwards_every_channel_including_alpha_extremes() {
6127        let (g, log) = graph_with_log();
6128        let fd = RefAny::new(NodeFieldLocalDataset {
6129            field_idx: 3,
6130            backref: RefAny::new(NodeLocalDataset {
6131                node_id: N1,
6132                backref: graph_dataset(&g),
6133            }),
6134        });
6135        // {1,2,3,4} catches a channel swap that greys would hide; 0 and 255 alpha are
6136        // the two extremes.
6137        for c in [
6138            ColorU { r: 1, g: 2, b: 3, a: 4 },
6139            ColorU { r: 0, g: 0, b: 0, a: 0 },
6140            ColorU { r: 255, g: 255, b: 255, a: 255 },
6141        ] {
6142            let _ = fire(|info| {
6143                nodegraph_on_colorinput_value_changed(fd.clone(), info, ColorInputState { color: c })
6144            });
6145        }
6146        log_of(&log, |l| {
6147            assert_eq!(
6148                l.color_values,
6149                vec![(1, 2, 3, 4), (0, 0, 0, 0), (255, 255, 255, 255)],
6150            );
6151        });
6152    }
6153
6154    #[test]
6155    fn fileinput_click_forwards_both_a_missing_and_a_unicode_path() {
6156        let (g, log) = graph_with_log();
6157        let fd = RefAny::new(NodeFieldLocalDataset {
6158            field_idx: 4,
6159            backref: RefAny::new(NodeLocalDataset {
6160                node_id: N1,
6161                backref: graph_dataset(&g),
6162            }),
6163        });
6164
6165        let _ = fire(|info| {
6166            nodegraph_on_fileinput_button_clicked(
6167                fd.clone(),
6168                info,
6169                FileInputState {
6170                    path: OptionString::None,
6171                },
6172            )
6173        });
6174        let _ = fire(|info| {
6175            nodegraph_on_fileinput_button_clicked(
6176                fd.clone(),
6177                info,
6178                FileInputState {
6179                    path: OptionString::Some(AzString::from_const_str("/tmp/日本語/🎉.txt")),
6180                },
6181            )
6182        });
6183
6184        log_of(&log, |l| {
6185            assert_eq!(
6186                l.file_values,
6187                vec![None, Some("/tmp/日本語/🎉.txt".to_string())],
6188            );
6189        });
6190    }
6191
6192    #[test]
6193    fn field_callbacks_bail_out_when_the_node_is_not_in_the_graph() {
6194        // Every one of the five handlers looks the node type up first; a stale
6195        // `node_id` must return `DoNothing` instead of unwrapping.
6196        let (g, log) = graph_with_log();
6197        let fd = RefAny::new(NodeFieldLocalDataset {
6198            field_idx: 0,
6199            backref: RefAny::new(NodeLocalDataset {
6200                node_id: MISSING,
6201                backref: graph_dataset(&g),
6202            }),
6203        });
6204
6205        assert_eq!(
6206            fire(|info| nodegraph_on_textinput_focus_lost(
6207                fd.clone(),
6208                info,
6209                TextInputState::default()
6210            )),
6211            Update::DoNothing,
6212        );
6213        assert_eq!(
6214            fire(|info| nodegraph_on_numberinput_focus_lost(
6215                fd.clone(),
6216                info,
6217                NumberInputState::default()
6218            )),
6219            Update::DoNothing,
6220        );
6221        assert_eq!(
6222            fire(|info| nodegraph_on_checkbox_value_changed(
6223                fd.clone(),
6224                info,
6225                CheckBoxState::default()
6226            )),
6227            Update::DoNothing,
6228        );
6229        assert_eq!(
6230            fire(|info| nodegraph_on_colorinput_value_changed(
6231                fd.clone(),
6232                info,
6233                ColorInputState::default()
6234            )),
6235            Update::DoNothing,
6236        );
6237        assert_eq!(
6238            fire(|info| nodegraph_on_fileinput_button_clicked(
6239                fd.clone(),
6240                info,
6241                FileInputState::default()
6242            )),
6243            Update::DoNothing,
6244        );
6245
6246        log_of(&log, |l| assert!(l.edited.is_empty()));
6247    }
6248
6249    #[test]
6250    fn field_callbacks_bail_out_on_a_payload_of_the_wrong_type() {
6251        assert_eq!(
6252            fire(|info| nodegraph_on_textinput_focus_lost(
6253                RefAny::new(0_u32),
6254                info,
6255                TextInputState::default()
6256            )),
6257            Update::DoNothing,
6258        );
6259        assert_eq!(
6260            fire(|info| nodegraph_on_numberinput_focus_lost(
6261                RefAny::new(0_u32),
6262                info,
6263                NumberInputState::default()
6264            )),
6265            Update::DoNothing,
6266        );
6267        assert_eq!(
6268            fire(|info| nodegraph_on_checkbox_value_changed(
6269                RefAny::new(0_u32),
6270                info,
6271                CheckBoxState::default()
6272            )),
6273            Update::DoNothing,
6274        );
6275        assert_eq!(
6276            fire(|info| nodegraph_on_colorinput_value_changed(
6277                RefAny::new(0_u32),
6278                info,
6279                ColorInputState::default()
6280            )),
6281            Update::DoNothing,
6282        );
6283        assert_eq!(
6284            fire(|info| nodegraph_on_fileinput_button_clicked(
6285                RefAny::new(0_u32),
6286                info,
6287                FileInputState::default()
6288            )),
6289            Update::DoNothing,
6290        );
6291    }
6292
6293    #[test]
6294    fn field_callbacks_forward_a_usize_max_field_index_unclamped() {
6295        // The field index is an opaque token as far as the widget is concerned — it is
6296        // never used to index anything here, so even `usize::MAX` must pass through.
6297        let (g, log) = graph_with_log();
6298        let fd = RefAny::new(NodeFieldLocalDataset {
6299            field_idx: usize::MAX,
6300            backref: RefAny::new(NodeLocalDataset {
6301                node_id: N1,
6302                backref: graph_dataset(&g),
6303            }),
6304        });
6305        let _ = fire(|info| {
6306            nodegraph_on_checkbox_value_changed(fd.clone(), info, CheckBoxState { checked: true })
6307        });
6308        log_of(&log, |l| {
6309            assert_eq!(l.edited, vec![(N1.inner, usize::MAX, TYPE_A.inner)]);
6310        });
6311    }
6312
6313    #[test]
6314    fn field_callbacks_without_a_user_callback_do_nothing() {
6315        let g = graph(); // no callbacks installed
6316        let fd = RefAny::new(NodeFieldLocalDataset {
6317            field_idx: 0,
6318            backref: RefAny::new(NodeLocalDataset {
6319                node_id: N1,
6320                backref: graph_dataset(&g),
6321            }),
6322        });
6323        assert_eq!(
6324            fire(|info| nodegraph_on_checkbox_value_changed(
6325                fd.clone(),
6326                info,
6327                CheckBoxState::default()
6328            )),
6329            Update::DoNothing,
6330        );
6331        assert_eq!(
6332            fire(|info| nodegraph_on_fileinput_button_clicked(
6333                fd.clone(),
6334                info,
6335                FileInputState::default()
6336            )),
6337            Update::DoNothing,
6338        );
6339    }
6340}