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

            
14
use alloc::vec::Vec;
15
use core::fmt;
16

            
17
use 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
29
use azul_css::{
30
    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
31
    props::{
32
        basic::*,
33
        layout::*,
34
        property::{CssProperty, *},
35
        style::*,
36
    },
37
    *,
38
};
39
use azul_css::css::BoxOrStatic;
40

            
41
use 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)]
55
pub 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

            
67
impl Default for NodeGraph {
68
140
    fn default() -> Self {
69
140
        Self {
70
140
            node_types: NodeTypeIdInfoMapVec::from_const_slice(&[]),
71
140
            input_output_types: InputOutputTypeIdInfoMapVec::from_const_slice(&[]),
72
140
            nodes: NodeIdNodeMapVec::from_const_slice(&[]),
73
140
            allow_multiple_root_nodes: false,
74
140
            offset: LogicalPosition::zero(),
75
140
            style: NodeGraphStyle::Default,
76
140
            callbacks: NodeGraphCallbacks::default(),
77
140
            add_node_str: AzString::from_const_str(""),
78
140
            scale_factor: 1.0,
79
140
        }
80
140
    }
81
}
82

            
83
impl NodeGraph {
84
    /// Generates a new `NodeId` that is unique in the graph
85
9
    #[must_use] pub fn generate_unique_node_id(&self) -> NodeGraphNodeId {
86
        NodeGraphNodeId {
87
9
            inner: self
88
9
                .nodes
89
9
                .iter()
90
9
                .map(|i| i.node_id.inner)
91
9
                .max()
92
9
                .unwrap_or(0)
93
9
                .saturating_add(1),
94
        }
95
9
    }
96
}
97

            
98
/// Maps a [`NodeTypeId`] to its [`NodeTypeInfo`] metadata.
99
#[derive(Debug, Clone)]
100
#[repr(C)]
101
pub struct NodeTypeIdInfoMap {
102
    pub node_type_id: NodeTypeId,
103
    pub node_type_info: NodeTypeInfo,
104
}
105

            
106
impl_option!(NodeTypeIdInfoMap, OptionNodeTypeIdInfoMap, copy = false, [Debug, Clone]);
107
impl_vec!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec, NodeTypeIdInfoMapVecDestructor, NodeTypeIdInfoMapVecDestructorType, NodeTypeIdInfoMapVecSlice, OptionNodeTypeIdInfoMap);
108
impl_vec_clone!(
109
    NodeTypeIdInfoMap,
110
    NodeTypeIdInfoMapVec,
111
    NodeTypeIdInfoMapVecDestructor
112
);
113
impl_vec_mut!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec);
114
impl_vec_debug!(NodeTypeIdInfoMap, NodeTypeIdInfoMapVec);
115

            
116
/// Maps an [`InputOutputTypeId`] to its [`InputOutputInfo`] metadata.
117
#[derive(Debug, Clone)]
118
#[repr(C)]
119
pub struct InputOutputTypeIdInfoMap {
120
    pub io_type_id: InputOutputTypeId,
121
    pub io_info: InputOutputInfo,
122
}
123

            
124
impl_option!(InputOutputTypeIdInfoMap, OptionInputOutputTypeIdInfoMap, copy = false, [Debug, Clone]);
125
impl_vec!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec, InputOutputTypeIdInfoMapVecDestructor, InputOutputTypeIdInfoMapVecDestructorType, InputOutputTypeIdInfoMapVecSlice, OptionInputOutputTypeIdInfoMap);
126
impl_vec_clone!(
127
    InputOutputTypeIdInfoMap,
128
    InputOutputTypeIdInfoMapVec,
129
    InputOutputTypeIdInfoMapVecDestructor
130
);
131
impl_vec_mut!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec);
132
impl_vec_debug!(InputOutputTypeIdInfoMap, InputOutputTypeIdInfoMapVec);
133

            
134
/// Maps a [`NodeGraphNodeId`] to its [`Node`] data.
135
#[derive(Debug, Clone)]
136
#[repr(C)]
137
pub struct NodeIdNodeMap {
138
    pub node_id: NodeGraphNodeId,
139
    pub node: Node,
140
}
141

            
142
impl_option!(NodeIdNodeMap, OptionNodeIdNodeMap, copy = false, [Debug, Clone]);
143
impl_vec!(NodeIdNodeMap, NodeIdNodeMapVec, NodeIdNodeMapVecDestructor, NodeIdNodeMapVecDestructorType, NodeIdNodeMapVecSlice, OptionNodeIdNodeMap);
144
impl_vec_clone!(NodeIdNodeMap, NodeIdNodeMapVec, NodeIdNodeMapVecDestructor);
145
impl_vec_mut!(NodeIdNodeMap, NodeIdNodeMapVec);
146
impl_vec_debug!(NodeIdNodeMap, NodeIdNodeMapVec);
147

            
148
#[derive(Debug, Copy, Clone)]
149
#[repr(C)]
150
pub 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)]
158
pub 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

            
169
pub 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;
176
impl_widget_callback!(
177
    OnNodeAdded,
178
    OptionOnNodeAdded,
179
    OnNodeAddedCallback,
180
    OnNodeAddedCallbackType
181
);
182

            
183
pub type OnNodeRemovedCallbackType =
184
    extern "C" fn(refany: RefAny, info: CallbackInfo, node_id_to_remove: NodeGraphNodeId) -> Update;
185
impl_widget_callback!(
186
    OnNodeRemoved,
187
    OptionOnNodeRemoved,
188
    OnNodeRemovedCallback,
189
    OnNodeRemovedCallbackType
190
);
191

            
192
pub type OnNodeGraphDraggedCallbackType =
193
    extern "C" fn(refany: RefAny, info: CallbackInfo, drag_amount: GraphDragAmount) -> Update;
194
impl_widget_callback!(
195
    OnNodeGraphDragged,
196
    OptionOnNodeGraphDragged,
197
    OnNodeGraphDraggedCallback,
198
    OnNodeGraphDraggedCallbackType
199
);
200

            
201
pub type OnNodeDraggedCallbackType = extern "C" fn(
202
    refany: RefAny,
203
    info: CallbackInfo,
204
    node_dragged: NodeGraphNodeId,
205
    drag_amount: NodeDragAmount,
206
) -> Update;
207
impl_widget_callback!(
208
    OnNodeDragged,
209
    OptionOnNodeDragged,
210
    OnNodeDraggedCallback,
211
    OnNodeDraggedCallbackType
212
);
213

            
214
pub 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;
222
impl_widget_callback!(
223
    OnNodeConnected,
224
    OptionOnNodeConnected,
225
    OnNodeConnectedCallback,
226
    OnNodeConnectedCallbackType
227
);
228

            
229
pub type OnNodeInputDisconnectedCallbackType = extern "C" fn(
230
    refany: RefAny,
231
    info: CallbackInfo,
232
    input: NodeGraphNodeId,
233
    input_index: usize,
234
) -> Update;
235
impl_widget_callback!(
236
    OnNodeInputDisconnected,
237
    OptionOnNodeInputDisconnected,
238
    OnNodeInputDisconnectedCallback,
239
    OnNodeInputDisconnectedCallbackType
240
);
241

            
242
pub type OnNodeOutputDisconnectedCallbackType = extern "C" fn(
243
    refany: RefAny,
244
    info: CallbackInfo,
245
    output: NodeGraphNodeId,
246
    output_index: usize,
247
) -> Update;
248
impl_widget_callback!(
249
    OnNodeOutputDisconnected,
250
    OptionOnNodeOutputDisconnected,
251
    OnNodeOutputDisconnectedCallback,
252
    OnNodeOutputDisconnectedCallbackType
253
);
254

            
255
pub 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;
263
impl_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)]
273
pub struct InputOutputTypeId {
274
    pub inner: u64,
275
}
276

            
277
impl_option!(InputOutputTypeId, OptionInputOutputTypeId, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
278
impl_vec!(InputOutputTypeId, InputOutputTypeIdVec, InputOutputTypeIdVecDestructor, InputOutputTypeIdVecDestructorType, InputOutputTypeIdVecSlice, OptionInputOutputTypeId);
279
impl_vec_clone!(
280
    InputOutputTypeId,
281
    InputOutputTypeIdVec,
282
    InputOutputTypeIdVecDestructor
283
);
284
impl_vec_mut!(InputOutputTypeId, InputOutputTypeIdVec);
285
impl_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)]
290
pub 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)]
297
pub 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)]
304
pub 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)]
315
pub struct NodeTypeField {
316
    pub key: AzString,
317
    pub value: NodeTypeFieldValue,
318
}
319

            
320
impl_option!(NodeTypeField, OptionNodeTypeField, copy = false, [Debug, Clone]);
321
impl_vec!(NodeTypeField, NodeTypeFieldVec, NodeTypeFieldVecDestructor, NodeTypeFieldVecDestructorType, NodeTypeFieldVecSlice, OptionNodeTypeField);
322
impl_vec_clone!(NodeTypeField, NodeTypeFieldVec, NodeTypeFieldVecDestructor);
323
impl_vec_debug!(NodeTypeField, NodeTypeFieldVec);
324
impl_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)]
329
pub 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)]
340
pub struct InputConnection {
341
    pub input_index: usize,
342
    pub connects_to: OutputNodeAndIndexVec,
343
}
344

            
345
impl_option!(InputConnection, OptionInputConnection, copy = false, [Debug, Clone]);
346
impl_vec!(InputConnection, InputConnectionVec, InputConnectionVecDestructor, InputConnectionVecDestructorType, InputConnectionVecSlice, OptionInputConnection);
347
impl_vec_clone!(
348
    InputConnection,
349
    InputConnectionVec,
350
    InputConnectionVecDestructor
351
);
352
impl_vec_debug!(InputConnection, InputConnectionVec);
353
impl_vec_mut!(InputConnection, InputConnectionVec);
354

            
355
/// Reference to a specific output port on a node.
356
#[derive(Copy, Debug, Clone)]
357
#[repr(C)]
358
pub struct OutputNodeAndIndex {
359
    pub node_id: NodeGraphNodeId,
360
    pub output_index: usize,
361
}
362

            
363
impl_option!(OutputNodeAndIndex, OptionOutputNodeAndIndex, copy = false, [Debug, Clone]);
364
impl_vec!(OutputNodeAndIndex, OutputNodeAndIndexVec, OutputNodeAndIndexVecDestructor, OutputNodeAndIndexVecDestructorType, OutputNodeAndIndexVecSlice, OptionOutputNodeAndIndex);
365
impl_vec_clone!(
366
    OutputNodeAndIndex,
367
    OutputNodeAndIndexVec,
368
    OutputNodeAndIndexVecDestructor
369
);
370
impl_vec_debug!(OutputNodeAndIndex, OutputNodeAndIndexVec);
371
impl_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)]
376
pub struct OutputConnection {
377
    pub output_index: usize,
378
    pub connects_to: InputNodeAndIndexVec,
379
}
380

            
381
impl_option!(OutputConnection, OptionOutputConnection, copy = false, [Debug, Clone]);
382
impl_vec!(OutputConnection, OutputConnectionVec, OutputConnectionVecDestructor, OutputConnectionVecDestructorType, OutputConnectionVecSlice, OptionOutputConnection);
383
impl_vec_clone!(
384
    OutputConnection,
385
    OutputConnectionVec,
386
    OutputConnectionVecDestructor
387
);
388
impl_vec_debug!(OutputConnection, OutputConnectionVec);
389
impl_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)]
394
pub struct InputNodeAndIndex {
395
    pub node_id: NodeGraphNodeId,
396
    pub input_index: usize,
397
}
398

            
399
impl_option!(InputNodeAndIndex, OptionInputNodeAndIndex, copy = false, [Debug, Clone]);
400
impl_vec!(InputNodeAndIndex, InputNodeAndIndexVec, InputNodeAndIndexVecDestructor, InputNodeAndIndexVecDestructorType, InputNodeAndIndexVecSlice, OptionInputNodeAndIndex);
401
impl_vec_clone!(
402
    InputNodeAndIndex,
403
    InputNodeAndIndexVec,
404
    InputNodeAndIndexVecDestructor
405
);
406
impl_vec_debug!(InputNodeAndIndex, InputNodeAndIndexVec);
407
impl_vec_mut!(InputNodeAndIndex, InputNodeAndIndexVec);
408

            
409
/// Metadata describing a node type and its I/O port configuration.
410
#[derive(Debug, Clone)]
411
#[repr(C)]
412
pub 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)]
426
pub 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)]
437
pub 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)]
446
pub 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

            
458
impl fmt::Display for NodeGraphError {
459
24
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460
        use self::NodeGraphError::{NodeMimeTypeMismatch, NodeInvalidIndex, NodeInvalidNode, NoRootNode};
461
24
        match self {
462
6
            NodeMimeTypeMismatch => write!(f, "MIME type mismatch"),
463
6
            NodeInvalidIndex => write!(f, "Invalid node index"),
464
6
            NodeInvalidNode => write!(f, "Invalid node"),
465
6
            NoRootNode => write!(f, "No root node found"),
466
        }
467
24
    }
468
}
469

            
470
/// Amount (in logical pixels) the entire graph was dragged.
471
#[derive(Debug, Copy, Clone, PartialEq)]
472
#[repr(C)]
473
pub 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)]
481
pub struct NodeDragAmount {
482
    pub x: f32,
483
    pub y: f32,
484
}
485

            
486
impl NodeGraph {
487
    #[must_use]
488
6
    pub fn swap_with_default(&mut self) -> Self {
489
6
        let mut default = Self::default();
490
6
        ::core::mem::swap(&mut default, self);
491
6
        default
492
6
    }
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
45
    fn connect_input_output(
513
45
        &mut self,
514
45
        input_node_id: NodeGraphNodeId,
515
45
        input_index: usize,
516
45
        output_node_id: NodeGraphNodeId,
517
45
        output_index: usize,
518
45
    ) -> Result<(), NodeGraphError> {
519
        // Verify that the node type of the connection matches
520
45
        self.verify_nodetype_match(output_node_id, output_index, input_node_id, input_index)?;
521

            
522
        // connect input -> output
523
34
        if let Some(input_node) = self
524
34
            .nodes
525
34
            .as_mut()
526
34
            .iter_mut()
527
105
            .find(|i| i.node_id == input_node_id)
528
        {
529
34
            if let Some(position) = input_node
530
34
                .node
531
34
                .connect_in
532
34
                .as_ref()
533
34
                .iter()
534
34
                .position(|i| i.input_index == input_index)
535
4
            {
536
4
                input_node.node.connect_in.as_mut()[position]
537
4
                    .connects_to
538
4
                    .push(OutputNodeAndIndex {
539
4
                        node_id: output_node_id,
540
4
                        output_index,
541
4
                    });
542
30
            } else {
543
30
                input_node.node.connect_in.push(InputConnection {
544
30
                    input_index,
545
30
                    connects_to: vec![OutputNodeAndIndex {
546
30
                        node_id: output_node_id,
547
30
                        output_index,
548
30
                    }]
549
30
                    .into(),
550
30
                });
551
30
            }
552
        } else {
553
            return Err(NodeGraphError::NodeInvalidNode);
554
        }
555

            
556
        // connect output -> input
557
34
        if let Some(output_node) = self
558
34
            .nodes
559
34
            .as_mut()
560
34
            .iter_mut()
561
40
            .find(|i| i.node_id == output_node_id)
562
        {
563
34
            if let Some(position) = output_node
564
34
                .node
565
34
                .connect_out
566
34
                .as_ref()
567
34
                .iter()
568
34
                .position(|i| i.output_index == output_index)
569
5
            {
570
5
                output_node.node.connect_out.as_mut()[position]
571
5
                    .connects_to
572
5
                    .push(InputNodeAndIndex {
573
5
                        node_id: input_node_id,
574
5
                        input_index,
575
5
                    });
576
29
            } else {
577
29
                output_node.node.connect_out.push(OutputConnection {
578
29
                    output_index,
579
29
                    connects_to: vec![InputNodeAndIndex {
580
29
                        node_id: input_node_id,
581
29
                        input_index,
582
29
                    }]
583
29
                    .into(),
584
29
                });
585
29
            }
586
        } else {
587
            return Err(NodeGraphError::NodeInvalidNode);
588
        }
589

            
590
34
        Ok(())
591
45
    }
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
6
    fn disconnect_input(
610
6
        &mut self,
611
6
        input_node_id: NodeGraphNodeId,
612
6
        input_index: usize,
613
6
    ) -> Result<(), NodeGraphError> {
614
3
        let output_connections = {
615
6
            let input_node = self
616
6
                .nodes
617
6
                .as_ref()
618
6
                .iter()
619
20
                .find(|i| i.node_id == input_node_id)
620
6
                .ok_or(NodeGraphError::NodeInvalidNode)?;
621

            
622
5
            match input_node
623
5
                .node
624
5
                .connect_in
625
5
                .iter()
626
5
                .find(|i| i.input_index == input_index)
627
            {
628
2
                None => return Ok(()),
629
3
                Some(s) => s.connects_to.clone(),
630
            }
631
        };
632

            
633
        // for every output that this input was connected to...
634
        for OutputNodeAndIndex {
635
4
            node_id,
636
4
            output_index,
637
3
        } in output_connections.as_ref()
638
        {
639
4
            let output_node_id = *node_id;
640
4
            let output_index = *output_index;
641

            
642
            // verify that the node type of the connection matches
643
4
            self.verify_nodetype_match(
644
4
                output_node_id,
645
4
                output_index,
646
4
                input_node_id,
647
4
                input_index,
648
            )?;
649

            
650
            // disconnect input -> output
651

            
652
4
            if let Some(input_node) = self
653
4
                .nodes
654
4
                .as_mut()
655
4
                .iter_mut()
656
14
                .find(|i| i.node_id == input_node_id)
657
            {
658
4
                if let Some(position) = input_node
659
4
                    .node
660
4
                    .connect_in
661
4
                    .iter()
662
4
                    .position(|i| i.input_index == input_index)
663
3
                {
664
3
                    input_node.node.connect_in.remove(position);
665
3
                }
666
            } else {
667
                return Err(NodeGraphError::NodeInvalidNode);
668
            }
669

            
670
4
            if let Some(output_node) = self
671
4
                .nodes
672
4
                .as_mut()
673
4
                .iter_mut()
674
6
                .find(|i| i.node_id == output_node_id)
675
            {
676
4
                if let Some(position) = output_node
677
4
                    .node
678
4
                    .connect_out
679
4
                    .iter()
680
4
                    .position(|i| i.output_index == output_index)
681
4
                {
682
4
                    output_node.node.connect_out.remove(position);
683
4
                }
684
            } else {
685
                return Err(NodeGraphError::NodeInvalidNode);
686
            }
687
        }
688

            
689
3
        Ok(())
690
6
    }
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
6
    fn disconnect_output(
707
6
        &mut self,
708
6
        output_node_id: NodeGraphNodeId,
709
6
        output_index: usize,
710
6
    ) -> Result<(), NodeGraphError> {
711
3
        let input_connections = {
712
6
            let output_node = self
713
6
                .nodes
714
6
                .as_ref()
715
6
                .iter()
716
9
                .find(|i| i.node_id == output_node_id)
717
6
                .ok_or(NodeGraphError::NodeInvalidNode)?;
718

            
719
5
            match output_node
720
5
                .node
721
5
                .connect_out
722
5
                .iter()
723
5
                .find(|i| i.output_index == output_index)
724
            {
725
2
                None => return Ok(()),
726
3
                Some(s) => s.connects_to.clone(),
727
            }
728
        };
729

            
730
        for InputNodeAndIndex {
731
4
            node_id,
732
4
            input_index,
733
7
        } in &input_connections
734
        {
735
4
            let input_node_id = *node_id;
736
4
            let input_index = *input_index;
737

            
738
            // verify that the node type of the connection matches
739
4
            self.verify_nodetype_match(
740
4
                output_node_id,
741
4
                output_index,
742
4
                input_node_id,
743
4
                input_index,
744
            )?;
745

            
746
4
            if let Some(output_node) = self
747
4
                .nodes
748
4
                .as_mut()
749
4
                .iter_mut()
750
4
                .find(|i| i.node_id == output_node_id)
751
            {
752
4
                if let Some(position) = output_node
753
4
                    .node
754
4
                    .connect_out
755
4
                    .iter()
756
4
                    .position(|i| i.output_index == output_index)
757
3
                {
758
3
                    output_node.node.connect_out.remove(position);
759
3
                }
760
            } else {
761
                return Err(NodeGraphError::NodeInvalidNode);
762
            }
763

            
764
4
            if let Some(input_node) = self
765
4
                .nodes
766
4
                .as_mut()
767
4
                .iter_mut()
768
11
                .find(|i| i.node_id == input_node_id)
769
            {
770
4
                if let Some(position) = input_node
771
4
                    .node
772
4
                    .connect_in
773
4
                    .iter()
774
4
                    .position(|i| i.input_index == input_index)
775
4
                {
776
4
                    input_node.node.connect_in.remove(position);
777
4
                }
778
            } else {
779
                return Err(NodeGraphError::NodeInvalidNode);
780
            }
781
        }
782

            
783
3
        Ok(())
784
6
    }
785

            
786
    /// Verifies that the node types of two connections match
787
68
    fn verify_nodetype_match(
788
68
        &self,
789
68
        output_node_id: NodeGraphNodeId,
790
68
        output_index: usize,
791
68
        input_node_id: NodeGraphNodeId,
792
68
        input_index: usize,
793
68
    ) -> Result<(), NodeGraphError> {
794
68
        let output_node = self
795
68
            .nodes
796
68
            .iter()
797
95
            .find(|i| i.node_id == output_node_id)
798
68
            .ok_or(NodeGraphError::NodeInvalidNode)?;
799

            
800
63
        let output_node_type = self
801
63
            .node_types
802
63
            .iter()
803
64
            .find(|i| i.node_type_id == output_node.node.node_type)
804
63
            .ok_or(NodeGraphError::NodeInvalidNode)?;
805

            
806
62
        let output_type = output_node_type
807
62
            .node_type_info
808
62
            .outputs
809
62
            .as_ref()
810
62
            .get(output_index)
811
62
            .copied()
812
62
            .ok_or(NodeGraphError::NodeInvalidIndex)?;
813

            
814
55
        let input_node = self
815
55
            .nodes
816
55
            .iter()
817
168
            .find(|i| i.node_id == input_node_id)
818
55
            .ok_or(NodeGraphError::NodeInvalidNode)?;
819

            
820
53
        let input_node_type = self
821
53
            .node_types
822
53
            .iter()
823
57
            .find(|i| i.node_type_id == input_node.node.node_type)
824
53
            .ok_or(NodeGraphError::NodeInvalidNode)?;
825

            
826
52
        let input_type = input_node_type
827
52
            .node_type_info
828
52
            .inputs
829
52
            .as_ref()
830
52
            .get(input_index)
831
52
            .copied()
832
52
            .ok_or(NodeGraphError::NodeInvalidIndex)?;
833

            
834
        // Input / Output do not have the same TypeId
835
48
        if input_type != output_type {
836
3
            return Err(NodeGraphError::NodeMimeTypeMismatch);
837
45
        }
838

            
839
45
        Ok(())
840
68
    }
841

            
842
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
843
16
    #[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
16
        let nodegraph_wrapper_props = vec![
859
16
            CssPropertyWithConditions::simple(CssProperty::overflow_x(LayoutOverflow::Hidden)),
860
16
            CssPropertyWithConditions::simple(CssProperty::overflow_y(LayoutOverflow::Hidden)),
861
16
            CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
862
16
            CssPropertyWithConditions::simple(CssProperty::background_content(
863
16
                StyleBackgroundContentVec::from_const_slice(NODEGRAPH_BACKGROUND),
864
            )),
865
16
            CssPropertyWithConditions::simple(CssProperty::background_repeat(
866
16
                vec![StyleBackgroundRepeat::PatternRepeat].into(),
867
            )),
868
16
            CssPropertyWithConditions::simple(CssProperty::background_position(
869
16
                vec![StyleBackgroundPosition {
870
16
                    horizontal: BackgroundPositionHorizontal::Exact(PixelValue::const_px(0)),
871
16
                    vertical: BackgroundPositionVertical::Exact(PixelValue::const_px(0)),
872
16
                }]
873
16
                .into(),
874
            )),
875
        ];
876

            
877
16
        let nodegraph_props = vec![
878
16
            CssPropertyWithConditions::simple(CssProperty::overflow_x(LayoutOverflow::Hidden)),
879
16
            CssPropertyWithConditions::simple(CssProperty::overflow_y(LayoutOverflow::Hidden)),
880
16
            CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
881
16
            CssPropertyWithConditions::simple(CssProperty::position(LayoutPosition::Relative)),
882
        ];
883

            
884
16
        let node_connection_marker = RefAny::new(NodeConnectionMarkerDataset {});
885

            
886
16
        let node_graph_local_dataset = RefAny::new(NodeGraphLocalDataset {
887
16
            node_graph: self.clone(), // TODO: expensive
888
16
            last_input_or_output_clicked: None,
889
16
            active_node_being_dragged: None,
890
16
            node_connection_marker: node_connection_marker.clone(),
891
16
            callbacks: self.callbacks.clone(),
892
16
        });
893

            
894
16
        let context_menu = Menu::create(
895
16
            vec![MenuItem::String(
896
16
                StringMenuItem::create(self.add_node_str.clone()).with_children(
897
16
                    self.node_types
898
16
                        .iter()
899
16
                        .map(
900
                            |NodeTypeIdInfoMap {
901
                                 node_type_id,
902
                                 node_type_info,
903
26
                             }| {
904
26
                                let context_menu_local_dataset =
905
26
                                    RefAny::new(ContextMenuEntryLocalDataset {
906
26
                                        node_type: *node_type_id,
907
26
                                        // RefAny<NodeGraphLocalDataset>
908
26
                                        backref: node_graph_local_dataset.clone(),
909
26
                                    });
910

            
911
26
                                MenuItem::String(
912
26
                                    StringMenuItem::create(
913
26
                                        node_type_info.node_type_name.clone(),
914
26
                                    )
915
26
                                    .with_callback(
916
26
                                        context_menu_local_dataset,
917
26
                                        nodegraph_context_menu_click as usize,
918
26
                                    ),
919
26
                                )
920
26
                            },
921
                        )
922
16
                        .collect::<Vec<_>>()
923
16
                        .into(),
924
                ),
925
            )]
926
16
            .into(),
927
        );
928

            
929
16
        Dom::create_div()
930
16
            .with_css_props(nodegraph_wrapper_props.into())
931
16
            .with_context_menu(context_menu)
932
16
            .with_children(
933
16
                vec![Dom::create_div()
934
16
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(NODEGRAPH_CLASS))
935
16
                    .with_css_props(nodegraph_props.into())
936
16
                    .with_callbacks(
937
16
                        vec![
938
16
                            CoreCallbackData {
939
16
                                event: EventFilter::Hover(HoverEventFilter::MouseOver),
940
16
                                refany: node_graph_local_dataset.clone(),
941
16
                                callback: CoreCallback {
942
16
                                    cb: nodegraph_drag_graph_or_nodes as usize,
943
16
                                    ctx: OptionRefAny::None,
944
16
                                },
945
16
                            },
946
16
                            CoreCallbackData {
947
16
                                event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
948
16
                                refany: node_graph_local_dataset.clone(),
949
16
                                callback: CoreCallback {
950
16
                                    cb: nodegraph_unset_active_node as usize,
951
16
                                    ctx: OptionRefAny::None,
952
16
                                },
953
16
                            },
954
                        ]
955
16
                        .into(),
956
                    )
957
16
                    .with_children({
958
16
                        vec![
959
                            // connections
960
16
                            render_connections(&self, node_connection_marker),
961
                            // nodes
962
16
                            self.nodes
963
16
                                .iter()
964
55
                                .filter_map(|NodeIdNodeMap { node_id, node }| {
965
55
                                    let node_type_info = self
966
55
                                        .node_types
967
55
                                        .iter()
968
64
                                        .find(|i| i.node_type_id == node.node_type)?;
969
50
                                    let node_local_dataset = NodeLocalDataset {
970
50
                                        node_id: *node_id,
971
50
                                        backref: node_graph_local_dataset.clone(),
972
50
                                    };
973

            
974
50
                                    Some(render_node(
975
50
                                        node,
976
50
                                        (self.offset.x, self.offset.y),
977
50
                                        &node_type_info.node_type_info,
978
50
                                        node_local_dataset,
979
50
                                        self.scale_factor,
980
50
                                    ))
981
55
                                })
982
16
                                .collect::<Dom>()
983
16
                                .with_ids_and_classes(IdOrClassVec::from_const_slice(
984
16
                                    NODEGRAPH_NODES_CONTAINER_CLASS,
985
                                ))
986
16
                                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
987
16
                                    NODEGRAPH_NODES_CONTAINER_PROPS,
988
                                )),
989
                        ]
990
16
                        .into()
991
                    })]
992
16
                .into(),
993
            )
994
16
            .with_dataset(Some(node_graph_local_dataset).into())
995
16
    }
996
}
997

            
998
// dataset set on the top-level nodegraph node,
999
// containing all the state of the node graph
struct NodeGraphLocalDataset {
    node_graph: NodeGraph,
    last_input_or_output_clicked: Option<(NodeGraphNodeId, InputOrOutput)>,
    // Ref<NodeLocalDataSet> - used as a marker for getting the visual node ID
    active_node_being_dragged: Option<(NodeGraphNodeId, RefAny)>,
    node_connection_marker: RefAny, // Ref<NodeConnectionMarkerDataset>
    callbacks: NodeGraphCallbacks,
}
struct ContextMenuEntryLocalDataset {
    node_type: NodeTypeId,
    backref: RefAny, // RefAny<NodeGraphLocalDataset>
}
struct NodeConnectionMarkerDataset {}
struct NodeLocalDataset {
    node_id: NodeGraphNodeId,
    backref: RefAny, // RefAny<NodeGraphLocalDataset>
}
#[derive(Debug, Copy, Clone)]
enum InputOrOutput {
    Input(usize),
    Output(usize),
}
struct NodeInputOutputLocalDataset {
    io_id: InputOrOutput,
    backref: RefAny, // RefAny<NodeLocalDataset>
}
struct NodeFieldLocalDataset {
    field_idx: usize,
    backref: RefAny, // RefAny<NodeLocalDataset>
}
#[derive(Copy, Clone)]
struct ConnectionLocalDataset {
    out_node_id: NodeGraphNodeId,
    out_idx: usize,
    in_node_id: NodeGraphNodeId,
    in_idx: usize,
    swap_vert: bool,
    swap_horz: bool,
    color: ColorU,
}
#[allow(clippy::float_cmp)] // intentional exact compare: change-detection / identity fast-path / cache-key match
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
80
fn render_node(
80
    node: &Node,
80
    graph_offset: (f32, f32),
80
    node_info: &NodeTypeInfo,
80
    mut node_local_dataset: NodeLocalDataset,
80
    scale_factor: f32,
80
) -> Dom {
    use azul_core::dom::{
        CssPropertyWithConditions, CssPropertyWithConditionsVec, Dom, DomVec, IdOrClass,
        IdOrClass::Class, IdOrClassVec,
    };
    #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
    use azul_css::*;
    const STRING_9416190750059025162: AzString = AzString::from_const_str("Material Icons");
    const STRING_16146701490593874959: AzString = AzString::from_const_str("system:ui");
    const STYLE_BACKGROUND_CONTENT_524016094839686509_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::Color(ColorU {
            r: 34,
            g: 34,
            b: 34,
            a: 255,
        })];
    const STYLE_BACKGROUND_CONTENT_10430246856047584562_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::LinearGradient(LinearGradient {
            direction: Direction::FromTo(DirectionCorners {
                dir_from: DirectionCorner::Left,
                dir_to: DirectionCorner::Right,
            }),
            extend_mode: ExtendMode::Clamp,
            stops: NormalizedLinearColorStopVec::from_const_slice(
                LINEAR_COLOR_STOP_4373556077110009258_ITEMS,
            ),
        })];
    const STYLE_BACKGROUND_CONTENT_11535310356736632656_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::RadialGradient(RadialGradient {
            shape: Shape::Ellipse,
            extend_mode: ExtendMode::Clamp,
            position: StyleBackgroundPosition {
                horizontal: BackgroundPositionHorizontal::Left,
                vertical: BackgroundPositionVertical::Top,
            },
            size: RadialGradientSize::FarthestCorner,
            stops: NormalizedLinearColorStopVec::from_const_slice(
                LINEAR_COLOR_STOP_15596411095679453272_ITEMS,
            ),
        })];
    const STYLE_BACKGROUND_CONTENT_11936041127084538304_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::LinearGradient(LinearGradient {
            direction: Direction::FromTo(DirectionCorners {
                dir_from: DirectionCorner::Right,
                dir_to: DirectionCorner::Left,
            }),
            extend_mode: ExtendMode::Clamp,
            stops: NormalizedLinearColorStopVec::from_const_slice(
                LINEAR_COLOR_STOP_4373556077110009258_ITEMS,
            ),
        })];
    const STYLE_BACKGROUND_CONTENT_15813232491335471489_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::Color(ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 85,
        })];
    const STYLE_BACKGROUND_CONTENT_17648039690071193942_ITEMS: &[StyleBackgroundContent] =
        &[StyleBackgroundContent::LinearGradient(LinearGradient {
            direction: Direction::FromTo(DirectionCorners {
                dir_from: DirectionCorner::Top,
                dir_to: DirectionCorner::Bottom,
            }),
            extend_mode: ExtendMode::Clamp,
            stops: NormalizedLinearColorStopVec::from_const_slice(
                LINEAR_COLOR_STOP_7397113864565941600_ITEMS,
            ),
        })];
    const STYLE_TRANSFORM_347117342922946953_ITEMS: &[StyleTransform] =
        &[StyleTransform::Translate(StyleTransformTranslate2D {
            x: PixelValue::const_px(200),
            y: PixelValue::const_px(100),
        })];
    const STYLE_TRANSFORM_14683950870521466298_ITEMS: &[StyleTransform] =
        &[StyleTransform::Translate(StyleTransformTranslate2D {
            x: PixelValue::const_px(240),
            y: PixelValue::const_px(-10),
        })];
    const STYLE_FONT_FAMILY_8122988506401935406_ITEMS: &[StyleFontFamily] =
        &[StyleFontFamily::System(STRING_16146701490593874959)];
    const STYLE_FONT_FAMILY_11383897783350685780_ITEMS: &[StyleFontFamily] =
        &[StyleFontFamily::System(STRING_9416190750059025162)];
    const LINEAR_COLOR_STOP_4373556077110009258_ITEMS: &[NormalizedLinearColorStop] = &[
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(20),
            color: ColorOrSystem::color(ColorU {
                r: 0,
                g: 0,
                b: 0,
                a: 204,
            }),
        },
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(100),
            color: ColorOrSystem::color(ColorU {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            }),
        },
    ];
    const LINEAR_COLOR_STOP_7397113864565941600_ITEMS: &[NormalizedLinearColorStop] = &[
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(0),
            color: ColorOrSystem::color(ColorU {
                r: 229,
                g: 57,
                b: 53,
                a: 255,
            }),
        },
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(100),
            color: ColorOrSystem::color(ColorU {
                r: 227,
                g: 93,
                b: 91,
                a: 255,
            }),
        },
    ];
    const LINEAR_COLOR_STOP_15596411095679453272_ITEMS: &[NormalizedLinearColorStop] = &[
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(0),
            color: ColorOrSystem::color(ColorU {
                r: 47,
                g: 49,
                b: 54,
                a: 255,
            }),
        },
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(50),
            color: ColorOrSystem::color(ColorU {
                r: 47,
                g: 49,
                b: 54,
                a: 255,
            }),
        },
        NormalizedLinearColorStop {
            offset: PercentageValue::const_new(100),
            color: ColorOrSystem::color(ColorU {
                r: 32,
                g: 34,
                b: 37,
                a: 255,
            }),
        },
    ];
    const CSS_MATCH_10339190304804100510_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_output_wrapper
        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
            LayoutDisplay::Flex,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
        )),
        CssPropertyWithConditions::simple(CssProperty::Left(LayoutLeftValue::Exact(LayoutLeft {
            inner: PixelValue::const_px(0),
        }))),
        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Absolute,
        ))),
    ];
    const CSS_MATCH_10339190304804100510: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_10339190304804100510_PROPERTIES);
    const CSS_MATCH_11452431279102104133_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_input_connection_label
        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
        ))),
        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
            StyleFontSize {
                inner: PixelValue::const_px(12),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
            LayoutHeight::Px(PixelValue::const_px(15)),
        ))),
        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
            StyleTextAlign::Right,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
            LayoutWidth::Px(PixelValue::const_px(100)),
        ))),
    ];
    const CSS_MATCH_11452431279102104133: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_11452431279102104133_PROPERTIES);
    const CSS_MATCH_1173826950760010563_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_configuration_field_value:focus
        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
                inner: ColorU {
                    r: 0,
                    g: 131,
                    b: 176,
                    a: 119,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
                inner: ColorU {
                    r: 0,
                    g: 131,
                    b: 176,
                    a: 119,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
                inner: ColorU {
                    r: 0,
                    g: 131,
                    b: 176,
                    a: 119,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
                inner: ColorU {
                    r: 0,
                    g: 131,
                    b: 176,
                    a: 119,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        // .node_configuration_field_value
        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
            LayoutAlignItems::Center,
        ))),
        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
                STYLE_BACKGROUND_CONTENT_524016094839686509_ITEMS,
            )),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
                inner: ColorU {
                    r: 54,
                    g: 57,
                    b: 63,
                    a: 255,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
                inner: ColorU {
                    r: 54,
                    g: 57,
                    b: 63,
                    a: 255,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
                inner: ColorU {
                    r: 54,
                    g: 57,
                    b: 63,
                    a: 255,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
                inner: ColorU {
                    r: 54,
                    g: 57,
                    b: 63,
                    a: 255,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::const_new(1),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
            StyleTextAlign::Left,
        ))),
    ];
    const CSS_MATCH_1173826950760010563: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1173826950760010563_PROPERTIES);
    const CSS_MATCH_1198521124955124418_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_configuration_field_label
        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
            LayoutAlignItems::Center,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::const_new(1),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::MaxWidth(LayoutMaxWidthValue::Exact(
            LayoutMaxWidth {
                inner: PixelValue::const_px(120),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
            LayoutPaddingLeft {
                inner: PixelValue::const_px(10),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
            StyleTextAlign::Left,
        ))),
    ];
    const CSS_MATCH_1198521124955124418: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1198521124955124418_PROPERTIES);
    const CSS_MATCH_12038890904436132038_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_output_connection_label_wrapper
        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
                STYLE_BACKGROUND_CONTENT_10430246856047584562_ITEMS,
            )),
        )),
        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
            LayoutPaddingLeft {
                inner: PixelValue::const_px(5),
            },
        ))),
    ];
    const CSS_MATCH_12038890904436132038: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_12038890904436132038_PROPERTIES);
    const CSS_MATCH_12400244273289328300_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_output_container
        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
            LayoutDisplay::Flex,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
        )),
        CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
            LayoutMarginTop {
                inner: PixelValue::const_px(10),
            },
        ))),
    ];
    const CSS_MATCH_12400244273289328300: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_12400244273289328300_PROPERTIES);
    const CSS_MATCH_14906563417280941890_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .outputs
        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::const_new(0),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Relative,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
            LayoutWidth::Px(PixelValue::const_px(0)),
        ))),
    ];
    const CSS_MATCH_14906563417280941890: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_14906563417280941890_PROPERTIES);
    const CSS_MATCH_16946967739775705757_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .inputs
        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::const_new(0),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Relative,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
            LayoutWidth::Px(PixelValue::const_px(0)),
        ))),
    ];
    const CSS_MATCH_16946967739775705757: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_16946967739775705757_PROPERTIES);
    const CSS_MATCH_1739273067404038547_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_label
        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
            StyleFontSize {
                inner: PixelValue::const_px(18),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
            LayoutHeight::Px(PixelValue::const_px(50)),
        ))),
        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
            LayoutPaddingLeft {
                inner: PixelValue::const_px(5),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
            LayoutPaddingTop {
                inner: PixelValue::const_px(10),
            },
        ))),
    ];
    const CSS_MATCH_1739273067404038547: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_1739273067404038547_PROPERTIES);
    const CSS_MATCH_2008162367868363199_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_output_connection_label
        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
        ))),
        CssPropertyWithConditions::simple(CssProperty::FontSize(StyleFontSizeValue::Exact(
            StyleFontSize {
                inner: PixelValue::const_px(12),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
            LayoutHeight::Px(PixelValue::const_px(15)),
        ))),
        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
            StyleTextAlign::Left,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
            LayoutWidth::Px(PixelValue::const_px(100)),
        ))),
    ];
    const CSS_MATCH_2008162367868363199: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_2008162367868363199_PROPERTIES);
    const CSS_MATCH_2639191696846875011_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_configuration_field_container
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
        )),
        CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
            LayoutPaddingTop {
                inner: PixelValue::const_px(3),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::PaddingBottom(
            LayoutPaddingBottomValue::Exact(LayoutPaddingBottom {
                inner: PixelValue::const_px(3),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
            LayoutPaddingLeft {
                inner: PixelValue::const_px(5),
            },
        ))),
        CssPropertyWithConditions::simple(CssProperty::PaddingRight(
            LayoutPaddingRightValue::Exact(LayoutPaddingRight {
                inner: PixelValue::const_px(5),
            }),
        )),
    ];
    const CSS_MATCH_2639191696846875011: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_2639191696846875011_PROPERTIES);
    const CSS_MATCH_3354247437065914166_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_body
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
        )),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Relative,
        ))),
    ];
    const CSS_MATCH_3354247437065914166: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_3354247437065914166_PROPERTIES);
    const CSS_MATCH_4700400755767504372_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_input_connection_label_wrapper
        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
                STYLE_BACKGROUND_CONTENT_11936041127084538304_ITEMS,
            )),
        )),
        CssPropertyWithConditions::simple(CssProperty::PaddingRight(
            LayoutPaddingRightValue::Exact(LayoutPaddingRight {
                inner: PixelValue::const_px(5),
            }),
        )),
    ];
    const CSS_MATCH_4700400755767504372: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_4700400755767504372_PROPERTIES);
    const CSS_MATCH_705881630351954657_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_input_wrapper
        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
            LayoutDisplay::Flex,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Column),
        )),
        CssPropertyWithConditions::simple(CssProperty::OverflowX(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::OverflowY(LayoutOverflowValue::Exact(
            LayoutOverflow::Visible,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Absolute,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Right(LayoutRightValue::Exact(
            LayoutRight {
                inner: PixelValue::const_px(0),
            },
        ))),
    ];
    const CSS_MATCH_705881630351954657: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_705881630351954657_PROPERTIES);
    const CSS_MATCH_7395766480280098891_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_close_button
        CssPropertyWithConditions::simple(CssProperty::AlignItems(LayoutAlignItemsValue::Exact(
            LayoutAlignItems::Center,
        ))),
        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
                STYLE_BACKGROUND_CONTENT_17648039690071193942_ITEMS,
            )),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
            StyleBorderTopColorValue::Exact(StyleBorderTopColor {
                inner: ColorU {
                    r: 255,
                    g: 255,
                    b: 255,
                    a: 153,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
            StyleBorderRightColorValue::Exact(StyleBorderRightColor {
                inner: ColorU {
                    r: 255,
                    g: 255,
                    b: 255,
                    a: 153,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
            StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
                inner: ColorU {
                    r: 255,
                    g: 255,
                    b: 255,
                    a: 153,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
            StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
                inner: ColorU {
                    r: 255,
                    g: 255,
                    b: 255,
                    a: 153,
                },
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
            StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
            StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
            StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
            StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
                inner: BorderStyle::Solid,
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
            LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
            LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
            LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
            LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
                inner: PixelValue::const_px(1),
            }),
        )),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 229,
                    g: 57,
                    b: 53,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(2),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Outset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 229,
                    g: 57,
                    b: 53,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(2),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Outset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 229,
                    g: 57,
                    b: 53,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(2),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Outset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
            StyleBoxShadowValue::Exact(BoxOrStatic::Static(&StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 229,
                    g: 57,
                    b: 53,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(2),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Outset,
            })),
        )),
        CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
            StyleCursor::Pointer,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
            StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_11383897783350685780_ITEMS),
        ))),
        CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
            LayoutHeight::Px(PixelValue::const_px(20)),
        ))),
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
            LayoutPosition::Absolute,
        ))),
        CssPropertyWithConditions::simple(CssProperty::TextAlign(StyleTextAlignValue::Exact(
            StyleTextAlign::Center,
        ))),
        CssPropertyWithConditions::simple(CssProperty::Transform(StyleTransformVecValue::Exact(
            StyleTransformVec::from_const_slice(STYLE_TRANSFORM_14683950870521466298_ITEMS),
        ))),
        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
            LayoutWidth::Px(PixelValue::const_px(20)),
        ))),
    ];
    const CSS_MATCH_7395766480280098891: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_7395766480280098891_PROPERTIES);
    const CSS_MATCH_7432473243011547380_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_content_wrapper
        CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
            StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
                STYLE_BACKGROUND_CONTENT_15813232491335471489_ITEMS,
            )),
        )),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(4),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Inset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(4),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Inset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::Static(&
            StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(4),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Inset,
            },
        )))),
        CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
            StyleBoxShadowValue::Exact(BoxOrStatic::Static(&StyleBoxShadow {
                offset_x: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                offset_y: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                color: ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 255,
                },
                blur_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(4),
                },
                spread_radius: PixelValueNoPercent {
                    inner: PixelValue::const_px(0),
                },
                clip_mode: BoxShadowClipMode::Inset,
            })),
        )),
        CssPropertyWithConditions::simple(CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(
            LayoutFlexGrow {
                inner: FloatValue::const_new(1),
            },
        ))),
    ];
    const CSS_MATCH_7432473243011547380: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_7432473243011547380_PROPERTIES);
    const CSS_MATCH_9863994880298313101_PROPERTIES: &[CssPropertyWithConditions] = &[
        // .node_input_container
        CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
            LayoutDisplay::Flex,
        ))),
        CssPropertyWithConditions::simple(CssProperty::FlexDirection(
            LayoutFlexDirectionValue::Exact(LayoutFlexDirection::Row),
        )),
        CssPropertyWithConditions::simple(CssProperty::MarginTop(LayoutMarginTopValue::Exact(
            LayoutMarginTop {
                inner: PixelValue::const_px(10),
            },
        ))),
    ];
    const CSS_MATCH_9863994880298313101: CssPropertyWithConditionsVec =
        CssPropertyWithConditionsVec::from_const_slice(CSS_MATCH_9863994880298313101_PROPERTIES);
    // NODE RENDER FUNCTION BEGIN
80
    let node_transform = StyleTransformTranslate2D {
80
        x: PixelValue::px(graph_offset.0 + node.position.x),
80
        y: PixelValue::px(graph_offset.1 + node.position.y),
80
    };
    // get names and colors for inputs / outputs
80
    let inputs = node_info
80
        .inputs
80
        .iter()
80
        .filter_map(|io_id| {
80
            let node_graph_ref = node_local_dataset
80
                .backref
80
                .downcast_ref::<NodeGraphLocalDataset>()?;
79
            let io_info = node_graph_ref
79
                .node_graph
79
                .input_output_types
79
                .iter()
92
                .find(|i| i.io_type_id == *io_id)?;
78
            Some((
78
                io_info.io_info.data_type.clone(),
78
                io_info.io_info.color,
78
            ))
80
        })
80
        .collect::<Vec<_>>();
80
    let outputs = node_info
80
        .outputs
80
        .iter()
80
        .filter_map(|io_id| {
80
            let node_graph_ref = node_local_dataset
80
                .backref
80
                .downcast_ref::<NodeGraphLocalDataset>()?;
79
            let io_info = node_graph_ref
79
                .node_graph
79
                .input_output_types
79
                .iter()
91
                .find(|i| i.io_type_id == *io_id)?;
79
            Some((
79
                io_info.io_info.data_type.clone(),
79
                io_info.io_info.color,
79
            ))
80
        })
80
        .collect::<Vec<_>>();
80
    let node_local_dataset = RefAny::new(node_local_dataset);
80
    Dom::create_div()
80
    .with_css_props(vec![
80
        CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
80
            LayoutPosition::Absolute,
80
        ))),
80
    ].into())
80
    .with_children(vec![
80
        Dom::create_div()
80
        .with_callbacks(vec![
80
           CoreCallbackData {
80
               event: EventFilter::Hover(HoverEventFilter::LeftMouseDown),
80
               refany: node_local_dataset.clone(),
80
               callback: CoreCallback { cb: nodegraph_set_active_node as usize, ctx: OptionRefAny::None },
80
           },
80
        ].into())
80
        .with_css_props(vec![
           // .node_graph_node
80
           CssPropertyWithConditions::simple(CssProperty::OverflowX(
80
               LayoutOverflowValue::Exact(LayoutOverflow::Visible)
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::Position(LayoutPositionValue::Exact(
80
               LayoutPosition::Relative,
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::OverflowY(
80
               LayoutOverflowValue::Exact(LayoutOverflow::Visible)
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
80
               StyleBackgroundContentVecValue::Exact(StyleBackgroundContentVec::from_const_slice(
80
                   STYLE_BACKGROUND_CONTENT_11535310356736632656_ITEMS,
80
               )),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderTopColor(
80
               StyleBorderTopColorValue::Exact(StyleBorderTopColor {
80
                   inner: ColorU {
80
                       r: 0,
80
                       g: 180,
80
                       b: 219,
80
                       a: 255,
80
                   },
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderRightColor(
80
               StyleBorderRightColorValue::Exact(StyleBorderRightColor {
80
                   inner: ColorU {
80
                       r: 0,
80
                       g: 180,
80
                       b: 219,
80
                       a: 255,
80
                   },
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderLeftColor(
80
               StyleBorderLeftColorValue::Exact(StyleBorderLeftColor {
80
                   inner: ColorU {
80
                       r: 0,
80
                       g: 180,
80
                       b: 219,
80
                       a: 255,
80
                   },
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderBottomColor(
80
               StyleBorderBottomColorValue::Exact(StyleBorderBottomColor {
80
                   inner: ColorU {
80
                       r: 0,
80
                       g: 180,
80
                       b: 219,
80
                       a: 255,
80
                   },
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderTopStyle(
80
               StyleBorderTopStyleValue::Exact(StyleBorderTopStyle {
80
                   inner: BorderStyle::Solid,
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderRightStyle(
80
               StyleBorderRightStyleValue::Exact(StyleBorderRightStyle {
80
                   inner: BorderStyle::Solid,
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderLeftStyle(
80
               StyleBorderLeftStyleValue::Exact(StyleBorderLeftStyle {
80
                   inner: BorderStyle::Solid,
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderBottomStyle(
80
               StyleBorderBottomStyleValue::Exact(StyleBorderBottomStyle {
80
                   inner: BorderStyle::Solid,
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderTopWidth(
80
               LayoutBorderTopWidthValue::Exact(LayoutBorderTopWidth {
80
                   inner: PixelValue::const_px(1),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderRightWidth(
80
               LayoutBorderRightWidthValue::Exact(LayoutBorderRightWidth {
80
                   inner: PixelValue::const_px(1),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderLeftWidth(
80
               LayoutBorderLeftWidthValue::Exact(LayoutBorderLeftWidth {
80
                   inner: PixelValue::const_px(1),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BorderBottomWidth(
80
               LayoutBorderBottomWidthValue::Exact(LayoutBorderBottomWidth {
80
                   inner: PixelValue::const_px(1),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::BoxShadowLeft(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
80
               StyleBoxShadow {
80
                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
80
                   color: ColorU {
80
                       r: 0,
80
                       g: 131,
80
                       b: 176,
80
                       a: 119,
80
                   },
80
                   blur_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(3),
80
                   },
80
                   spread_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(0),
80
                   },
80
                   clip_mode: BoxShadowClipMode::Outset,
80
               },
80
           )))),
80
           CssPropertyWithConditions::simple(CssProperty::BoxShadowRight(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
80
               StyleBoxShadow {
80
                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
80
                   color: ColorU {
80
                       r: 0,
80
                       g: 131,
80
                       b: 176,
80
                       a: 119,
80
                   },
80
                   blur_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(3),
80
                   },
80
                   spread_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(0),
80
                   },
80
                   clip_mode: BoxShadowClipMode::Outset,
80
               },
80
           )))),
80
           CssPropertyWithConditions::simple(CssProperty::BoxShadowTop(StyleBoxShadowValue::Exact(BoxOrStatic::heap(
80
               StyleBoxShadow {
80
                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
80
                   color: ColorU {
80
                       r: 0,
80
                       g: 131,
80
                       b: 176,
80
                       a: 119,
80
                   },
80
                   blur_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(3),
80
                   },
80
                   spread_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(0),
80
                   },
80
                   clip_mode: BoxShadowClipMode::Outset,
80
               },
80
           )))),
80
           CssPropertyWithConditions::simple(CssProperty::BoxShadowBottom(
80
               StyleBoxShadowValue::Exact(BoxOrStatic::heap(StyleBoxShadow {
80
                   offset_x: PixelValueNoPercent { inner: PixelValue::const_px(0) }, offset_y: PixelValueNoPercent { inner: PixelValue::const_px(0) },
80
                   color: ColorU {
80
                       r: 0,
80
                       g: 131,
80
                       b: 176,
80
                       a: 119,
80
                   },
80
                   blur_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(3),
80
                   },
80
                   spread_radius: PixelValueNoPercent {
80
                       inner: PixelValue::const_px(0),
80
                   },
80
                   clip_mode: BoxShadowClipMode::Outset,
80
               })),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::TextColor(StyleTextColorValue::Exact(
80
               StyleTextColor {
80
                   inner: ColorU {
80
                       r: 255,
80
                       g: 255,
80
                       b: 255,
80
                       a: 255,
80
                   },
80
               },
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::Display(LayoutDisplayValue::Exact(
80
               LayoutDisplay::Block
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::FontFamily(StyleFontFamilyVecValue::Exact(
80
               StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_8122988506401935406_ITEMS),
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::PaddingTop(LayoutPaddingTopValue::Exact(
80
               LayoutPaddingTop {
80
                   inner: PixelValue::const_px(10),
80
               },
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::PaddingBottom(
80
               LayoutPaddingBottomValue::Exact(LayoutPaddingBottom {
80
                   inner: PixelValue::const_px(10),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::PaddingLeft(LayoutPaddingLeftValue::Exact(
80
               LayoutPaddingLeft {
80
                   inner: PixelValue::const_px(10),
80
               },
80
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::PaddingRight(
80
               LayoutPaddingRightValue::Exact(LayoutPaddingRight {
80
                   inner: PixelValue::const_px(10),
80
               }),
80
           )),
80
           CssPropertyWithConditions::simple(CssProperty::Transform(StyleTransformVecValue::Exact(
80
               if scale_factor == 1.0 {
45
                    vec![
45
                         StyleTransform::Translate(node_transform)
                    ]
               } else {
35
                    vec![
35
                         StyleTransform::Translate(node_transform),
35
                         StyleTransform::ScaleX(PercentageValue::new(scale_factor * 100.0)),
35
                         StyleTransform::ScaleY(PercentageValue::new(scale_factor * 100.0)),
                    ]
80
               }.into()
           ))),
80
           CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
80
               LayoutWidth::Px(PixelValue::const_px(250),),
80
           ))),
80
        ].into())
80
        .with_ids_and_classes({
           const IDS_AND_CLASSES_4480169002427296613: &[IdOrClass] =
               &[Class(AzString::from_const_str("node_graph_node"))];
80
           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_4480169002427296613)
        })
80
        .with_children(DomVec::from_vec(vec![
80
           Dom::create_p_with_text(AzString::from_const_str("X"))
80
               .with_css_props(CSS_MATCH_7395766480280098891)
80
               .with_callbacks(vec![
80
                   CoreCallbackData {
80
                       event: EventFilter::Hover(HoverEventFilter::MouseUp),
80
                       refany: node_local_dataset.clone(),
80
                       callback: CoreCallback { cb: nodegraph_delete_node as usize, ctx: OptionRefAny::None },
80
                   },
80
               ].into())
80
               .with_ids_and_classes({
                   const IDS_AND_CLASSES_7122017923389407516: &[IdOrClass] =
                       &[Class(AzString::from_const_str("node_close_button"))];
80
                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_7122017923389407516)
               }),
80
           Dom::create_p_with_text(node_info.node_type_name.clone())
80
               .with_css_props(CSS_MATCH_1739273067404038547)
80
               .with_ids_and_classes({
                   const IDS_AND_CLASSES_15777790571346582635: &[IdOrClass] =
                       &[Class(AzString::from_const_str("node_label"))];
80
                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_15777790571346582635)
               }),
80
           Dom::create_div()
80
               .with_css_props(CSS_MATCH_3354247437065914166)
80
               .with_ids_and_classes({
                   const IDS_AND_CLASSES_5590500152394859708: &[IdOrClass] =
                       &[Class(AzString::from_const_str("node_body"))];
80
                   IdOrClassVec::from_const_slice(IDS_AND_CLASSES_5590500152394859708)
               })
80
               .with_children(DomVec::from_vec(vec![
80
                   Dom::create_div()
80
                       .with_css_props(CSS_MATCH_16946967739775705757)
80
                       .with_ids_and_classes({
                           const IDS_AND_CLASSES_3626404106673061698: &[IdOrClass] =
                               &[Class(AzString::from_const_str("inputs"))];
80
                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_3626404106673061698)
                       })
80
                       .with_children(DomVec::from_vec(vec![Dom::create_div()
80
                           .with_css_props(CSS_MATCH_705881630351954657)
80
                           .with_ids_and_classes({
                               const IDS_AND_CLASSES_12825690349660780627: &[IdOrClass] =
                                   &[Class(AzString::from_const_str("node_input_wrapper"))];
80
                               IdOrClassVec::from_const_slice(
                                   IDS_AND_CLASSES_12825690349660780627,
                               )
                           })
80
                           .with_children(DomVec::from_vec(
80
                               inputs
80
                               .into_iter()
80
                               .enumerate()
80
                               .map(|(io_id, (input_label, input_color))| {
                                   use self::InputOrOutput::Input;
78
                                   Dom::create_div()
78
                                       .with_css_props(CSS_MATCH_9863994880298313101)
78
                                       .with_ids_and_classes({
                                           const IDS_AND_CLASSES_5020681879750641508:
                                               &[IdOrClass] = &[Class(AzString::from_const_str(
                                               "node_input_container",
                                           ))];
78
                                           IdOrClassVec::from_const_slice(
                                               IDS_AND_CLASSES_5020681879750641508,
                                           )
                                       })
78
                                       .with_children(DomVec::from_vec(vec![
78
                                           Dom::create_div()
78
                                               .with_css_props(
78
                                                   CSS_MATCH_4700400755767504372,
                                               )
78
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_9154857442066749879:
                                                       &[IdOrClass] =
                                                       &[Class(AzString::from_const_str(
                                                           "node_input_connection_label_wrapper",
                                                       ))];
78
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_9154857442066749879,
                                                   )
                                               })
78
                                               .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(
78
                                                   input_label,
                                               )
78
                                               .with_css_props(
78
                                                   CSS_MATCH_11452431279102104133,
                                               )
78
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_16291496011772407931:
                                                       &[IdOrClass] =
                                                       &[Class(AzString::from_const_str(
                                                           "node_input_connection_label",
                                                       ))];
78
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_16291496011772407931,
                                                   )
                                               })])),
78
                                           Dom::create_div()
78
                                               .with_callbacks(vec![
78
                                                   CoreCallbackData {
78
                                                       event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
78
                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
78
                                                           io_id: Input(io_id),
78
                                                           backref: node_local_dataset.clone(),
78
                                                       }),
78
                                                       callback: CoreCallback { cb: nodegraph_input_output_connect as usize, ctx: OptionRefAny::None },
78
                                                   },
78
                                                   CoreCallbackData {
78
                                                       event: EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
78
                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
78
                                                           io_id: Input(io_id),
78
                                                           backref: node_local_dataset.clone(),
78
                                                       }),
78
                                                       callback: CoreCallback { cb: nodegraph_input_output_disconnect as usize, ctx: OptionRefAny::None },
78
                                                   },
78
                                               ].into())
78
                                               .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
                                                       // .node_input
78
                                                       CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
78
                                                           StyleBackgroundContentVecValue::Exact(vec![StyleBackgroundContent::Color(input_color)].into()),
78
                                                       )),
78
                                                       CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
78
                                                           StyleCursor::Pointer,
78
                                                       ))),
78
                                                       CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
78
                                                           LayoutHeight::Px(PixelValue::const_px(15),),
78
                                                       ))),
78
                                                       CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
78
                                                           LayoutWidth::Px(PixelValue::const_px(15),),
78
                                                       ))),
                                                   ])
                                               )
78
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_2128818677168244823:
                                                       &[IdOrClass] = &[Class(
                                                       AzString::from_const_str("node_input"),
                                                   )];
78
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_2128818677168244823,
                                                   )
                                               }),
                                       ]))
80
                               }).collect()
                           ))
                       ])),
80
                   Dom::create_div()
80
                       .with_css_props(CSS_MATCH_7432473243011547380)
80
                       .with_ids_and_classes({
                           const IDS_AND_CLASSES_746059979773622802: &[IdOrClass] =
                               &[Class(AzString::from_const_str("node_content_wrapper"))];
80
                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_746059979773622802)
                       })
80
                       .with_children({
80
                           let mut fields = Vec::new();
80
                           for (field_idx, field) in node.fields.iter().enumerate() {
25
                               let field_local_dataset = RefAny::new(NodeFieldLocalDataset {
25
                                   field_idx,
25
                                   backref: node_local_dataset.clone(),
25
                               });
25
                               let div = Dom::create_div()
25
                               .with_css_props(CSS_MATCH_2639191696846875011)
25
                               .with_ids_and_classes({
                                   const IDS_AND_CLASSES_4413230059125905311: &[IdOrClass] =
                                       &[Class(AzString::from_const_str(
                                           "node_configuration_field_container",
                                       ))];
25
                                   IdOrClassVec::from_const_slice(
                                       IDS_AND_CLASSES_4413230059125905311,
                                   )
                               })
25
                               .with_children(DomVec::from_vec(vec![
25
                                   Dom::create_p_with_text(field.key.clone())
25
                                   .with_css_props(CSS_MATCH_1198521124955124418)
25
                                   .with_ids_and_classes({
                                       const IDS_AND_CLASSES_12334207996395559585:
                                           &[IdOrClass] =
                                           &[Class(AzString::from_const_str(
                                               "node_configuration_field_label",
                                           ))];
25
                                       IdOrClassVec::from_const_slice(
                                           IDS_AND_CLASSES_12334207996395559585,
                                       )
                                   }),
25
                                   match &field.value {
3
                                       NodeTypeFieldValue::TextInput(initial_text) => {
3
                                           let cb: TextInputOnFocusLostCallbackType = nodegraph_on_textinput_focus_lost;
3
                                           TextInput::create()
3
                                           .with_text(initial_text.clone())
3
                                           .with_on_focus_lost(field_local_dataset, cb)
3
                                           .dom()
                                       },
5
                                       NodeTypeFieldValue::NumberInput(initial_value) => {
5
                                           let cb: NumberInputOnFocusLostCallbackType = nodegraph_on_numberinput_focus_lost;
5
                                           NumberInput::create(*initial_value)
5
                                           .with_on_focus_lost(field_local_dataset, cb)
5
                                           .dom()
                                       },
13
                                       NodeTypeFieldValue::CheckBox(initial_checked) => {
13
                                           let cb: CheckBoxOnToggleCallbackType = nodegraph_on_checkbox_value_changed;
13
                                           CheckBox::create(*initial_checked)
13
                                           .with_on_toggle(field_local_dataset, cb)
13
                                           .dom()
                                       },
2
                                       NodeTypeFieldValue::ColorInput(initial_color) => {
2
                                           let cb: ColorInputOnValueChangeCallbackType = nodegraph_on_colorinput_value_changed;
2
                                           ColorInput::create(*initial_color)
2
                                           .with_on_value_change(field_local_dataset, cb)
2
                                           .dom()
                                       },
2
                                       NodeTypeFieldValue::FileInput(file_path) => {
2
                                           let cb: FileInputOnPathChangeCallbackType = nodegraph_on_fileinput_button_clicked;
2
                                           FileInput::create(file_path.clone())
2
                                           .with_on_path_change(field_local_dataset, cb)
2
                                           .dom()
                                       },
                                   }
                               ]));
25
                               fields.push(div);
                           }
80
                           DomVec::from_vec(fields)
                       }),
80
                   Dom::create_div()
80
                       .with_css_props(CSS_MATCH_14906563417280941890)
80
                       .with_ids_and_classes({
                           const IDS_AND_CLASSES_4737474624251936466: &[IdOrClass] =
                               &[Class(AzString::from_const_str("outputs"))];
80
                           IdOrClassVec::from_const_slice(IDS_AND_CLASSES_4737474624251936466)
                       })
80
                       .with_children(DomVec::from_vec(vec![Dom::create_div()
80
                           .with_css_props(CSS_MATCH_10339190304804100510)
80
                           .with_ids_and_classes({
                               const IDS_AND_CLASSES_12883576328110161157: &[IdOrClass] =
                                   &[Class(AzString::from_const_str("node_output_wrapper"))];
80
                               IdOrClassVec::from_const_slice(
                                   IDS_AND_CLASSES_12883576328110161157,
                               )
                           })
80
                           .with_children(DomVec::from_vec(
80
                               outputs
80
                               .into_iter()
80
                               .enumerate()
80
                               .map(|(io_id, (output_label, output_color))| {
                                   use self::InputOrOutput::Output;
79
                                   Dom::create_div()
79
                                       .with_css_props(CSS_MATCH_12400244273289328300)
79
                                       .with_ids_and_classes({
                                           const IDS_AND_CLASSES_10917819668096233812:
                                               &[IdOrClass] = &[Class(AzString::from_const_str(
                                               "node_output_container",
                                           ))];
79
                                           IdOrClassVec::from_const_slice(
                                               IDS_AND_CLASSES_10917819668096233812,
                                           )
                                       })
79
                                       .with_children(DomVec::from_vec(vec![
79
                                           Dom::create_div()
79
                                               .with_callbacks(vec![
79
                                                   CoreCallbackData {
79
                                                       event: EventFilter::Hover(HoverEventFilter::LeftMouseUp),
79
                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
79
                                                           io_id: Output(io_id),
79
                                                           backref: node_local_dataset.clone(),
79
                                                       }),
79
                                                       callback: CoreCallback { cb: nodegraph_input_output_connect as usize, ctx: OptionRefAny::None },
79
                                                   },
79
                                                   CoreCallbackData {
79
                                                       event: EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
79
                                                       refany: RefAny::new(NodeInputOutputLocalDataset {
79
                                                           io_id: Output(io_id),
79
                                                           backref: node_local_dataset.clone(),
79
                                                       }),
79
                                                       callback: CoreCallback { cb: nodegraph_input_output_disconnect as usize, ctx: OptionRefAny::None },
79
                                                   },
79
                                               ].into())
79
                                               .with_css_props(
79
                                                   CssPropertyWithConditionsVec::from_vec(vec![
                                                       // .node_output
79
                                                       CssPropertyWithConditions::simple(CssProperty::BackgroundContent(
79
                                                           StyleBackgroundContentVecValue::Exact(vec![
79
                                                               StyleBackgroundContent::Color(output_color)
79
                                                           ].into()),
79
                                                       )),
79
                                                       CssPropertyWithConditions::simple(CssProperty::Cursor(StyleCursorValue::Exact(
79
                                                           StyleCursor::Pointer,
79
                                                       ))),
79
                                                       CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
79
                                                           LayoutHeight::Px(PixelValue::const_px(15),),
79
                                                       ))),
79
                                                       CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
79
                                                           LayoutWidth::Px(PixelValue::const_px(15),),
79
                                                       ))),
                                                   ])
                                               )
79
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_17632471664405317563:
                                                       &[IdOrClass] = &[Class(
                                                       AzString::from_const_str("node_output"),
                                                   )];
79
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_17632471664405317563,
                                                   )
                                               }),
79
                                           Dom::create_div()
79
                                               .with_css_props(
79
                                                   CSS_MATCH_12038890904436132038,
                                               )
79
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_1667960214206134147:
                                                       &[IdOrClass] =
                                                       &[Class(AzString::from_const_str(
                                                           "node_output_connection_label_wrapper",
                                                       ))];
79
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_1667960214206134147,
                                                   )
                                               })
79
                                               .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(
79
                                                   output_label,
                                               )
79
                                               .with_css_props(
79
                                                   CSS_MATCH_2008162367868363199,
                                               )
79
                                               .with_ids_and_classes({
                                                   const IDS_AND_CLASSES_2974914452796301884:
                                                       &[IdOrClass] =
                                                       &[Class(AzString::from_const_str(
                                                           "node_output_connection_label",
                                                       ))];
79
                                                   IdOrClassVec::from_const_slice(
                                                       IDS_AND_CLASSES_2974914452796301884,
                                                   )
                                               })])),
                                       ]))
80
                               }).collect()
                           ))])),
               ])),
        ]))
80
        .with_dataset(Some(node_local_dataset).into())
80
    ].into())
80
}
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
28
fn render_connections(node_graph: &NodeGraph, root_marker_nodedata: RefAny) -> Dom {
    static NODEGRAPH_CONNECTIONS_CONTAINER_CLASS: &[IdOrClass] = &[Class(
        AzString::from_const_str("nodegraph-connections-container"),
    )];
    static NODEGRAPH_CONNECTIONS_CONTAINER_PROPS: &[CssPropertyWithConditions] = &[
        CssPropertyWithConditions::simple(CssProperty::position(LayoutPosition::Absolute)),
        CssPropertyWithConditions::simple(CssProperty::flex_grow(LayoutFlexGrow::const_new(1))),
    ];
28
    Dom::create_div()
28
        .with_ids_and_classes(IdOrClassVec::from_const_slice(
28
            NODEGRAPH_CONNECTIONS_CONTAINER_CLASS,
        ))
28
        .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
28
            NODEGRAPH_CONNECTIONS_CONTAINER_PROPS,
        ))
28
        .with_dataset(Some(root_marker_nodedata).into())
28
        .with_children({
28
            let mut children = Vec::new();
104
            for NodeIdNodeMap { node_id, node } in node_graph.nodes.as_ref() {
104
                let out_node_id = node_id;
104
                let node_type_info = match node_graph
104
                    .node_types
104
                    .iter()
126
                    .find(|i| i.node_type_id == node.node_type)
                {
98
                    Some(s) => &s.node_type_info,
6
                    None => continue,
                };
                for OutputConnection {
18
                    output_index,
18
                    connects_to,
98
                } in node.connect_out.as_ref()
                {
18
                    let Some(output_type_id) = node_type_info.outputs.get(*output_index) else {
3
                        continue;
                    };
15
                    let output_color = match node_graph
15
                        .input_output_types
15
                        .iter()
16
                        .find(|o| o.io_type_id == *output_type_id)
                    {
14
                        Some(s) => s.io_info.color,
1
                        None => continue,
                    };
                    for InputNodeAndIndex {
16
                        node_id,
16
                        input_index,
14
                    } in connects_to.as_ref()
                    {
16
                        let in_node_id = node_id;
16
                        let mut cld = ConnectionLocalDataset {
16
                            out_node_id: *out_node_id,
16
                            out_idx: *output_index,
16
                            in_node_id: *in_node_id,
16
                            in_idx: *input_index,
16
                            swap_vert: false,
16
                            swap_horz: false,
16
                            color: output_color,
16
                        };
16
                        let Some((rect, swap_vert, swap_horz)) = get_rect(node_graph, cld) else {
1
                            continue;
                        };
15
                        cld.swap_vert = swap_vert;
15
                        cld.swap_horz = swap_horz;
15
                        let cld_refany = RefAny::new(cld);
15
                        let connection_div = Dom::create_image(ImageRef::callback(
15
                            draw_connection as usize,
15
                            cld_refany.clone(),
                        ))
15
                        .with_dataset(Some(cld_refany).into())
15
                        .with_css_props(
15
                            vec![
15
                                CssPropertyWithConditions::simple(CssProperty::Transform(
15
                                    StyleTransformVecValue::Exact(
15
                                        vec![
15
                                            StyleTransform::Translate(StyleTransformTranslate2D {
15
                                                x: PixelValue::px(
15
                                                    node_graph.offset.x + rect.origin.x,
15
                                                ),
15
                                                y: PixelValue::px(
15
                                                    node_graph.offset.y + rect.origin.y,
15
                                                ),
15
                                            }),
15
                                            StyleTransform::ScaleX(PercentageValue::new(
15
                                                node_graph.scale_factor * 100.0,
15
                                            )),
15
                                            StyleTransform::ScaleY(PercentageValue::new(
15
                                                node_graph.scale_factor * 100.0,
15
                                            )),
15
                                        ]
15
                                        .into(),
15
                                    ),
15
                                )),
15
                                CssPropertyWithConditions::simple(CssProperty::Width(
15
                                    LayoutWidthValue::Exact(LayoutWidth::Px(PixelValue::px(
15
                                        rect.size.width,
15
                                    ))),
15
                                )),
15
                                CssPropertyWithConditions::simple(CssProperty::Height(
15
                                    LayoutHeightValue::Exact(LayoutHeight::Px(PixelValue::px(
15
                                        rect.size.height,
15
                                    ))),
15
                                )),
                            ]
15
                            .into(),
                        );
15
                        children.push(
15
                            Dom::create_div()
15
                                .with_css(
15
                                    "flex-grow: 1; position: absolute; overflow: hidden;",
                                )
15
                                .with_children(vec![connection_div].into()),
                        );
                    }
                }
            }
28
            children.into()
        })
28
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded layout/render numeric cast
8
extern "C" fn draw_connection(mut refany: RefAny, _info: ()) -> ImageRef {
    // RenderImageCallbackInfo not available in memtest
    // let size = info.get_bounds().get_physical_size();
8
    let size = LogicalSize {
8
        width: 100.0,
8
        height: 100.0,
8
    };
    // Cannot call draw_connection_inner without RenderImageCallbackInfo
8
    ImageRef::null_image(
8
        size.width as usize,
8
        size.height as usize,
8
        RawImageFormat::R8,
8
        Vec::new(),
    )
8
}
const NODE_WIDTH: f32 = 250.0;
const V_OFFSET: f32 = 71.0;
const DIST_BETWEEN_NODES: f32 = 10.0;
const CONNECTION_DOT_HEIGHT: f32 = 15.0;
// calculates the rect on which the connection is drawn in the UI
#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
#[allow(clippy::cast_precision_loss)] // bounded layout/render numeric cast
29
fn get_rect(
29
    node_graph: &NodeGraph,
29
    connection: ConnectionLocalDataset,
29
) -> Option<(LogicalRect, bool, bool)> {
    let ConnectionLocalDataset {
29
        out_node_id,
29
        out_idx,
29
        in_node_id,
29
        in_idx,
        ..
29
    } = connection;
39
    let out_node = node_graph.nodes.iter().find(|i| i.node_id == out_node_id)?;
82
    let in_node = node_graph.nodes.iter().find(|i| i.node_id == in_node_id)?;
25
    let x_out = out_node.node.position.x + NODE_WIDTH;
25
    let y_out = out_node.node.position.y
25
        + V_OFFSET
25
        + (out_idx as f32 * (DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT));
25
    let x_in = in_node.node.position.x;
25
    let y_in = in_node.node.position.y
25
        + V_OFFSET
25
        + (in_idx as f32 * (DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT));
25
    let should_swap_vertical = y_in > y_out;
25
    let should_swap_horizontal = x_in < x_out;
25
    let width = (x_in - x_out).abs();
25
    let height = (y_in - y_out).abs() + CONNECTION_DOT_HEIGHT;
25
    let x = x_in.min(x_out);
25
    let y = y_in.min(y_out);
25
    Some((
25
        LogicalRect {
25
            size: LogicalSize { width, height },
25
            origin: LogicalPosition { x, y },
25
        },
25
        should_swap_vertical,
25
        should_swap_horizontal,
25
    ))
29
}
3
extern "C" fn nodegraph_set_active_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
3
    let data_clone = refany.clone();
3
    if let Some(mut refany) = refany.downcast_mut::<NodeLocalDataset>() {
2
        let node_id = refany.node_id;
2
        if let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() {
1
            backref.active_node_being_dragged = Some((node_id, data_clone));
1
        }
1
    }
3
    Update::DoNothing
3
}
5
extern "C" fn nodegraph_unset_active_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
5
    if let Some(mut refany) = refany.downcast_mut::<NodeGraphLocalDataset>() {
4
        refany.active_node_being_dragged = None;
4
    }
5
    Update::DoNothing
5
}
// drag either the graph or the currently active nodes
#[allow(clippy::float_cmp)] // intentional exact compare: change-detection / identity fast-path / cache-key match
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
#[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
3
extern "C" fn nodegraph_drag_graph_or_nodes(mut refany: RefAny, mut info: CallbackInfo) -> Update {
3
    let Some(mut refany) = refany.downcast_mut::<NodeGraphLocalDataset>() else {
1
        return Update::DoNothing;
    };
2
    let refany = &mut *refany;
2
    let Some(prev) = info.get_previous_mouse_state() else {
2
        return Update::DoNothing;
    };
    let cur = info.get_current_mouse_state();
    if !(cur.left_down && prev.left_down) {
        // event is not a drag event
        return Update::DoNothing;
    }
    let (InWindow(current_mouse_pos), InWindow(previous_mouse_pos)) =
        (cur.cursor_position, prev.cursor_position)
    else {
        return Update::DoNothing;
    };
    let dx = (current_mouse_pos.x - previous_mouse_pos.x) * (1.0 / refany.node_graph.scale_factor);
    let dy = (current_mouse_pos.y - previous_mouse_pos.y) * (1.0 / refany.node_graph.scale_factor);
    let nodegraph_node = info.get_hit_node();
    let should_update = match refany.active_node_being_dragged.clone() {
        // drag node
        Some((node_graph_node_id, data_marker)) => {
            let node_connection_marker = &mut refany.node_connection_marker;
            let _nodegraph_node = info.get_hit_node();
            let result = match refany.callbacks.on_node_dragged.as_ref() {
                Some(OnNodeDragged { callback, refany }) => (callback.cb)(
                    refany.clone(),
                    info,
                    node_graph_node_id,
                    NodeDragAmount { x: dx, y: dy },
                ),
                None => Update::DoNothing,
            };
            // update the visual transform of the node in the UI
            let node_position = match refany
                .node_graph
                .nodes
                .iter_mut()
                .find(|i| i.node_id == node_graph_node_id)
            {
                Some(s) => {
                    s.node.position.x += dx;
                    s.node.position.y += dy;
                    s.node.position
                }
                None => return Update::DoNothing,
            };
            let Some(visual_node_id) = info.get_node_id_of_root_dataset(data_marker) else {
                return Update::DoNothing;
            };
            let node_transform = StyleTransformTranslate2D {
                x: PixelValue::px(node_position.x + refany.node_graph.offset.x),
                y: PixelValue::px(node_position.y + refany.node_graph.offset.y),
            };
            info.set_css_property(
                visual_node_id,
                CssProperty::transform(
                    if refany.node_graph.scale_factor == 1.0 {
                        vec![StyleTransform::Translate(node_transform)]
                    } else {
                        vec![
                            StyleTransform::Translate(node_transform),
                            StyleTransform::ScaleX(PercentageValue::new(
                                refany.node_graph.scale_factor * 100.0,
                            )),
                            StyleTransform::ScaleY(PercentageValue::new(
                                refany.node_graph.scale_factor * 100.0,
                            )),
                        ]
                    }
                    .into(),
                ),
            );
            // get the NodeId of the node containing all the connection lines
            let Some(connection_container_nodeid) =
                info.get_node_id_of_root_dataset(node_connection_marker.clone())
            else {
                return result;
            };
            // animate all the connections
            let mut first_connection_child = info.get_first_child(connection_container_nodeid);
            while let Some(connection_nodeid) = first_connection_child {
                first_connection_child = info.get_next_sibling(connection_nodeid);
                let Some(first_child) = info.get_first_child(connection_nodeid) else {
                    continue;
                };
                let Some(mut dataset) = info.get_dataset(first_child) else {
                    continue;
                };
                let Some(mut cld) = dataset.downcast_mut::<ConnectionLocalDataset>() else {
                    continue;
                };
                if !(cld.out_node_id == node_graph_node_id || cld.in_node_id == node_graph_node_id)
                {
                    continue; // connection does not need to be modified
                }
                let Some((new_rect, swap_vert, swap_horz)) = get_rect(&refany.node_graph, *cld)
                else {
                    continue;
                };
                cld.swap_vert = swap_vert;
                cld.swap_horz = swap_horz;
                let node_transform = StyleTransformTranslate2D {
                    x: PixelValue::px(refany.node_graph.offset.x + new_rect.origin.x),
                    y: PixelValue::px(refany.node_graph.offset.y + new_rect.origin.y),
                };
                info.set_css_property(
                    first_child,
                    CssProperty::transform(
                        if refany.node_graph.scale_factor == 1.0 {
                            vec![StyleTransform::Translate(node_transform)]
                        } else {
                            vec![
                                StyleTransform::Translate(node_transform),
                                StyleTransform::ScaleX(PercentageValue::new(
                                    refany.node_graph.scale_factor * 100.0,
                                )),
                                StyleTransform::ScaleY(PercentageValue::new(
                                    refany.node_graph.scale_factor * 100.0,
                                )),
                            ]
                        }
                        .into(),
                    ),
                );
                info.set_css_property(
                    first_child,
                    CssProperty::Width(LayoutWidthValue::Exact(LayoutWidth::Px(PixelValue::px(
                        new_rect.size.width,
                    )))),
                );
                info.set_css_property(
                    first_child,
                    CssProperty::Height(LayoutHeightValue::Exact(LayoutHeight::Px(
                        PixelValue::px(new_rect.size.height),
                    ))),
                );
            }
            result
        }
        // drag graph
        None => {
            let result = match refany.callbacks.on_node_graph_dragged.as_ref() {
                Some(OnNodeGraphDragged { callback, refany }) => (callback.cb)(
                    refany.clone(),
                    info,
                    GraphDragAmount { x: dx, y: dy },
                ),
                None => Update::DoNothing,
            };
            refany.node_graph.offset.x += dx;
            refany.node_graph.offset.y += dy;
            // Update the visual node positions
            let Some(node_container) = info.get_first_child(nodegraph_node) else {
                return Update::DoNothing;
            };
            let Some(node_container) = info.get_next_sibling(node_container) else {
                return Update::DoNothing;
            };
            let Some(mut node) = info.get_first_child(node_container) else {
                return Update::DoNothing;
            };
            loop {
                let Some(node_first_child) = info.get_first_child(node) else {
                    return Update::DoNothing;
                };
                let mut node_local_dataset = match info.get_dataset(node_first_child) {
                    None => return Update::DoNothing,
                    Some(s) => s,
                };
                let Some(node_graph_node_id) =
                    node_local_dataset.downcast_ref::<NodeLocalDataset>()
                else {
                    continue;
                };
                let node_graph_node_id = node_graph_node_id.node_id;
                let node_position = match refany
                    .node_graph
                    .nodes
                    .iter()
                    .find(|i| i.node_id == node_graph_node_id)
                {
                    Some(s) => s.node.position,
                    None => continue,
                };
                let node_transform = StyleTransformTranslate2D {
                    x: PixelValue::px(node_position.x + refany.node_graph.offset.x),
                    y: PixelValue::px(node_position.y + refany.node_graph.offset.y),
                };
                info.set_css_property(
                    node_first_child,
                    CssProperty::transform(
                        if refany.node_graph.scale_factor == 1.0 {
                            vec![StyleTransform::Translate(node_transform)]
                        } else {
                            vec![
                                StyleTransform::Translate(node_transform),
                                StyleTransform::ScaleX(PercentageValue::new(
                                    refany.node_graph.scale_factor * 100.0,
                                )),
                                StyleTransform::ScaleY(PercentageValue::new(
                                    refany.node_graph.scale_factor * 100.0,
                                )),
                            ]
                        }
                        .into(),
                    ),
                );
                node = match info.get_next_sibling(node) {
                    Some(s) => s,
                    None => break,
                };
            }
            let node_connection_marker = &mut refany.node_connection_marker;
            // Update the connection positions
            let Some(connection_container_nodeid) =
                info.get_node_id_of_root_dataset(node_connection_marker.clone())
            else {
                return result;
            };
            let mut first_connection_child = info.get_first_child(connection_container_nodeid);
            while let Some(connection_nodeid) = first_connection_child {
                first_connection_child = info.get_next_sibling(connection_nodeid);
                let Some(first_child) = info.get_first_child(connection_nodeid) else {
                    continue;
                };
                let Some(mut dataset) = info.get_dataset(first_child) else {
                    continue;
                };
                let Some(cld) = dataset.downcast_ref::<ConnectionLocalDataset>() else {
                    continue;
                };
                let Some((new_rect, _, _)) = get_rect(&refany.node_graph, *cld) else {
                    continue;
                };
                info.set_css_property(
                    first_child,
                    CssProperty::transform(
                        vec![
                            StyleTransform::Translate(StyleTransformTranslate2D {
                                x: PixelValue::px(refany.node_graph.offset.x + new_rect.origin.x),
                                y: PixelValue::px(refany.node_graph.offset.y + new_rect.origin.y),
                            }),
                            StyleTransform::ScaleX(PercentageValue::new(
                                refany.node_graph.scale_factor * 100.0,
                            )),
                            StyleTransform::ScaleY(PercentageValue::new(
                                refany.node_graph.scale_factor * 100.0,
                            )),
                        ]
                        .into(),
                    ),
                );
            }
            result
        }
    };
    info.stop_propagation();
    should_update
3
}
2
extern "C" fn nodegraph_duplicate_node(mut refany: RefAny, _info: CallbackInfo) -> Update {
2
    let Some(_data) = refany.downcast_mut::<NodeLocalDataset>() else {
1
        return Update::DoNothing;
    };
1
    Update::DoNothing // TODO
2
}
5
extern "C" fn nodegraph_delete_node(mut refany: RefAny, mut info: CallbackInfo) -> Update {
5
    let Some(mut refany) = refany.downcast_mut::<NodeLocalDataset>() else {
1
        return Update::DoNothing;
    };
4
    let node_id = refany.node_id;
4
    let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() else {
1
        return Update::DoNothing;
    };
3
    let result = match backref.callbacks.on_node_removed.as_ref() {
2
        Some(OnNodeRemoved { callback, refany }) => (callback.cb)(refany.clone(), info, node_id),
1
        None => Update::DoNothing,
    };
3
    result
5
}
#[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
4
extern "C" fn nodegraph_context_menu_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    use azul_core::window::CursorPosition;
4
    let Some(mut refany) = refany.downcast_mut::<ContextMenuEntryLocalDataset>() else {
1
        return Update::DoNothing;
    };
3
    let new_node_type = refany.node_type;
3
    let Some(node_graph_wrapper_id) = info.get_node_id_of_root_dataset(refany.backref.clone())
    else {
1
        return Update::DoNothing;
    };
2
    let Some(mut backref) = refany.backref.downcast_mut::<NodeGraphLocalDataset>() else {
        return Update::DoNothing;
    };
2
    let node_wrapper_offset = info
2
        .get_node_position(node_graph_wrapper_id)
2
        .map_or((0.0, 0.0), |p| (p.x, p.y));
2
    let cursor_in_viewport = match info.get_current_mouse_state().cursor_position {
        InWindow(i) => i,
        CursorPosition::OutOfWindow(i) => i,
2
        CursorPosition::Uninitialized => LogicalPosition::zero(),
    };
2
    let new_node_pos = NodeGraphNodePosition {
2
        x: (cursor_in_viewport.x - node_wrapper_offset.0) * (1.0 / backref.node_graph.scale_factor)
2
            - backref.node_graph.offset.x,
2
        y: (cursor_in_viewport.y - node_wrapper_offset.1) * (1.0 / backref.node_graph.scale_factor)
2
            - backref.node_graph.offset.y,
2
    };
2
    let new_node_id = backref.node_graph.generate_unique_node_id();
2
    let result = match backref.callbacks.on_node_added.as_ref() {
2
        Some(OnNodeAdded { callback, refany }) => (callback.cb)(
2
            refany.clone(),
2
            info,
2
            new_node_type,
2
            new_node_id,
2
            new_node_pos,
2
        ),
        None => Update::DoNothing,
    };
2
    result
4
}
21
extern "C" fn nodegraph_input_output_connect(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    use self::InputOrOutput::{Input, Output};
21
    let Some(mut refany) = refany.downcast_mut::<NodeInputOutputLocalDataset>() else {
1
        return Update::DoNothing;
    };
20
    let io_id = refany.io_id;
20
    let Some(mut backref) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
1
        return Update::DoNothing;
    };
19
    let node_id = backref.node_id;
19
    let Some(mut backref) = backref.backref.downcast_mut::<NodeGraphLocalDataset>() else {
1
        return Update::DoNothing;
    };
7
    let (input_node, input_index, output_node, output_index) =
18
        match backref.last_input_or_output_clicked {
            None => {
9
                backref.last_input_or_output_clicked = Some((node_id, io_id));
9
                return Update::DoNothing;
            }
9
            Some((prev_node_id, prev_io_id)) => {
9
                match (prev_io_id, io_id) {
1
                    (Input(i), Output(o)) => (prev_node_id, i, node_id, o),
6
                    (Output(o), Input(i)) => (node_id, i, prev_node_id, o),
                    _ => {
                        // error: trying to connect input to input or output to output
2
                        backref.last_input_or_output_clicked = None;
2
                        return Update::DoNothing;
                    }
                }
            }
        };
    // verify that the nodetype matches
7
    match backref.node_graph.connect_input_output(
7
        input_node,
7
        input_index,
7
        output_node,
7
        output_index,
7
    ) {
5
        Ok(()) => {}
2
        Err(e) => {
2
            eprintln!("{e:?}");
2
            backref.last_input_or_output_clicked = None;
2
            return Update::DoNothing;
        }
    }
5
    let result = match backref.callbacks.on_node_connected.as_ref() {
3
        Some(OnNodeConnected { callback, refany }) => {
3
            let r = (callback.cb)(
3
                refany.clone(),
3
                info,
3
                input_node,
3
                input_index,
3
                output_node,
3
                output_index,
3
            );
3
            backref.last_input_or_output_clicked = None;
3
            r
        }
2
        None => Update::DoNothing,
    };
5
    result
21
}
6
extern "C" fn nodegraph_input_output_disconnect(mut refany: RefAny, info: CallbackInfo) -> Update {
    use self::InputOrOutput::{Input, Output};
6
    let Some(mut refany) = refany.downcast_mut::<NodeInputOutputLocalDataset>() else {
1
        return Update::DoNothing;
    };
5
    let io_id = refany.io_id;
5
    let Some(mut backref) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
5
    let node_id = backref.node_id;
5
    let Some(mut backref) = backref.backref.downcast_mut::<NodeGraphLocalDataset>() else {
        return Update::DoNothing;
    };
5
    let mut result = Update::DoNothing;
5
    match io_id {
3
        Input(i) => {
3
            result.max_self(
3
                match backref.callbacks.on_node_input_disconnected.as_ref() {
2
                    Some(OnNodeInputDisconnected { callback, refany }) => {
2
                        (callback.cb)(refany.clone(), info, node_id, i)
                    }
1
                    None => Update::DoNothing,
                },
            );
        }
2
        Output(o) => {
2
            result.max_self(
2
                match backref.callbacks.on_node_output_disconnected.as_ref() {
2
                    Some(OnNodeOutputDisconnected { callback, refany }) => {
2
                        (callback.cb)(refany.clone(), info, node_id, o)
                    }
                    None => Update::DoNothing,
                },
            );
        }
    }
5
    result
6
}
6
extern "C" fn nodegraph_on_textinput_focus_lost(
6
    mut refany: RefAny,
6
    info: CallbackInfo,
6
    textinputstate: TextInputState,
6
) -> Update {
6
    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
1
        return Update::DoNothing;
    };
5
    let field_idx = refany.field_idx;
5
    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
5
    let node_id = node_local_dataset.node_id;
5
    let Some(mut node_graph) = node_local_dataset
5
        .backref
5
        .downcast_mut::<NodeGraphLocalDataset>()
    else {
        return Update::DoNothing;
    };
5
    let node_type = match node_graph
5
        .node_graph
5
        .nodes
5
        .iter()
9
        .find(|i| i.node_id == node_id)
    {
4
        Some(s) => s.node.node_type,
1
        None => return Update::DoNothing,
    };
4
    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
4
        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
4
            refany.clone(),
4
            info,
4
            node_id,
4
            field_idx,
4
            node_type,
4
            NodeTypeFieldValue::TextInput(textinputstate.get_text().into()),
4
        ),
        None => Update::DoNothing,
    };
4
    result
6
}
11
extern "C" fn nodegraph_on_numberinput_focus_lost(
11
    mut refany: RefAny,
11
    info: CallbackInfo,
11
    numberinputstate: NumberInputState,
11
) -> Update {
11
    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
1
        return Update::DoNothing;
    };
10
    let field_idx = refany.field_idx;
10
    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
10
    let node_id = node_local_dataset.node_id;
10
    let Some(mut node_graph) = node_local_dataset
10
        .backref
10
        .downcast_mut::<NodeGraphLocalDataset>()
    else {
        return Update::DoNothing;
    };
10
    let node_type = match node_graph
10
        .node_graph
10
        .nodes
10
        .iter()
13
        .find(|i| i.node_id == node_id)
    {
9
        Some(s) => s.node.node_type,
1
        None => return Update::DoNothing,
    };
9
    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
9
        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
9
            refany.clone(),
9
            info,
9
            node_id,
9
            field_idx,
9
            node_type,
9
            NodeTypeFieldValue::NumberInput(numberinputstate.number),
9
        ),
        None => Update::DoNothing,
    };
9
    result
11
}
7
extern "C" fn nodegraph_on_checkbox_value_changed(
7
    mut refany: RefAny,
7
    info: CallbackInfo,
7
    checkboxinputstate: CheckBoxState,
7
) -> Update {
7
    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
1
        return Update::DoNothing;
    };
6
    let field_idx = refany.field_idx;
6
    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
6
    let node_id = node_local_dataset.node_id;
6
    let Some(mut node_graph) = node_local_dataset
6
        .backref
6
        .downcast_mut::<NodeGraphLocalDataset>()
    else {
        return Update::DoNothing;
    };
6
    let node_type = match node_graph
6
        .node_graph
6
        .nodes
6
        .iter()
9
        .find(|i| i.node_id == node_id)
    {
5
        Some(s) => s.node.node_type,
1
        None => return Update::DoNothing,
    };
5
    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
4
        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
4
            refany.clone(),
4
            info,
4
            node_id,
4
            field_idx,
4
            node_type,
4
            NodeTypeFieldValue::CheckBox(checkboxinputstate.checked),
4
        ),
1
        None => Update::DoNothing,
    };
5
    result
7
}
5
extern "C" fn nodegraph_on_colorinput_value_changed(
5
    mut refany: RefAny,
5
    info: CallbackInfo,
5
    colorinputstate: ColorInputState,
5
) -> Update {
5
    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
1
        return Update::DoNothing;
    };
4
    let field_idx = refany.field_idx;
4
    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
4
    let node_id = node_local_dataset.node_id;
4
    let Some(mut node_graph) = node_local_dataset
4
        .backref
4
        .downcast_mut::<NodeGraphLocalDataset>()
    else {
        return Update::DoNothing;
    };
4
    let node_type = match node_graph
4
        .node_graph
4
        .nodes
4
        .iter()
7
        .find(|i| i.node_id == node_id)
    {
3
        Some(s) => s.node.node_type,
1
        None => return Update::DoNothing,
    };
3
    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
3
        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
3
            refany.clone(),
3
            info,
3
            node_id,
3
            field_idx,
3
            node_type,
3
            NodeTypeFieldValue::ColorInput(colorinputstate.color),
3
        ),
        None => Update::DoNothing,
    };
3
    result
5
}
5
extern "C" fn nodegraph_on_fileinput_button_clicked(
5
    mut refany: RefAny,
5
    info: CallbackInfo,
5
    file: FileInputState,
5
) -> Update {
5
    let Some(mut refany) = refany.downcast_mut::<NodeFieldLocalDataset>() else {
1
        return Update::DoNothing;
    };
4
    let field_idx = refany.field_idx;
4
    let Some(mut node_local_dataset) = refany.backref.downcast_mut::<NodeLocalDataset>() else {
        return Update::DoNothing;
    };
4
    let node_id = node_local_dataset.node_id;
4
    let Some(mut node_graph) = node_local_dataset
4
        .backref
4
        .downcast_mut::<NodeGraphLocalDataset>()
    else {
        return Update::DoNothing;
    };
4
    let node_type = match node_graph
4
        .node_graph
4
        .nodes
4
        .iter()
7
        .find(|i| i.node_id == node_id)
    {
3
        Some(s) => s.node.node_type,
1
        None => return Update::DoNothing,
    };
    // If a new file was selected, invoke callback
3
    let result = match node_graph.callbacks.on_node_field_edited.as_ref() {
2
        Some(OnNodeFieldEdited { refany, callback }) => (callback.cb)(
2
            refany.clone(),
2
            info,
2
            node_id,
2
            field_idx,
2
            node_type,
2
            NodeTypeFieldValue::FileInput(file.path),
2
        ),
1
        None => return Update::DoNothing,
    };
2
    result
5
}
#[cfg(all(test, feature = "std"))]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use std::{
        collections::{BTreeMap, HashMap},
        sync::{Arc, Mutex},
    };
    use azul_core::{
        dom::{DomId, DomNodeId},
        geom::{LogicalRect, OptionLogicalPosition},
        gl::OptionGlContextPtr,
        hit_test::ScrollPosition,
        resources::RendererResources,
        styled_dom::{NodeHierarchyItemId, StyledDom},
        window::{MonitorVec, RawWindowHandle},
    };
    use rust_fontconfig::FcFontCache;
    use super::*;
    #[cfg(feature = "icu")]
    use crate::icu::IcuLocalizerHandle;
    use crate::{
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
        solver3::{display_list::DisplayList, layout_tree::LayoutTree},
        window::{DomLayoutResult, LayoutWindow},
        window_state::FullWindowState,
    };
    // ------------------------------------------------------------------
    // Fixtures
    // ------------------------------------------------------------------
    /// Two node types that are deliberately *type-incompatible*: `TYPE_A` speaks
    /// `IO_INT` on both ends, `TYPE_B` speaks `IO_FLOAT`. Every "mime type mismatch"
    /// assertion below is A-to-B; every legal connection is A-to-A.
    const TYPE_A: NodeTypeId = NodeTypeId { inner: 1 };
    const TYPE_B: NodeTypeId = NodeTypeId { inner: 2 };
    /// A node type id that is *never* registered in `node_types`.
    const TYPE_UNREGISTERED: NodeTypeId = NodeTypeId { inner: 99 };
    const IO_INT: InputOutputTypeId = InputOutputTypeId { inner: 10 };
    const IO_FLOAT: InputOutputTypeId = InputOutputTypeId { inner: 20 };
    /// An I/O type id that has no entry in `input_output_types` (so no color).
    const IO_COLORLESS: InputOutputTypeId = InputOutputTypeId { inner: 77 };
    const N1: NodeGraphNodeId = NodeGraphNodeId { inner: 1 };
    const N2: NodeGraphNodeId = NodeGraphNodeId { inner: 2 };
    const N3: NodeGraphNodeId = NodeGraphNodeId { inner: 3 };
    const N4: NodeGraphNodeId = NodeGraphNodeId { inner: 4 };
    /// A node id that is never in the graph.
    const MISSING: NodeGraphNodeId = NodeGraphNodeId { inner: 999 };
    /// The four geometry constants `get_rect` is built from, restated here so that a
    /// silent change to any of them fails the geometry tests loudly instead of
    /// silently re-deriving the "expected" value from the same source.
    const EXPECT_NODE_WIDTH: f32 = 250.0;
    const EXPECT_V_OFFSET: f32 = 71.0;
    const EXPECT_PORT_PITCH: f32 = 25.0; // DIST_BETWEEN_NODES + CONNECTION_DOT_HEIGHT
    const EXPECT_DOT_HEIGHT: f32 = 15.0;
    fn io_types() -> InputOutputTypeIdInfoMapVec {
        vec![
            InputOutputTypeIdInfoMap {
                io_type_id: IO_INT,
                io_info: InputOutputInfo {
                    data_type: AzString::from_const_str("int"),
                    color: ColorU {
                        r: 1,
                        g: 2,
                        b: 3,
                        a: 255,
                    },
                },
            },
            InputOutputTypeIdInfoMap {
                io_type_id: IO_FLOAT,
                io_info: InputOutputInfo {
                    data_type: AzString::from_const_str("float"),
                    color: ColorU {
                        r: 4,
                        g: 5,
                        b: 6,
                        a: 255,
                    },
                },
            },
        ]
        .into()
    }
    fn node_types() -> NodeTypeIdInfoMapVec {
        vec![
            NodeTypeIdInfoMap {
                node_type_id: TYPE_A,
                node_type_info: NodeTypeInfo {
                    is_root: true,
                    node_type_name: AzString::from_const_str("A"),
                    inputs: vec![IO_INT].into(),
                    outputs: vec![IO_INT].into(),
                },
            },
            NodeTypeIdInfoMap {
                node_type_id: TYPE_B,
                node_type_info: NodeTypeInfo {
                    is_root: false,
                    node_type_name: AzString::from_const_str("B"),
                    inputs: vec![IO_FLOAT].into(),
                    outputs: vec![IO_FLOAT].into(),
                },
            },
        ]
        .into()
    }
    fn mk_node(node_type: NodeTypeId, x: f32, y: f32) -> Node {
        Node {
            node_type,
            position: NodeGraphNodePosition { x, y },
            fields: NodeTypeFieldVec::new(),
            connect_in: InputConnectionVec::new(),
            connect_out: OutputConnectionVec::new(),
        }
    }
    /// Four nodes: `N1`, `N3`, `N4` are `TYPE_A` (int), `N2` is `TYPE_B` (float).
    /// So `N1 -> N3`, `N1 -> N4` and `N3 -> N4` are legal connections and anything
    /// touching `N2` is a mime-type mismatch.
    fn graph() -> NodeGraph {
        NodeGraph {
            node_types: node_types(),
            input_output_types: io_types(),
            nodes: vec![
                NodeIdNodeMap {
                    node_id: N1,
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
                NodeIdNodeMap {
                    node_id: N2,
                    node: mk_node(TYPE_B, 400.0, 100.0),
                },
                NodeIdNodeMap {
                    node_id: N3,
                    node: mk_node(TYPE_A, 800.0, 50.0),
                },
                NodeIdNodeMap {
                    node_id: N4,
                    node: mk_node(TYPE_A, -100.0, 200.0),
                },
            ]
            .into(),
            ..NodeGraph::default()
        }
    }
    /// `(input_index, [(output_node_id, output_index)])` for every input port of `id`.
    fn inputs_of(g: &NodeGraph, id: NodeGraphNodeId) -> Vec<(usize, Vec<(u64, usize)>)> {
        g.nodes
            .iter()
            .find(|n| n.node_id == id)
            .map_or_else(Vec::new, |n| {
                n.node
                    .connect_in
                    .iter()
                    .map(|c| {
                        (
                            c.input_index,
                            c.connects_to
                                .iter()
                                .map(|o| (o.node_id.inner, o.output_index))
                                .collect(),
                        )
                    })
                    .collect()
            })
    }
    /// `(output_index, [(input_node_id, input_index)])` for every output port of `id`.
    fn outputs_of(g: &NodeGraph, id: NodeGraphNodeId) -> Vec<(usize, Vec<(u64, usize)>)> {
        g.nodes
            .iter()
            .find(|n| n.node_id == id)
            .map_or_else(Vec::new, |n| {
                n.node
                    .connect_out
                    .iter()
                    .map(|c| {
                        (
                            c.output_index,
                            c.connects_to
                                .iter()
                                .map(|i| (i.node_id.inner, i.input_index))
                                .collect(),
                        )
                    })
                    .collect()
            })
    }
    /// The full wiring of the graph, as a comparable value — the "encoding" that the
    /// connect/disconnect round-trip tests compare before and after.
    type Wiring = Vec<(u64, Vec<(usize, Vec<(u64, usize)>)>, Vec<(usize, Vec<(u64, usize)>)>)>;
    fn wiring(g: &NodeGraph) -> Wiring {
        g.nodes
            .iter()
            .map(|n| {
                (
                    n.node_id.inner,
                    inputs_of(g, n.node_id),
                    outputs_of(g, n.node_id),
                )
            })
            .collect()
    }
    /// Pushes an output connection *without* going through `connect_input_output`, so
    /// that structurally-impossible graphs (dangling target, out-of-range port, port
    /// with no registered color) can be handed to the renderers.
    fn force_out_connection(
        mut g: NodeGraph,
        from: NodeGraphNodeId,
        out_idx: usize,
        to: NodeGraphNodeId,
        in_idx: usize,
    ) -> NodeGraph {
        if let Some(n) = g.nodes.as_mut().iter_mut().find(|n| n.node_id == from) {
            n.node.connect_out.push(OutputConnection {
                output_index: out_idx,
                connects_to: vec![InputNodeAndIndex {
                    node_id: to,
                    input_index: in_idx,
                }]
                .into(),
            });
        }
        g
    }
    fn count_nodes(dom: &Dom) -> usize {
        1 + dom.children.iter().map(count_nodes).sum::<usize>()
    }
    /// A `RefAny<NodeGraphLocalDataset>` wrapping a snapshot of `g` — the payload every
    /// node-graph callback expects to find at the end of its `backref` chain.
    fn graph_dataset(g: &NodeGraph) -> RefAny {
        RefAny::new(NodeGraphLocalDataset {
            node_graph: g.clone(),
            last_input_or_output_clicked: None,
            active_node_being_dragged: None,
            node_connection_marker: RefAny::new(NodeConnectionMarkerDataset {}),
            callbacks: g.callbacks.clone(),
        })
    }
    /// Reads the graph back out of a `NodeGraphLocalDataset` handle.
    fn dataset_graph(handle: &RefAny) -> NodeGraph {
        let mut handle = handle.clone();
        let d = handle
            .downcast_ref::<NodeGraphLocalDataset>()
            .expect("not a NodeGraphLocalDataset");
        d.node_graph.clone()
    }
    /// `InputOrOutput` is not `PartialEq`, so flatten it into something that is.
    fn io_kind(io: InputOrOutput) -> (bool, usize) {
        match io {
            InputOrOutput::Input(i) => (true, i),
            InputOrOutput::Output(o) => (false, o),
        }
    }
    fn pending_click(handle: &RefAny) -> Option<(u64, (bool, usize))> {
        let mut handle = handle.clone();
        let d = handle
            .downcast_ref::<NodeGraphLocalDataset>()
            .expect("not a NodeGraphLocalDataset");
        d.last_input_or_output_clicked
            .map(|(id, io)| (id.inner, io_kind(io)))
    }
    // ------------------------------------------------------------------
    // Callback harness (mirrors the one in check_box.rs / color_input.rs)
    // ------------------------------------------------------------------
    /// A `DomNodeId` whose node component is `None` — "no concrete node was hit".
    fn hit_none() -> DomNodeId {
        DomNodeId {
            dom: DomId::ROOT_ID,
            node: NodeHierarchyItemId::NONE,
        }
    }
    fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
        DomLayoutResult {
            styled_dom,
            layout_tree: LayoutTree {
                nodes: Vec::new(),
                warm: Vec::new(),
                cold: Vec::new(),
                root: 0,
                dom_to_layout: BTreeMap::new(),
                children_arena: Vec::new(),
                children_offsets: Vec::new(),
                subtree_needs_intrinsic: Vec::new(),
            },
            calculated_positions: Vec::new(),
            viewport: LogicalRect::zero(),
            display_list: Arc::new(DisplayList::default()),
            scroll_ids: HashMap::new(),
            scroll_id_to_node_id: HashMap::new(),
        }
    }
    /// Runs `f` with a `CallbackInfo` whose window holds `styled_dom` as the root DOM.
    /// `previous_window_state` is deliberately `None`, which is what makes
    /// `get_previous_mouse_state()` return `None` in the drag tests.
    fn with_info<R>(
        styled_dom: StyledDom,
        hit: DomNodeId,
        f: impl FnOnce(&mut CallbackInfo) -> R,
    ) -> (R, Vec<CallbackChange>) {
        let mut layout_window =
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
        layout_window
            .layout_results
            .insert(DomId::ROOT_ID, layout_result(styled_dom));
        let renderer_resources = RendererResources::default();
        let previous_window_state: Option<FullWindowState> = None;
        let current_window_state = FullWindowState::default();
        let gl_context = OptionGlContextPtr::None;
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
            BTreeMap::new();
        let window_handle = RawWindowHandle::Unsupported;
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
        let ref_data = CallbackInfoRefData {
            layout_window: &layout_window,
            renderer_resources: &renderer_resources,
            previous_window_state: &previous_window_state,
            current_window_state: &current_window_state,
            gl_context: &gl_context,
            current_scroll_manager: &scroll_states,
            current_window_handle: &window_handle,
            system_callbacks: &system_callbacks,
            system_style: Arc::new(system::SystemStyle::default()),
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
            #[cfg(feature = "icu")]
            icu_localizer: IcuLocalizerHandle::default(),
            ctx: OptionRefAny::None,
        };
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
        let mut info = CallbackInfo::new(
            &ref_data,
            &changes,
            hit,
            OptionLogicalPosition::None,
            OptionLogicalPosition::None,
        );
        let r = f(&mut info);
        let pushed = info.take_changes();
        (r, pushed)
    }
    /// Shorthand for "deliver one event to `cb` with an otherwise-empty window".
    fn fire(cb: impl FnOnce(CallbackInfo) -> Update) -> Update {
        with_info(StyledDom::default(), hit_none(), |info| cb(*info)).0
    }
    // ------------------------------------------------------------------
    // User-callback recorder
    // ------------------------------------------------------------------
    /// Everything the widget's user-facing callbacks were handed, in call order.
    #[derive(Debug, Default)]
    struct Log {
        removed: Vec<u64>,
        added: Vec<(u64, u64, f32, f32)>,
        connected: Vec<(u64, usize, u64, usize)>,
        input_disconnected: Vec<(u64, usize)>,
        output_disconnected: Vec<(u64, usize)>,
        /// `(node_id, field_idx, node_type)` of every `on_node_field_edited` call.
        edited: Vec<(u64, usize, u64)>,
        text_values: Vec<String>,
        number_values: Vec<f32>,
        bool_values: Vec<bool>,
        color_values: Vec<(u8, u8, u8, u8)>,
        file_values: Vec<Option<String>>,
    }
    fn log_of(handle: &RefAny, f: impl FnOnce(&Log)) {
        let mut handle = handle.clone();
        let l = handle.downcast_ref::<Log>().expect("not a Log");
        f(&l);
    }
    extern "C" fn rec_removed(
        mut refany: RefAny,
        _info: CallbackInfo,
        node_id: NodeGraphNodeId,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.removed.push(node_id.inner);
        }
        Update::RefreshDom
    }
    extern "C" fn rec_added(
        mut refany: RefAny,
        _info: CallbackInfo,
        new_node_type: NodeTypeId,
        new_node_id: NodeGraphNodeId,
        new_node_position: NodeGraphNodePosition,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.added.push((
                new_node_type.inner,
                new_node_id.inner,
                new_node_position.x,
                new_node_position.y,
            ));
        }
        Update::RefreshDomAllWindows
    }
    extern "C" fn rec_connected(
        mut refany: RefAny,
        _info: CallbackInfo,
        input: NodeGraphNodeId,
        input_index: usize,
        output: NodeGraphNodeId,
        output_index: usize,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.connected
                .push((input.inner, input_index, output.inner, output_index));
        }
        Update::RefreshDom
    }
    extern "C" fn rec_input_disconnected(
        mut refany: RefAny,
        _info: CallbackInfo,
        input: NodeGraphNodeId,
        input_index: usize,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.input_disconnected.push((input.inner, input_index));
        }
        Update::RefreshDom
    }
    extern "C" fn rec_output_disconnected(
        mut refany: RefAny,
        _info: CallbackInfo,
        output: NodeGraphNodeId,
        output_index: usize,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.output_disconnected.push((output.inner, output_index));
        }
        Update::RefreshDomAllWindows
    }
    extern "C" fn rec_field_edited(
        mut refany: RefAny,
        _info: CallbackInfo,
        node_id: NodeGraphNodeId,
        field_id: usize,
        node_type: NodeTypeId,
        new_value: NodeTypeFieldValue,
    ) -> Update {
        if let Some(mut l) = refany.downcast_mut::<Log>() {
            l.edited.push((node_id.inner, field_id, node_type.inner));
            match new_value {
                NodeTypeFieldValue::TextInput(s) => l.text_values.push(s.as_str().to_string()),
                NodeTypeFieldValue::NumberInput(n) => l.number_values.push(n),
                NodeTypeFieldValue::CheckBox(b) => l.bool_values.push(b),
                NodeTypeFieldValue::ColorInput(c) => l.color_values.push((c.r, c.g, c.b, c.a)),
                NodeTypeFieldValue::FileInput(p) => l
                    .file_values
                    .push(p.as_ref().map(|s| s.as_str().to_string())),
            }
        }
        Update::RefreshDom
    }
    /// A graph whose callbacks all funnel into one freshly-created `Log`.
    fn graph_with_log() -> (NodeGraph, RefAny) {
        let log = RefAny::new(Log::default());
        let mut g = graph();
        g.callbacks = NodeGraphCallbacks {
            on_node_removed: OptionOnNodeRemoved::Some(OnNodeRemoved {
                refany: log.clone(),
                callback: OnNodeRemovedCallback {
                    cb: rec_removed,
                    ctx: OptionRefAny::None,
                },
            }),
            on_node_added: OptionOnNodeAdded::Some(OnNodeAdded {
                refany: log.clone(),
                callback: OnNodeAddedCallback {
                    cb: rec_added,
                    ctx: OptionRefAny::None,
                },
            }),
            on_node_connected: OptionOnNodeConnected::Some(OnNodeConnected {
                refany: log.clone(),
                callback: OnNodeConnectedCallback {
                    cb: rec_connected,
                    ctx: OptionRefAny::None,
                },
            }),
            on_node_input_disconnected: OptionOnNodeInputDisconnected::Some(
                OnNodeInputDisconnected {
                    refany: log.clone(),
                    callback: OnNodeInputDisconnectedCallback {
                        cb: rec_input_disconnected,
                        ctx: OptionRefAny::None,
                    },
                },
            ),
            on_node_output_disconnected: OptionOnNodeOutputDisconnected::Some(
                OnNodeOutputDisconnected {
                    refany: log.clone(),
                    callback: OnNodeOutputDisconnectedCallback {
                        cb: rec_output_disconnected,
                        ctx: OptionRefAny::None,
                    },
                },
            ),
            on_node_field_edited: OptionOnNodeFieldEdited::Some(OnNodeFieldEdited {
                refany: log.clone(),
                callback: OnNodeFieldEditedCallback {
                    cb: rec_field_edited,
                    ctx: OptionRefAny::None,
                },
            }),
            ..NodeGraphCallbacks::default()
        };
        (g, log)
    }
    // ==================================================================
    // 1. NodeGraph::generate_unique_node_id
    // ==================================================================
    #[test]
    fn generate_unique_node_id_on_an_empty_graph_is_one_not_zero() {
        // `0` is a perfectly valid node id, so the generator must not hand it out for
        // the first node either — `max().unwrap_or(0) + 1`.
        assert_eq!(NodeGraph::default().generate_unique_node_id().inner, 1);
    }
    #[test]
    fn generate_unique_node_id_returns_max_plus_one_and_ignores_gaps_and_order() {
        // Ids are deliberately unsorted and non-contiguous: the generator must take the
        // maximum, not the last element and not the length.
        let g = NodeGraph {
            nodes: vec![
                NodeIdNodeMap {
                    node_id: NodeGraphNodeId { inner: 7 },
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
                NodeIdNodeMap {
                    node_id: NodeGraphNodeId { inner: 0 },
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
                NodeIdNodeMap {
                    node_id: NodeGraphNodeId { inner: 3 },
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
            ]
            .into(),
            ..Default::default()
        };
        assert_eq!(g.generate_unique_node_id().inner, 8);
    }
    #[test]
    fn generate_unique_node_id_tolerates_duplicate_ids_in_the_graph() {
        let g = NodeGraph {
            nodes: vec![
                NodeIdNodeMap {
                    node_id: N2,
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
                NodeIdNodeMap {
                    node_id: N2,
                    node: mk_node(TYPE_A, 0.0, 0.0),
                },
            ]
            .into(),
            ..Default::default()
        };
        assert_eq!(g.generate_unique_node_id().inner, 3);
    }
    #[test]
    fn generate_unique_node_id_saturates_instead_of_overflowing_at_u64_max() {
        // `saturating_add(1)` means the id at the top of the range is NOT unique: it
        // collides with the existing node. That is a real (if unreachable in practice)
        // limitation — what matters here is that it saturates rather than wrapping to
        // 0 or panicking in a debug build.
        let mut g = NodeGraph {
            nodes: vec![NodeIdNodeMap {
                node_id: NodeGraphNodeId { inner: u64::MAX },
                node: mk_node(TYPE_A, 0.0, 0.0),
            }]
            .into(),
            ..Default::default()
        };
        let id = g.generate_unique_node_id();
        assert_eq!(id.inner, u64::MAX);
        assert!(
            g.nodes.iter().any(|n| n.node_id == id),
            "at u64::MAX the generated id collides — documented saturation, not wraparound",
        );
        // ...and one below the top still behaves normally.
        g.nodes = vec![NodeIdNodeMap {
            node_id: NodeGraphNodeId {
                inner: u64::MAX - 1,
            },
            node: mk_node(TYPE_A, 0.0, 0.0),
        }]
        .into();
        assert_eq!(g.generate_unique_node_id().inner, u64::MAX);
    }
    #[test]
    fn generate_unique_node_id_is_pure_and_repeats_until_the_node_is_inserted() {
        let g = graph();
        let first = g.generate_unique_node_id();
        assert_eq!(first, g.generate_unique_node_id());
        assert_eq!(first.inner, 5); // max(1,2,3,4) + 1
    }
    // ==================================================================
    // 2. NodeGraphError: Display / Debug
    // ==================================================================
    const ALL_ERRORS: [NodeGraphError; 4] = [
        NodeGraphError::NodeMimeTypeMismatch,
        NodeGraphError::NodeInvalidIndex,
        NodeGraphError::NodeInvalidNode,
        NodeGraphError::NoRootNode,
    ];
    #[test]
    fn node_graph_error_display_is_non_empty_ascii_and_single_line() {
        for e in ALL_ERRORS {
            let s = format!("{e}");
            assert!(!s.is_empty(), "{e:?} formatted to the empty string");
            assert!(!s.contains('\n'), "{e:?} formatted to a multi-line string");
            assert!(s.is_ascii(), "{e:?} formatted to non-ascii: {s}");
        }
    }
    #[test]
    fn node_graph_error_display_distinguishes_every_variant() {
        // A copy-pasted match arm that returns the same message for two variants would
        // make the error useless in a log; this is the assertion that catches it.
        let mut seen: Vec<String> = ALL_ERRORS.iter().map(|e| format!("{e}")).collect();
        seen.sort();
        seen.dedup();
        assert_eq!(seen.len(), ALL_ERRORS.len());
    }
    #[test]
    fn node_graph_error_display_survives_width_precision_and_fill_specifiers() {
        // `write!` inside a Display impl ignores the outer format spec, but the spec
        // must not make the impl panic or truncate to nothing.
        for e in ALL_ERRORS {
            assert!(!format!("{e:>80}").is_empty());
            assert!(!format!("{e:*^3}").is_empty());
            assert!(!format!("{e:.1}").is_empty());
            assert!(!format!("{e:?}").is_empty());
        }
    }
    #[test]
    fn node_graph_error_debug_and_display_are_both_usable_and_differ_in_style() {
        // Debug is the derived variant name; Display is prose. They should not be the
        // same string, otherwise one of the two impls is missing.
        for e in ALL_ERRORS {
            assert_ne!(format!("{e:?}"), format!("{e}"));
        }
    }
    // ==================================================================
    // 3. NodeGraph::swap_with_default
    // ==================================================================
    /// Everything about a `NodeGraph` that is cheaply comparable.
    fn summary(g: &NodeGraph) -> (usize, usize, usize, bool, f32, f32, f32, String) {
        (
            g.node_types.len(),
            g.input_output_types.len(),
            g.nodes.len(),
            g.allow_multiple_root_nodes,
            g.offset.x,
            g.offset.y,
            g.scale_factor,
            g.add_node_str.as_str().to_string(),
        )
    }
    fn distinctive() -> NodeGraph {
        NodeGraph {
            allow_multiple_root_nodes: true,
            offset: LogicalPosition { x: -3.5, y: 12.25 },
            scale_factor: 2.5,
            add_node_str: AzString::from_const_str("Ajouter un nœud"),
            ..graph()
        }
    }
    #[test]
    fn swap_with_default_hands_back_the_old_value_and_leaves_a_default_behind() {
        let mut g = distinctive();
        let expected = summary(&g);
        let taken = g.swap_with_default();
        assert_eq!(summary(&taken), expected);
        assert_eq!(summary(&g), summary(&NodeGraph::default()));
    }
    #[test]
    fn swap_with_default_round_trips_a_graph_through_two_owners() {
        // encode == decode: moving a graph out and back must not lose a single field.
        let mut a = distinctive();
        let expected = summary(&a);
        let mut b = a.swap_with_default(); // a := default, b := original
        let c = b.swap_with_default(); // b := default, c := original again
        assert_eq!(summary(&c), expected);
        assert_eq!(summary(&b), summary(&NodeGraph::default()));
        assert_eq!(summary(&a), summary(&NodeGraph::default()));
    }
    #[test]
    fn swap_with_default_on_an_already_default_graph_is_a_no_op() {
        let mut g = NodeGraph::default();
        let taken = g.swap_with_default();
        assert_eq!(summary(&taken), summary(&NodeGraph::default()));
        assert_eq!(summary(&g), summary(&NodeGraph::default()));
    }
    #[test]
    fn swap_with_default_preserves_non_finite_offsets_and_scale_verbatim() {
        // The swap is a `mem::swap`, so NaN/inf must survive bit-for-bit rather than
        // being normalised away.
        let mut g = NodeGraph {
            offset: LogicalPosition {
                x: f32::INFINITY,
                y: f32::NEG_INFINITY,
            },
            scale_factor: f32::NAN,
            ..NodeGraph::default()
        };
        let taken = g.swap_with_default();
        assert!(taken.offset.x.is_infinite() && taken.offset.x.is_sign_positive());
        assert!(taken.offset.y.is_infinite() && taken.offset.y.is_sign_negative());
        assert!(taken.scale_factor.is_nan());
        assert_eq!(g.scale_factor, 1.0);
    }
    #[test]
    fn swap_with_default_moves_the_connections_not_just_the_node_list() {
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("legal A->A wire");
        let before = wiring(&g);
        let taken = g.swap_with_default();
        assert_eq!(wiring(&taken), before);
        assert!(g.nodes.is_empty());
    }
    // ==================================================================
    // 4. NodeGraph::verify_nodetype_match
    // ==================================================================
    #[test]
    fn verify_nodetype_match_accepts_matching_types_at_index_zero() {
        let g = graph();
        assert_eq!(g.verify_nodetype_match(N1, 0, N3, 0), Ok(()));
    }
    #[test]
    fn verify_nodetype_match_rejects_a_type_mismatch() {
        let g = graph();
        // N1 emits `int`, N2 consumes `float`.
        assert_eq!(
            g.verify_nodetype_match(N1, 0, N2, 0),
            Err(NodeGraphError::NodeMimeTypeMismatch)
        );
    }
    #[test]
    fn verify_nodetype_match_reports_a_missing_node_on_either_side() {
        let g = graph();
        assert_eq!(
            g.verify_nodetype_match(MISSING, 0, N3, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
        assert_eq!(
            g.verify_nodetype_match(N1, 0, MISSING, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
    }
    #[test]
    fn verify_nodetype_match_reports_a_node_whose_type_is_not_registered() {
        let mut g = graph();
        g.nodes.push(NodeIdNodeMap {
            node_id: NodeGraphNodeId { inner: 50 },
            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
        });
        assert_eq!(
            g.verify_nodetype_match(NodeGraphNodeId { inner: 50 }, 0, N3, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
        assert_eq!(
            g.verify_nodetype_match(N1, 0, NodeGraphNodeId { inner: 50 }, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
    }
    #[test]
    fn verify_nodetype_match_rejects_out_of_range_port_indices() {
        let g = graph();
        // Both node types declare exactly one input and one output, so index 1 is the
        // first out-of-range index.
        assert_eq!(
            g.verify_nodetype_match(N1, 1, N3, 0),
            Err(NodeGraphError::NodeInvalidIndex)
        );
        assert_eq!(
            g.verify_nodetype_match(N1, 0, N3, 1),
            Err(NodeGraphError::NodeInvalidIndex)
        );
    }
    #[test]
    fn verify_nodetype_match_does_not_panic_at_usize_max_indices() {
        // `Vec::get(usize::MAX)` must be the thing that fails, not an unchecked index.
        let g = graph();
        assert_eq!(
            g.verify_nodetype_match(N1, usize::MAX, N3, 0),
            Err(NodeGraphError::NodeInvalidIndex)
        );
        assert_eq!(
            g.verify_nodetype_match(N1, 0, N3, usize::MAX),
            Err(NodeGraphError::NodeInvalidIndex)
        );
        assert_eq!(
            g.verify_nodetype_match(N1, usize::MAX, N3, usize::MAX),
            Err(NodeGraphError::NodeInvalidIndex)
        );
    }
    #[test]
    fn verify_nodetype_match_checks_nodes_before_indices() {
        // Ordering matters for the error a user sees: a missing node is reported even
        // when the index is also nonsense.
        let g = graph();
        assert_eq!(
            g.verify_nodetype_match(MISSING, usize::MAX, N3, usize::MAX),
            Err(NodeGraphError::NodeInvalidNode)
        );
    }
    #[test]
    fn verify_nodetype_match_allows_a_node_to_be_wired_to_itself() {
        // Documented behaviour: there is no self-loop / cycle check at this layer.
        let g = graph();
        assert_eq!(g.verify_nodetype_match(N1, 0, N1, 0), Ok(()));
    }
    #[test]
    fn verify_nodetype_match_does_not_mutate_the_graph() {
        let g = graph();
        let before = wiring(&g);
        let _ = g.verify_nodetype_match(N1, 0, N3, 0);
        let _ = g.verify_nodetype_match(MISSING, usize::MAX, N2, 9);
        assert_eq!(wiring(&g), before);
    }
    // ==================================================================
    // 5. NodeGraph::connect_input_output
    // ==================================================================
    #[test]
    fn connect_input_output_wires_both_directions_at_index_zero() {
        let mut g = graph();
        assert_eq!(g.connect_input_output(N3, 0, N1, 0), Ok(()));
        assert_eq!(inputs_of(&g, N3), vec![(0, vec![(N1.inner, 0)])]);
        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N3.inner, 0)])]);
        // ...and nothing else moved.
        assert!(outputs_of(&g, N3).is_empty());
        assert!(inputs_of(&g, N1).is_empty());
    }
    #[test]
    fn connect_input_output_rejects_a_mime_type_mismatch_without_mutating() {
        let mut g = graph();
        let before = wiring(&g);
        assert_eq!(
            g.connect_input_output(N2, 0, N1, 0),
            Err(NodeGraphError::NodeMimeTypeMismatch)
        );
        assert_eq!(wiring(&g), before, "a rejected connect must be atomic");
    }
    #[test]
    fn connect_input_output_rejects_missing_nodes_without_mutating() {
        for (input, output) in [(MISSING, N1), (N3, MISSING), (MISSING, MISSING)] {
            let mut g = graph();
            let before = wiring(&g);
            assert_eq!(
                g.connect_input_output(input, 0, output, 0),
                Err(NodeGraphError::NodeInvalidNode)
            );
            assert_eq!(wiring(&g), before);
        }
    }
    #[test]
    fn connect_input_output_rejects_out_of_range_and_usize_max_indices() {
        for (in_idx, out_idx) in [
            (1_usize, 0_usize),
            (0, 1),
            (usize::MAX, 0),
            (0, usize::MAX),
            (usize::MAX, usize::MAX),
        ] {
            let mut g = graph();
            let before = wiring(&g);
            assert_eq!(
                g.connect_input_output(N3, in_idx, N1, out_idx),
                Err(NodeGraphError::NodeInvalidIndex),
                "in={in_idx} out={out_idx}",
            );
            assert_eq!(wiring(&g), before);
        }
    }
    #[test]
    fn connect_input_output_appends_to_an_existing_port_rather_than_replacing_it() {
        // Two different sources feeding the same input port must both be recorded.
        let mut g = graph();
        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
        assert_eq!(
            inputs_of(&g, N4),
            vec![(0, vec![(N1.inner, 0), (N3.inner, 0)])],
            "the second wire must not overwrite the first",
        );
        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N4.inner, 0)])]);
        assert_eq!(outputs_of(&g, N3), vec![(0, vec![(N4.inner, 0)])]);
    }
    #[test]
    fn connect_input_output_records_a_duplicate_wire_twice() {
        // Documented behaviour: there is no de-duplication, so connecting the same two
        // ports twice yields two identical entries on both sides.
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("first");
        g.connect_input_output(N3, 0, N1, 0).expect("second");
        assert_eq!(inputs_of(&g, N3), vec![(0, vec![(N1.inner, 0), (N1.inner, 0)])]);
        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N3.inner, 0), (N3.inner, 0)])]);
    }
    #[test]
    fn connect_input_output_permits_a_self_loop() {
        // No cycle detection at this layer — the node ends up wired to itself.
        let mut g = graph();
        assert_eq!(g.connect_input_output(N1, 0, N1, 0), Ok(()));
        assert_eq!(inputs_of(&g, N1), vec![(0, vec![(N1.inner, 0)])]);
        assert_eq!(outputs_of(&g, N1), vec![(0, vec![(N1.inner, 0)])]);
    }
    // ==================================================================
    // 6. NodeGraph::disconnect_input
    // ==================================================================
    #[test]
    fn disconnect_input_round_trips_a_single_connection() {
        // encode == decode: connect then disconnect restores the exact wiring.
        let mut g = graph();
        let before = wiring(&g);
        g.connect_input_output(N3, 0, N1, 0).expect("connect");
        assert_ne!(wiring(&g), before);
        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_input_reports_a_missing_node() {
        let mut g = graph();
        assert_eq!(
            g.disconnect_input(MISSING, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
    }
    #[test]
    fn disconnect_input_on_an_unconnected_port_is_ok_and_changes_nothing() {
        let mut g = graph();
        let before = wiring(&g);
        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_input_at_usize_max_is_ok_rather_than_invalid_index() {
        // Documented behaviour: an index that is not present short-circuits to `Ok(())`
        // *before* any range validation, so even `usize::MAX` is accepted silently.
        let mut g = graph();
        let before = wiring(&g);
        assert_eq!(g.disconnect_input(N3, usize::MAX), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_input_clears_every_source_feeding_that_port() {
        let mut g = graph();
        let before = wiring(&g);
        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
        assert_eq!(g.disconnect_input(N4, 0), Ok(()));
        assert_eq!(
            wiring(&g),
            before,
            "both upstream ports must be released, not just the first",
        );
    }
    #[test]
    fn disconnect_input_orphans_a_sibling_sharing_the_same_output_port() {
        // BUG (characterised, not endorsed): `disconnect_input` removes the *whole*
        // `OutputConnection` entry of the upstream port instead of removing just the
        // one `InputNodeAndIndex` that pointed back. When two inputs are fed by the
        // same output, disconnecting one of them silently drops the other's
        // forward edge while leaving its backward edge in place — the two halves of
        // the graph disagree afterwards.
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
        assert_eq!(
            outputs_of(&g, N1),
            vec![(0, vec![(N3.inner, 0), (N4.inner, 0)])]
        );
        assert_eq!(g.disconnect_input(N3, 0), Ok(()));
        assert!(inputs_of(&g, N3).is_empty(), "the requested edge is gone");
        assert_eq!(
            inputs_of(&g, N4),
            vec![(0, vec![(N1.inner, 0)])],
            "N4 still believes it is connected to N1",
        );
        assert!(
            outputs_of(&g, N1).is_empty(),
            "...but N1 no longer lists N4 — the collateral damage this test pins down",
        );
    }
    // ==================================================================
    // 7. NodeGraph::disconnect_output
    // ==================================================================
    #[test]
    fn disconnect_output_round_trips_a_single_connection() {
        let mut g = graph();
        let before = wiring(&g);
        g.connect_input_output(N3, 0, N1, 0).expect("connect");
        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_output_reports_a_missing_node() {
        let mut g = graph();
        assert_eq!(
            g.disconnect_output(MISSING, 0),
            Err(NodeGraphError::NodeInvalidNode)
        );
    }
    #[test]
    fn disconnect_output_on_an_unconnected_port_is_ok_and_changes_nothing() {
        let mut g = graph();
        let before = wiring(&g);
        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_output_at_usize_max_is_ok_rather_than_invalid_index() {
        let mut g = graph();
        let before = wiring(&g);
        assert_eq!(g.disconnect_output(N1, usize::MAX), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    #[test]
    fn disconnect_output_releases_every_downstream_input_it_fed() {
        // The mirror image of `disconnect_input_orphans_a_sibling...`: here the fan-out
        // case *is* handled correctly, because the loop walks the cloned target list.
        let mut g = graph();
        let before = wiring(&g);
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
        assert_eq!(wiring(&g), before, "no dangling back-reference may survive");
    }
    #[test]
    fn disconnect_output_of_a_self_loop_leaves_no_residue() {
        let mut g = graph();
        let before = wiring(&g);
        g.connect_input_output(N1, 0, N1, 0).expect("self loop");
        assert_eq!(g.disconnect_output(N1, 0), Ok(()));
        assert_eq!(wiring(&g), before);
    }
    // ==================================================================
    // 8. get_rect
    // ==================================================================
    fn connection(out: NodeGraphNodeId, out_idx: usize, inn: NodeGraphNodeId, in_idx: usize)
        -> ConnectionLocalDataset {
        ConnectionLocalDataset {
            out_node_id: out,
            out_idx,
            in_node_id: inn,
            in_idx,
            // Deliberately wrong: `get_rect` must recompute both flags from geometry.
            swap_vert: true,
            swap_horz: true,
            color: ColorU {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            },
        }
    }
    #[test]
    fn get_rect_returns_none_for_a_dangling_endpoint() {
        let g = graph();
        assert!(get_rect(&g, connection(MISSING, 0, N3, 0)).is_none());
        assert!(get_rect(&g, connection(N1, 0, MISSING, 0)).is_none());
        assert!(get_rect(&g, connection(MISSING, 0, MISSING, 0)).is_none());
    }
    #[test]
    fn get_rect_computes_the_bounding_box_of_the_two_ports() {
        // N1 sits at (0, 0), N3 at (800, 50); both use port 0.
        let g = graph();
        let (rect, swap_vert, swap_horz) =
            get_rect(&g, connection(N1, 0, N3, 0)).expect("both nodes exist");
        let x_out = 0.0 + EXPECT_NODE_WIDTH;
        let y_out = 0.0 + EXPECT_V_OFFSET;
        let x_in = 800.0;
        let y_in = 50.0 + EXPECT_V_OFFSET;
        assert_eq!(rect.origin.x, x_out.min(x_in));
        assert_eq!(rect.origin.y, y_out.min(y_in));
        assert_eq!(rect.size.width, (x_in - x_out).abs());
        assert_eq!(rect.size.height, (y_in - y_out).abs() + EXPECT_DOT_HEIGHT);
        assert!(swap_vert, "the input port sits below the output port");
        assert!(!swap_horz, "the input node is to the right of the output");
    }
    #[test]
    fn get_rect_recomputes_the_swap_flags_and_ignores_the_ones_it_was_handed() {
        // The fixture passes `swap_vert: true, swap_horz: true` every time; here both
        // must come back `false`, proving the incoming values are not echoed.
        let g = graph();
        // N3 (800, 50) -> N1 (0, 0): input is left of, and above, the output.
        let (_, swap_vert, swap_horz) = get_rect(&g, connection(N3, 0, N1, 0)).expect("exists");
        assert!(!swap_vert);
        assert!(swap_horz);
    }
    #[test]
    fn get_rect_height_is_never_below_the_connection_dot() {
        // Two nodes at the same height give a zero-height span; the dot height is the
        // floor that keeps the rect drawable.
        let mut g = graph();
        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition { x: 800.0, y: 0.0 };
        let (rect, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
        assert_eq!(rect.size.height, EXPECT_DOT_HEIGHT);
    }
    #[test]
    fn get_rect_port_index_shifts_the_endpoint_by_a_fixed_pitch() {
        // N1's output port (y = 71) is the topmost point of the rect; moving N3's input
        // down by three port pitches must therefore grow the height by exactly three
        // pitches and leave the origin where it was.
        let g = graph();
        let (base, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
        let (shifted, _, _) = get_rect(&g, connection(N1, 0, N3, 3)).expect("exists");
        assert_eq!(shifted.origin.y, base.origin.y);
        assert_eq!(
            shifted.size.height - base.size.height,
            3.0 * EXPECT_PORT_PITCH,
        );
    }
    #[test]
    fn get_rect_stays_finite_at_usize_max_port_indices() {
        // `usize::MAX as f32` is ~1.8e19 — large, but multiplying by the 25px pitch
        // still lands well inside f32 range, so nothing may become inf or NaN.
        let g = graph();
        let (rect, _, _) = get_rect(&g, connection(N1, usize::MAX, N3, usize::MAX))
            .expect("both nodes exist");
        assert!(rect.origin.y.is_finite(), "y = {}", rect.origin.y);
        assert!(rect.size.height.is_finite(), "h = {}", rect.size.height);
        assert!(rect.size.width.is_finite());
        assert!(rect.size.height >= EXPECT_DOT_HEIGHT);
    }
    #[test]
    fn get_rect_with_a_nan_position_yields_nan_extent_but_a_finite_origin() {
        // `f32::min` returns the non-NaN operand, so the origin survives even though
        // the extent does not. Neither may panic.
        let mut g = graph();
        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition {
            x: f32::NAN,
            y: f32::NAN,
        };
        let (rect, swap_vert, swap_horz) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
        assert!(rect.size.width.is_nan());
        assert!(rect.size.height.is_nan());
        assert_eq!(rect.origin.x, EXPECT_NODE_WIDTH);
        assert_eq!(rect.origin.y, EXPECT_V_OFFSET);
        // NaN compares false against everything, so both flags fall to `false`.
        assert!(!swap_vert);
        assert!(!swap_horz);
    }
    #[test]
    fn get_rect_with_infinite_positions_does_not_panic() {
        let mut g = graph();
        g.nodes.as_mut()[2].node.position = NodeGraphNodePosition {
            x: f32::INFINITY,
            y: f32::NEG_INFINITY,
        };
        let (rect, swap_vert, swap_horz) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
        assert!(rect.size.width.is_infinite());
        assert!(rect.size.height.is_infinite());
        assert!(!swap_vert, "-inf is not above the output port");
        assert!(!swap_horz, "+inf is not left of the output port");
        // Both endpoints infinite in the same direction => inf - inf => NaN extent.
        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
            x: f32::INFINITY,
            y: f32::NEG_INFINITY,
        };
        let (rect, _, _) = get_rect(&g, connection(N1, 0, N3, 0)).expect("exists");
        assert!(rect.size.width.is_nan());
    }
    #[test]
    fn get_rect_is_a_pure_query() {
        let g = graph();
        let before = wiring(&g);
        let _ = get_rect(&g, connection(N1, usize::MAX, N3, 0));
        assert_eq!(wiring(&g), before);
    }
    // ==================================================================
    // 9. render_node
    // ==================================================================
    fn node_dataset(g: &NodeGraph, id: NodeGraphNodeId) -> NodeLocalDataset {
        NodeLocalDataset {
            node_id: id,
            backref: graph_dataset(g),
        }
    }
    fn render_one(g: &NodeGraph, id: NodeGraphNodeId, offset: (f32, f32), scale: f32) -> Dom {
        let n = g
            .nodes
            .iter()
            .find(|n| n.node_id == id)
            .expect("node in fixture");
        let ty = g
            .node_types
            .iter()
            .find(|t| t.node_type_id == n.node.node_type)
            .expect("type in fixture");
        render_node(
            &n.node,
            offset,
            &ty.node_type_info,
            node_dataset(g, id),
            scale,
        )
    }
    #[test]
    fn render_node_produces_a_single_wrapper_child_carrying_the_dataset() {
        let g = graph();
        let dom = render_one(&g, N1, (0.0, 0.0), 1.0);
        assert_eq!(dom.children.len(), 1);
        let inner = &dom.children.as_slice()[0];
        let mut ds = inner
            .root
            .get_dataset()
            .cloned()
            .expect("the node body must carry its NodeLocalDataset");
        assert!(ds.downcast_ref::<NodeLocalDataset>().is_some());
    }
    #[test]
    fn render_node_survives_every_pathological_scale_factor() {
        // `scale_factor == 1.0` picks a shorter transform list; every other value takes
        // the scale branch, where the f32 is pushed through `PercentageValue::new`.
        let g = graph();
        let baseline = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
        for scale in [
            0.0,
            -0.0,
            -1.0,
            1e-30,
            f32::MAX,
            f32::MIN,
            f32::EPSILON,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::NAN,
        ] {
            let dom = render_one(&g, N1, (0.0, 0.0), scale);
            assert_eq!(
                count_nodes(&dom),
                baseline,
                "scale {scale} changed the node structure",
            );
        }
    }
    #[test]
    fn render_node_survives_every_pathological_graph_offset() {
        let g = graph();
        let baseline = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
        for offset in [
            (f32::NAN, f32::NAN),
            (f32::INFINITY, f32::NEG_INFINITY),
            (f32::MAX, f32::MIN),
            (-1e30, 1e30),
        ] {
            assert_eq!(count_nodes(&render_one(&g, N1, offset, 1.0)), baseline);
        }
    }
    #[test]
    fn render_node_survives_a_node_positioned_at_nan_and_infinity() {
        let mut g = graph();
        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
            x: f32::NAN,
            y: f32::INFINITY,
        };
        let dom = render_one(&g, N1, (0.0, 0.0), 1.0);
        assert!(count_nodes(&dom) > 1);
    }
    #[test]
    fn render_node_drops_all_ports_when_the_backref_is_not_a_node_graph_dataset() {
        // Both port lists are built by downcasting through `backref`; a foreign payload
        // must degrade to "no ports" rather than panicking.
        let g = graph();
        let n = g.nodes.iter().find(|n| n.node_id == N1).expect("fixture");
        let ty = g
            .node_types
            .iter()
            .find(|t| t.node_type_id == TYPE_A)
            .expect("fixture");
        let broken = render_node(
            &n.node,
            (0.0, 0.0),
            &ty.node_type_info,
            NodeLocalDataset {
                node_id: N1,
                backref: RefAny::new(0xDEAD_BEEF_u32),
            },
            1.0,
        );
        let intact = render_one(&g, N1, (0.0, 0.0), 1.0);
        assert!(count_nodes(&broken) > 1, "the node body still renders");
        assert!(
            count_nodes(&broken) < count_nodes(&intact),
            "a broken backref must cost the ports: {} vs {}",
            count_nodes(&broken),
            count_nodes(&intact),
        );
    }
    #[test]
    fn render_node_drops_ports_whose_io_type_has_no_registered_info() {
        let mut g = graph();
        // Point TYPE_A's single input at an I/O id that has no `InputOutputInfo`.
        g.node_types.as_mut()[0].node_type_info.inputs = vec![IO_COLORLESS].into();
        let stripped = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
        let intact = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
        assert!(stripped < intact, "{stripped} vs {intact}");
    }
    #[test]
    fn render_node_renders_all_five_field_widget_kinds() {
        let mut g = graph();
        g.nodes.as_mut()[0].node.fields = vec![
            NodeTypeField {
                key: AzString::from_const_str("text"),
                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("hello")),
            },
            NodeTypeField {
                key: AzString::from_const_str("number"),
                value: NodeTypeFieldValue::NumberInput(1.5),
            },
            NodeTypeField {
                key: AzString::from_const_str("check"),
                value: NodeTypeFieldValue::CheckBox(true),
            },
            NodeTypeField {
                key: AzString::from_const_str("color"),
                value: NodeTypeFieldValue::ColorInput(ColorU {
                    r: 9,
                    g: 8,
                    b: 7,
                    a: 6,
                }),
            },
            NodeTypeField {
                key: AzString::from_const_str("file"),
                value: NodeTypeFieldValue::FileInput(OptionString::None),
            },
        ]
        .into();
        let with_fields = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
        let without = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
        assert!(with_fields > without, "{with_fields} vs {without}");
    }
    #[test]
    fn render_node_field_count_is_monotonic() {
        let base = count_nodes(&render_one(&graph(), N1, (0.0, 0.0), 1.0));
        let mut previous = base;
        for count in 1..=4_usize {
            let mut g = graph();
            g.nodes.as_mut()[0].node.fields = (0..count)
                .map(|_| NodeTypeField {
                    key: AzString::from_const_str("f"),
                    value: NodeTypeFieldValue::CheckBox(false),
                })
                .collect::<Vec<_>>()
                .into();
            let now = count_nodes(&render_one(&g, N1, (0.0, 0.0), 1.0));
            assert!(now > previous, "{count} fields: {now} !> {previous}");
            previous = now;
        }
    }
    #[test]
    fn render_node_accepts_pathological_field_values() {
        // Empty / emoji / RTL / zero-width labels, NaN and infinite numbers, a fully
        // transparent color and a unicode file path.
        let mut g = graph();
        g.nodes.as_mut()[0].node.fields = vec![
            NodeTypeField {
                key: AzString::from_const_str(""),
                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("")),
            },
            NodeTypeField {
                key: AzString::from_const_str("🎉\u{200b}اختبار"),
                value: NodeTypeFieldValue::TextInput(AzString::from_const_str("𝕬\u{0301}\u{feff}")),
            },
            NodeTypeField {
                key: AzString::from_const_str("nan"),
                value: NodeTypeFieldValue::NumberInput(f32::NAN),
            },
            NodeTypeField {
                key: AzString::from_const_str("inf"),
                value: NodeTypeFieldValue::NumberInput(f32::NEG_INFINITY),
            },
            NodeTypeField {
                key: AzString::from_const_str("max"),
                value: NodeTypeFieldValue::NumberInput(f32::MAX),
            },
            NodeTypeField {
                key: AzString::from_const_str("clear"),
                value: NodeTypeFieldValue::ColorInput(ColorU {
                    r: 0,
                    g: 0,
                    b: 0,
                    a: 0,
                }),
            },
            NodeTypeField {
                key: AzString::from_const_str("path"),
                value: NodeTypeFieldValue::FileInput(OptionString::Some(
                    AzString::from_const_str("/tmp/日本語/🎉.txt"),
                )),
            },
        ]
        .into();
        assert!(count_nodes(&render_one(&g, N1, (f32::NAN, f32::NAN), f32::NAN)) > 1);
    }
    // ==================================================================
    // 10. render_connections
    // ==================================================================
    fn marker() -> RefAny {
        RefAny::new(NodeConnectionMarkerDataset {})
    }
    #[test]
    fn render_connections_of_an_unwired_graph_has_no_children() {
        let dom = render_connections(&graph(), marker());
        assert_eq!(dom.children.len(), 0);
        assert!(
            dom.root.get_dataset().is_some(),
            "the container must keep the marker dataset that drag-handling looks up",
        );
    }
    #[test]
    fn render_connections_emits_exactly_one_child_per_wire() {
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        assert_eq!(render_connections(&g, marker()).children.len(), 1);
        g.connect_input_output(N4, 0, N1, 0).expect("N1 -> N4");
        assert_eq!(render_connections(&g, marker()).children.len(), 2);
        g.connect_input_output(N4, 0, N3, 0).expect("N3 -> N4");
        assert_eq!(render_connections(&g, marker()).children.len(), 3);
    }
    #[test]
    fn render_connections_skips_a_wire_to_a_node_that_no_longer_exists() {
        // `get_rect` returns `None`; the renderer must drop the wire, not unwrap it.
        let g = force_out_connection(graph(), N1, 0, MISSING, 0);
        assert_eq!(render_connections(&g, marker()).children.len(), 0);
    }
    #[test]
    fn render_connections_skips_an_out_of_range_output_port() {
        for out_idx in [1_usize, 99, usize::MAX] {
            let g = force_out_connection(graph(), N1, out_idx, N3, 0);
            assert_eq!(
                render_connections(&g, marker()).children.len(),
                0,
                "output index {out_idx} must be skipped",
            );
        }
    }
    #[test]
    fn render_connections_skips_a_node_whose_type_is_not_registered() {
        let mut g = graph();
        g.nodes.push(NodeIdNodeMap {
            node_id: NodeGraphNodeId { inner: 50 },
            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
        });
        let g = force_out_connection(g, NodeGraphNodeId { inner: 50 }, 0, N3, 0);
        assert_eq!(render_connections(&g, marker()).children.len(), 0);
    }
    #[test]
    fn render_connections_skips_a_port_whose_io_type_has_no_color() {
        let mut g = graph();
        g.node_types.as_mut()[0].node_type_info.outputs = vec![IO_COLORLESS].into();
        let g = force_out_connection(g, N1, 0, N3, 0);
        assert_eq!(render_connections(&g, marker()).children.len(), 0);
    }
    #[test]
    fn render_connections_survives_nan_positions_and_scale() {
        let mut g = graph();
        g.scale_factor = f32::NAN;
        g.offset = LogicalPosition {
            x: f32::INFINITY,
            y: f32::NAN,
        };
        g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
            x: f32::NAN,
            y: f32::NAN,
        };
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        assert_eq!(render_connections(&g, marker()).children.len(), 1);
    }
    #[test]
    fn render_connections_renders_a_self_loop() {
        let mut g = graph();
        g.connect_input_output(N1, 0, N1, 0).expect("self loop");
        assert_eq!(render_connections(&g, marker()).children.len(), 1);
    }
    // ==================================================================
    // 11. draw_connection
    // ==================================================================
    #[test]
    fn draw_connection_returns_a_fixed_100x100_null_image() {
        // The real curve rendering is stubbed out pending `RenderImageCallbackInfo`;
        // until then the size is a constant and must not depend on the payload.
        let img = draw_connection(RefAny::new(connection(N1, 0, N3, 0)), ());
        assert_eq!(img.get_size().width, 100.0);
        assert_eq!(img.get_size().height, 100.0);
    }
    #[test]
    fn draw_connection_ignores_a_payload_of_the_wrong_type() {
        for payload in [
            RefAny::new(NodeConnectionMarkerDataset {}),
            RefAny::new(0_u8),
            RefAny::new(String::new()),
        ] {
            let img = draw_connection(payload, ());
            assert_eq!(img.get_size().width, 100.0);
        }
    }
    #[test]
    fn draw_connection_does_not_consume_or_corrupt_its_payload() {
        let cld = RefAny::new(connection(N1, 2, N3, 5));
        let _ = draw_connection(cld.clone(), ());
        let _ = draw_connection(cld.clone(), ());
        let mut probe = cld.clone();
        let read_back = probe
            .downcast_ref::<ConnectionLocalDataset>()
            .expect("payload must still be downcastable after the callback ran");
        assert_eq!(read_back.out_idx, 2);
        assert_eq!(read_back.in_idx, 5);
    }
    #[test]
    fn draw_connection_returns_distinct_image_handles_per_call() {
        let cld = RefAny::new(connection(N1, 0, N3, 0));
        let a = draw_connection(cld.clone(), ());
        let b = draw_connection(cld.clone(), ());
        assert_ne!(a, b, "each call must mint a fresh ImageRef id");
    }
    // ==================================================================
    // 12. NodeGraph::dom
    // ==================================================================
    #[test]
    fn dom_of_a_default_graph_has_the_expected_skeleton() {
        let dom = NodeGraph::default().dom();
        assert!(
            dom.root.get_context_menu().is_some(),
            "the 'add node' context menu is the only way to create nodes",
        );
        assert!(dom.root.get_dataset().is_some());
        assert_eq!(dom.children.len(), 1, "wrapper holds exactly the .nodegraph");
        let nodegraph = &dom.children.as_slice()[0];
        assert_eq!(
            nodegraph.children.len(),
            2,
            "connections container + nodes container",
        );
    }
    #[test]
    fn dom_root_dataset_round_trips_back_to_a_node_graph_local_dataset() {
        let dom = graph().dom();
        let mut ds = dom.root.get_dataset().cloned().expect("root dataset");
        let inner = ds
            .downcast_ref::<NodeGraphLocalDataset>()
            .expect("root dataset must be the NodeGraphLocalDataset");
        assert_eq!(inner.node_graph.nodes.len(), 4);
        assert!(inner.last_input_or_output_clicked.is_none());
        assert!(inner.active_node_being_dragged.is_none());
    }
    #[test]
    fn dom_renders_one_child_per_node_and_silently_drops_unregistered_types() {
        let mut g = graph();
        g.nodes.push(NodeIdNodeMap {
            node_id: NodeGraphNodeId { inner: 50 },
            node: mk_node(TYPE_UNREGISTERED, 0.0, 0.0),
        });
        let dom = g.dom();
        let nodes_container = &dom.children.as_slice()[0].children.as_slice()[1];
        assert_eq!(
            nodes_container.children.len(),
            4,
            "the 5th node has no registered type and must be filtered out",
        );
    }
    #[test]
    fn dom_context_menu_lists_one_submenu_entry_per_node_type() {
        let g = graph();
        let dom = g.dom();
        let menu = dom.root.get_context_menu().expect("context menu").clone();
        assert_eq!(menu.items.len(), 1, "one top-level 'add node' entry");
        match &menu.items.as_slice()[0] {
            MenuItem::String(s) => assert_eq!(s.children.len(), 2, "TYPE_A and TYPE_B"),
            other => panic!("expected a string menu item, got {other:?}"),
        }
    }
    #[test]
    fn dom_context_menu_is_present_even_with_no_node_types_at_all() {
        let mut g = graph();
        g.node_types = NodeTypeIdInfoMapVec::new();
        let dom = g.dom();
        let menu = dom.root.get_context_menu().expect("context menu").clone();
        assert_eq!(menu.items.len(), 1);
        match &menu.items.as_slice()[0] {
            MenuItem::String(s) => assert_eq!(s.children.len(), 0),
            other => panic!("expected a string menu item, got {other:?}"),
        }
    }
    #[test]
    fn dom_survives_pathological_scale_offset_and_positions() {
        for (scale, ox, oy) in [
            (f32::NAN, f32::NAN, f32::NAN),
            (0.0, 0.0, 0.0),
            (-1.0, -1e30, 1e30),
            (f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY),
            (f32::MAX, f32::MAX, f32::MIN),
        ] {
            let mut g = graph();
            g.scale_factor = scale;
            g.offset = LogicalPosition { x: ox, y: oy };
            g.nodes.as_mut()[0].node.position = NodeGraphNodePosition {
                x: f32::NAN,
                y: f32::INFINITY,
            };
            g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
            let dom = g.dom();
            assert_eq!(dom.children.len(), 1, "scale {scale}");
        }
    }
    #[test]
    fn dom_of_a_wired_graph_renders_the_connection_container_children() {
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        let dom = g.dom();
        let connections = &dom.children.as_slice()[0].children.as_slice()[0];
        assert_eq!(connections.children.len(), 1);
    }
    #[test]
    fn dom_can_be_converted_into_a_styled_dom() {
        // `StyledDom::create_from_dom` re-derives the child counters; a mismatch there
        // would panic while building the compact arena.
        let mut g = graph();
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        g.nodes.as_mut()[0].node.fields = vec![NodeTypeField {
            key: AzString::from_const_str("k"),
            value: NodeTypeFieldValue::NumberInput(f32::NAN),
        }]
        .into();
        let styled = StyledDom::create_from_dom(g.dom());
        assert!(styled.node_data.len() > 1);
    }
    // ==================================================================
    // 13. nodegraph_set_active_node / nodegraph_unset_active_node
    // ==================================================================
    #[test]
    fn set_active_node_records_the_node_and_unset_clears_it() {
        let g = graph();
        let gd = graph_dataset(&g);
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N2,
            backref: gd.clone(),
        });
        assert_eq!(
            fire(|info| nodegraph_set_active_node(nd.clone(), info)),
            Update::DoNothing,
        );
        {
            let mut probe = gd.clone();
            let d = probe.downcast_ref::<NodeGraphLocalDataset>().expect("gd");
            assert_eq!(
                d.active_node_being_dragged.as_ref().map(|(id, _)| *id),
                Some(N2),
            );
        }
        assert_eq!(
            fire(|info| nodegraph_unset_active_node(gd.clone(), info)),
            Update::DoNothing,
        );
        {
            let mut probe = gd.clone();
            let d = probe.downcast_ref::<NodeGraphLocalDataset>().expect("gd");
            assert!(d.active_node_being_dragged.is_none());
        }
    }
    #[test]
    fn set_active_node_ignores_a_payload_of_the_wrong_type() {
        assert_eq!(
            fire(|info| nodegraph_set_active_node(RefAny::new(1_u64), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn set_active_node_ignores_a_node_dataset_with_a_broken_backref() {
        // The outer downcast succeeds, the inner one does not — no state may change and
        // nothing may panic.
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N2,
            backref: RefAny::new(0_u8),
        });
        assert_eq!(
            fire(|info| nodegraph_set_active_node(nd.clone(), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn unset_active_node_is_idempotent_and_ignores_wrong_payloads() {
        let g = graph();
        let gd = graph_dataset(&g);
        for _ in 0..3 {
            assert_eq!(
                fire(|info| nodegraph_unset_active_node(gd.clone(), info)),
                Update::DoNothing,
            );
        }
        assert_eq!(
            fire(|info| nodegraph_unset_active_node(RefAny::new(0_i8), info)),
            Update::DoNothing,
        );
    }
    // ==================================================================
    // 14. nodegraph_duplicate_node / nodegraph_delete_node
    // ==================================================================
    #[test]
    fn duplicate_node_is_a_documented_no_op_for_valid_and_invalid_payloads() {
        let g = graph();
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N1,
            backref: graph_dataset(&g),
        });
        assert_eq!(
            fire(|info| nodegraph_duplicate_node(nd.clone(), info)),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_duplicate_node(RefAny::new(0_u16), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn delete_node_forwards_the_node_id_to_on_node_removed() {
        let (g, log) = graph_with_log();
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N3,
            backref: graph_dataset(&g),
        });
        assert_eq!(
            fire(|info| nodegraph_delete_node(nd.clone(), info)),
            Update::RefreshDom,
            "the user callback's Update must be propagated verbatim",
        );
        log_of(&log, |l| assert_eq!(l.removed, vec![N3.inner]));
    }
    #[test]
    fn delete_node_without_a_user_callback_does_nothing() {
        let g = graph();
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N3,
            backref: graph_dataset(&g),
        });
        assert_eq!(
            fire(|info| nodegraph_delete_node(nd.clone(), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn delete_node_reports_a_node_id_that_is_not_even_in_the_graph() {
        // Documented behaviour: the handler does not validate the id, it just forwards
        // it — removal is entirely the user callback's job.
        let (g, log) = graph_with_log();
        let nd = RefAny::new(NodeLocalDataset {
            node_id: MISSING,
            backref: graph_dataset(&g),
        });
        let _ = fire(|info| nodegraph_delete_node(nd.clone(), info));
        log_of(&log, |l| assert_eq!(l.removed, vec![MISSING.inner]));
    }
    #[test]
    fn delete_node_ignores_broken_payloads() {
        assert_eq!(
            fire(|info| nodegraph_delete_node(RefAny::new(0_u32), info)),
            Update::DoNothing,
        );
        let nd = RefAny::new(NodeLocalDataset {
            node_id: N1,
            backref: RefAny::new(0_u8),
        });
        assert_eq!(
            fire(|info| nodegraph_delete_node(nd.clone(), info)),
            Update::DoNothing,
        );
    }
    // ==================================================================
    // 15. nodegraph_drag_graph_or_nodes
    // ==================================================================
    #[test]
    fn drag_without_a_previous_window_state_does_nothing() {
        // The harness leaves `previous_window_state` as `None`, which is exactly the
        // very-first-event case: no delta can be computed, so nothing may move.
        let (g, _log) = graph_with_log();
        let gd = graph_dataset(&g);
        let before = wiring(&dataset_graph(&gd));
        assert_eq!(
            fire(|info| nodegraph_drag_graph_or_nodes(gd.clone(), info)),
            Update::DoNothing,
        );
        assert_eq!(wiring(&dataset_graph(&gd)), before);
        let after = dataset_graph(&gd);
        assert_eq!(after.offset.x, 0.0);
        assert_eq!(after.offset.y, 0.0);
    }
    #[test]
    fn drag_ignores_a_payload_of_the_wrong_type() {
        assert_eq!(
            fire(|info| nodegraph_drag_graph_or_nodes(RefAny::new(0_u64), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn drag_does_not_dereference_the_active_node_before_checking_the_mouse() {
        // An "active node" that is not in the graph would be an unwrap hazard if the
        // mouse-state guard were ever reordered after the lookup.
        let g = graph();
        let gd = graph_dataset(&g);
        {
            let mut probe = gd.clone();
            let mut d = probe.downcast_mut::<NodeGraphLocalDataset>().expect("gd");
            d.active_node_being_dragged = Some((MISSING, RefAny::new(0_u8)));
        }
        assert_eq!(
            fire(|info| nodegraph_drag_graph_or_nodes(gd.clone(), info)),
            Update::DoNothing,
        );
    }
    // ==================================================================
    // 16. nodegraph_input_output_connect / _disconnect
    // ==================================================================
    fn io_dataset(gd: &RefAny, node_id: NodeGraphNodeId, io: InputOrOutput) -> RefAny {
        RefAny::new(NodeInputOutputLocalDataset {
            io_id: io,
            backref: RefAny::new(NodeLocalDataset {
                node_id,
                backref: gd.clone(),
            }),
        })
    }
    #[test]
    fn connect_click_one_only_arms_the_pending_port() {
        let g = graph();
        let gd = graph_dataset(&g);
        let first = io_dataset(&gd, N1, InputOrOutput::Output(0));
        assert_eq!(
            fire(|info| nodegraph_input_output_connect(first.clone(), info)),
            Update::DoNothing,
        );
        assert_eq!(pending_click(&gd), Some((N1.inner, (false, 0))));
        assert_eq!(
            wiring(&dataset_graph(&gd)),
            wiring(&graph()),
            "arming a port must not wire anything yet",
        );
    }
    #[test]
    fn connect_output_then_input_wires_the_graph_inside_the_dataset() {
        let (g, log) = graph_with_log();
        let gd = graph_dataset(&g);
        let out = io_dataset(&gd, N1, InputOrOutput::Output(0));
        let inn = io_dataset(&gd, N3, InputOrOutput::Input(0));
        let _ = fire(|info| nodegraph_input_output_connect(out.clone(), info));
        assert_eq!(
            fire(|info| nodegraph_input_output_connect(inn.clone(), info)),
            Update::RefreshDom,
        );
        let wired = dataset_graph(&gd);
        assert_eq!(inputs_of(&wired, N3), vec![(0, vec![(N1.inner, 0)])]);
        assert_eq!(outputs_of(&wired, N1), vec![(0, vec![(N3.inner, 0)])]);
        log_of(&log, |l| {
            assert_eq!(l.connected, vec![(N3.inner, 0, N1.inner, 0)]);
        });
        assert_eq!(
            pending_click(&gd),
            None,
            "a completed connection must disarm the pending port",
        );
    }
    #[test]
    fn connect_input_then_output_wires_the_same_edge_in_the_same_direction() {
        // Clicking input-first and output-first must produce identical graphs — the
        // handler swaps the roles itself.
        let (g, _log) = graph_with_log();
        let gd_a = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd_a, N1, InputOrOutput::Output(0)), info)
        });
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd_a, N3, InputOrOutput::Input(0)), info)
        });
        let gd_b = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd_b, N3, InputOrOutput::Input(0)), info)
        });
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd_b, N1, InputOrOutput::Output(0)), info)
        });
        assert_eq!(wiring(&dataset_graph(&gd_a)), wiring(&dataset_graph(&gd_b)));
    }
    #[test]
    fn connect_output_to_output_disarms_instead_of_wiring() {
        let (g, log) = graph_with_log();
        let gd = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
        });
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Output(0)), info)
            }),
            Update::DoNothing,
        );
        assert_eq!(pending_click(&gd), None);
        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
        log_of(&log, |l| assert!(l.connected.is_empty()));
    }
    #[test]
    fn connect_input_to_input_disarms_instead_of_wiring() {
        let (g, _log) = graph_with_log();
        let gd = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Input(0)), info)
        });
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
            }),
            Update::DoNothing,
        );
        assert_eq!(pending_click(&gd), None);
        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
    }
    #[test]
    fn connect_across_incompatible_types_disarms_and_leaves_the_graph_alone() {
        let (g, log) = graph_with_log();
        let gd = graph_dataset(&g);
        // N1 emits `int`, N2 consumes `float`.
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
        });
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_connect(io_dataset(&gd, N2, InputOrOutput::Input(0)), info)
            }),
            Update::DoNothing,
        );
        assert_eq!(pending_click(&gd), None);
        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
        log_of(&log, |l| assert!(l.connected.is_empty()));
    }
    #[test]
    fn connect_with_an_out_of_range_port_index_is_rejected() {
        let (g, _log) = graph_with_log();
        let gd = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(
                io_dataset(&gd, N1, InputOrOutput::Output(usize::MAX)),
                info,
            )
        });
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
            }),
            Update::DoNothing,
        );
        assert_eq!(wiring(&dataset_graph(&gd)), wiring(&graph()));
    }
    #[test]
    fn connect_leaves_the_pending_port_armed_when_no_user_callback_is_installed() {
        // BUG (characterised): `last_input_or_output_clicked` is only cleared inside the
        // `Some(OnNodeConnected)` arm. A graph with no `on_node_connected` callback
        // therefore keeps the *first* click armed after a successful wire, so the next
        // port click re-uses the stale port and wires the wrong edge.
        let g = graph(); // no callbacks
        let gd = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N1, InputOrOutput::Output(0)), info)
        });
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
        });
        let wired = dataset_graph(&gd);
        assert_eq!(inputs_of(&wired, N3), vec![(0, vec![(N1.inner, 0)])]);
        assert_eq!(
            pending_click(&gd),
            Some((N1.inner, (false, 0))),
            "N1's output stays armed after the wire was already made",
        );
        // ...and the very next input click silently wires a second edge from it.
        let _ = fire(|info| {
            nodegraph_input_output_connect(io_dataset(&gd, N4, InputOrOutput::Input(0)), info)
        });
        assert_eq!(
            inputs_of(&dataset_graph(&gd), N4),
            vec![(0, vec![(N1.inner, 0)])],
            "the stale port produced an edge the user never armed",
        );
    }
    #[test]
    fn connect_ignores_broken_payloads_at_every_level_of_the_backref_chain() {
        assert_eq!(
            fire(|info| nodegraph_input_output_connect(RefAny::new(0_u32), info)),
            Update::DoNothing,
        );
        let bad_mid = RefAny::new(NodeInputOutputLocalDataset {
            io_id: InputOrOutput::Input(0),
            backref: RefAny::new(0_u8),
        });
        assert_eq!(
            fire(|info| nodegraph_input_output_connect(bad_mid.clone(), info)),
            Update::DoNothing,
        );
        let bad_tail = RefAny::new(NodeInputOutputLocalDataset {
            io_id: InputOrOutput::Input(0),
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: RefAny::new(0_u16),
            }),
        });
        assert_eq!(
            fire(|info| nodegraph_input_output_connect(bad_tail.clone(), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn disconnect_notifies_the_input_or_the_output_callback_but_never_both() {
        let (g, log) = graph_with_log();
        let gd = graph_dataset(&g);
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_disconnect(io_dataset(&gd, N3, InputOrOutput::Input(4)), info)
            }),
            Update::RefreshDom,
        );
        log_of(&log, |l| {
            assert_eq!(l.input_disconnected, vec![(N3.inner, 4)]);
            assert!(l.output_disconnected.is_empty());
        });
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_disconnect(
                    io_dataset(&gd, N1, InputOrOutput::Output(7)),
                    info,
                )
            }),
            Update::RefreshDomAllWindows,
        );
        log_of(&log, |l| {
            assert_eq!(l.input_disconnected, vec![(N3.inner, 4)]);
            assert_eq!(l.output_disconnected, vec![(N1.inner, 7)]);
        });
    }
    #[test]
    fn disconnect_notifies_but_does_not_actually_unwire_the_graph() {
        // BUG (characterised): the handler calls neither `disconnect_input` nor
        // `disconnect_output`, so the middle-click gesture fires the user callback while
        // the widget's own copy of the graph keeps the edge. Unless the user callback
        // rebuilds the graph, the connection stays on screen.
        let (mut g, _log) = graph_with_log();
        g.connect_input_output(N3, 0, N1, 0).expect("N1 -> N3");
        let gd = graph_dataset(&g);
        let before = wiring(&dataset_graph(&gd));
        let _ = fire(|info| {
            nodegraph_input_output_disconnect(io_dataset(&gd, N3, InputOrOutput::Input(0)), info)
        });
        assert_eq!(
            wiring(&dataset_graph(&gd)),
            before,
            "the model is untouched by the disconnect gesture",
        );
    }
    #[test]
    fn disconnect_without_user_callbacks_does_nothing() {
        let g = graph();
        let gd = graph_dataset(&g);
        assert_eq!(
            fire(|info| {
                nodegraph_input_output_disconnect(io_dataset(&gd, N1, InputOrOutput::Input(0)), info)
            }),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_input_output_disconnect(RefAny::new(0_u32), info)),
            Update::DoNothing,
        );
    }
    #[test]
    fn disconnect_forwards_usize_max_port_indices_unclamped() {
        let (g, log) = graph_with_log();
        let gd = graph_dataset(&g);
        let _ = fire(|info| {
            nodegraph_input_output_disconnect(
                io_dataset(&gd, N1, InputOrOutput::Output(usize::MAX)),
                info,
            )
        });
        log_of(&log, |l| {
            assert_eq!(l.output_disconnected, vec![(N1.inner, usize::MAX)]);
        });
    }
    // ==================================================================
    // 17. nodegraph_context_menu_click
    // ==================================================================
    #[test]
    fn context_menu_click_does_nothing_when_the_graph_is_not_in_the_dom() {
        // `get_node_id_of_root_dataset` finds nothing in an empty window, so the handler
        // must bail out before touching the (still valid) backref.
        let (g, log) = graph_with_log();
        let cm = RefAny::new(ContextMenuEntryLocalDataset {
            node_type: TYPE_A,
            backref: graph_dataset(&g),
        });
        assert_eq!(
            fire(|info| nodegraph_context_menu_click(cm.clone(), info)),
            Update::DoNothing,
        );
        log_of(&log, |l| assert!(l.added.is_empty()));
    }
    #[test]
    fn context_menu_click_reports_a_fresh_node_id_at_the_cursor() {
        let (g, log) = graph_with_log();
        let dom = g.dom();
        let gd = dom.root.get_dataset().cloned().expect("root dataset");
        let styled = StyledDom::create_from_dom(dom);
        let cm = RefAny::new(ContextMenuEntryLocalDataset {
            node_type: TYPE_B,
            backref: gd,
        });
        let (update, _) = with_info(styled, hit_none(), |info| {
            nodegraph_context_menu_click(cm.clone(), *info)
        });
        assert_eq!(update, Update::RefreshDomAllWindows);
        log_of(&log, |l| {
            // Cursor is `Uninitialized` and there is no layout, so both the cursor and
            // the wrapper offset are (0, 0) — the position must be exactly zero, not NaN.
            assert_eq!(l.added, vec![(TYPE_B.inner, 5, 0.0, 0.0)]);
        });
    }
    #[test]
    fn context_menu_click_position_degrades_to_nan_at_a_zero_scale_factor() {
        // `1.0 / scale_factor` is `inf` at zero; `0 * inf` is NaN. This pins down what
        // the widget actually hands the user callback in that case.
        let (mut g, log) = graph_with_log();
        g.scale_factor = 0.0;
        let dom = g.dom();
        let gd = dom.root.get_dataset().cloned().expect("root dataset");
        let styled = StyledDom::create_from_dom(dom);
        let cm = RefAny::new(ContextMenuEntryLocalDataset {
            node_type: TYPE_A,
            backref: gd,
        });
        let (_, _) = with_info(styled, hit_none(), |info| {
            nodegraph_context_menu_click(cm.clone(), *info)
        });
        log_of(&log, |l| {
            assert_eq!(l.added.len(), 1);
            let (_, id, x, y) = l.added[0];
            assert_eq!(id, 5, "the id must still be generated normally");
            assert!(x.is_nan() && y.is_nan(), "0 * inf == NaN, got ({x}, {y})");
        });
    }
    #[test]
    fn context_menu_click_ignores_a_payload_of_the_wrong_type() {
        assert_eq!(
            fire(|info| nodegraph_context_menu_click(RefAny::new(0_u32), info)),
            Update::DoNothing,
        );
    }
    // ==================================================================
    // 18. field-edit callbacks
    // ==================================================================
    #[test]
    fn textinput_focus_lost_forwards_the_decoded_text() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 2,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N2,
                backref: graph_dataset(&g),
            }),
        });
        // 'H', a lone surrogate (never a valid char), then U+1F389 — `get_text` drops
        // the surrogate, so the callback must see exactly "H🎉".
        let state = TextInputState {
            text: vec![0x48_u32, 0xD800, 0x1F389].into(),
            ..TextInputState::default()
        };
        assert_eq!(
            fire(|info| nodegraph_on_textinput_focus_lost(fd.clone(), info, state.clone())),
            Update::RefreshDom,
        );
        log_of(&log, |l| {
            assert_eq!(l.edited, vec![(N2.inner, 2, TYPE_B.inner)]);
            assert_eq!(l.text_values, vec!["H\u{1F389}".to_string()]);
        });
    }
    #[test]
    fn textinput_focus_lost_handles_an_empty_and_an_out_of_range_scalar() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 0,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        for (raw, expected) in [
            (vec![], ""),
            (vec![0x11_0000_u32, 0xFFFF_FFFF], ""), // both above the Unicode range
            (vec![0x0041, 0x0301], "A\u{0301}"),    // combining mark survives
        ] {
            let state = TextInputState {
                text: raw.into(),
                ..TextInputState::default()
            };
            let _ = fire(|info| nodegraph_on_textinput_focus_lost(fd.clone(), info, state.clone()));
            log_of(&log, |l| {
                assert_eq!(l.text_values.last().map(String::as_str), Some(expected));
            });
        }
    }
    #[test]
    fn numberinput_focus_lost_forwards_nan_and_infinities_verbatim() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 1,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        for n in [
            0.0_f32,
            -0.0,
            f32::MAX,
            f32::MIN,
            f32::MIN_POSITIVE,
            f32::EPSILON,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::NAN,
        ] {
            let state = NumberInputState {
                number: n,
                ..NumberInputState::default()
            };
            assert_eq!(
                fire(|info| nodegraph_on_numberinput_focus_lost(fd.clone(), info, state)),
                Update::RefreshDom,
            );
        }
        log_of(&log, |l| {
            assert_eq!(l.number_values.len(), 9);
            assert_eq!(l.number_values[0], 0.0);
            assert_eq!(l.number_values[2], f32::MAX);
            assert!(l.number_values[6].is_infinite());
            assert!(l.number_values[8].is_nan(), "NaN must not be normalised");
            assert!(l.edited.iter().all(|e| *e == (N1.inner, 1, TYPE_A.inner)));
        });
    }
    #[test]
    fn checkbox_change_forwards_both_states() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 0,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        for checked in [true, false, true] {
            let _ = fire(|info| {
                nodegraph_on_checkbox_value_changed(fd.clone(), info, CheckBoxState { checked })
            });
        }
        log_of(&log, |l| assert_eq!(l.bool_values, vec![true, false, true]));
    }
    #[test]
    fn colorinput_change_forwards_every_channel_including_alpha_extremes() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 3,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        // {1,2,3,4} catches a channel swap that greys would hide; 0 and 255 alpha are
        // the two extremes.
        for c in [
            ColorU { r: 1, g: 2, b: 3, a: 4 },
            ColorU { r: 0, g: 0, b: 0, a: 0 },
            ColorU { r: 255, g: 255, b: 255, a: 255 },
        ] {
            let _ = fire(|info| {
                nodegraph_on_colorinput_value_changed(fd.clone(), info, ColorInputState { color: c })
            });
        }
        log_of(&log, |l| {
            assert_eq!(
                l.color_values,
                vec![(1, 2, 3, 4), (0, 0, 0, 0), (255, 255, 255, 255)],
            );
        });
    }
    #[test]
    fn fileinput_click_forwards_both_a_missing_and_a_unicode_path() {
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 4,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        let _ = fire(|info| {
            nodegraph_on_fileinput_button_clicked(
                fd.clone(),
                info,
                FileInputState {
                    path: OptionString::None,
                },
            )
        });
        let _ = fire(|info| {
            nodegraph_on_fileinput_button_clicked(
                fd.clone(),
                info,
                FileInputState {
                    path: OptionString::Some(AzString::from_const_str("/tmp/日本語/🎉.txt")),
                },
            )
        });
        log_of(&log, |l| {
            assert_eq!(
                l.file_values,
                vec![None, Some("/tmp/日本語/🎉.txt".to_string())],
            );
        });
    }
    #[test]
    fn field_callbacks_bail_out_when_the_node_is_not_in_the_graph() {
        // Every one of the five handlers looks the node type up first; a stale
        // `node_id` must return `DoNothing` instead of unwrapping.
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 0,
            backref: RefAny::new(NodeLocalDataset {
                node_id: MISSING,
                backref: graph_dataset(&g),
            }),
        });
        assert_eq!(
            fire(|info| nodegraph_on_textinput_focus_lost(
                fd.clone(),
                info,
                TextInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_numberinput_focus_lost(
                fd.clone(),
                info,
                NumberInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_checkbox_value_changed(
                fd.clone(),
                info,
                CheckBoxState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_colorinput_value_changed(
                fd.clone(),
                info,
                ColorInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_fileinput_button_clicked(
                fd.clone(),
                info,
                FileInputState::default()
            )),
            Update::DoNothing,
        );
        log_of(&log, |l| assert!(l.edited.is_empty()));
    }
    #[test]
    fn field_callbacks_bail_out_on_a_payload_of_the_wrong_type() {
        assert_eq!(
            fire(|info| nodegraph_on_textinput_focus_lost(
                RefAny::new(0_u32),
                info,
                TextInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_numberinput_focus_lost(
                RefAny::new(0_u32),
                info,
                NumberInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_checkbox_value_changed(
                RefAny::new(0_u32),
                info,
                CheckBoxState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_colorinput_value_changed(
                RefAny::new(0_u32),
                info,
                ColorInputState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_fileinput_button_clicked(
                RefAny::new(0_u32),
                info,
                FileInputState::default()
            )),
            Update::DoNothing,
        );
    }
    #[test]
    fn field_callbacks_forward_a_usize_max_field_index_unclamped() {
        // The field index is an opaque token as far as the widget is concerned — it is
        // never used to index anything here, so even `usize::MAX` must pass through.
        let (g, log) = graph_with_log();
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: usize::MAX,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        let _ = fire(|info| {
            nodegraph_on_checkbox_value_changed(fd.clone(), info, CheckBoxState { checked: true })
        });
        log_of(&log, |l| {
            assert_eq!(l.edited, vec![(N1.inner, usize::MAX, TYPE_A.inner)]);
        });
    }
    #[test]
    fn field_callbacks_without_a_user_callback_do_nothing() {
        let g = graph(); // no callbacks installed
        let fd = RefAny::new(NodeFieldLocalDataset {
            field_idx: 0,
            backref: RefAny::new(NodeLocalDataset {
                node_id: N1,
                backref: graph_dataset(&g),
            }),
        });
        assert_eq!(
            fire(|info| nodegraph_on_checkbox_value_changed(
                fd.clone(),
                info,
                CheckBoxState::default()
            )),
            Update::DoNothing,
        );
        assert_eq!(
            fire(|info| nodegraph_on_fileinput_button_clicked(
                fd.clone(),
                info,
                FileInputState::default()
            )),
            Update::DoNothing,
        );
    }
}