Skip to main content

demo_hello_world/
lib.rs

1// Pre-existing warnings allowed (not part of StyleFn refactoring)
2#![allow(clippy::large_enum_variant)]
3
4//! # Hello World Demo
5//!
6//! Basic node graph with command palette (Cmd/Ctrl+Space) for adding nodes and changing themes.
7//! Now includes interactive style configuration nodes!
8//!
9//! ## Interactive Demo
10//!
11//! <link rel="stylesheet" href="pkg/demo.css">
12//! <div id="demo-container">
13//!   <div id="demo-loading">
14//!     <div class="demo-spinner"></div>
15//!     <p>Loading demo...</p>
16//!   </div>
17//!   <div id="demo-canvas-container"></div>
18//!   <div id="demo-error">
19//!     <strong>Failed to load demo.</strong> WebGPU required.
20//!   </div>
21//! </div>
22//! <script type="module" src="pkg/demo-loader.js"></script>
23//!
24//! ## Controls
25//!
26//! - **Cmd/Ctrl+Space** - Open command palette
27//! - **Drag nodes** - Move nodes around the canvas
28//! - **Drag from pins** - Create connections between nodes
29//! - **Click edges** - Disconnect existing connections
30//! - **Scroll** - Zoom in/out
31//! - **Middle-drag** - Pan the canvas
32//!
33//! ## Style Configuration
34//!
35//! Add input nodes (sliders, color pickers) and connect them to config nodes
36//! to dynamically adjust the graph's appearance!
37
38mod ids;
39mod nodes;
40#[cfg(not(target_arch = "wasm32"))]
41mod persistence;
42mod style_overlay;
43
44use iced::{
45    Color, Event, Length, Point, Subscription, Task, Theme, Vector, event, keyboard,
46    widget::{container, opaque, stack, text},
47    window,
48};
49use iced_nodegraph::{
50    ColorQuad, EdgeStatus, EdgeStyle, PinRef, default_edge_style, default_node_style,
51    default_pin_style, edge as ng_edge, node as ng_node,
52};
53use iced_nodegraph::{EdgeCurve, PinShape, TilingKind};
54use iced_palette::{
55    Command, Shortcut, command, command_palette, find_matching_shortcut, focus_input,
56    get_filtered_command_index, get_filtered_count, is_toggle_shortcut, navigate_down, navigate_up,
57};
58use ids::{EdgeId, NodeId, PinLabel, generate_edge_id, generate_node_id};
59use nodes::{
60    BoolToggleConfig, ColorQuadNode, ConfigNodeType, EdgeConfigInputs, EdgeSection, EdgeSections,
61    FloatSliderConfig, GraphConfigInputs, InputNodeType, IntSliderConfig, MathNodeState,
62    MathOperation, NodeConfigInputs, NodeSection, NodeSections, NodeType, NodeValue, PatternType,
63    PinConfigInputs, Vec2Node, apply_to_graph_node, apply_to_node_node, bool_toggle_node,
64    color_picker_node, color_preset_node, color_quad_node, edge_config_node,
65    edge_curve_selector_node, float_slider_node, graph_config_node, int_slider_node, math_node,
66    node, node_config_node, pattern_type_selector_node, pin_config_node, pin_shape_selector_node,
67    theme_extended_node, theme_node, tiling_kind_selector_node, vec2_node,
68};
69#[cfg(not(target_arch = "wasm32"))]
70use persistence::EdgeData;
71use std::collections::{HashMap, HashSet};
72use style_overlay::{EdgeOverlay, GraphOverlay, NodeOverlay, PinOverlay};
73
74/// Edge data for in-memory representation (WASM version).
75#[cfg(target_arch = "wasm32")]
76#[derive(Debug, Clone)]
77pub struct EdgeData {
78    pub from_node: NodeId,
79    pub from_pin: PinLabel,
80    pub to_node: NodeId,
81    pub to_pin: PinLabel,
82}
83
84#[cfg(target_arch = "wasm32")]
85use wasm_bindgen::prelude::*;
86
87#[cfg(target_arch = "wasm32")]
88#[wasm_bindgen(start)]
89pub fn wasm_init() {
90    console_error_panic_hook::set_once();
91}
92
93pub fn main() -> iced::Result {
94    #[cfg(target_arch = "wasm32")]
95    let window_settings = iced::window::Settings {
96        platform_specific: iced::window::settings::PlatformSpecific {
97            target: Some(String::from("demo-canvas-container")),
98        },
99        ..Default::default()
100    };
101
102    #[cfg(not(target_arch = "wasm32"))]
103    let window_settings = {
104        // Try to load saved window settings
105        let (position, size, maximized) = persistence::load_state()
106            .map(|s| (s.window_position, s.window_size, s.window_maximized))
107            .unwrap_or((None, None, None));
108
109        iced::window::Settings {
110            position: position
111                .map(|(x, y)| iced::window::Position::Specific(Point::new(x as f32, y as f32)))
112                .unwrap_or(iced::window::Position::Centered),
113            size: size
114                .map(|(w, h)| iced::Size::new(w as f32, h as f32))
115                .unwrap_or(iced::Size::new(1280.0, 800.0)),
116            maximized: maximized.unwrap_or(false),
117            ..Default::default()
118        }
119    };
120
121    iced::application(Application::new, Application::update, Application::view)
122        .subscription(Application::subscription)
123        .title("Hello World - iced_nodegraph Demo")
124        .theme(Application::theme)
125        .window(window_settings)
126        .run()
127}
128
129#[cfg(target_arch = "wasm32")]
130#[wasm_bindgen]
131pub fn run_demo() {
132    let _ = main();
133}
134
135#[derive(Debug, Clone)]
136#[allow(dead_code)]
137enum ApplicationMessage {
138    Noop,
139    EdgeConnected {
140        from: PinRef<NodeId, PinLabel>,
141        to: PinRef<NodeId, PinLabel>,
142    },
143    EdgeDisconnected {
144        from: PinRef<NodeId, PinLabel>,
145        to: PinRef<NodeId, PinLabel>,
146    },
147    ToggleCommandPalette,
148    CommandPaletteInput(String),
149    CommandPaletteNavigateUp,
150    CommandPaletteNavigateDown,
151    CommandPaletteSelect(usize),
152    CommandPaletteConfirm,
153    CommandPaletteCancel,
154    ExecuteShortcut(String),
155    CommandPaletteNavigate(usize),
156    SpawnNode {
157        node_type: NodeType,
158    },
159    ChangeTheme(Theme),
160    CameraChanged {
161        position: Point,
162        zoom: f32,
163    },
164    WindowResized(iced::Size),
165    WindowMoved(Point),
166    WindowMaximizedChanged(bool),
167    NavigateToSubmenu(String),
168    NavigateBack,
169    // Selection-related messages
170    SelectionChanged(Vec<NodeId>),
171    CloneNodes(Vec<NodeId>),
172    DeleteNodes(Vec<NodeId>),
173    NodesMoved {
174        delta: Vector,
175        node_ids: Vec<NodeId>,
176    },
177    // State export for Claude
178    ExportState,
179    /// Reset the entire app to its initial state (clears graph, config, and the
180    /// persisted save file).
181    Reset,
182    /// Move keyboard focus to the next / previous focusable widget (Tab /
183    /// Shift+Tab), e.g. between a slider node's min/max/step fields.
184    FocusNext,
185    FocusPrevious,
186    // Input node value changes
187    SliderChanged {
188        node_id: NodeId,
189        value: f32,
190    },
191    IntSliderChanged {
192        node_id: NodeId,
193        value: i32,
194    },
195    BoolChanged {
196        node_id: NodeId,
197        value: bool,
198    },
199    EdgeCurveChanged {
200        node_id: NodeId,
201        value: EdgeCurve,
202    },
203    PinShapeChanged {
204        node_id: NodeId,
205        value: PinShape,
206    },
207    PatternTypeChanged {
208        node_id: NodeId,
209        value: PatternType,
210    },
211    TilingKindChanged {
212        node_id: NodeId,
213        value: TilingKind,
214    },
215    ColorChanged {
216        node_id: NodeId,
217        color: Color,
218    },
219    // Collapsible node messages
220    ToggleNodeExpanded {
221        node_id: NodeId,
222    },
223    UpdateFloatSliderConfig {
224        node_id: NodeId,
225        config: FloatSliderConfig,
226    },
227    UpdateIntSliderConfig {
228        node_id: NodeId,
229        config: IntSliderConfig,
230    },
231    // Config section collapse/expand
232    ToggleEdgeSection {
233        node_id: NodeId,
234        section: EdgeSection,
235    },
236    ToggleNodeSection {
237        node_id: NodeId,
238        section: NodeSection,
239    },
240}
241
242#[derive(Debug, Clone, PartialEq)]
243enum PaletteView {
244    Main,
245    Submenu(String),
246}
247
248/// Output types from config nodes for propagation
249#[derive(Debug, Clone)]
250#[allow(dead_code)]
251enum ConfigOutput {
252    Node(NodeOverlay),
253    Edge(EdgeOverlay),
254    Pin(PinOverlay),
255    Graph(GraphOverlay),
256}
257
258/// Maps a pin's data-type marker to its semantic color. Pins carry no style;
259/// the owning node colors them via `pin_style`, keyed on this.
260fn pin_color_for(ty: std::any::TypeId) -> Color {
261    use nodes::{colors, pins};
262    use std::any::TypeId;
263    if ty == TypeId::of::<pins::ColorData>() {
264        colors::PIN_COLOR
265    } else if ty == TypeId::of::<pins::Float>() || ty == TypeId::of::<pins::Int>() {
266        colors::PIN_NUMBER
267    } else if ty == TypeId::of::<pins::Bool>() {
268        colors::PIN_BOOL
269    } else if ty == TypeId::of::<pins::StringData>() {
270        colors::PIN_STRING
271    } else if ty == TypeId::of::<pins::Email>() {
272        colors::PIN_EMAIL
273    } else if ty == TypeId::of::<pins::DateTime>() {
274        colors::PIN_DATETIME
275    } else if ty == TypeId::of::<pins::NodeConfigData>()
276        || ty == TypeId::of::<pins::EdgeConfigData>()
277        || ty == TypeId::of::<pins::PinConfigData>()
278        || ty == TypeId::of::<pins::GraphConfigData>()
279    {
280        colors::PIN_CONFIG
281    } else {
282        colors::PIN_ANY
283    }
284}
285
286/// Computed style overlays accumulated from connected config nodes. Each is a
287/// partial overlay resolved against the theme base at draw time.
288#[derive(Debug, Clone, Default)]
289struct ComputedStyle {
290    node: NodeOverlay,
291    edge: EdgeOverlay,
292    pin: PinOverlay,
293    graph: GraphOverlay,
294}
295
296struct Application {
297    /// Nodes stored by unique ID
298    nodes: HashMap<NodeId, (Point, NodeType)>,
299    /// Node order for deterministic iteration
300    node_order: Vec<NodeId>,
301    /// Edges stored by unique ID
302    edges: HashMap<EdgeId, EdgeData>,
303    /// Edge order for deterministic iteration
304    edge_order: Vec<EdgeId>,
305    /// Currently selected nodes
306    selected_nodes: HashSet<NodeId>,
307    /// Nodes with expanded options panels
308    expanded_nodes: HashSet<NodeId>,
309    /// Section expansion state for EdgeConfig nodes
310    edge_config_sections: HashMap<NodeId, EdgeSections>,
311    /// Section expansion state for NodeConfig nodes
312    node_config_sections: HashMap<NodeId, NodeSections>,
313    command_palette_open: bool,
314    command_input: String,
315    current_theme: Theme,
316    palette_view: PaletteView,
317    palette_selected_index: usize,
318    palette_preview_theme: Option<Theme>,
319    palette_original_theme: Option<Theme>,
320    /// Computed style values from config node connections
321    computed_style: ComputedStyle,
322    /// Pending config outputs from config nodes to be applied by ApplyToGraph
323    pending_configs: HashMap<NodeId, Vec<(PinLabel, ConfigOutput)>>,
324    /// Current viewport size for spawn-at-center calculation
325    viewport_size: iced::Size,
326    /// Current camera position from NodeGraph
327    camera_position: Point,
328    /// Current camera zoom from NodeGraph
329    camera_zoom: f32,
330    /// Window position (x, y) for persistence
331    window_position: Option<(i32, i32)>,
332    /// Window size (width, height) for persistence
333    window_size: Option<(u32, u32)>,
334    /// Whether window is maximized
335    window_maximized: Option<bool>,
336}
337
338impl Default for Application {
339    fn default() -> Self {
340        use nodes::pins::workflow;
341
342        // Create nodes with stable NanoIDs
343        let node0_id = generate_node_id();
344        let node1_id = generate_node_id();
345        let node2_id = generate_node_id();
346        let node3_id = generate_node_id();
347
348        let mut nodes = HashMap::new();
349        nodes.insert(
350            node0_id.clone(),
351            (
352                Point::new(45.5, 149.0),
353                NodeType::Workflow("email_trigger".to_string()),
354            ),
355        );
356        nodes.insert(
357            node1_id.clone(),
358            (
359                Point::new(274.5, 227.5),
360                NodeType::Workflow("email_parser".to_string()),
361            ),
362        );
363        nodes.insert(
364            node2_id.clone(),
365            (
366                Point::new(459.5, 432.5),
367                NodeType::Workflow("filter".to_string()),
368            ),
369        );
370        nodes.insert(
371            node3_id.clone(),
372            (
373                Point::new(679.0, 252.5),
374                NodeType::Workflow("calendar".to_string()),
375            ),
376        );
377
378        let node_order = vec![
379            node0_id.clone(),
380            node1_id.clone(),
381            node2_id.clone(),
382            node3_id.clone(),
383        ];
384
385        // Create edges with stable NanoIDs and string pin labels
386        let mut edges = HashMap::new();
387        let mut edge_order = Vec::new();
388
389        // Edge 0: email_trigger "on email" -> email_parser "email"
390        let edge0_id = generate_edge_id();
391        edges.insert(
392            edge0_id.clone(),
393            EdgeData {
394                from_node: node0_id.clone(),
395                from_pin: workflow::ON_EMAIL,
396                to_node: node1_id.clone(),
397                to_pin: workflow::EMAIL,
398            },
399        );
400        edge_order.push(edge0_id);
401
402        // Edge 1: email_parser "subject" -> filter "input"
403        let edge1_id = generate_edge_id();
404        edges.insert(
405            edge1_id.clone(),
406            EdgeData {
407                from_node: node1_id.clone(),
408                from_pin: workflow::SUBJECT,
409                to_node: node2_id.clone(),
410                to_pin: workflow::INPUT,
411            },
412        );
413        edge_order.push(edge1_id);
414
415        // Edge 2: email_parser "datetime" -> calendar "datetime"
416        let edge2_id = generate_edge_id();
417        edges.insert(
418            edge2_id.clone(),
419            EdgeData {
420                from_node: node1_id.clone(),
421                from_pin: workflow::DATETIME,
422                to_node: node3_id.clone(),
423                to_pin: workflow::DATETIME,
424            },
425        );
426        edge_order.push(edge2_id);
427
428        // Edge 3: filter "matches" -> calendar "title"
429        let edge3_id = generate_edge_id();
430        edges.insert(
431            edge3_id.clone(),
432            EdgeData {
433                from_node: node2_id.clone(),
434                from_pin: workflow::MATCHES,
435                to_node: node3_id.clone(),
436                to_pin: workflow::TITLE,
437            },
438        );
439        edge_order.push(edge3_id);
440
441        Self {
442            nodes,
443            node_order,
444            edges,
445            edge_order,
446            selected_nodes: HashSet::new(),
447            expanded_nodes: HashSet::new(),
448            edge_config_sections: HashMap::new(),
449            node_config_sections: HashMap::new(),
450            command_palette_open: false,
451            command_input: String::new(),
452            current_theme: Theme::CatppuccinFrappe,
453            palette_view: PaletteView::Main,
454            palette_selected_index: 0,
455            palette_preview_theme: None,
456            palette_original_theme: None,
457            computed_style: ComputedStyle::default(),
458            pending_configs: HashMap::new(),
459            viewport_size: iced::Size::new(800.0, 600.0), // Default size
460            camera_position: Point::ORIGIN,
461            camera_zoom: 1.0,
462            window_position: None,
463            window_size: None,
464            window_maximized: None,
465        }
466    }
467}
468
469impl Application {
470    fn new() -> Self {
471        // Try to load saved state, fall back to default
472        #[cfg(not(target_arch = "wasm32"))]
473        {
474            match persistence::load_state() {
475                Ok(saved) => {
476                    let (
477                        nodes,
478                        node_order,
479                        edges,
480                        edge_order,
481                        theme,
482                        camera_pos,
483                        camera_zoom,
484                        window_pos,
485                        window_size,
486                        edge_config_sections,
487                        node_config_sections,
488                        window_maximized,
489                    ) = saved.to_app();
490                    println!(
491                        "Loaded saved state: {} nodes, {} edges",
492                        nodes.len(),
493                        edges.len()
494                    );
495                    let mut app = Self {
496                        nodes,
497                        node_order,
498                        edges,
499                        edge_order,
500                        current_theme: theme,
501                        camera_position: camera_pos,
502                        camera_zoom,
503                        window_position: window_pos,
504                        window_size,
505                        edge_config_sections,
506                        node_config_sections,
507                        window_maximized,
508                        ..Self::default()
509                    };
510                    // Apply computed styles from config nodes immediately
511                    app.propagate_values();
512                    return app;
513                }
514                Err(e) => {
515                    println!("No saved state found: {}", e);
516                }
517            }
518        }
519        Self::default()
520    }
521
522    /// Resets the entire app to its initial state, keeping only the live window
523    /// geometry so the OS window does not jump. Clears any stale config/style and
524    /// overwrites the persisted save file with the fresh default.
525    fn reset_to_default(&mut self) {
526        let viewport_size = self.viewport_size;
527        let window_position = self.window_position;
528        let window_size = self.window_size;
529        let window_maximized = self.window_maximized;
530        *self = Self {
531            viewport_size,
532            window_position,
533            window_size,
534            window_maximized,
535            ..Self::default()
536        };
537        self.propagate_values();
538        self.save_state();
539    }
540
541    /// Saves current state to disk (native only).
542    #[cfg(not(target_arch = "wasm32"))]
543    fn save_state(&self) {
544        let saved = persistence::SavedState::from_app(
545            &self.nodes,
546            &self.node_order,
547            &self.edges,
548            &self.edge_order,
549            &self.current_theme,
550            self.camera_position,
551            self.camera_zoom,
552            self.window_position,
553            self.window_size,
554            &self.edge_config_sections,
555            &self.node_config_sections,
556            self.window_maximized,
557        );
558        if let Err(e) = persistence::save_state(&saved) {
559            eprintln!("Failed to save state: {}", e);
560        }
561    }
562
563    #[cfg(target_arch = "wasm32")]
564    fn save_state(&self) {
565        // No-op on WASM
566    }
567
568    /// Calculate spawn position at screen center, converted to world coordinates.
569    fn spawn_position(&self) -> Point {
570        // Screen center
571        let screen_center_x = self.viewport_size.width / 2.0;
572        let screen_center_y = self.viewport_size.height / 2.0;
573
574        // Convert to world coordinates: world = screen / zoom - camera_position
575        let world_x = screen_center_x / self.camera_zoom - self.camera_position.x;
576        let world_y = screen_center_y / self.camera_zoom - self.camera_position.y;
577
578        // Offset for node size (approximate center, ~100x80 typical node)
579        Point::new(world_x - 50.0, world_y - 40.0)
580    }
581
582    /// Export current graph state to a file for Claude to read and update demos.
583    /// Format is designed to be human-readable and easily parseable.
584    #[cfg(not(target_arch = "wasm32"))]
585    fn export_state_to_file(&self) {
586        use std::io::Write;
587
588        // Create out/ directory if it doesn't exist
589        let out_dir = std::path::Path::new("out");
590        if !out_dir.exists()
591            && let Err(e) = std::fs::create_dir(out_dir)
592        {
593            eprintln!("Failed to create out/ directory: {}", e);
594            return;
595        }
596
597        // Generate random filename
598        let filename = Self::generate_random_name();
599        let path = out_dir.join(format!("{}.txt", filename));
600
601        let mut output = String::new();
602        output.push_str("# Graph State Export\n");
603        output.push_str(
604            "# Generated by hello_world demo - use this to update demo initial state\n\n",
605        );
606
607        // Export nodes
608        output.push_str("## Nodes\n");
609        output.push_str(&format!("# Total: {} nodes\n\n", self.nodes.len()));
610
611        for node_id in &self.node_order {
612            if let Some((pos, node_type)) = self.nodes.get(node_id) {
613                output.push_str(&format!("Node {}: ({:.1}, {:.1})\n", node_id, pos.x, pos.y));
614                match node_type {
615                    NodeType::Workflow(name) => {
616                        output.push_str(&format!("  Type: Workflow(\"{}\")\n", name));
617                    }
618                    NodeType::Input(input) => {
619                        output.push_str(&format!("  Type: Input({:?})\n", input));
620                    }
621                    NodeType::Config(config) => {
622                        output.push_str(&format!("  Type: Config({:?})\n", config));
623                    }
624                    NodeType::Math(state) => {
625                        output.push_str(&format!("  Type: Math({:?})\n", state));
626                    }
627                    NodeType::ColorQuad(state) => {
628                        output.push_str(&format!("  Type: ColorQuad({:?})\n", state));
629                    }
630                    NodeType::Vec2(state) => {
631                        output.push_str(&format!("  Type: Vec2({:?})\n", state));
632                    }
633                    NodeType::Theme => {
634                        output.push_str("  Type: Theme\n");
635                    }
636                    NodeType::ThemeExtended => {
637                        output.push_str("  Type: ThemeExtended\n");
638                    }
639                }
640                output.push('\n');
641            }
642        }
643
644        // Export edges
645        output.push_str("## Edges\n");
646        output.push_str(&format!("# Total: {} edges\n\n", self.edges.len()));
647
648        for edge_id in &self.edge_order {
649            if let Some(edge) = self.edges.get(edge_id) {
650                output.push_str(&format!(
651                    "Edge {}: Node {}.Pin \"{}\" -> Node {}.Pin \"{}\"\n",
652                    edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin
653                ));
654            }
655        }
656
657        // Export JSON snippet for easy copy-paste
658        output.push_str("\n## JSON Format (for state.json)\n\n");
659        output.push_str("```json\n");
660        output.push_str("{\n  \"nodes\": [\n");
661        for (i, node_id) in self.node_order.iter().enumerate() {
662            if let Some((pos, node_type)) = self.nodes.get(node_id) {
663                let type_str = match node_type {
664                    NodeType::Workflow(name) => {
665                        format!("{{\"type\": \"Workflow\", \"name\": \"{}\"}}", name)
666                    }
667                    _ => format!("{:?}", node_type),
668                };
669                let comma = if i < self.node_order.len() - 1 {
670                    ","
671                } else {
672                    ""
673                };
674                output.push_str(&format!(
675                    "    {{\"id\": \"{}\", \"x\": {:.1}, \"y\": {:.1}, \"node_type\": {}}}{}\n",
676                    node_id, pos.x, pos.y, type_str, comma
677                ));
678            }
679        }
680        output.push_str("  ],\n  \"edges\": [\n");
681        for (i, edge_id) in self.edge_order.iter().enumerate() {
682            if let Some(edge) = self.edges.get(edge_id) {
683                let comma = if i < self.edge_order.len() - 1 {
684                    ","
685                } else {
686                    ""
687                };
688                output.push_str(&format!(
689                    "    {{\"id\": \"{}\", \"from_node\": \"{}\", \"from_pin\": \"{}\", \"to_node\": \"{}\", \"to_pin\": \"{}\"}}{}\n",
690                    edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin, comma
691                ));
692            }
693        }
694        output.push_str("  ]\n}\n");
695        output.push_str("```\n");
696
697        // Write to file
698        match std::fs::File::create(&path) {
699            Ok(mut file) => {
700                if let Err(e) = file.write_all(output.as_bytes()) {
701                    eprintln!("Failed to write state export: {}", e);
702                } else {
703                    println!("State exported to: {}", path.display());
704                }
705            }
706            Err(e) => {
707                eprintln!("Failed to create export file: {}", e);
708            }
709        }
710    }
711
712    /// Generate a random two-word name for export files
713    #[cfg(not(target_arch = "wasm32"))]
714    fn generate_random_name() -> String {
715        use std::time::{SystemTime, UNIX_EPOCH};
716
717        const ADJECTIVES: &[&str] = &[
718            "swift", "bright", "calm", "bold", "keen", "warm", "cool", "wild", "soft", "sharp",
719            "quick", "slow", "deep", "wide", "tall", "tiny", "grand", "pure", "rare", "wise",
720            "fair", "dark", "light", "fresh",
721        ];
722        const NOUNS: &[&str] = &[
723            "river", "mountain", "forest", "ocean", "meadow", "valley", "canyon", "island",
724            "sunset", "sunrise", "thunder", "breeze", "garden", "crystal", "shadow", "ember",
725            "falcon", "phoenix", "dragon", "tiger", "wolf", "eagle", "raven", "fox",
726        ];
727
728        // Simple random using system time nanoseconds
729        let nanos = SystemTime::now()
730            .duration_since(UNIX_EPOCH)
731            .map(|d| d.as_nanos())
732            .unwrap_or(0);
733
734        let adj_idx = (nanos % ADJECTIVES.len() as u128) as usize;
735        let noun_idx = ((nanos / 7) % NOUNS.len() as u128) as usize;
736
737        format!("{}-{}", ADJECTIVES[adj_idx], NOUNS[noun_idx])
738    }
739
740    #[cfg(target_arch = "wasm32")]
741    fn export_state_to_file(&self) {
742        // WASM: State export not available in browser
743    }
744
745    /// Propagates values from input nodes to connected config nodes
746    /// The value a node emits on a given output pin. For most nodes this is the
747    /// pin-agnostic `output_value()`; the Theme node resolves per-pin colors from
748    /// the current theme.
749    fn pin_output_value(&self, node_id: &NodeId, pin: &PinLabel) -> Option<NodeValue> {
750        match self.nodes.get(node_id) {
751            Some((_, NodeType::Theme | NodeType::ThemeExtended)) => {
752                theme_color(&self.current_theme, pin).map(NodeValue::Color)
753            }
754            Some((_, node_type)) => node_type.output_value(),
755            None => None,
756        }
757    }
758
759    fn propagate_values(&mut self) {
760        let mut new_computed = ComputedStyle::default();
761        self.pending_configs.clear();
762
763        // Phase 1: Reset all config node and math node inputs to defaults
764        for (_, node_type) in self.nodes.values_mut() {
765            match node_type {
766                NodeType::Config(config) => match config {
767                    ConfigNodeType::NodeConfig(inputs) => *inputs = NodeConfigInputs::default(),
768                    ConfigNodeType::EdgeConfig(inputs) => *inputs = EdgeConfigInputs::default(),
769                    ConfigNodeType::PinConfig(inputs) => *inputs = PinConfigInputs::default(),
770                    ConfigNodeType::GraphConfig(inputs) => *inputs = GraphConfigInputs::default(),
771                    ConfigNodeType::ApplyToGraph {
772                        has_node_config,
773                        has_edge_config,
774                        has_pin_config,
775                        has_graph_config,
776                    } => {
777                        *has_node_config = false;
778                        *has_edge_config = false;
779                        *has_pin_config = false;
780                        *has_graph_config = false;
781                    }
782                    ConfigNodeType::ApplyToNode {
783                        has_node_config,
784                        target_id,
785                    } => {
786                        *has_node_config = false;
787                        *target_id = None;
788                    }
789                },
790                NodeType::Math(state) => {
791                    state.input_a = None;
792                    state.input_b = None;
793                }
794                NodeType::ColorQuad(state) => *state = ColorQuadNode::default(),
795                NodeType::Vec2(state) => *state = Vec2Node::default(),
796                _ => {}
797            }
798        }
799
800        // Phase 1.5: Propagate values INTO Math nodes (iteratively for chaining)
801        // Math nodes can be chained (e.g., (A+B)*C), so we iterate until stable
802        let edges_snapshot: Vec<_> = self.edges.values().cloned().collect();
803
804        // We need multiple passes because Math→Math chains require the source
805        // to have computed its result before the target can use it
806        const MAX_ITERATIONS: usize = 10;
807        for _ in 0..MAX_ITERATIONS {
808            let mut changed = false;
809
810            for edge in &edges_snapshot {
811                // Feed the target's combiner inputs (Math/ColorQuad/Vec2) from the
812                // source's output, in both edge directions (edges connect either
813                // way). `pin_output_value` resolves Theme node colors per pin.
814                let forward = self.pin_output_value(&edge.from_node, &edge.from_pin);
815                if let Some(value) = forward
816                    && let Some((_, node)) = self.nodes.get_mut(&edge.to_node)
817                    && feed_combiner_input(node, &edge.to_pin, &value)
818                {
819                    changed = true;
820                }
821
822                let reverse = self.pin_output_value(&edge.to_node, &edge.to_pin);
823                if let Some(value) = reverse
824                    && let Some((_, node)) = self.nodes.get_mut(&edge.from_node)
825                    && feed_combiner_input(node, &edge.from_pin, &value)
826                {
827                    changed = true;
828                }
829            }
830
831            if !changed {
832                break;
833            }
834        }
835
836        // Phase 2: Apply Input → Config connections (in both edge directions)
837        // Also apply Math → Config connections
838
839        for edge in &edges_snapshot {
840            let from_node_type = self.nodes.get(&edge.from_node).map(|(_, t)| t.clone());
841            let to_node_type = self.nodes.get(&edge.to_node).map(|(_, t)| t.clone());
842
843            if let (Some(from_type), Some(to_type)) = (from_node_type, to_node_type) {
844                // Handle Input → Config connections
845                if let (NodeType::Input(input), NodeType::Config(_)) = (&from_type, &to_type) {
846                    let value = input.output_value();
847                    self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
848                }
849                // Handle Config → Input connections (reverse direction)
850                if let (NodeType::Config(_), NodeType::Input(input)) = (&from_type, &to_type) {
851                    let value = input.output_value();
852                    self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
853                }
854                // Handle Math → Config connections
855                if let (NodeType::Math(state), NodeType::Config(_)) = (&from_type, &to_type)
856                    && let Some(result) = state.result()
857                {
858                    let value = NodeValue::Float(result);
859                    self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
860                }
861                // Handle Config → Math connections (reverse direction)
862                if let (NodeType::Config(_), NodeType::Math(state)) = (&from_type, &to_type)
863                    && let Some(result) = state.result()
864                {
865                    let value = NodeValue::Float(result);
866                    self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
867                }
868                // Handle Builder (ColorQuad/Vec2) → Config connections
869                if let (NodeType::ColorQuad(_) | NodeType::Vec2(_), NodeType::Config(_)) =
870                    (&from_type, &to_type)
871                    && let Some(value) = from_type.output_value()
872                {
873                    self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
874                }
875                // Handle Config → Builder connections (reverse direction)
876                if let (NodeType::Config(_), NodeType::ColorQuad(_) | NodeType::Vec2(_)) =
877                    (&from_type, &to_type)
878                    && let Some(value) = to_type.output_value()
879                {
880                    self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
881                }
882                // Handle Theme → Config connections (per-pin palette color)
883                if let (NodeType::Theme | NodeType::ThemeExtended, NodeType::Config(_)) =
884                    (&from_type, &to_type)
885                    && let Some(color) = theme_color(&self.current_theme, &edge.from_pin)
886                {
887                    self.apply_value_to_config_node(
888                        &edge.to_node,
889                        &edge.to_pin,
890                        &NodeValue::Color(color),
891                    );
892                }
893                // Handle Config → Theme connections (reverse direction)
894                if let (NodeType::Config(_), NodeType::Theme | NodeType::ThemeExtended) =
895                    (&from_type, &to_type)
896                    && let Some(color) = theme_color(&self.current_theme, &edge.to_pin)
897                {
898                    self.apply_value_to_config_node(
899                        &edge.from_node,
900                        &edge.from_pin,
901                        &NodeValue::Color(color),
902                    );
903                }
904            }
905        }
906
907        // Phase 3: After all inputs applied, process Config → ApplyToGraph connections
908        // Now config nodes have their updated inputs, so we can build configs
909        for edge in &edges_snapshot {
910            let from_node_type = self.nodes.get(&edge.from_node).map(|(_, t)| t.clone());
911            let to_node_type = self.nodes.get(&edge.to_node).map(|(_, t)| t.clone());
912
913            if let (Some(from_type), Some(to_type)) = (from_node_type, to_node_type) {
914                // Handle Config → ApplyToGraph connections
915                if let (
916                    NodeType::Config(config),
917                    NodeType::Config(ConfigNodeType::ApplyToGraph { .. }),
918                ) = (&from_type, &to_type)
919                {
920                    self.connect_config_to_apply(
921                        &edge.from_node,
922                        config,
923                        &edge.to_node,
924                        &edge.to_pin,
925                    );
926                }
927                // Handle ApplyToGraph → Config connections (reverse)
928                if let (
929                    NodeType::Config(ConfigNodeType::ApplyToGraph { .. }),
930                    NodeType::Config(config),
931                ) = (&from_type, &to_type)
932                {
933                    self.connect_config_to_apply(
934                        &edge.to_node,
935                        config,
936                        &edge.from_node,
937                        &edge.from_pin,
938                    );
939                }
940            }
941        }
942
943        // Phase 4: Build configs from ApplyToGraph nodes and apply to computed style
944        self.apply_graph_configs(&mut new_computed);
945
946        self.computed_style = new_computed;
947    }
948
949    /// Applies an input value to a specific pin on a config node
950    fn apply_value_to_config_node(
951        &mut self,
952        node_id: &NodeId,
953        pin_label: &PinLabel,
954        value: &NodeValue,
955    ) {
956        use nodes::pins::{cfg, edge as epin, graph as gpin, node as npin, pin as ppin};
957
958        let Some((_, node_type)) = self.nodes.get_mut(node_id) else {
959            return;
960        };
961
962        let NodeType::Config(config) = node_type else {
963            return;
964        };
965
966        match config {
967            ConfigNodeType::NodeConfig(inputs) => {
968                // NodeConfig pin labels mirror NodeStyle: fill, border, pattern, shadow
969                if *pin_label == npin::FILL_COLOR {
970                    inputs.fill_color = value.as_color_quad();
971                } else if *pin_label == npin::CORNER_RADIUS {
972                    inputs.corner_radius = value.as_float();
973                } else if *pin_label == npin::OPACITY {
974                    inputs.opacity = value.as_float();
975                } else if *pin_label == npin::BORDER_COLOR {
976                    inputs.border_color = value.as_color_quad();
977                } else if *pin_label == npin::BORDER_WIDTH {
978                    inputs.border_width = value.as_float();
979                } else if *pin_label == npin::BORDER_OUTLINE_WIDTH {
980                    inputs.border_outline_width = value.as_float();
981                } else if *pin_label == npin::BORDER_OUTLINE_COLOR {
982                    inputs.border_outline_color = value.as_color_quad();
983                } else if *pin_label == npin::PATTERN {
984                    inputs.pattern_type = value.as_pattern_type();
985                } else if *pin_label == npin::DASH {
986                    inputs.dash_length = value.as_float();
987                } else if *pin_label == npin::GAP {
988                    inputs.gap_length = value.as_float();
989                } else if *pin_label == npin::ANGLE {
990                    // Convert degrees from slider to radians for pattern angle
991                    inputs.pattern_angle = value.as_float().map(|deg| deg.to_radians());
992                } else if *pin_label == npin::SPEED {
993                    inputs.animation_speed = value.as_float();
994                } else if *pin_label == npin::SHADOW_COLOR {
995                    inputs.shadow_color = value.as_color_quad();
996                } else if *pin_label == npin::SHADOW_DISTANCE {
997                    inputs.shadow_distance = value.as_float();
998                } else if *pin_label == npin::SHADOW_OFFSET {
999                    inputs.shadow_offset = value.as_vec2();
1000                }
1001            }
1002            ConfigNodeType::EdgeConfig(inputs) => {
1003                // EdgeConfig pin labels
1004                if *pin_label == epin::STROKE_COLOR {
1005                    inputs.stroke_color = value.as_color_quad();
1006                } else if *pin_label == epin::THICKNESS {
1007                    inputs.thickness = value.as_float();
1008                } else if *pin_label == epin::CURVE {
1009                    inputs.curve = value.as_edge_curve();
1010                } else if *pin_label == epin::PATTERN {
1011                    inputs.pattern_type = value.as_pattern_type();
1012                } else if *pin_label == epin::DASH {
1013                    inputs.dash_length = value.as_float();
1014                } else if *pin_label == epin::GAP {
1015                    inputs.gap_length = value.as_float();
1016                } else if *pin_label == epin::ANGLE {
1017                    // Convert degrees from slider to radians for pattern angle
1018                    inputs.pattern_angle = value.as_float().map(|deg| deg.to_radians());
1019                } else if *pin_label == epin::SPEED {
1020                    inputs.animation_speed = value.as_float();
1021                // Stroke outline
1022                } else if *pin_label == epin::STROKE_OUTLINE_WIDTH {
1023                    inputs.stroke_outline_width = value.as_float();
1024                } else if *pin_label == epin::STROKE_OUTLINE_COLOR {
1025                    inputs.stroke_outline_color = value.as_color_quad();
1026                // Border settings
1027                } else if *pin_label == epin::BORDER_WIDTH {
1028                    inputs.border_width = value.as_float();
1029                } else if *pin_label == epin::BORDER_GAP {
1030                    inputs.border_gap = value.as_float();
1031                } else if *pin_label == epin::BORDER_COLOR {
1032                    inputs.border_color = value.as_color_quad();
1033                } else if *pin_label == epin::BORDER_BACKGROUND {
1034                    inputs.border_background = value.as_color_quad();
1035                } else if *pin_label == epin::BORDER_OUTLINE_WIDTH {
1036                    inputs.border_outline_width = value.as_float();
1037                } else if *pin_label == epin::BORDER_OUTLINE_COLOR {
1038                    inputs.border_outline_color = value.as_color_quad();
1039                // Shadow settings
1040                } else if *pin_label == epin::SHADOW_BLUR {
1041                    inputs.shadow_blur = value.as_float();
1042                } else if *pin_label == epin::SHADOW_EXPAND {
1043                    inputs.shadow_expand = value.as_float();
1044                } else if *pin_label == epin::SHADOW_COLOR {
1045                    inputs.shadow_color = value.as_color_quad();
1046                } else if *pin_label == epin::SHADOW_OFFSET {
1047                    inputs.shadow_offset = value.as_vec2();
1048                }
1049            }
1050            ConfigNodeType::PinConfig(inputs) => {
1051                // PinConfig pin labels mirror PinStyle
1052                if *pin_label == ppin::COLOR {
1053                    inputs.color = value.as_color_quad();
1054                } else if *pin_label == ppin::RADIUS {
1055                    inputs.radius = value.as_float();
1056                } else if *pin_label == ppin::SHAPE {
1057                    inputs.shape = value.as_pin_shape();
1058                } else if *pin_label == ppin::BORDER_COLOR {
1059                    inputs.border_color = value.as_color_quad();
1060                } else if *pin_label == ppin::BORDER_WIDTH {
1061                    inputs.border_width = value.as_float();
1062                }
1063            }
1064            ConfigNodeType::GraphConfig(inputs) => {
1065                // GraphConfig pin labels mirror GraphStyle + TilingBackground.
1066                if *pin_label == gpin::BACKGROUND {
1067                    inputs.background_color = value.as_color_quad();
1068                } else if *pin_label == gpin::TILING_KIND {
1069                    inputs.tiling_kind = value.as_tiling_kind();
1070                } else if *pin_label == gpin::SPACING {
1071                    inputs.tiling_spacing = value.as_float();
1072                } else if *pin_label == gpin::THICKNESS {
1073                    inputs.tiling_thickness = value.as_float();
1074                } else if *pin_label == gpin::LINE_COLOR {
1075                    inputs.tiling_color = value.as_color_quad();
1076                }
1077            }
1078            ConfigNodeType::ApplyToNode { target_id, .. } if *pin_label == cfg::TARGET => {
1079                *target_id = value.as_int();
1080            }
1081            _ => {}
1082        }
1083    }
1084
1085    /// Connects a config node's output to an ApplyToGraph node
1086    fn connect_config_to_apply(
1087        &mut self,
1088        config_node_id: &NodeId,
1089        _config_type: &ConfigNodeType, // Ignored - we read from current state
1090        apply_node_id: &NodeId,
1091        apply_pin_label: &PinLabel,
1092    ) {
1093        use nodes::pins::cfg as pin;
1094
1095        // Build the config from the CURRENT state of the config node (not the snapshot)
1096        let built_config = match self.nodes.get(config_node_id) {
1097            Some((_, NodeType::Config(ConfigNodeType::NodeConfig(inputs)))) => {
1098                Some(ConfigOutput::Node(inputs.build()))
1099            }
1100            Some((_, NodeType::Config(ConfigNodeType::EdgeConfig(inputs)))) => {
1101                Some(ConfigOutput::Edge(inputs.build()))
1102            }
1103            Some((_, NodeType::Config(ConfigNodeType::PinConfig(inputs)))) => {
1104                Some(ConfigOutput::Pin(inputs.build()))
1105            }
1106            Some((_, NodeType::Config(ConfigNodeType::GraphConfig(inputs)))) => {
1107                Some(ConfigOutput::Graph(inputs.build()))
1108            }
1109            _ => None,
1110        };
1111
1112        let Some((_, node_type)) = self.nodes.get_mut(apply_node_id) else {
1113            return;
1114        };
1115
1116        if let NodeType::Config(ConfigNodeType::ApplyToGraph {
1117            has_node_config,
1118            has_edge_config,
1119            has_pin_config,
1120            has_graph_config,
1121        }) = node_type
1122        {
1123            if *apply_pin_label == pin::NODE_CONFIG {
1124                if matches!(&built_config, Some(ConfigOutput::Node(_))) {
1125                    *has_node_config = true;
1126                }
1127            } else if *apply_pin_label == pin::EDGE_CONFIG {
1128                if matches!(&built_config, Some(ConfigOutput::Edge(_))) {
1129                    *has_edge_config = true;
1130                }
1131            } else if *apply_pin_label == pin::PIN_CONFIG {
1132                if matches!(&built_config, Some(ConfigOutput::Pin(_))) {
1133                    *has_pin_config = true;
1134                }
1135            } else if *apply_pin_label == pin::GRAPH_CONFIG
1136                && matches!(&built_config, Some(ConfigOutput::Graph(_)))
1137            {
1138                *has_graph_config = true;
1139            }
1140        }
1141
1142        // Store the config for later application
1143        if let Some(config) = built_config {
1144            self.pending_configs
1145                .entry(apply_node_id.clone())
1146                .or_default()
1147                .push((*apply_pin_label, config));
1148        }
1149    }
1150
1151    /// Applies configs from ApplyToGraph nodes to the computed style
1152    fn apply_graph_configs(&mut self, computed: &mut ComputedStyle) {
1153        // Find ApplyToGraph nodes and apply their connected configs
1154        for (node_id, (_, node_type)) in &self.nodes {
1155            if let NodeType::Config(ConfigNodeType::ApplyToGraph {
1156                has_node_config,
1157                has_edge_config,
1158                has_pin_config,
1159                has_graph_config,
1160            }) = node_type
1161                && let Some(configs) = self.pending_configs.get(node_id)
1162            {
1163                for (_, config) in configs {
1164                    // Each config node's overlay wins over what is already accumulated.
1165                    match config {
1166                        ConfigOutput::Node(node) => {
1167                            if *has_node_config {
1168                                computed.node = node.merge(&computed.node);
1169                            }
1170                        }
1171                        ConfigOutput::Edge(edge) => {
1172                            if *has_edge_config {
1173                                computed.edge = edge.merge(&computed.edge);
1174                            }
1175                        }
1176                        ConfigOutput::Pin(pin) => {
1177                            if *has_pin_config {
1178                                computed.pin = pin.merge(&computed.pin);
1179                            }
1180                        }
1181                        ConfigOutput::Graph(graph) => {
1182                            if *has_graph_config {
1183                                computed.graph = graph.merge(&computed.graph);
1184                            }
1185                        }
1186                    }
1187                }
1188            }
1189        }
1190        // Clear pending configs after application
1191        self.pending_configs.clear();
1192    }
1193
1194    fn update(&mut self, message: ApplicationMessage) -> Task<ApplicationMessage> {
1195        match message {
1196            ApplicationMessage::Noop => Task::none(),
1197            ApplicationMessage::EdgeConnected { from, to } => {
1198                let edge_id = generate_edge_id();
1199                self.edges.insert(
1200                    edge_id.clone(),
1201                    EdgeData {
1202                        from_node: from.node_id,
1203                        from_pin: from.pin_id,
1204                        to_node: to.node_id,
1205                        to_pin: to.pin_id,
1206                    },
1207                );
1208                self.edge_order.push(edge_id);
1209                self.propagate_values();
1210                self.save_state();
1211                Task::none()
1212            }
1213            ApplicationMessage::EdgeDisconnected { from, to } => {
1214                // Find and remove the edge by matching from/to
1215                let edge_to_remove: Option<EdgeId> = self
1216                    .edges
1217                    .iter()
1218                    .find(|(_, e)| {
1219                        e.from_node == from.node_id
1220                            && e.from_pin == from.pin_id
1221                            && e.to_node == to.node_id
1222                            && e.to_pin == to.pin_id
1223                    })
1224                    .map(|(id, _)| id.clone());
1225
1226                if let Some(edge_id) = edge_to_remove {
1227                    self.edges.remove(&edge_id);
1228                    self.edge_order.retain(|id| id != &edge_id);
1229                }
1230                self.propagate_values();
1231                self.save_state();
1232                Task::none()
1233            }
1234            ApplicationMessage::ToggleCommandPalette => {
1235                self.command_palette_open = !self.command_palette_open;
1236                if !self.command_palette_open {
1237                    if let Some(original) = self.palette_original_theme.take() {
1238                        self.current_theme = original;
1239                    }
1240                    self.palette_preview_theme = None;
1241                    self.command_input.clear();
1242                    self.palette_view = PaletteView::Main;
1243                    self.palette_selected_index = 0;
1244                    Task::none()
1245                } else {
1246                    self.palette_original_theme = Some(self.current_theme.clone());
1247                    self.palette_view = PaletteView::Main;
1248                    self.palette_selected_index = 0;
1249                    focus_input()
1250                }
1251            }
1252            ApplicationMessage::CommandPaletteInput(input) => {
1253                self.command_input = input;
1254                self.palette_selected_index = 0;
1255                Task::none()
1256            }
1257            ApplicationMessage::ExecuteShortcut(cmd_id) => match cmd_id.as_str() {
1258                "add_node" => {
1259                    self.command_palette_open = true;
1260                    self.palette_original_theme = Some(self.current_theme.clone());
1261                    self.palette_view = PaletteView::Submenu("nodes".to_string());
1262                    self.palette_selected_index = 0;
1263                    self.command_input.clear();
1264                    focus_input()
1265                }
1266                "change_theme" => {
1267                    self.command_palette_open = true;
1268                    self.palette_original_theme = Some(self.current_theme.clone());
1269                    self.palette_view = PaletteView::Submenu("themes".to_string());
1270                    self.palette_selected_index = 0;
1271                    self.command_input.clear();
1272                    focus_input()
1273                }
1274                "export_state" => {
1275                    self.export_state_to_file();
1276                    Task::none()
1277                }
1278                _ => Task::none(),
1279            },
1280            ApplicationMessage::CommandPaletteNavigate(new_index) => {
1281                if !self.command_palette_open {
1282                    return Task::none();
1283                }
1284                self.palette_selected_index = new_index;
1285
1286                if let PaletteView::Submenu(ref submenu) = self.palette_view
1287                    && submenu == "themes"
1288                {
1289                    let (_, commands) = self.build_palette_commands();
1290                    if let Some(original_idx) = get_filtered_command_index(
1291                        &self.command_input,
1292                        &commands,
1293                        self.palette_selected_index,
1294                    ) {
1295                        let themes = Self::get_available_themes();
1296                        if original_idx < themes.len() {
1297                            self.palette_preview_theme = Some(themes[original_idx].clone());
1298                        }
1299                    }
1300                }
1301                Task::none()
1302            }
1303            ApplicationMessage::CommandPaletteNavigateUp => {
1304                if !self.command_palette_open {
1305                    return Task::none();
1306                }
1307                let (_, commands) = self.build_palette_commands();
1308                let filtered_count = get_filtered_count(&self.command_input, &commands);
1309                let new_index = navigate_up(self.palette_selected_index, filtered_count);
1310                self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1311            }
1312            ApplicationMessage::CommandPaletteNavigateDown => {
1313                if !self.command_palette_open {
1314                    return Task::none();
1315                }
1316                let (_, commands) = self.build_palette_commands();
1317                let filtered_count = get_filtered_count(&self.command_input, &commands);
1318                let new_index = navigate_down(self.palette_selected_index, filtered_count);
1319                self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1320            }
1321            ApplicationMessage::CommandPaletteSelect(index) => {
1322                if !self.command_palette_open {
1323                    return Task::none();
1324                }
1325                self.palette_selected_index = index;
1326                self.update(ApplicationMessage::CommandPaletteConfirm)
1327            }
1328            ApplicationMessage::CommandPaletteConfirm => {
1329                if !self.command_palette_open {
1330                    return Task::none();
1331                }
1332                let (_, commands) = self.build_palette_commands();
1333                let Some(original_idx) = get_filtered_command_index(
1334                    &self.command_input,
1335                    &commands,
1336                    self.palette_selected_index,
1337                ) else {
1338                    return Task::none();
1339                };
1340
1341                use iced_palette::CommandAction;
1342                let cmd = &commands[original_idx];
1343                match &cmd.action {
1344                    CommandAction::Message(msg) => {
1345                        let msg = msg.clone();
1346                        self.command_input.clear();
1347                        self.palette_selected_index = 0;
1348                        match msg {
1349                            ApplicationMessage::NavigateToSubmenu(submenu) => {
1350                                self.palette_view = PaletteView::Submenu(submenu);
1351                                focus_input()
1352                            }
1353                            ApplicationMessage::SpawnNode { node_type } => {
1354                                let new_id = generate_node_id();
1355                                let pos = self.spawn_position();
1356                                self.nodes.insert(new_id.clone(), (pos, node_type));
1357                                self.node_order.push(new_id.clone());
1358                                self.selected_nodes = HashSet::from([new_id]);
1359                                self.command_palette_open = false;
1360                                self.palette_view = PaletteView::Main;
1361                                self.save_state();
1362                                Task::none()
1363                            }
1364                            ApplicationMessage::ChangeTheme(theme) => {
1365                                self.current_theme = theme;
1366                                self.palette_preview_theme = None;
1367                                self.palette_original_theme = None;
1368                                self.command_palette_open = false;
1369                                self.palette_view = PaletteView::Main;
1370                                // Theme nodes feed palette colors into configs;
1371                                // recompute so styling follows the new theme.
1372                                self.propagate_values();
1373                                self.save_state();
1374                                Task::none()
1375                            }
1376                            ApplicationMessage::ExportState => {
1377                                self.command_palette_open = false;
1378                                self.palette_view = PaletteView::Main;
1379                                self.export_state_to_file();
1380                                Task::none()
1381                            }
1382                            ApplicationMessage::Reset => {
1383                                self.reset_to_default();
1384                                Task::none()
1385                            }
1386                            _ => Task::none(),
1387                        }
1388                    }
1389                    _ => Task::none(),
1390                }
1391            }
1392            ApplicationMessage::CommandPaletteCancel => {
1393                if !self.command_palette_open {
1394                    return Task::none();
1395                }
1396                if let Some(original) = self.palette_original_theme.take() {
1397                    self.current_theme = original;
1398                }
1399                self.palette_preview_theme = None;
1400                self.command_palette_open = false;
1401                self.command_input.clear();
1402                self.palette_view = PaletteView::Main;
1403                self.palette_selected_index = 0;
1404                Task::none()
1405            }
1406            ApplicationMessage::SpawnNode { node_type } => {
1407                let new_id = generate_node_id();
1408                let pos = self.spawn_position();
1409                self.nodes.insert(new_id.clone(), (pos, node_type));
1410                self.node_order.push(new_id.clone());
1411                self.selected_nodes = HashSet::from([new_id]);
1412                self.command_palette_open = false;
1413                self.command_input.clear();
1414                self.palette_view = PaletteView::Main;
1415                self.save_state();
1416                Task::none()
1417            }
1418            ApplicationMessage::CameraChanged { position, zoom } => {
1419                self.camera_position = position;
1420                self.camera_zoom = zoom;
1421                self.save_state();
1422                Task::none()
1423            }
1424            ApplicationMessage::WindowResized(size) => {
1425                self.viewport_size = size;
1426                self.window_size = Some((size.width as u32, size.height as u32));
1427                // Query maximize state on resize - it may have changed
1428                window::oldest()
1429                    .and_then(window::is_maximized)
1430                    .map(ApplicationMessage::WindowMaximizedChanged)
1431            }
1432            ApplicationMessage::WindowMoved(position) => {
1433                self.window_position = Some((position.x as i32, position.y as i32));
1434                self.save_state();
1435                Task::none()
1436            }
1437            ApplicationMessage::WindowMaximizedChanged(maximized) => {
1438                self.window_maximized = Some(maximized);
1439                self.save_state();
1440                Task::none()
1441            }
1442            ApplicationMessage::ChangeTheme(theme) => {
1443                self.current_theme = theme;
1444                self.command_palette_open = false;
1445                self.command_input.clear();
1446                self.palette_view = PaletteView::Main;
1447                // Theme nodes feed palette colors into configs; recompute so
1448                // styling follows the new theme.
1449                self.propagate_values();
1450                self.save_state();
1451                Task::none()
1452            }
1453            ApplicationMessage::NavigateToSubmenu(submenu) => {
1454                self.palette_view = PaletteView::Submenu(submenu);
1455                self.command_input.clear();
1456                focus_input()
1457            }
1458            ApplicationMessage::NavigateBack => {
1459                self.palette_view = PaletteView::Main;
1460                self.command_input.clear();
1461                focus_input()
1462            }
1463            ApplicationMessage::ExportState => {
1464                self.export_state_to_file();
1465                Task::none()
1466            }
1467            ApplicationMessage::Reset => {
1468                self.reset_to_default();
1469                Task::none()
1470            }
1471            ApplicationMessage::FocusNext => iced::widget::operation::focus_next(),
1472            ApplicationMessage::FocusPrevious => iced::widget::operation::focus_previous(),
1473            ApplicationMessage::SelectionChanged(node_ids) => {
1474                self.selected_nodes = node_ids.into_iter().collect();
1475                Task::none()
1476            }
1477            ApplicationMessage::CloneNodes(node_ids) => {
1478                let offset = Vector::new(50.0, 50.0);
1479                let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
1480                let mut new_ids = Vec::new();
1481
1482                // Clone selected nodes
1483                for old_id in &node_ids {
1484                    if let Some((pos, node_type)) = self.nodes.get(old_id) {
1485                        let new_id = generate_node_id();
1486                        let new_pos = Point::new(pos.x + offset.x, pos.y + offset.y);
1487                        self.nodes
1488                            .insert(new_id.clone(), (new_pos, node_type.clone()));
1489                        self.node_order.push(new_id.clone());
1490                        id_map.insert(old_id.clone(), new_id.clone());
1491                        new_ids.push(new_id);
1492                    }
1493                }
1494
1495                // Clone edges between selected nodes
1496                let edges_to_clone: Vec<_> = self
1497                    .edges
1498                    .iter()
1499                    .filter(|(_, e)| {
1500                        node_ids.contains(&e.from_node) && node_ids.contains(&e.to_node)
1501                    })
1502                    .map(|(_, e)| e.clone())
1503                    .collect();
1504
1505                for edge in edges_to_clone {
1506                    if let (Some(new_from), Some(new_to)) =
1507                        (id_map.get(&edge.from_node), id_map.get(&edge.to_node))
1508                    {
1509                        let new_edge_id = generate_edge_id();
1510                        self.edges.insert(
1511                            new_edge_id.clone(),
1512                            EdgeData {
1513                                from_node: new_from.clone(),
1514                                from_pin: edge.from_pin,
1515                                to_node: new_to.clone(),
1516                                to_pin: edge.to_pin,
1517                            },
1518                        );
1519                        self.edge_order.push(new_edge_id);
1520                    }
1521                }
1522
1523                self.selected_nodes = new_ids.into_iter().collect();
1524                self.propagate_values();
1525                self.save_state();
1526                Task::none()
1527            }
1528            ApplicationMessage::DeleteNodes(node_ids) => {
1529                // With NanoIDs, deletion is simple - no index remapping needed!
1530                for node_id in &node_ids {
1531                    // Remove node
1532                    self.nodes.remove(node_id);
1533                    self.node_order.retain(|id| id != node_id);
1534
1535                    // Remove edges connected to this node
1536                    let edges_to_remove: Vec<_> = self
1537                        .edges
1538                        .iter()
1539                        .filter(|(_, e)| &e.from_node == node_id || &e.to_node == node_id)
1540                        .map(|(id, _)| id.clone())
1541                        .collect();
1542
1543                    for edge_id in edges_to_remove {
1544                        self.edges.remove(&edge_id);
1545                        self.edge_order.retain(|id| id != &edge_id);
1546                    }
1547                }
1548
1549                self.selected_nodes.clear();
1550                self.propagate_values();
1551                self.save_state();
1552                Task::none()
1553            }
1554            ApplicationMessage::NodesMoved { delta, node_ids } => {
1555                for node_id in node_ids {
1556                    if let Some((pos, _)) = self.nodes.get_mut(&node_id) {
1557                        pos.x += delta.x;
1558                        pos.y += delta.y;
1559                    }
1560                }
1561                self.save_state();
1562                Task::none()
1563            }
1564            ApplicationMessage::SliderChanged { node_id, value } => {
1565                if let Some((_, NodeType::Input(InputNodeType::FloatSlider { value: v, .. }))) =
1566                    self.nodes.get_mut(&node_id)
1567                {
1568                    *v = value;
1569                    self.propagate_values();
1570                }
1571                Task::none()
1572            }
1573            ApplicationMessage::IntSliderChanged { node_id, value } => {
1574                if let Some((_, NodeType::Input(InputNodeType::IntSlider { value: v, .. }))) =
1575                    self.nodes.get_mut(&node_id)
1576                {
1577                    *v = value;
1578                    self.propagate_values();
1579                }
1580                Task::none()
1581            }
1582            ApplicationMessage::BoolChanged { node_id, value } => {
1583                if let Some((_, NodeType::Input(InputNodeType::BoolToggle { value: v, .. }))) =
1584                    self.nodes.get_mut(&node_id)
1585                {
1586                    *v = value;
1587                    self.propagate_values();
1588                }
1589                Task::none()
1590            }
1591            ApplicationMessage::EdgeCurveChanged { node_id, value } => {
1592                if let Some((_, NodeType::Input(InputNodeType::EdgeCurveSelector { value: v }))) =
1593                    self.nodes.get_mut(&node_id)
1594                {
1595                    *v = value;
1596                    self.propagate_values();
1597                }
1598                Task::none()
1599            }
1600            ApplicationMessage::PinShapeChanged { node_id, value } => {
1601                if let Some((_, NodeType::Input(InputNodeType::PinShapeSelector { value: v }))) =
1602                    self.nodes.get_mut(&node_id)
1603                {
1604                    *v = value;
1605                    self.propagate_values();
1606                }
1607                Task::none()
1608            }
1609            ApplicationMessage::PatternTypeChanged { node_id, value } => {
1610                if let Some((_, NodeType::Input(InputNodeType::PatternTypeSelector { value: v }))) =
1611                    self.nodes.get_mut(&node_id)
1612                {
1613                    *v = value;
1614                    self.propagate_values();
1615                }
1616                Task::none()
1617            }
1618            ApplicationMessage::TilingKindChanged { node_id, value } => {
1619                if let Some((_, NodeType::Input(InputNodeType::TilingKindSelector { value: v }))) =
1620                    self.nodes.get_mut(&node_id)
1621                {
1622                    *v = value;
1623                    self.propagate_values();
1624                }
1625                Task::none()
1626            }
1627            ApplicationMessage::ColorChanged { node_id, color } => {
1628                if let Some((_, node_type)) = self.nodes.get_mut(&node_id) {
1629                    match node_type {
1630                        NodeType::Input(InputNodeType::ColorPicker { color: c }) => {
1631                            *c = color;
1632                            self.propagate_values();
1633                        }
1634                        NodeType::Input(InputNodeType::ColorPreset { color: c }) => {
1635                            *c = color;
1636                            self.propagate_values();
1637                        }
1638                        _ => {}
1639                    }
1640                }
1641                Task::none()
1642            }
1643            ApplicationMessage::ToggleNodeExpanded { node_id } => {
1644                if self.expanded_nodes.contains(&node_id) {
1645                    self.expanded_nodes.remove(&node_id);
1646                } else {
1647                    self.expanded_nodes.insert(node_id);
1648                }
1649                Task::none()
1650            }
1651            ApplicationMessage::UpdateFloatSliderConfig { node_id, config } => {
1652                if let Some((_, NodeType::Input(InputNodeType::FloatSlider { config: c, value }))) =
1653                    self.nodes.get_mut(&node_id)
1654                {
1655                    // Clamp value to new range if needed
1656                    *value = value.clamp(config.min, config.max);
1657                    *c = config;
1658                }
1659                Task::none()
1660            }
1661            ApplicationMessage::UpdateIntSliderConfig { node_id, config } => {
1662                if let Some((_, NodeType::Input(InputNodeType::IntSlider { config: c, value }))) =
1663                    self.nodes.get_mut(&node_id)
1664                {
1665                    // Clamp value to new range if needed
1666                    *value = (*value).clamp(config.min, config.max);
1667                    *c = config;
1668                }
1669                Task::none()
1670            }
1671            ApplicationMessage::ToggleEdgeSection { node_id, section } => {
1672                let sections = self
1673                    .edge_config_sections
1674                    .entry(node_id)
1675                    .or_insert_with(EdgeSections::new_all_expanded);
1676                match section {
1677                    EdgeSection::Stroke => sections.stroke = !sections.stroke,
1678                    EdgeSection::Pattern => sections.pattern = !sections.pattern,
1679                    EdgeSection::Border => sections.border = !sections.border,
1680                    EdgeSection::Shadow => sections.shadow = !sections.shadow,
1681                }
1682                Task::none()
1683            }
1684            ApplicationMessage::ToggleNodeSection { node_id, section } => {
1685                let sections = self
1686                    .node_config_sections
1687                    .entry(node_id)
1688                    .or_insert_with(NodeSections::new_all_expanded);
1689                match section {
1690                    NodeSection::Fill => sections.fill = !sections.fill,
1691                    NodeSection::Border => sections.border = !sections.border,
1692                    NodeSection::Pattern => sections.pattern = !sections.pattern,
1693                    NodeSection::Shadow => sections.shadow = !sections.shadow,
1694                }
1695                Task::none()
1696            }
1697        }
1698    }
1699
1700    fn theme(&self) -> Theme {
1701        self.palette_preview_theme
1702            .as_ref()
1703            .unwrap_or(&self.current_theme)
1704            .clone()
1705    }
1706
1707    fn get_main_commands_with_shortcuts() -> Vec<Command<ApplicationMessage>> {
1708        vec![
1709            command("add_node", "Add Node")
1710                .description("Add a new node to the graph")
1711                .shortcut(Shortcut::cmd('n'))
1712                .action(ApplicationMessage::ExecuteShortcut("add_node".to_string())),
1713            command("change_theme", "Change Theme")
1714                .description("Switch to a different color theme")
1715                .shortcut(Shortcut::cmd('t'))
1716                .action(ApplicationMessage::ExecuteShortcut(
1717                    "change_theme".to_string(),
1718                )),
1719            command("export_state", "Export State")
1720                .description("Export graph state to file for Claude")
1721                .shortcut(Shortcut::cmd('e'))
1722                .action(ApplicationMessage::ExecuteShortcut(
1723                    "export_state".to_string(),
1724                )),
1725        ]
1726    }
1727
1728    fn get_available_themes() -> Vec<Theme> {
1729        vec![
1730            Theme::Dark,
1731            Theme::Light,
1732            Theme::Dracula,
1733            Theme::Nord,
1734            Theme::SolarizedLight,
1735            Theme::SolarizedDark,
1736            Theme::GruvboxLight,
1737            Theme::GruvboxDark,
1738            Theme::CatppuccinLatte,
1739            Theme::CatppuccinFrappe,
1740            Theme::CatppuccinMacchiato,
1741            Theme::CatppuccinMocha,
1742            Theme::TokyoNight,
1743            Theme::TokyoNightStorm,
1744            Theme::TokyoNightLight,
1745            Theme::KanagawaWave,
1746            Theme::KanagawaDragon,
1747            Theme::KanagawaLotus,
1748            Theme::Moonfly,
1749            Theme::Nightfly,
1750            Theme::Oxocarbon,
1751            Theme::Ferra,
1752        ]
1753    }
1754
1755    fn get_theme_name(theme: &Theme) -> &'static str {
1756        match theme {
1757            Theme::Dark => "Dark",
1758            Theme::Light => "Light",
1759            Theme::Dracula => "Dracula",
1760            Theme::Nord => "Nord",
1761            Theme::SolarizedLight => "Solarized Light",
1762            Theme::SolarizedDark => "Solarized Dark",
1763            Theme::GruvboxLight => "Gruvbox Light",
1764            Theme::GruvboxDark => "Gruvbox Dark",
1765            Theme::CatppuccinLatte => "Catppuccin Latte",
1766            Theme::CatppuccinFrappe => "Catppuccin Frappe",
1767            Theme::CatppuccinMacchiato => "Catppuccin Macchiato",
1768            Theme::CatppuccinMocha => "Catppuccin Mocha",
1769            Theme::TokyoNight => "Tokyo Night",
1770            Theme::TokyoNightStorm => "Tokyo Night Storm",
1771            Theme::TokyoNightLight => "Tokyo Night Light",
1772            Theme::KanagawaWave => "Kanagawa Wave",
1773            Theme::KanagawaDragon => "Kanagawa Dragon",
1774            Theme::KanagawaLotus => "Kanagawa Lotus",
1775            Theme::Moonfly => "Moonfly",
1776            Theme::Nightfly => "Nightfly",
1777            Theme::Oxocarbon => "Oxocarbon",
1778            Theme::Ferra => "Ferra",
1779            _ => "Unknown",
1780        }
1781    }
1782
1783    fn view(&self) -> iced::Element<'_, ApplicationMessage> {
1784        use iced_nodegraph::{NodeGraph, PinRef};
1785
1786        // Use preview theme if active (for theme selection), otherwise current theme
1787        let theme = self
1788            .palette_preview_theme
1789            .as_ref()
1790            .unwrap_or(&self.current_theme);
1791
1792        // Graph-wide node defaults - combined with per-node overlays via merge()
1793        let node_defaults = NodeOverlay::new().corner_radius(8.0).opacity(0.88);
1794        // The dragging edge preview uses the graph-wide computed edge overlay
1795        // (e.g. from an EdgeConfig -> ApplyToGraph chain).
1796        let drag_overlay = self.computed_style.edge.clone();
1797        // Canvas chrome (background + tiling) from a GraphConfig -> ApplyToGraph
1798        // chain, layered over the theme-derived base each frame.
1799        let graph_overlay = self.computed_style.graph.clone();
1800
1801        let mut ng: NodeGraph<
1802            '_,
1803            NodeId,
1804            PinLabel,
1805            ::std::any::TypeId,
1806            ApplicationMessage,
1807            Theme,
1808            iced::Renderer,
1809            EdgeId,
1810        > = NodeGraph::default();
1811
1812        ng = ng
1813            .on_connect(
1814                |from: PinRef<NodeId, PinLabel>, to: PinRef<NodeId, PinLabel>| {
1815                    ApplicationMessage::EdgeConnected { from, to }
1816                },
1817            )
1818            .on_disconnect(
1819                |from: PinRef<NodeId, PinLabel>, to: PinRef<NodeId, PinLabel>| {
1820                    ApplicationMessage::EdgeDisconnected { from, to }
1821                },
1822            )
1823            .on_move(|delta, node_ids| ApplicationMessage::NodesMoved { delta, node_ids })
1824            .on_select(ApplicationMessage::SelectionChanged)
1825            .on_clone(ApplicationMessage::CloneNodes)
1826            .on_delete(ApplicationMessage::DeleteNodes)
1827            .on_pan(|position, zoom| ApplicationMessage::CameraChanged { position, zoom })
1828            .view(self.camera_position, self.camera_zoom)
1829            // A connection is valid only between opposite directions (output ->
1830            // input) carrying the same data type (the pin's TypeId marker). Color
1831            // and ColorQuad share the `ColorData` marker, so a color pin accepts
1832            // both a picker and the ColorQuad builder; Vec2 only matches Vec2.
1833            .can_connect(|from, to| from.direction() != to.direction() && from.info() == to.info())
1834            // Selection highlight and pending-cut feedback are handled internally
1835            // by the widget; per-node/edge styling flows through push_*_styled.
1836            .box_select_style(|_theme| {
1837                (
1838                    iced::Color::from_rgba(0.3, 0.6, 1.0, 0.15), // fill
1839                    iced::Color::from_rgb(0.3, 0.6, 1.0),        // border
1840                )
1841            })
1842            .cutting_tool_style(|_theme| iced::Color::from_rgb(1.0, 0.3, 0.3))
1843            .dragging_edge_style(move |theme, source| {
1844                // The loose edge takes the held pin's data-type color on both ends.
1845                let base = EdgeStyle {
1846                    stroke_color: ColorQuad::solid(pin_color_for(*source.info())),
1847                    ..default_edge_style(theme, EdgeStatus::Idle)
1848                };
1849                drag_overlay.resolve_over(base)
1850            })
1851            .graph_style(move |theme| {
1852                // With no GraphConfig connected the overlay is empty and this is
1853                // exactly the widget's own default (`GraphStyle::from_theme`).
1854                graph_overlay.resolve_over(iced_nodegraph::GraphStyle::from_theme(theme))
1855            });
1856
1857        // Add all nodes from state (in order)
1858        for node_id in &self.node_order {
1859            let Some((position, node_type)) = self.nodes.get(node_id) else {
1860                continue;
1861            };
1862            let node_id_clone = node_id.clone();
1863            let element: iced::Element<'_, ApplicationMessage> = match node_type {
1864                NodeType::Workflow(name) => node(name.as_str(), theme),
1865                NodeType::Input(input) => match input {
1866                    InputNodeType::FloatSlider { config, value } => {
1867                        let id = node_id_clone.clone();
1868                        let expanded = self.expanded_nodes.contains(node_id);
1869                        float_slider_node(
1870                            theme,
1871                            *value,
1872                            config,
1873                            expanded,
1874                            {
1875                                let id = id.clone();
1876                                move |v| ApplicationMessage::SliderChanged {
1877                                    node_id: id.clone(),
1878                                    value: v,
1879                                }
1880                            },
1881                            {
1882                                let id = id.clone();
1883                                move |cfg| ApplicationMessage::UpdateFloatSliderConfig {
1884                                    node_id: id.clone(),
1885                                    config: cfg,
1886                                }
1887                            },
1888                            ApplicationMessage::ToggleNodeExpanded { node_id: id },
1889                        )
1890                    }
1891                    InputNodeType::IntSlider { config, value } => {
1892                        let id = node_id_clone.clone();
1893                        let expanded = self.expanded_nodes.contains(node_id);
1894                        int_slider_node(
1895                            theme,
1896                            *value,
1897                            config,
1898                            expanded,
1899                            {
1900                                let id = id.clone();
1901                                move |v| ApplicationMessage::IntSliderChanged {
1902                                    node_id: id.clone(),
1903                                    value: v,
1904                                }
1905                            },
1906                            {
1907                                let id = id.clone();
1908                                move |cfg| ApplicationMessage::UpdateIntSliderConfig {
1909                                    node_id: id.clone(),
1910                                    config: cfg,
1911                                }
1912                            },
1913                            ApplicationMessage::ToggleNodeExpanded { node_id: id },
1914                        )
1915                    }
1916                    InputNodeType::BoolToggle { config, value } => {
1917                        let id = node_id_clone.clone();
1918                        bool_toggle_node(theme, *value, config, move |v| {
1919                            ApplicationMessage::BoolChanged {
1920                                node_id: id.clone(),
1921                                value: v,
1922                            }
1923                        })
1924                    }
1925                    InputNodeType::EdgeCurveSelector { value } => {
1926                        let id = node_id_clone.clone();
1927                        edge_curve_selector_node(theme, *value, move |v| {
1928                            ApplicationMessage::EdgeCurveChanged {
1929                                node_id: id.clone(),
1930                                value: v,
1931                            }
1932                        })
1933                    }
1934                    InputNodeType::PinShapeSelector { value } => {
1935                        let id = node_id_clone.clone();
1936                        pin_shape_selector_node(theme, *value, move |v| {
1937                            ApplicationMessage::PinShapeChanged {
1938                                node_id: id.clone(),
1939                                value: v,
1940                            }
1941                        })
1942                    }
1943                    InputNodeType::PatternTypeSelector { value } => {
1944                        let id = node_id_clone.clone();
1945                        pattern_type_selector_node(theme, *value, move |v| {
1946                            ApplicationMessage::PatternTypeChanged {
1947                                node_id: id.clone(),
1948                                value: v,
1949                            }
1950                        })
1951                    }
1952                    InputNodeType::TilingKindSelector { value } => {
1953                        let id = node_id_clone.clone();
1954                        tiling_kind_selector_node(theme, *value, move |v| {
1955                            ApplicationMessage::TilingKindChanged {
1956                                node_id: id.clone(),
1957                                value: v,
1958                            }
1959                        })
1960                    }
1961                    InputNodeType::ColorPicker { color } => {
1962                        let id = node_id_clone.clone();
1963                        color_picker_node(theme, *color, move |c| {
1964                            ApplicationMessage::ColorChanged {
1965                                node_id: id.clone(),
1966                                color: c,
1967                            }
1968                        })
1969                    }
1970                    InputNodeType::ColorPreset { color } => {
1971                        let id = node_id_clone.clone();
1972                        color_preset_node(theme, *color, move |c| {
1973                            ApplicationMessage::ColorChanged {
1974                                node_id: id.clone(),
1975                                color: c,
1976                            }
1977                        })
1978                    }
1979                },
1980                NodeType::Config(config) => match config {
1981                    ConfigNodeType::NodeConfig(inputs) => {
1982                        let id = node_id_clone.clone();
1983                        let sections = self
1984                            .node_config_sections
1985                            .get(&id)
1986                            .cloned()
1987                            .unwrap_or_else(NodeSections::new_all_expanded);
1988                        node_config_node(theme, inputs, &sections, move |section| {
1989                            ApplicationMessage::ToggleNodeSection {
1990                                node_id: id.clone(),
1991                                section,
1992                            }
1993                        })
1994                    }
1995                    ConfigNodeType::EdgeConfig(inputs) => {
1996                        let id = node_id_clone.clone();
1997                        let sections = self
1998                            .edge_config_sections
1999                            .get(&id)
2000                            .cloned()
2001                            .unwrap_or_else(EdgeSections::new_all_expanded);
2002                        edge_config_node(theme, inputs, &sections, move |section| {
2003                            ApplicationMessage::ToggleEdgeSection {
2004                                node_id: id.clone(),
2005                                section,
2006                            }
2007                        })
2008                    }
2009                    ConfigNodeType::PinConfig(inputs) => pin_config_node(theme, inputs),
2010                    ConfigNodeType::GraphConfig(inputs) => graph_config_node(theme, inputs),
2011                    ConfigNodeType::ApplyToGraph {
2012                        has_node_config,
2013                        has_edge_config,
2014                        has_pin_config,
2015                        has_graph_config,
2016                    } => apply_to_graph_node(
2017                        theme,
2018                        *has_node_config,
2019                        *has_edge_config,
2020                        *has_pin_config,
2021                        *has_graph_config,
2022                    ),
2023                    ConfigNodeType::ApplyToNode {
2024                        has_node_config,
2025                        target_id,
2026                    } => apply_to_node_node(theme, *has_node_config, *target_id),
2027                },
2028                NodeType::Math(state) => math_node(theme, state),
2029                NodeType::ColorQuad(state) => color_quad_node(theme, state),
2030                NodeType::Vec2(state) => vec2_node(theme, state),
2031                NodeType::Theme => theme_node(theme),
2032                NodeType::ThemeExtended => theme_extended_node(theme),
2033            };
2034
2035            // Apply the computed node-config overlay to every node ("Apply to
2036            // Graph" means the whole graph); the per-node closure resolves it
2037            // over the theme base. With no config connected, computed_style.node
2038            // is empty and this is just node_defaults.
2039            let overlay = self.computed_style.node.merge(&node_defaults);
2040            // Pins: the per-pin data-type color wins, then the global Pin Config
2041            // overlay (radius/shape/border from an ApplyToGraph chain), then the
2042            // status default fills the rest.
2043            let pin_overlay = self.computed_style.pin.clone();
2044            ng.push_node(
2045                ng_node(node_id.clone(), *position, element)
2046                    .style(move |theme, status| {
2047                        overlay.resolve_over(default_node_style(theme, status))
2048                    })
2049                    .pin_style(move |theme, pin, _other, status| {
2050                        PinOverlay::new()
2051                            .color(pin_color_for(*pin.info()))
2052                            .merge(&pin_overlay)
2053                            .resolve_over(default_pin_style(theme, status))
2054                    }),
2055            );
2056        }
2057
2058        // Add stored edges with the computed overlay resolved per edge.
2059        let edge_overlay = self.computed_style.edge.clone();
2060        for edge_id in &self.edge_order {
2061            if let Some(edge_data) = self.edges.get(edge_id) {
2062                let from = PinRef::new(edge_data.from_node.clone(), edge_data.from_pin);
2063                let to = PinRef::new(edge_data.to_node.clone(), edge_data.to_pin);
2064                let overlay = edge_overlay.clone();
2065                ng.push_edge(ng_edge(from, to, edge_id.clone()).style(
2066                    move |theme, status, start, end| {
2067                        // Edges follow their connected pins' data-type colors; the
2068                        // Edge Config overlay (if set) overrides the inherited stroke.
2069                        let base = EdgeStyle {
2070                            stroke_color: ColorQuad::arc(
2071                                pin_color_for(*start.info()),
2072                                pin_color_for(*end.info()),
2073                            ),
2074                            ..default_edge_style(theme, status)
2075                        };
2076                        overlay.resolve_over(base)
2077                    },
2078                ));
2079            }
2080        }
2081
2082        let graph_view: iced::Element<'_, ApplicationMessage> = ng.into();
2083
2084        // Always use the same widget structure to preserve NodeGraph state
2085        // The command palette is conditionally shown as an overlay
2086        let overlay: iced::Element<'_, ApplicationMessage> = if self.command_palette_open {
2087            let (_, commands) = self.build_palette_commands();
2088            // `opaque` blocks wheel events from reaching the NodeGraph behind
2089            // the palette; `command_palette`'s own `mouse_area` only captures
2090            // `on_press`, not scroll.
2091            opaque(command_palette(
2092                &self.command_input,
2093                &commands,
2094                self.palette_selected_index,
2095                ApplicationMessage::CommandPaletteInput,
2096                ApplicationMessage::CommandPaletteSelect,
2097                ApplicationMessage::CommandPaletteNavigate,
2098                || ApplicationMessage::CommandPaletteCancel,
2099            ))
2100        } else {
2101            // Invisible placeholder to maintain widget tree structure
2102            container(text("")).width(0).height(0).into()
2103        };
2104
2105        stack!(graph_view, overlay)
2106            .width(Length::Fill)
2107            .height(Length::Fill)
2108            .into()
2109    }
2110
2111    fn build_palette_commands(&self) -> (&'static str, Vec<Command<ApplicationMessage>>) {
2112        match &self.palette_view {
2113            PaletteView::Main => {
2114                let commands = vec![
2115                    command("add_node", "Add Node")
2116                        .description("Add a new node to the graph")
2117                        .shortcut(Shortcut::cmd('n'))
2118                        .action(ApplicationMessage::NavigateToSubmenu("nodes".to_string())),
2119                    command("change_theme", "Change Theme")
2120                        .description("Switch to a different color theme")
2121                        .shortcut(Shortcut::cmd('t'))
2122                        .action(ApplicationMessage::NavigateToSubmenu("themes".to_string())),
2123                    command("export_state", "Export State")
2124                        .description("Export graph state to file for Claude")
2125                        .shortcut(Shortcut::cmd('e'))
2126                        .action(ApplicationMessage::ExportState),
2127                    command("reset", "Reset")
2128                        .description("Reset the app to its initial state")
2129                        .action(ApplicationMessage::Reset),
2130                ];
2131                ("Command Palette", commands)
2132            }
2133            PaletteView::Submenu(submenu) if submenu == "nodes" => {
2134                let commands = vec![
2135                    // Workflow nodes
2136                    command("workflow", "Workflow Nodes")
2137                        .description("Original demo nodes")
2138                        .action(ApplicationMessage::NavigateToSubmenu(
2139                            "workflow_nodes".to_string(),
2140                        )),
2141                    // Input nodes
2142                    command("inputs", "Input Nodes")
2143                        .description("Sliders, color pickers, etc.")
2144                        .action(ApplicationMessage::NavigateToSubmenu(
2145                            "input_nodes".to_string(),
2146                        )),
2147                    // Math nodes
2148                    command("math", "Math Nodes")
2149                        .description("Add, Subtract, Multiply, Divide")
2150                        .action(ApplicationMessage::NavigateToSubmenu(
2151                            "math_nodes".to_string(),
2152                        )),
2153                    // Config nodes
2154                    command("config", "Style Config Nodes")
2155                        .description("Configure node and edge styling")
2156                        .action(ApplicationMessage::NavigateToSubmenu(
2157                            "config_nodes".to_string(),
2158                        )),
2159                ];
2160                ("Add Node", commands)
2161            }
2162            PaletteView::Submenu(submenu) if submenu == "workflow_nodes" => {
2163                let workflow_nodes = vec!["email_trigger", "email_parser", "filter", "calendar"];
2164                let commands = workflow_nodes
2165                    .into_iter()
2166                    .map(|name| {
2167                        command(name, name).action(ApplicationMessage::SpawnNode {
2168                            node_type: NodeType::Workflow(name.to_string()),
2169                        })
2170                    })
2171                    .collect();
2172                ("Workflow Nodes", commands)
2173            }
2174            PaletteView::Submenu(submenu) if submenu == "input_nodes" => {
2175                let commands = vec![
2176                    command("float_slider", "Float Slider")
2177                        .description("Generic float slider (0-20)")
2178                        .action(ApplicationMessage::SpawnNode {
2179                            node_type: NodeType::Input(InputNodeType::FloatSlider {
2180                                config: FloatSliderConfig::default(),
2181                                value: 5.0,
2182                            }),
2183                        }),
2184                    command("pattern_angle", "Pattern Angle")
2185                        .description("Angle for Arrowed/Angled patterns (-90 to 90 degrees)")
2186                        .action(ApplicationMessage::SpawnNode {
2187                            node_type: NodeType::Input(InputNodeType::FloatSlider {
2188                                config: FloatSliderConfig::pattern_angle(),
2189                                value: 45.0,
2190                            }),
2191                        }),
2192                    command("color_picker", "Color Picker (RGB)")
2193                        .description("Full RGB color picker with sliders")
2194                        .action(ApplicationMessage::SpawnNode {
2195                            node_type: NodeType::Input(InputNodeType::ColorPicker {
2196                                color: Color::from_rgb(0.5, 0.5, 0.5),
2197                            }),
2198                        }),
2199                    command("color_preset", "Color Presets")
2200                        .description("Quick color selection from presets")
2201                        .action(ApplicationMessage::SpawnNode {
2202                            node_type: NodeType::Input(InputNodeType::ColorPreset {
2203                                color: Color::from_rgb(0.5, 0.5, 0.5),
2204                            }),
2205                        }),
2206                    command("int_slider", "Int Slider")
2207                        .description("Integer slider (0-100)")
2208                        .action(ApplicationMessage::SpawnNode {
2209                            node_type: NodeType::Input(InputNodeType::IntSlider {
2210                                config: IntSliderConfig::default(),
2211                                value: 50,
2212                            }),
2213                        }),
2214                    command("bool_toggle", "Boolean Toggle")
2215                        .description("Toggle for boolean values")
2216                        .action(ApplicationMessage::SpawnNode {
2217                            node_type: NodeType::Input(InputNodeType::BoolToggle {
2218                                config: BoolToggleConfig::default(),
2219                                value: true,
2220                            }),
2221                        }),
2222                    command("edge_curve", "Edge Curve Selector")
2223                        .description("Select edge curve (Bezier, Line, Orthogonal)")
2224                        .action(ApplicationMessage::SpawnNode {
2225                            node_type: NodeType::Input(InputNodeType::EdgeCurveSelector {
2226                                value: EdgeCurve::BezierCubic,
2227                            }),
2228                        }),
2229                    command("pin_shape", "Pin Shape Selector")
2230                        .description("Select pin shape (Circle, Square, Diamond)")
2231                        .action(ApplicationMessage::SpawnNode {
2232                            node_type: NodeType::Input(InputNodeType::PinShapeSelector {
2233                                value: PinShape::Circle,
2234                            }),
2235                        }),
2236                    command("pattern_type", "Pattern Type Selector")
2237                        .description("Select edge pattern (Solid, Dashed, Dotted)")
2238                        .action(ApplicationMessage::SpawnNode {
2239                            node_type: NodeType::Input(InputNodeType::PatternTypeSelector {
2240                                value: PatternType::Solid,
2241                            }),
2242                        }),
2243                    command("tiling_kind", "Tiling Kind Selector")
2244                        .description("Select canvas tiling (Grid, Dots, Triangles, Hex)")
2245                        .action(ApplicationMessage::SpawnNode {
2246                            node_type: NodeType::Input(InputNodeType::TilingKindSelector {
2247                                value: TilingKind::Grid,
2248                            }),
2249                        }),
2250                    command("theme", "Theme")
2251                        .description("Active theme's basic palette as color outputs")
2252                        .action(ApplicationMessage::SpawnNode {
2253                            node_type: NodeType::Theme,
2254                        }),
2255                    command("theme_extended", "Theme Extended")
2256                        .description("Extended palette (base/weak/strong) as color outputs")
2257                        .action(ApplicationMessage::SpawnNode {
2258                            node_type: NodeType::ThemeExtended,
2259                        }),
2260                ];
2261                ("Input Nodes", commands)
2262            }
2263            PaletteView::Submenu(submenu) if submenu == "math_nodes" => {
2264                let commands = vec![
2265                    command("add", "Add").description("A + B").action(
2266                        ApplicationMessage::SpawnNode {
2267                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Add)),
2268                        },
2269                    ),
2270                    command("subtract", "Subtract").description("A - B").action(
2271                        ApplicationMessage::SpawnNode {
2272                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Subtract)),
2273                        },
2274                    ),
2275                    command("multiply", "Multiply").description("A * B").action(
2276                        ApplicationMessage::SpawnNode {
2277                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Multiply)),
2278                        },
2279                    ),
2280                    command("divide", "Divide").description("A / B").action(
2281                        ApplicationMessage::SpawnNode {
2282                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Divide)),
2283                        },
2284                    ),
2285                ];
2286                ("Math Nodes", commands)
2287            }
2288            PaletteView::Submenu(submenu) if submenu == "config_nodes" => {
2289                let commands = vec![
2290                    command("node_config", "Node Config")
2291                        .description("Node config with all fields and inheritance")
2292                        .action(ApplicationMessage::SpawnNode {
2293                            node_type: NodeType::Config(ConfigNodeType::NodeConfig(
2294                                NodeConfigInputs::default(),
2295                            )),
2296                        }),
2297                    command("edge_config", "Edge Config")
2298                        .description("Edge config with colors, thickness, type")
2299                        .action(ApplicationMessage::SpawnNode {
2300                            node_type: NodeType::Config(ConfigNodeType::EdgeConfig(
2301                                EdgeConfigInputs::default(),
2302                            )),
2303                        }),
2304                    command("pin_config", "Pin Config")
2305                        .description("Pin configuration with shape, color, radius")
2306                        .action(ApplicationMessage::SpawnNode {
2307                            node_type: NodeType::Config(ConfigNodeType::PinConfig(
2308                                PinConfigInputs::default(),
2309                            )),
2310                        }),
2311                    command("graph_config", "Graph Config")
2312                        .description("Canvas background and tiling (grid/dots/...)")
2313                        .action(ApplicationMessage::SpawnNode {
2314                            node_type: NodeType::Config(ConfigNodeType::GraphConfig(
2315                                GraphConfigInputs::default(),
2316                            )),
2317                        }),
2318                    // Builder nodes
2319                    command("color_quad", "Color Quad")
2320                        .description("Combine 4 corner colors into one ColorQuad")
2321                        .action(ApplicationMessage::SpawnNode {
2322                            node_type: NodeType::ColorQuad(ColorQuadNode::default()),
2323                        }),
2324                    command("vec2", "Vec2")
2325                        .description("Combine x and y into a 2D vector (e.g. offset)")
2326                        .action(ApplicationMessage::SpawnNode {
2327                            node_type: NodeType::Vec2(Vec2Node::default()),
2328                        }),
2329                    // Apply nodes
2330                    command("apply_to_graph", "Apply to Graph")
2331                        .description("Apply configs to all nodes/edges in graph")
2332                        .action(ApplicationMessage::SpawnNode {
2333                            node_type: NodeType::Config(ConfigNodeType::ApplyToGraph {
2334                                has_node_config: false,
2335                                has_edge_config: false,
2336                                has_pin_config: false,
2337                                has_graph_config: false,
2338                            }),
2339                        }),
2340                    command("apply_to_node", "Apply to Node")
2341                        .description("Apply config to a specific node by ID")
2342                        .action(ApplicationMessage::SpawnNode {
2343                            node_type: NodeType::Config(ConfigNodeType::ApplyToNode {
2344                                has_node_config: false,
2345                                target_id: None,
2346                            }),
2347                        }),
2348                ];
2349                ("Style Config Nodes", commands)
2350            }
2351            PaletteView::Submenu(submenu) if submenu == "themes" => {
2352                let commands = Self::get_available_themes()
2353                    .iter()
2354                    .map(|theme| {
2355                        let name = Self::get_theme_name(theme);
2356                        command(name, name).action(ApplicationMessage::ChangeTheme(theme.clone()))
2357                    })
2358                    .collect();
2359                ("Choose Theme", commands)
2360            }
2361            _ => ("Command Palette", vec![]),
2362        }
2363    }
2364
2365    fn subscription(&self) -> Subscription<ApplicationMessage> {
2366        Subscription::batch(vec![
2367            event::listen_with(handle_keyboard_event),
2368            event::listen_with(|event, _, _| match event {
2369                Event::Window(window::Event::Resized(size)) => {
2370                    Some(ApplicationMessage::WindowResized(size))
2371                }
2372                Event::Window(window::Event::Moved(position)) => {
2373                    Some(ApplicationMessage::WindowMoved(position))
2374                }
2375                _ => None,
2376            }),
2377        ])
2378    }
2379}
2380
2381/// Resolves a Theme node output pin to a color from the theme's extended palette.
2382/// Returns None for unknown pins.
2383/// Resolves a Theme / Theme Extended node output pin to its color. Basic
2384/// [`theme`](nodes::pins::theme) labels map to the flat [`Theme::palette`];
2385/// [`theme_ext`](nodes::pins::theme_ext) labels map to the graded
2386/// [`Theme::extended_palette`]. The two label sets are disjoint, so one lookup
2387/// serves both node kinds.
2388fn theme_color(theme: &Theme, pin: &PinLabel) -> Option<iced::Color> {
2389    use nodes::pins::{theme as t, theme_ext as x};
2390
2391    // Basic palette (flat entries).
2392    let pal = theme.palette();
2393    if *pin == t::BACKGROUND {
2394        return Some(pal.background);
2395    } else if *pin == t::TEXT {
2396        return Some(pal.text);
2397    } else if *pin == t::PRIMARY {
2398        return Some(pal.primary);
2399    } else if *pin == t::SUCCESS {
2400        return Some(pal.success);
2401    } else if *pin == t::WARNING {
2402        return Some(pal.warning);
2403    } else if *pin == t::DANGER {
2404        return Some(pal.danger);
2405    }
2406
2407    // Extended palette (base/weak/strong per accent group).
2408    let p = theme.extended_palette();
2409    let color = if *pin == x::BACKGROUND_BASE {
2410        p.background.base.color
2411    } else if *pin == x::BACKGROUND_WEAK {
2412        p.background.weak.color
2413    } else if *pin == x::BACKGROUND_STRONG {
2414        p.background.strong.color
2415    } else if *pin == x::PRIMARY_BASE {
2416        p.primary.base.color
2417    } else if *pin == x::PRIMARY_WEAK {
2418        p.primary.weak.color
2419    } else if *pin == x::PRIMARY_STRONG {
2420        p.primary.strong.color
2421    } else if *pin == x::SECONDARY_BASE {
2422        p.secondary.base.color
2423    } else if *pin == x::SECONDARY_WEAK {
2424        p.secondary.weak.color
2425    } else if *pin == x::SECONDARY_STRONG {
2426        p.secondary.strong.color
2427    } else if *pin == x::SUCCESS_BASE {
2428        p.success.base.color
2429    } else if *pin == x::SUCCESS_WEAK {
2430        p.success.weak.color
2431    } else if *pin == x::SUCCESS_STRONG {
2432        p.success.strong.color
2433    } else if *pin == x::WARNING_BASE {
2434        p.warning.base.color
2435    } else if *pin == x::WARNING_WEAK {
2436        p.warning.weak.color
2437    } else if *pin == x::WARNING_STRONG {
2438        p.warning.strong.color
2439    } else if *pin == x::DANGER_BASE {
2440        p.danger.base.color
2441    } else if *pin == x::DANGER_WEAK {
2442        p.danger.weak.color
2443    } else if *pin == x::DANGER_STRONG {
2444        p.danger.strong.color
2445    } else {
2446        return None;
2447    };
2448    Some(color)
2449}
2450
2451/// Feeds a propagated `value` into a combiner node's input pin (Math, ColorQuad,
2452/// or Vec2). Returns true if the stored input changed, so the propagation loop
2453/// knows to keep iterating. No-op for any other node type or unknown pin.
2454fn feed_combiner_input(node: &mut NodeType, pin: &PinLabel, value: &NodeValue) -> bool {
2455    use nodes::pins::{build as pin_build, math as pin_math};
2456
2457    let mut changed = false;
2458    match node {
2459        NodeType::Math(state) => {
2460            if let Some(f) = value.as_float() {
2461                if *pin == pin_math::A && state.input_a != Some(f) {
2462                    state.input_a = Some(f);
2463                    changed = true;
2464                } else if *pin == pin_math::B && state.input_b != Some(f) {
2465                    state.input_b = Some(f);
2466                    changed = true;
2467                }
2468            }
2469        }
2470        NodeType::ColorQuad(state) => {
2471            if let Some(c) = value.as_color() {
2472                let slot = if *pin == pin_build::NEAR_START {
2473                    Some(&mut state.near_start)
2474                } else if *pin == pin_build::NEAR_END {
2475                    Some(&mut state.near_end)
2476                } else if *pin == pin_build::FAR_START {
2477                    Some(&mut state.far_start)
2478                } else if *pin == pin_build::FAR_END {
2479                    Some(&mut state.far_end)
2480                } else {
2481                    None
2482                };
2483                if let Some(slot) = slot
2484                    && *slot != Some(c)
2485                {
2486                    *slot = Some(c);
2487                    changed = true;
2488                }
2489            }
2490        }
2491        NodeType::Vec2(state) => {
2492            if let Some(f) = value.as_float() {
2493                if *pin == pin_build::X && state.x != Some(f) {
2494                    state.x = Some(f);
2495                    changed = true;
2496                } else if *pin == pin_build::Y && state.y != Some(f) {
2497                    state.y = Some(f);
2498                    changed = true;
2499                }
2500            }
2501        }
2502        _ => {}
2503    }
2504    changed
2505}
2506
2507fn handle_keyboard_event(
2508    event: Event,
2509    _status: iced::event::Status,
2510    _window: iced::window::Id,
2511) -> Option<ApplicationMessage> {
2512    match event {
2513        Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
2514            if is_toggle_shortcut(&key, modifiers) {
2515                return Some(ApplicationMessage::ToggleCommandPalette);
2516            }
2517
2518            if modifiers.command() {
2519                let main_commands = Application::get_main_commands_with_shortcuts();
2520                if let Some(cmd_id) = find_matching_shortcut(&main_commands, &key, modifiers) {
2521                    return Some(ApplicationMessage::ExecuteShortcut(cmd_id.to_string()));
2522                }
2523            }
2524
2525            match key {
2526                keyboard::Key::Named(keyboard::key::Named::Tab) => {
2527                    if modifiers.shift() {
2528                        Some(ApplicationMessage::FocusPrevious)
2529                    } else {
2530                        Some(ApplicationMessage::FocusNext)
2531                    }
2532                }
2533                keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
2534                    Some(ApplicationMessage::CommandPaletteNavigateUp)
2535                }
2536                keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
2537                    Some(ApplicationMessage::CommandPaletteNavigateDown)
2538                }
2539                keyboard::Key::Named(keyboard::key::Named::Enter) => {
2540                    Some(ApplicationMessage::CommandPaletteConfirm)
2541                }
2542                keyboard::Key::Named(keyboard::key::Named::Escape) => {
2543                    Some(ApplicationMessage::CommandPaletteCancel)
2544                }
2545                _ => None,
2546            }
2547        }
2548        _ => None,
2549    }
2550}
2551
2552#[cfg(test)]
2553mod tests {
2554    use super::*;
2555    use nodes::{MathNodeState, MathOperation, NodeType};
2556
2557    // === Math Operation Tests ===
2558
2559    #[test]
2560    fn test_math_add() {
2561        let op = MathOperation::Add;
2562        assert_eq!(op.compute(5.0, 3.0), 8.0);
2563        assert_eq!(op.symbol(), "+");
2564        assert_eq!(op.name(), "Add");
2565    }
2566
2567    #[test]
2568    fn test_math_subtract() {
2569        let op = MathOperation::Subtract;
2570        assert_eq!(op.compute(5.0, 3.0), 2.0);
2571        assert_eq!(op.compute(3.0, 5.0), -2.0);
2572        assert_eq!(op.symbol(), "-");
2573    }
2574
2575    #[test]
2576    fn test_math_multiply() {
2577        let op = MathOperation::Multiply;
2578        assert_eq!(op.compute(5.0, 3.0), 15.0);
2579        assert_eq!(op.compute(0.0, 100.0), 0.0);
2580        assert_eq!(op.symbol(), "*");
2581    }
2582
2583    #[test]
2584    fn test_math_divide() {
2585        let op = MathOperation::Divide;
2586        assert_eq!(op.compute(6.0, 2.0), 3.0);
2587        assert_eq!(op.symbol(), "/");
2588    }
2589
2590    #[test]
2591    fn test_math_divide_by_zero() {
2592        let op = MathOperation::Divide;
2593        let result = op.compute(5.0, 0.0);
2594        assert!(result.is_infinite());
2595    }
2596
2597    // === MathNodeState Tests ===
2598
2599    #[test]
2600    fn test_math_node_result_with_both_inputs() {
2601        let mut state = MathNodeState::new(MathOperation::Add);
2602        state.input_a = Some(10.0);
2603        state.input_b = Some(5.0);
2604        assert_eq!(state.result(), Some(15.0));
2605    }
2606
2607    #[test]
2608    fn test_math_node_result_with_missing_a() {
2609        let mut state = MathNodeState::new(MathOperation::Add);
2610        state.input_a = None;
2611        state.input_b = Some(5.0);
2612        assert_eq!(state.result(), None);
2613    }
2614
2615    #[test]
2616    fn test_math_node_result_with_missing_b() {
2617        let mut state = MathNodeState::new(MathOperation::Add);
2618        state.input_a = Some(10.0);
2619        state.input_b = None;
2620        assert_eq!(state.result(), None);
2621    }
2622
2623    // === NodeType Output Value Tests ===
2624
2625    #[test]
2626    fn test_math_node_output_value() {
2627        let mut state = MathNodeState::new(MathOperation::Multiply);
2628        state.input_a = Some(4.0);
2629        state.input_b = Some(3.0);
2630        let node_type = NodeType::Math(state);
2631
2632        let output = node_type.output_value();
2633        assert!(output.is_some());
2634        if let Some(NodeValue::Float(f)) = output {
2635            assert_eq!(f, 12.0);
2636        } else {
2637            panic!("Expected Float value");
2638        }
2639    }
2640
2641    #[test]
2642    fn test_math_node_output_value_no_result() {
2643        let state = MathNodeState::new(MathOperation::Add); // No inputs
2644        let node_type = NodeType::Math(state);
2645        assert!(node_type.output_value().is_none());
2646    }
2647
2648    #[test]
2649    fn test_input_node_output_value() {
2650        let input = InputNodeType::FloatSlider {
2651            config: FloatSliderConfig::default(),
2652            value: 7.5,
2653        };
2654        let node_type = NodeType::Input(input);
2655
2656        let output = node_type.output_value();
2657        assert!(output.is_some());
2658        if let Some(NodeValue::Float(f)) = output {
2659            assert!((f - 7.5).abs() < 0.001);
2660        } else {
2661            panic!("Expected Float value");
2662        }
2663    }
2664
2665    // === ComputedStyle Tests ===
2666
2667    #[test]
2668    fn test_computed_style_pin_overlay_empty() {
2669        let style = ComputedStyle::default();
2670        // Empty overlay leaves every field to inherit.
2671        assert!(style.pin.color.is_none());
2672        assert!(style.pin.radius.is_none());
2673        assert!(style.pin.shape.is_none());
2674    }
2675
2676    #[test]
2677    fn test_computed_style_pin_overlay_with_values() {
2678        let style = ComputedStyle {
2679            pin: PinOverlay::new()
2680                .color(Color::from_rgb(1.0, 0.0, 0.0))
2681                .radius(10.0)
2682                .shape(PinShape::Diamond),
2683            ..Default::default()
2684        };
2685        assert!(style.pin.color.is_some());
2686        assert_eq!(style.pin.radius, Some(10.0));
2687        assert_eq!(style.pin.shape, Some(PinShape::Diamond));
2688    }
2689
2690    #[test]
2691    fn test_node_config_chain_applies_to_computed_style() {
2692        // Full chain: ColorPicker -> NodeConfig.fill_color, NodeConfig -> ApplyToGraph.
2693        // After propagation, computed_style.node must carry the fill color.
2694        let mut app = Application::default();
2695        app.nodes.clear();
2696        app.node_order.clear();
2697        app.edges.clear();
2698        app.edge_order.clear();
2699
2700        let red = Color::from_rgb(1.0, 0.0, 0.0);
2701        let picker = generate_node_id();
2702        let cfg = generate_node_id();
2703        let apply = generate_node_id();
2704        let p = Point::new(0.0, 0.0);
2705        app.nodes.insert(
2706            picker.clone(),
2707            (
2708                p,
2709                NodeType::Input(InputNodeType::ColorPicker { color: red }),
2710            ),
2711        );
2712        app.nodes.insert(
2713            cfg.clone(),
2714            (
2715                p,
2716                NodeType::Config(ConfigNodeType::NodeConfig(NodeConfigInputs::default())),
2717            ),
2718        );
2719        app.nodes.insert(
2720            apply.clone(),
2721            (
2722                p,
2723                NodeType::Config(ConfigNodeType::ApplyToGraph {
2724                    has_node_config: false,
2725                    has_edge_config: false,
2726                    has_pin_config: false,
2727                    has_graph_config: false,
2728                }),
2729            ),
2730        );
2731
2732        let e1 = generate_edge_id();
2733        app.edges.insert(
2734            e1.clone(),
2735            EdgeData {
2736                from_node: picker.clone(),
2737                from_pin: nodes::pins::input::COLOR,
2738                to_node: cfg.clone(),
2739                to_pin: nodes::pins::node::FILL_COLOR,
2740            },
2741        );
2742        app.edge_order.push(e1);
2743        let e2 = generate_edge_id();
2744        app.edges.insert(
2745            e2.clone(),
2746            EdgeData {
2747                from_node: cfg.clone(),
2748                from_pin: nodes::pins::cfg::NODE_OUT,
2749                to_node: apply.clone(),
2750                to_pin: nodes::pins::cfg::NODE_CONFIG,
2751            },
2752        );
2753        app.edge_order.push(e2);
2754
2755        app.propagate_values();
2756
2757        // The ApplyToGraph must have registered the node config...
2758        if let Some((
2759            _,
2760            NodeType::Config(ConfigNodeType::ApplyToGraph {
2761                has_node_config, ..
2762            }),
2763        )) = app.nodes.get(&apply)
2764        {
2765            assert!(
2766                *has_node_config,
2767                "ApplyToGraph did not register node config"
2768            );
2769        } else {
2770            panic!("apply node missing");
2771        }
2772        // ...and the computed style must carry the fill color.
2773        assert_eq!(
2774            app.computed_style.node.fill_color.map(|q| q.near_start),
2775            Some(red),
2776            "computed node style did not receive the config fill color",
2777        );
2778    }
2779
2780    #[test]
2781    fn test_graph_config_chain_applies_to_computed_style() {
2782        // ColorPicker -> GraphConfig.background and TilingKind -> GraphConfig.tiling_kind,
2783        // then GraphConfig -> ApplyToGraph.graph. The computed graph overlay must
2784        // carry both, and resolve_over must install them onto the theme base.
2785        use iced_nodegraph::{GraphStyle, TilingKind};
2786
2787        let mut app = Application::default();
2788        app.nodes.clear();
2789        app.node_order.clear();
2790        app.edges.clear();
2791        app.edge_order.clear();
2792
2793        let blue = Color::from_rgb(0.0, 0.0, 1.0);
2794        let picker = generate_node_id();
2795        let kind = generate_node_id();
2796        let cfg = generate_node_id();
2797        let apply = generate_node_id();
2798        let p = Point::new(0.0, 0.0);
2799        app.nodes.insert(
2800            picker.clone(),
2801            (
2802                p,
2803                NodeType::Input(InputNodeType::ColorPicker { color: blue }),
2804            ),
2805        );
2806        app.nodes.insert(
2807            kind.clone(),
2808            (
2809                p,
2810                NodeType::Input(InputNodeType::TilingKindSelector {
2811                    value: TilingKind::Dots,
2812                }),
2813            ),
2814        );
2815        app.nodes.insert(
2816            cfg.clone(),
2817            (
2818                p,
2819                NodeType::Config(ConfigNodeType::GraphConfig(GraphConfigInputs::default())),
2820            ),
2821        );
2822        app.nodes.insert(
2823            apply.clone(),
2824            (
2825                p,
2826                NodeType::Config(ConfigNodeType::ApplyToGraph {
2827                    has_node_config: false,
2828                    has_edge_config: false,
2829                    has_pin_config: false,
2830                    has_graph_config: false,
2831                }),
2832            ),
2833        );
2834
2835        use nodes::pins;
2836        let mut edge = |from: NodeId, fp: PinLabel, to: NodeId, tp: PinLabel| {
2837            let e = generate_edge_id();
2838            app.edges.insert(
2839                e.clone(),
2840                EdgeData {
2841                    from_node: from,
2842                    from_pin: fp,
2843                    to_node: to,
2844                    to_pin: tp,
2845                },
2846            );
2847            app.edge_order.push(e);
2848        };
2849        edge(
2850            picker,
2851            pins::input::COLOR,
2852            cfg.clone(),
2853            pins::graph::BACKGROUND,
2854        );
2855        edge(
2856            kind,
2857            pins::input::VALUE,
2858            cfg.clone(),
2859            pins::graph::TILING_KIND,
2860        );
2861        edge(
2862            cfg,
2863            pins::cfg::GRAPH_OUT,
2864            apply.clone(),
2865            pins::cfg::GRAPH_CONFIG,
2866        );
2867
2868        app.propagate_values();
2869
2870        // The ApplyToGraph must have registered the graph config.
2871        if let Some((
2872            _,
2873            NodeType::Config(ConfigNodeType::ApplyToGraph {
2874                has_graph_config, ..
2875            }),
2876        )) = app.nodes.get(&apply)
2877        {
2878            assert!(
2879                *has_graph_config,
2880                "ApplyToGraph did not register graph config"
2881            );
2882        } else {
2883            panic!("apply node missing");
2884        }
2885
2886        // The computed overlay carries background + tiling kind...
2887        assert_eq!(
2888            app.computed_style.graph.background_color,
2889            Some(blue),
2890            "computed graph style did not receive the background color",
2891        );
2892        assert_eq!(
2893            app.computed_style.graph.tiling_kind,
2894            Some(TilingKind::Dots),
2895            "computed graph style did not receive the tiling kind",
2896        );
2897
2898        // ...and resolve_over installs them onto a theme base.
2899        let resolved = app
2900            .computed_style
2901            .graph
2902            .resolve_over(GraphStyle::from_theme(&app.current_theme));
2903        assert_eq!(resolved.background_color, blue);
2904        assert_eq!(resolved.tiling.map(|t| t.kind), Some(TilingKind::Dots));
2905    }
2906
2907    #[test]
2908    fn test_node_config_shadow_chain_with_vec2() {
2909        // The user's exact scenario: ColorPicker -> shadow_color, slider ->
2910        // shadow_distance, two sliders -> Vec2 -> shadow_offset, NodeConfig ->
2911        // ApplyToGraph. Exercises the Vec2 builder propagation too.
2912        let mut app = Application::default();
2913        app.nodes.clear();
2914        app.node_order.clear();
2915        app.edges.clear();
2916        app.edge_order.clear();
2917
2918        let red = Color::from_rgb(1.0, 0.0, 0.0);
2919        let p = Point::new(0.0, 0.0);
2920        let slider = |v: f32| {
2921            NodeType::Input(InputNodeType::FloatSlider {
2922                config: FloatSliderConfig::default(),
2923                value: v,
2924            })
2925        };
2926        let picker = generate_node_id();
2927        let dist = generate_node_id();
2928        let sx = generate_node_id();
2929        let sy = generate_node_id();
2930        let vec2 = generate_node_id();
2931        let cfg = generate_node_id();
2932        let apply = generate_node_id();
2933        app.nodes.insert(
2934            picker.clone(),
2935            (
2936                p,
2937                NodeType::Input(InputNodeType::ColorPicker { color: red }),
2938            ),
2939        );
2940        app.nodes.insert(dist.clone(), (p, slider(8.0)));
2941        app.nodes.insert(sx.clone(), (p, slider(5.0)));
2942        app.nodes.insert(sy.clone(), (p, slider(7.0)));
2943        app.nodes
2944            .insert(vec2.clone(), (p, NodeType::Vec2(Vec2Node::default())));
2945        app.nodes.insert(
2946            cfg.clone(),
2947            (
2948                p,
2949                NodeType::Config(ConfigNodeType::NodeConfig(NodeConfigInputs::default())),
2950            ),
2951        );
2952        app.nodes.insert(
2953            apply.clone(),
2954            (
2955                p,
2956                NodeType::Config(ConfigNodeType::ApplyToGraph {
2957                    has_node_config: false,
2958                    has_edge_config: false,
2959                    has_pin_config: false,
2960                    has_graph_config: false,
2961                }),
2962            ),
2963        );
2964
2965        use nodes::pins;
2966        let mut edge = |from: NodeId, fp: PinLabel, to: NodeId, tp: PinLabel| {
2967            let e = generate_edge_id();
2968            app.edges.insert(
2969                e.clone(),
2970                EdgeData {
2971                    from_node: from,
2972                    from_pin: fp,
2973                    to_node: to,
2974                    to_pin: tp,
2975                },
2976            );
2977            app.edge_order.push(e);
2978        };
2979        edge(
2980            picker,
2981            pins::input::COLOR,
2982            cfg.clone(),
2983            pins::node::SHADOW_COLOR,
2984        );
2985        edge(
2986            dist,
2987            pins::input::VALUE,
2988            cfg.clone(),
2989            pins::node::SHADOW_DISTANCE,
2990        );
2991        edge(sx, pins::input::VALUE, vec2.clone(), pins::build::X);
2992        edge(sy, pins::input::VALUE, vec2.clone(), pins::build::Y);
2993        edge(
2994            vec2,
2995            pins::build::VEC2_OUT,
2996            cfg.clone(),
2997            pins::node::SHADOW_OFFSET,
2998        );
2999        edge(cfg, pins::cfg::NODE_OUT, apply, pins::cfg::NODE_CONFIG);
3000
3001        app.propagate_values();
3002
3003        let node = &app.computed_style.node;
3004        assert_eq!(
3005            node.shadow_color,
3006            Some(red),
3007            "shadow color did not propagate",
3008        );
3009        assert_eq!(
3010            node.shadow_distance,
3011            Some(8.0),
3012            "shadow distance did not propagate",
3013        );
3014        assert_eq!(
3015            node.shadow_offset,
3016            Some((5.0, 7.0)),
3017            "shadow offset (via Vec2 builder) did not propagate",
3018        );
3019    }
3020
3021    #[test]
3022    fn test_theme_node_feeds_config() {
3023        // Theme.primary -> NodeConfig.fill_color -> ApplyToGraph. The computed
3024        // node style must carry the active theme's primary color.
3025        let mut app = Application::default();
3026        app.nodes.clear();
3027        app.node_order.clear();
3028        app.edges.clear();
3029        app.edge_order.clear();
3030
3031        let p = Point::new(0.0, 0.0);
3032        let theme = generate_node_id();
3033        let cfg = generate_node_id();
3034        let apply = generate_node_id();
3035        app.nodes.insert(theme.clone(), (p, NodeType::Theme));
3036        app.nodes.insert(
3037            cfg.clone(),
3038            (
3039                p,
3040                NodeType::Config(ConfigNodeType::NodeConfig(NodeConfigInputs::default())),
3041            ),
3042        );
3043        app.nodes.insert(
3044            apply.clone(),
3045            (
3046                p,
3047                NodeType::Config(ConfigNodeType::ApplyToGraph {
3048                    has_node_config: false,
3049                    has_edge_config: false,
3050                    has_pin_config: false,
3051                    has_graph_config: false,
3052                }),
3053            ),
3054        );
3055
3056        use nodes::pins;
3057        let mut edge = |from: NodeId, fp: PinLabel, to: NodeId, tp: PinLabel| {
3058            let e = generate_edge_id();
3059            app.edges.insert(
3060                e.clone(),
3061                EdgeData {
3062                    from_node: from,
3063                    from_pin: fp,
3064                    to_node: to,
3065                    to_pin: tp,
3066                },
3067            );
3068            app.edge_order.push(e);
3069        };
3070        edge(
3071            theme,
3072            pins::theme::PRIMARY,
3073            cfg.clone(),
3074            pins::node::FILL_COLOR,
3075        );
3076        edge(cfg, pins::cfg::NODE_OUT, apply, pins::cfg::NODE_CONFIG);
3077
3078        let expected = app.current_theme.palette().primary;
3079        app.propagate_values();
3080        assert_eq!(
3081            app.computed_style.node.fill_color.map(|q| q.near_start),
3082            Some(expected),
3083            "theme primary color did not propagate to the node config",
3084        );
3085    }
3086
3087    #[test]
3088    fn test_theme_extended_node_feeds_config() {
3089        // ThemeExtended.primary_strong -> NodeConfig.fill_color -> ApplyToGraph.
3090        // The computed node style must carry the extended palette's strong primary.
3091        let mut app = Application::default();
3092        app.nodes.clear();
3093        app.node_order.clear();
3094        app.edges.clear();
3095        app.edge_order.clear();
3096
3097        let p = Point::new(0.0, 0.0);
3098        let theme = generate_node_id();
3099        let cfg = generate_node_id();
3100        let apply = generate_node_id();
3101        app.nodes
3102            .insert(theme.clone(), (p, NodeType::ThemeExtended));
3103        app.nodes.insert(
3104            cfg.clone(),
3105            (
3106                p,
3107                NodeType::Config(ConfigNodeType::NodeConfig(NodeConfigInputs::default())),
3108            ),
3109        );
3110        app.nodes.insert(
3111            apply.clone(),
3112            (
3113                p,
3114                NodeType::Config(ConfigNodeType::ApplyToGraph {
3115                    has_node_config: false,
3116                    has_edge_config: false,
3117                    has_pin_config: false,
3118                    has_graph_config: false,
3119                }),
3120            ),
3121        );
3122
3123        use nodes::pins;
3124        let mut edge = |from: NodeId, fp: PinLabel, to: NodeId, tp: PinLabel| {
3125            let e = generate_edge_id();
3126            app.edges.insert(
3127                e.clone(),
3128                EdgeData {
3129                    from_node: from,
3130                    from_pin: fp,
3131                    to_node: to,
3132                    to_pin: tp,
3133                },
3134            );
3135            app.edge_order.push(e);
3136        };
3137        edge(
3138            theme,
3139            pins::theme_ext::PRIMARY_STRONG,
3140            cfg.clone(),
3141            pins::node::FILL_COLOR,
3142        );
3143        edge(cfg, pins::cfg::NODE_OUT, apply, pins::cfg::NODE_CONFIG);
3144
3145        let expected = app.current_theme.extended_palette().primary.strong.color;
3146        app.propagate_values();
3147        assert_eq!(
3148            app.computed_style.node.fill_color.map(|q| q.near_start),
3149            Some(expected),
3150            "extended palette strong primary did not propagate to the node config",
3151        );
3152    }
3153
3154    #[test]
3155    fn test_computed_style_node_overlay() {
3156        let style = ComputedStyle {
3157            node: NodeOverlay::new()
3158                .corner_radius(12.0)
3159                .opacity(0.8)
3160                .fill_color(Color::from_rgb(0.2, 0.3, 0.4)),
3161            ..Default::default()
3162        };
3163        assert_eq!(style.node.corner_radius, Some(12.0));
3164        assert_eq!(style.node.opacity, Some(0.8));
3165        assert!(style.node.fill_color.is_some());
3166    }
3167
3168    #[test]
3169    fn test_node_config_shadow_resolves() {
3170        use iced_nodegraph::{ColorQuad, NodeStyle};
3171
3172        // A NodeConfig with shadow fields set must carry them through build()
3173        // (overlay) and resolve_over() (concrete style) unchanged.
3174        let inputs = NodeConfigInputs {
3175            shadow_color: Some(ColorQuad::solid(Color::from_rgb(1.0, 0.0, 0.0))),
3176            shadow_distance: Some(12.0),
3177            shadow_offset: Some((4.0, 6.0)),
3178            ..Default::default()
3179        };
3180        let overlay = inputs.build();
3181        assert_eq!(overlay.shadow_color, Some(Color::from_rgb(1.0, 0.0, 0.0)));
3182        assert_eq!(overlay.shadow_distance, Some(12.0));
3183        assert_eq!(overlay.shadow_offset, Some((4.0, 6.0)));
3184
3185        // NodeStyle::comment() has no shadow; the overlay must install one.
3186        let resolved = overlay.resolve_over(NodeStyle::comment());
3187        assert_eq!(resolved.shadow_color, Color::from_rgb(1.0, 0.0, 0.0));
3188        assert_eq!(resolved.shadow_distance, 12.0);
3189        assert_eq!(resolved.shadow_offset, (4.0, 6.0));
3190    }
3191
3192    #[test]
3193    fn test_computed_style_edge_overlay() {
3194        let style = ComputedStyle::default();
3195        assert!(style.edge.pattern.is_none());
3196
3197        let style = ComputedStyle {
3198            edge: EdgeOverlay::new()
3199                .pattern(iced_nodegraph::Pattern::solid(5.0))
3200                .curve(EdgeCurve::Line),
3201            ..Default::default()
3202        };
3203        assert_eq!(style.edge.pattern.unwrap().thickness, 5.0);
3204        assert_eq!(style.edge.curve, Some(EdgeCurve::Line));
3205    }
3206
3207    #[test]
3208    fn test_edge_config_inputs_pattern_type_dashed() {
3209        use iced_nodegraph::SdfPatternType;
3210
3211        let inputs = EdgeConfigInputs {
3212            pattern_type: Some(PatternType::Dashed),
3213            thickness: Some(3.0),
3214            dash_length: Some(10.0),
3215            gap_length: Some(5.0),
3216            ..Default::default()
3217        };
3218
3219        let config = inputs.build();
3220        let pattern = config.pattern.expect("pattern should be Some");
3221        assert_eq!(pattern.thickness, 3.0);
3222        assert!(
3223            matches!(pattern.pattern_type, SdfPatternType::Dashed { dash, gap, .. } if (dash - 10.0).abs() < 0.01 && (gap - 5.0).abs() < 0.01),
3224            "Expected Dashed pattern, got {:?}",
3225            pattern.pattern_type
3226        );
3227    }
3228
3229    #[test]
3230    fn test_edge_config_inputs_pattern_type_arrowed() {
3231        use iced_nodegraph::SdfPatternType;
3232
3233        let inputs = EdgeConfigInputs {
3234            pattern_type: Some(PatternType::Arrowed),
3235            ..Default::default()
3236        };
3237
3238        let config = inputs.build();
3239        let pattern = config.pattern.expect("pattern should be Some");
3240        assert!(
3241            matches!(pattern.pattern_type, SdfPatternType::Arrowed { .. }),
3242            "Expected Arrowed pattern, got {:?}",
3243            pattern.pattern_type
3244        );
3245    }
3246
3247    #[test]
3248    fn test_edge_config_inputs_pattern_type_dotted() {
3249        use iced_nodegraph::SdfPatternType;
3250
3251        let inputs = EdgeConfigInputs {
3252            pattern_type: Some(PatternType::Dotted),
3253            dot_radius: Some(3.0),
3254            gap_length: Some(4.0),
3255            ..Default::default()
3256        };
3257
3258        let config = inputs.build();
3259        let pattern = config.pattern.expect("pattern should be Some");
3260        assert!(
3261            matches!(pattern.pattern_type, SdfPatternType::Dotted { .. }),
3262            "Expected Dotted pattern, got {:?}",
3263            pattern.pattern_type
3264        );
3265    }
3266
3267    #[test]
3268    fn test_edge_config_inputs_pattern_type_dash_dotted() {
3269        use iced_nodegraph::SdfPatternType;
3270
3271        let inputs = EdgeConfigInputs {
3272            pattern_type: Some(PatternType::DashDotted),
3273            ..Default::default()
3274        };
3275
3276        let config = inputs.build();
3277        let pattern = config.pattern.expect("pattern should be Some");
3278        assert!(
3279            matches!(pattern.pattern_type, SdfPatternType::DashDotted { .. }),
3280            "Expected DashDotted pattern, got {:?}",
3281            pattern.pattern_type
3282        );
3283    }
3284
3285    #[test]
3286    fn test_edge_config_inputs_pattern_preserved_through_build() {
3287        // Verify the full pipeline: EdgeConfigInputs -> build() -> EdgeConfig
3288        // Pattern, border, shadow must all survive
3289        use iced_nodegraph::SdfPatternType;
3290
3291        let inputs = EdgeConfigInputs {
3292            pattern_type: Some(PatternType::Dashed),
3293            thickness: Some(4.0),
3294            dash_length: Some(8.0),
3295            gap_length: Some(4.0),
3296            animation_speed: Some(50.0),
3297            stroke_color: Some(iced_nodegraph::ColorQuad::arc(
3298                Color::from_rgb(1.0, 0.0, 0.0),
3299                Color::from_rgb(0.0, 0.0, 1.0),
3300            )),
3301            border_width: Some(2.0),
3302            border_gap: Some(1.0),
3303            shadow_blur: Some(6.0),
3304            shadow_expand: Some(3.0),
3305            ..Default::default()
3306        };
3307
3308        let config = inputs.build();
3309
3310        // Pattern
3311        let pattern = config.pattern.expect("pattern must be present");
3312        assert_eq!(pattern.thickness, 4.0);
3313        assert!(matches!(
3314            pattern.pattern_type,
3315            SdfPatternType::Dashed { .. }
3316        ));
3317        assert!((pattern.flow_speed - 50.0).abs() < 0.01);
3318
3319        // Colors: arc gradient start -> end
3320        let stroke = config.stroke_color.expect("stroke color present");
3321        assert_eq!(stroke.near_start, Color::from_rgb(1.0, 0.0, 0.0));
3322        assert_eq!(stroke.near_end, Color::from_rgb(0.0, 0.0, 1.0));
3323
3324        // Border
3325        assert_eq!(config.border_width, Some(2.0));
3326        assert_eq!(config.border_gap, Some(1.0));
3327
3328        // Shadow
3329        assert_eq!(config.shadow_blur, Some(6.0));
3330        assert_eq!(config.shadow_expand, Some(3.0));
3331    }
3332}