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;
42
43use iced::{
44    Color, Event, Length, Point, Subscription, Task, Theme, Vector, event, keyboard,
45    widget::{container, stack, text},
46    window,
47};
48use iced_nodegraph::{EdgeConfig, NodeConfig, PinConfig, PinRef, ShadowConfig};
49use iced_nodegraph::{EdgeCurve, PinShape};
50use iced_palette::{
51    Command, Shortcut, command, command_palette, find_matching_shortcut, focus_input,
52    get_filtered_command_index, get_filtered_count, is_toggle_shortcut, navigate_down, navigate_up,
53};
54use ids::{EdgeId, NodeId, PinLabel, generate_edge_id, generate_node_id};
55use nodes::{
56    BoolToggleConfig, ConfigNodeType, EdgeConfigInputs, EdgeSection,
57    EdgeSections, FloatSliderConfig, InputNodeType, IntSliderConfig, MathNodeState, MathOperation,
58    NodeConfigInputs, NodeSection, NodeSections, NodeType, NodeValue, PatternType,
59    PinConfigInputs, ShadowConfigInputs, apply_to_graph_node,
60    apply_to_node_node, bool_toggle_node,
61    color_picker_node, color_preset_node, edge_config_node, edge_curve_selector_node,
62    float_slider_node, int_slider_node, math_node, node, node_config_node,
63    pattern_type_selector_node, pin_config_node, pin_shape_selector_node, shadow_config_node,
64};
65#[cfg(not(target_arch = "wasm32"))]
66use persistence::EdgeData;
67use std::collections::{HashMap, HashSet};
68
69/// Edge data for in-memory representation (WASM version).
70#[cfg(target_arch = "wasm32")]
71#[derive(Debug, Clone)]
72pub struct EdgeData {
73    pub from_node: NodeId,
74    pub from_pin: PinLabel,
75    pub to_node: NodeId,
76    pub to_pin: PinLabel,
77}
78
79#[cfg(target_arch = "wasm32")]
80use wasm_bindgen::prelude::*;
81
82#[cfg(target_arch = "wasm32")]
83#[wasm_bindgen(start)]
84pub fn wasm_init() {
85    console_error_panic_hook::set_once();
86}
87
88pub fn main() -> iced::Result {
89    #[cfg(target_arch = "wasm32")]
90    let window_settings = iced::window::Settings {
91        platform_specific: iced::window::settings::PlatformSpecific {
92            target: Some(String::from("demo-canvas-container")),
93        },
94        ..Default::default()
95    };
96
97    #[cfg(not(target_arch = "wasm32"))]
98    let window_settings = {
99        // Try to load saved window settings
100        let (position, size, maximized) = persistence::load_state()
101            .map(|s| (s.window_position, s.window_size, s.window_maximized))
102            .unwrap_or((None, None, None));
103
104        iced::window::Settings {
105            position: position
106                .map(|(x, y)| iced::window::Position::Specific(Point::new(x as f32, y as f32)))
107                .unwrap_or(iced::window::Position::Centered),
108            size: size
109                .map(|(w, h)| iced::Size::new(w as f32, h as f32))
110                .unwrap_or(iced::Size::new(1280.0, 800.0)),
111            maximized: maximized.unwrap_or(false),
112            ..Default::default()
113        }
114    };
115
116    iced::application(Application::new, Application::update, Application::view)
117        .subscription(Application::subscription)
118        .title("Hello World - iced_nodegraph Demo")
119        .theme(Application::theme)
120        .window(window_settings)
121        .run()
122}
123
124#[cfg(target_arch = "wasm32")]
125#[wasm_bindgen]
126pub fn run_demo() {
127    let _ = main();
128}
129
130#[derive(Debug, Clone)]
131#[allow(dead_code)]
132enum ApplicationMessage {
133    Noop,
134    EdgeConnected {
135        from: PinRef<NodeId, PinLabel>,
136        to: PinRef<NodeId, PinLabel>,
137    },
138    NodeMoved {
139        node_id: NodeId,
140        new_position: Point,
141    },
142    EdgeDisconnected {
143        from: PinRef<NodeId, PinLabel>,
144        to: PinRef<NodeId, PinLabel>,
145    },
146    ToggleCommandPalette,
147    CommandPaletteInput(String),
148    CommandPaletteNavigateUp,
149    CommandPaletteNavigateDown,
150    CommandPaletteSelect(usize),
151    CommandPaletteConfirm,
152    CommandPaletteCancel,
153    ExecuteShortcut(String),
154    CommandPaletteNavigate(usize),
155    SpawnNode {
156        node_type: NodeType,
157    },
158    ChangeTheme(Theme),
159    CameraChanged {
160        position: Point,
161        zoom: f32,
162    },
163    WindowResized(iced::Size),
164    WindowMoved(Point),
165    WindowMaximizedChanged(bool),
166    NavigateToSubmenu(String),
167    NavigateBack,
168    Tick,
169    // Selection-related messages
170    SelectionChanged(Vec<NodeId>),
171    CloneNodes(Vec<NodeId>),
172    DeleteNodes(Vec<NodeId>),
173    GroupMoved {
174        node_ids: Vec<NodeId>,
175        delta: Vector,
176    },
177    // State export for Claude
178    ExportState,
179    // Input node value changes
180    SliderChanged {
181        node_id: NodeId,
182        value: f32,
183    },
184    IntSliderChanged {
185        node_id: NodeId,
186        value: i32,
187    },
188    BoolChanged {
189        node_id: NodeId,
190        value: bool,
191    },
192    EdgeCurveChanged {
193        node_id: NodeId,
194        value: EdgeCurve,
195    },
196    PinShapeChanged {
197        node_id: NodeId,
198        value: PinShape,
199    },
200    PatternTypeChanged {
201        node_id: NodeId,
202        value: PatternType,
203    },
204    ColorChanged {
205        node_id: NodeId,
206        color: Color,
207    },
208    // Collapsible node messages
209    ToggleNodeExpanded {
210        node_id: NodeId,
211    },
212    UpdateFloatSliderConfig {
213        node_id: NodeId,
214        config: FloatSliderConfig,
215    },
216    UpdateIntSliderConfig {
217        node_id: NodeId,
218        config: IntSliderConfig,
219    },
220    // Config section collapse/expand
221    ToggleEdgeSection {
222        node_id: NodeId,
223        section: EdgeSection,
224    },
225    ToggleNodeSection {
226        node_id: NodeId,
227        section: NodeSection,
228    },
229}
230
231#[derive(Debug, Clone, PartialEq)]
232enum PaletteView {
233    Main,
234    Submenu(String),
235}
236
237/// Output types from config nodes for propagation
238#[derive(Debug, Clone)]
239#[allow(dead_code)]
240enum ConfigOutput {
241    Node(NodeConfig),
242    Edge(EdgeConfig),
243    Pin(iced_nodegraph::PinConfig),
244}
245
246/// Computed style values from connected config nodes
247#[derive(Debug, Clone, Default)]
248struct ComputedStyle {
249    corner_radius: Option<f32>,
250    opacity: Option<f32>,
251    border_width: Option<f32>,
252    fill_color: Option<Color>,
253    shadow: Option<ShadowConfig>,
254    /// Full EdgeConfig from connected Edge Config node (passed through as-is)
255    edge_config: Option<EdgeConfig>,
256    // Pin config values
257    pin_color: Option<Color>,
258    pin_radius: Option<f32>,
259    pin_shape: Option<iced_nodegraph::PinShape>,
260    pin_border_color: Option<Color>,
261    pin_border_width: Option<f32>,
262}
263
264impl ComputedStyle {
265    /// Builds a NodeConfig from computed values (partial overrides).
266    /// Only properties that are explicitly set will override theme defaults.
267    fn to_node_config(&self) -> NodeConfig {
268        let mut config = NodeConfig::new();
269        if let Some(r) = self.corner_radius {
270            config = config.corner_radius(r);
271        }
272        if let Some(o) = self.opacity {
273            config = config.opacity(o);
274        }
275        if let Some(w) = self.border_width {
276            config = config.border_width(w);
277        }
278        if let Some(c) = self.fill_color {
279            config = config.fill_color(c);
280        }
281        if let Some(ref s) = self.shadow {
282            config = config.shadow(s.clone());
283        }
284        config
285    }
286
287    /// Returns the EdgeConfig (passed through from Edge Config node as-is).
288    fn to_edge_config(&self) -> EdgeConfig {
289        self.edge_config.clone().unwrap_or_default()
290    }
291
292    /// Builds a PinConfig from computed values
293    fn to_pin_config(&self) -> PinConfig {
294        let mut config = PinConfig::new();
295        if let Some(c) = self.pin_color {
296            config = config.color(c);
297        }
298        if let Some(r) = self.pin_radius {
299            config = config.radius(r);
300        }
301        if let Some(s) = self.pin_shape {
302            config = config.shape(s);
303        }
304        if let Some(bc) = self.pin_border_color {
305            config = config.border_color(bc);
306        }
307        if let Some(bw) = self.pin_border_width {
308            config = config.border_width(bw);
309        }
310        config
311    }
312}
313
314struct Application {
315    /// Nodes stored by unique ID
316    nodes: HashMap<NodeId, (Point, NodeType)>,
317    /// Node order for deterministic iteration
318    node_order: Vec<NodeId>,
319    /// Edges stored by unique ID
320    edges: HashMap<EdgeId, EdgeData>,
321    /// Edge order for deterministic iteration
322    edge_order: Vec<EdgeId>,
323    /// Currently selected nodes
324    selected_nodes: HashSet<NodeId>,
325    /// Nodes with expanded options panels
326    expanded_nodes: HashSet<NodeId>,
327    /// Section expansion state for EdgeConfig nodes
328    edge_config_sections: HashMap<NodeId, EdgeSections>,
329    /// Section expansion state for NodeConfig nodes
330    node_config_sections: HashMap<NodeId, NodeSections>,
331    command_palette_open: bool,
332    command_input: String,
333    current_theme: Theme,
334    palette_view: PaletteView,
335    palette_selected_index: usize,
336    palette_preview_theme: Option<Theme>,
337    palette_original_theme: Option<Theme>,
338    /// Computed style values from config node connections
339    computed_style: ComputedStyle,
340    /// Pending config outputs from config nodes to be applied by ApplyToGraph
341    pending_configs: HashMap<NodeId, Vec<(PinLabel, ConfigOutput)>>,
342    /// Current viewport size for spawn-at-center calculation
343    viewport_size: iced::Size,
344    /// Current camera position from NodeGraph
345    camera_position: Point,
346    /// Current camera zoom from NodeGraph
347    camera_zoom: f32,
348    /// Window position (x, y) for persistence
349    window_position: Option<(i32, i32)>,
350    /// Window size (width, height) for persistence
351    window_size: Option<(u32, u32)>,
352    /// Whether window is maximized
353    window_maximized: Option<bool>,
354}
355
356impl Default for Application {
357    fn default() -> Self {
358        use nodes::pins::workflow;
359
360        // Create nodes with stable NanoIDs
361        let node0_id = generate_node_id();
362        let node1_id = generate_node_id();
363        let node2_id = generate_node_id();
364        let node3_id = generate_node_id();
365
366        let mut nodes = HashMap::new();
367        nodes.insert(
368            node0_id.clone(),
369            (
370                Point::new(45.5, 149.0),
371                NodeType::Workflow("email_trigger".to_string()),
372            ),
373        );
374        nodes.insert(
375            node1_id.clone(),
376            (
377                Point::new(274.5, 227.5),
378                NodeType::Workflow("email_parser".to_string()),
379            ),
380        );
381        nodes.insert(
382            node2_id.clone(),
383            (
384                Point::new(459.5, 432.5),
385                NodeType::Workflow("filter".to_string()),
386            ),
387        );
388        nodes.insert(
389            node3_id.clone(),
390            (
391                Point::new(679.0, 252.5),
392                NodeType::Workflow("calendar".to_string()),
393            ),
394        );
395
396        let node_order = vec![
397            node0_id.clone(),
398            node1_id.clone(),
399            node2_id.clone(),
400            node3_id.clone(),
401        ];
402
403        // Create edges with stable NanoIDs and string pin labels
404        let mut edges = HashMap::new();
405        let mut edge_order = Vec::new();
406
407        // Edge 0: email_trigger "on email" -> email_parser "email"
408        let edge0_id = generate_edge_id();
409        edges.insert(
410            edge0_id.clone(),
411            EdgeData {
412                from_node: node0_id.clone(),
413                from_pin: workflow::ON_EMAIL,
414                to_node: node1_id.clone(),
415                to_pin: workflow::EMAIL,
416            },
417        );
418        edge_order.push(edge0_id);
419
420        // Edge 1: email_parser "subject" -> filter "input"
421        let edge1_id = generate_edge_id();
422        edges.insert(
423            edge1_id.clone(),
424            EdgeData {
425                from_node: node1_id.clone(),
426                from_pin: workflow::SUBJECT,
427                to_node: node2_id.clone(),
428                to_pin: workflow::INPUT,
429            },
430        );
431        edge_order.push(edge1_id);
432
433        // Edge 2: email_parser "datetime" -> calendar "datetime"
434        let edge2_id = generate_edge_id();
435        edges.insert(
436            edge2_id.clone(),
437            EdgeData {
438                from_node: node1_id.clone(),
439                from_pin: workflow::DATETIME,
440                to_node: node3_id.clone(),
441                to_pin: workflow::DATETIME,
442            },
443        );
444        edge_order.push(edge2_id);
445
446        // Edge 3: filter "matches" -> calendar "title"
447        let edge3_id = generate_edge_id();
448        edges.insert(
449            edge3_id.clone(),
450            EdgeData {
451                from_node: node2_id.clone(),
452                from_pin: workflow::MATCHES,
453                to_node: node3_id.clone(),
454                to_pin: workflow::TITLE,
455            },
456        );
457        edge_order.push(edge3_id);
458
459        Self {
460            nodes,
461            node_order,
462            edges,
463            edge_order,
464            selected_nodes: HashSet::new(),
465            expanded_nodes: HashSet::new(),
466            edge_config_sections: HashMap::new(),
467            node_config_sections: HashMap::new(),
468            command_palette_open: false,
469            command_input: String::new(),
470            current_theme: Theme::CatppuccinFrappe,
471            palette_view: PaletteView::Main,
472            palette_selected_index: 0,
473            palette_preview_theme: None,
474            palette_original_theme: None,
475            computed_style: ComputedStyle::default(),
476            pending_configs: HashMap::new(),
477            viewport_size: iced::Size::new(800.0, 600.0), // Default size
478            camera_position: Point::ORIGIN,
479            camera_zoom: 1.0,
480            window_position: None,
481            window_size: None,
482            window_maximized: None,
483        }
484    }
485}
486
487impl Application {
488    fn new() -> Self {
489        // Try to load saved state, fall back to default
490        #[cfg(not(target_arch = "wasm32"))]
491        {
492            match persistence::load_state() {
493                Ok(saved) => {
494                    let (
495                        nodes,
496                        node_order,
497                        edges,
498                        edge_order,
499                        theme,
500                        camera_pos,
501                        camera_zoom,
502                        window_pos,
503                        window_size,
504                        edge_config_sections,
505                        node_config_sections,
506                        window_maximized,
507                    ) = saved.to_app();
508                    println!(
509                        "Loaded saved state: {} nodes, {} edges",
510                        nodes.len(),
511                        edges.len()
512                    );
513                    let mut app = Self {
514                        nodes,
515                        node_order,
516                        edges,
517                        edge_order,
518                        current_theme: theme,
519                        camera_position: camera_pos,
520                        camera_zoom,
521                        window_position: window_pos,
522                        window_size,
523                        edge_config_sections,
524                        node_config_sections,
525                        window_maximized,
526                        ..Self::default()
527                    };
528                    // Apply computed styles from config nodes immediately
529                    app.propagate_values();
530                    return app;
531                }
532                Err(e) => {
533                    println!("No saved state found: {}", e);
534                }
535            }
536        }
537        Self::default()
538    }
539
540    /// Saves current state to disk (native only).
541    #[cfg(not(target_arch = "wasm32"))]
542    fn save_state(&self) {
543        let saved = persistence::SavedState::from_app(
544            &self.nodes,
545            &self.node_order,
546            &self.edges,
547            &self.edge_order,
548            &self.current_theme,
549            self.camera_position,
550            self.camera_zoom,
551            self.window_position,
552            self.window_size,
553            &self.edge_config_sections,
554            &self.node_config_sections,
555            self.window_maximized,
556        );
557        if let Err(e) = persistence::save_state(&saved) {
558            eprintln!("Failed to save state: {}", e);
559        }
560    }
561
562    #[cfg(target_arch = "wasm32")]
563    fn save_state(&self) {
564        // No-op on WASM
565    }
566
567    /// Calculate spawn position at screen center, converted to world coordinates.
568    fn spawn_position(&self) -> Point {
569        // Screen center
570        let screen_center_x = self.viewport_size.width / 2.0;
571        let screen_center_y = self.viewport_size.height / 2.0;
572
573        // Convert to world coordinates: world = screen / zoom - camera_position
574        let world_x = screen_center_x / self.camera_zoom - self.camera_position.x;
575        let world_y = screen_center_y / self.camera_zoom - self.camera_position.y;
576
577        // Offset for node size (approximate center, ~100x80 typical node)
578        Point::new(world_x - 50.0, world_y - 40.0)
579    }
580
581    /// Export current graph state to a file for Claude to read and update demos.
582    /// Format is designed to be human-readable and easily parseable.
583    #[cfg(not(target_arch = "wasm32"))]
584    fn export_state_to_file(&self) {
585        use std::io::Write;
586
587        // Create out/ directory if it doesn't exist
588        let out_dir = std::path::Path::new("out");
589        if !out_dir.exists()
590            && let Err(e) = std::fs::create_dir(out_dir) {
591                eprintln!("Failed to create out/ directory: {}", e);
592                return;
593            }
594
595        // Generate random filename
596        let filename = Self::generate_random_name();
597        let path = out_dir.join(format!("{}.txt", filename));
598
599        let mut output = String::new();
600        output.push_str("# Graph State Export\n");
601        output.push_str(
602            "# Generated by hello_world demo - use this to update demo initial state\n\n",
603        );
604
605        // Export nodes
606        output.push_str("## Nodes\n");
607        output.push_str(&format!("# Total: {} nodes\n\n", self.nodes.len()));
608
609        for node_id in &self.node_order {
610            if let Some((pos, node_type)) = self.nodes.get(node_id) {
611                output.push_str(&format!("Node {}: ({:.1}, {:.1})\n", node_id, pos.x, pos.y));
612                match node_type {
613                    NodeType::Workflow(name) => {
614                        output.push_str(&format!("  Type: Workflow(\"{}\")\n", name));
615                    }
616                    NodeType::Input(input) => {
617                        output.push_str(&format!("  Type: Input({:?})\n", input));
618                    }
619                    NodeType::Config(config) => {
620                        output.push_str(&format!("  Type: Config({:?})\n", config));
621                    }
622                    NodeType::Math(state) => {
623                        output.push_str(&format!("  Type: Math({:?})\n", state));
624                    }
625                }
626                output.push('\n');
627            }
628        }
629
630        // Export edges
631        output.push_str("## Edges\n");
632        output.push_str(&format!("# Total: {} edges\n\n", self.edges.len()));
633
634        for edge_id in &self.edge_order {
635            if let Some(edge) = self.edges.get(edge_id) {
636                output.push_str(&format!(
637                    "Edge {}: Node {}.Pin \"{}\" -> Node {}.Pin \"{}\"\n",
638                    edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin
639                ));
640            }
641        }
642
643        // Export JSON snippet for easy copy-paste
644        output.push_str("\n## JSON Format (for state.json)\n\n");
645        output.push_str("```json\n");
646        output.push_str("{\n  \"nodes\": [\n");
647        for (i, node_id) in self.node_order.iter().enumerate() {
648            if let Some((pos, node_type)) = self.nodes.get(node_id) {
649                let type_str = match node_type {
650                    NodeType::Workflow(name) => {
651                        format!("{{\"type\": \"Workflow\", \"name\": \"{}\"}}", name)
652                    }
653                    _ => format!("{:?}", node_type),
654                };
655                let comma = if i < self.node_order.len() - 1 {
656                    ","
657                } else {
658                    ""
659                };
660                output.push_str(&format!(
661                    "    {{\"id\": \"{}\", \"x\": {:.1}, \"y\": {:.1}, \"node_type\": {}}}{}\n",
662                    node_id, pos.x, pos.y, type_str, comma
663                ));
664            }
665        }
666        output.push_str("  ],\n  \"edges\": [\n");
667        for (i, edge_id) in self.edge_order.iter().enumerate() {
668            if let Some(edge) = self.edges.get(edge_id) {
669                let comma = if i < self.edge_order.len() - 1 {
670                    ","
671                } else {
672                    ""
673                };
674                output.push_str(&format!(
675                    "    {{\"id\": \"{}\", \"from_node\": \"{}\", \"from_pin\": \"{}\", \"to_node\": \"{}\", \"to_pin\": \"{}\"}}{}\n",
676                    edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin, comma
677                ));
678            }
679        }
680        output.push_str("  ]\n}\n");
681        output.push_str("```\n");
682
683        // Write to file
684        match std::fs::File::create(&path) {
685            Ok(mut file) => {
686                if let Err(e) = file.write_all(output.as_bytes()) {
687                    eprintln!("Failed to write state export: {}", e);
688                } else {
689                    println!("State exported to: {}", path.display());
690                }
691            }
692            Err(e) => {
693                eprintln!("Failed to create export file: {}", e);
694            }
695        }
696    }
697
698    /// Generate a random two-word name for export files
699    #[cfg(not(target_arch = "wasm32"))]
700    fn generate_random_name() -> String {
701        use std::time::{SystemTime, UNIX_EPOCH};
702
703        const ADJECTIVES: &[&str] = &[
704            "swift", "bright", "calm", "bold", "keen", "warm", "cool", "wild", "soft", "sharp",
705            "quick", "slow", "deep", "wide", "tall", "tiny", "grand", "pure", "rare", "wise",
706            "fair", "dark", "light", "fresh",
707        ];
708        const NOUNS: &[&str] = &[
709            "river", "mountain", "forest", "ocean", "meadow", "valley", "canyon", "island",
710            "sunset", "sunrise", "thunder", "breeze", "garden", "crystal", "shadow", "ember",
711            "falcon", "phoenix", "dragon", "tiger", "wolf", "eagle", "raven", "fox",
712        ];
713
714        // Simple random using system time nanoseconds
715        let nanos = SystemTime::now()
716            .duration_since(UNIX_EPOCH)
717            .map(|d| d.as_nanos())
718            .unwrap_or(0);
719
720        let adj_idx = (nanos % ADJECTIVES.len() as u128) as usize;
721        let noun_idx = ((nanos / 7) % NOUNS.len() as u128) as usize;
722
723        format!("{}-{}", ADJECTIVES[adj_idx], NOUNS[noun_idx])
724    }
725
726    #[cfg(target_arch = "wasm32")]
727    fn export_state_to_file(&self) {
728        // WASM: State export not available in browser
729    }
730
731    /// Propagates values from input nodes to connected config nodes
732    fn propagate_values(&mut self) {
733        use nodes::pins::{config as pin_config, math as pin_math};
734
735        let mut new_computed = ComputedStyle::default();
736        self.pending_configs.clear();
737
738        // Phase 1: Reset all config node and math node inputs to defaults
739        for (_, node_type) in self.nodes.values_mut() {
740            match node_type {
741                NodeType::Config(config) => match config {
742                    ConfigNodeType::NodeConfig(inputs) => *inputs = NodeConfigInputs::default(),
743                    ConfigNodeType::EdgeConfig(inputs) => *inputs = EdgeConfigInputs::default(),
744                    ConfigNodeType::ShadowConfig(inputs) => *inputs = ShadowConfigInputs::default(),
745                    ConfigNodeType::PinConfig(inputs) => *inputs = PinConfigInputs::default(),
746                    ConfigNodeType::ApplyToGraph {
747                        has_node_config,
748                        has_edge_config,
749                        has_pin_config,
750                    } => {
751                        *has_node_config = false;
752                        *has_edge_config = false;
753                        *has_pin_config = false;
754                    }
755                    ConfigNodeType::ApplyToNode {
756                        has_node_config,
757                        target_id,
758                    } => {
759                        *has_node_config = false;
760                        *target_id = None;
761                    }
762                },
763                NodeType::Math(state) => {
764                    state.input_a = None;
765                    state.input_b = None;
766                }
767                _ => {}
768            }
769        }
770
771        // Phase 1.5: Propagate values INTO Math nodes (iteratively for chaining)
772        // Math nodes can be chained (e.g., (A+B)*C), so we iterate until stable
773        let edges_snapshot: Vec<_> = self.edges.values().cloned().collect();
774
775        // We need multiple passes because Math→Math chains require the source
776        // to have computed its result before the target can use it
777        const MAX_ITERATIONS: usize = 10;
778        for _ in 0..MAX_ITERATIONS {
779            let mut changed = false;
780
781            for edge in &edges_snapshot {
782                // Get source node's output value
783                let source_value = self
784                    .nodes
785                    .get(&edge.from_node)
786                    .and_then(|(_, t)| t.output_value());
787
788                if let Some(value) = source_value {
789                    // Try to apply to target if it's a Math node
790                    if let Some((_, NodeType::Math(state))) = self.nodes.get_mut(&edge.to_node) {
791                        // Math pins: A, B (inputs), result (output)
792                        if let Some(float_val) = value.as_float() {
793                            if edge.to_pin == pin_math::A {
794                                if state.input_a != Some(float_val) {
795                                    state.input_a = Some(float_val);
796                                    changed = true;
797                                }
798                            } else if edge.to_pin == pin_math::B
799                                && state.input_b != Some(float_val) {
800                                    state.input_b = Some(float_val);
801                                    changed = true;
802                                }
803                        }
804                    }
805                }
806
807                // Also check reverse direction (edges can connect either way)
808                let source_value = self
809                    .nodes
810                    .get(&edge.to_node)
811                    .and_then(|(_, t)| t.output_value());
812
813                if let Some(value) = source_value
814                    && let Some((_, NodeType::Math(state))) = self.nodes.get_mut(&edge.from_node)
815                        && let Some(float_val) = value.as_float() {
816                            if edge.from_pin == pin_math::A {
817                                if state.input_a != Some(float_val) {
818                                    state.input_a = Some(float_val);
819                                    changed = true;
820                                }
821                            } else if edge.from_pin == pin_math::B
822                                && state.input_b != Some(float_val) {
823                                    state.input_b = Some(float_val);
824                                    changed = true;
825                                }
826                        }
827            }
828
829            if !changed {
830                break;
831            }
832        }
833
834        // Phase 2: Apply Input → Config connections (in both edge directions)
835        // Also apply Math → Config connections
836
837        for edge in &edges_snapshot {
838            let from_node_type = self.nodes.get(&edge.from_node).map(|(_, t)| t.clone());
839            let to_node_type = self.nodes.get(&edge.to_node).map(|(_, t)| t.clone());
840
841            if let (Some(from_type), Some(to_type)) = (from_node_type, to_node_type) {
842                // Handle Input → Config connections
843                if let (NodeType::Input(input), NodeType::Config(_)) = (&from_type, &to_type) {
844                    let value = input.output_value();
845                    self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
846                }
847                // Handle Config → Input connections (reverse direction)
848                if let (NodeType::Config(_), NodeType::Input(input)) = (&from_type, &to_type) {
849                    let value = input.output_value();
850                    self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
851                }
852                // Handle Math → Config connections
853                if let (NodeType::Math(state), NodeType::Config(_)) = (&from_type, &to_type)
854                    && let Some(result) = state.result() {
855                        let value = NodeValue::Float(result);
856                        self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
857                    }
858                // Handle Config → Math connections (reverse direction)
859                if let (NodeType::Config(_), NodeType::Math(state)) = (&from_type, &to_type)
860                    && let Some(result) = state.result() {
861                        let value = NodeValue::Float(result);
862                        self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
863                    }
864            }
865        }
866
867        // Phase 2.5: Handle ShadowConfig → NodeConfig connections
868        // ShadowConfig's output connects to NodeConfig's shadow input
869        for edge in &edges_snapshot {
870            let from_node_type = self.nodes.get(&edge.from_node).map(|(_, t)| t.clone());
871            let to_node_type = self.nodes.get(&edge.to_node).map(|(_, t)| t.clone());
872
873            if let (Some(from_type), Some(to_type)) = (from_node_type, to_node_type) {
874                // ShadowConfig (output CONFIG) → NodeConfig (shadow input SHADOW)
875                if let (
876                    NodeType::Config(ConfigNodeType::ShadowConfig(shadow_inputs)),
877                    NodeType::Config(ConfigNodeType::NodeConfig(_)),
878                ) = (&from_type, &to_type)
879                    && edge.from_pin == pin_config::CONFIG && edge.to_pin == pin_config::SHADOW {
880                        let shadow_config = shadow_inputs.build();
881                        let value = NodeValue::ShadowConfig(shadow_config);
882                        self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
883                    }
884                // Reverse: NodeConfig ← ShadowConfig
885                if let (
886                    NodeType::Config(ConfigNodeType::NodeConfig(_)),
887                    NodeType::Config(ConfigNodeType::ShadowConfig(shadow_inputs)),
888                ) = (&from_type, &to_type)
889                    && edge.to_pin == pin_config::CONFIG && edge.from_pin == pin_config::SHADOW {
890                        let shadow_config = shadow_inputs.build();
891                        let value = NodeValue::ShadowConfig(shadow_config);
892                        self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
893                    }
894            }
895        }
896
897        // Phase 3: After all inputs applied, process Config → ApplyToGraph connections
898        // Now config nodes have their updated inputs, so we can build configs
899        for edge in &edges_snapshot {
900            let from_node_type = self.nodes.get(&edge.from_node).map(|(_, t)| t.clone());
901            let to_node_type = self.nodes.get(&edge.to_node).map(|(_, t)| t.clone());
902
903            if let (Some(from_type), Some(to_type)) = (from_node_type, to_node_type) {
904                // Handle Config → ApplyToGraph connections
905                if let (
906                    NodeType::Config(config),
907                    NodeType::Config(ConfigNodeType::ApplyToGraph { .. }),
908                ) = (&from_type, &to_type)
909                {
910                    self.connect_config_to_apply(
911                        &edge.from_node,
912                        config,
913                        &edge.to_node,
914                        &edge.to_pin,
915                    );
916                }
917                // Handle ApplyToGraph → Config connections (reverse)
918                if let (
919                    NodeType::Config(ConfigNodeType::ApplyToGraph { .. }),
920                    NodeType::Config(config),
921                ) = (&from_type, &to_type)
922                {
923                    self.connect_config_to_apply(
924                        &edge.to_node,
925                        config,
926                        &edge.from_node,
927                        &edge.from_pin,
928                    );
929                }
930            }
931        }
932
933        // Phase 4: Build configs from ApplyToGraph nodes and apply to computed style
934        self.apply_graph_configs(&mut new_computed);
935
936        self.computed_style = new_computed;
937    }
938
939    /// Applies an input value to a specific pin on a config node
940    fn apply_value_to_config_node(
941        &mut self,
942        node_id: &NodeId,
943        pin_label: &PinLabel,
944        value: &NodeValue,
945    ) {
946        use nodes::pins::config as pin;
947
948        let Some((_, node_type)) = self.nodes.get_mut(node_id) else {
949            return;
950        };
951
952        let NodeType::Config(config) = node_type else {
953            return;
954        };
955
956        match config {
957            ConfigNodeType::NodeConfig(inputs) => {
958                // NodeConfig pin labels: config, bg, color, width, radius, opacity, shadow
959                if *pin_label == pin::BG_COLOR {
960                    inputs.fill_color = value.as_color();
961                } else if *pin_label == pin::COLOR {
962                    inputs.border_color = value.as_color();
963                } else if *pin_label == pin::WIDTH {
964                    inputs.border_width = value.as_float();
965                } else if *pin_label == pin::RADIUS {
966                    inputs.corner_radius = value.as_float();
967                } else if *pin_label == pin::OPACITY {
968                    inputs.opacity = value.as_float();
969                } else if *pin_label == pin::SHADOW
970                    && let Some(shadow) = value.as_shadow_config() {
971                        inputs.shadow = Some(shadow.clone());
972                    }
973            }
974            ConfigNodeType::EdgeConfig(inputs) => {
975                // EdgeConfig pin labels
976                if *pin_label == pin::START {
977                    inputs.start_color = value.as_color();
978                } else if *pin_label == pin::END {
979                    inputs.end_color = value.as_color();
980                } else if *pin_label == pin::THICK {
981                    inputs.thickness = value.as_float();
982                } else if *pin_label == pin::CURVE {
983                    inputs.curve = value.as_edge_curve();
984                } else if *pin_label == pin::PATTERN {
985                    inputs.pattern_type = value.as_pattern_type();
986                } else if *pin_label == pin::DASH {
987                    inputs.dash_length = value.as_float();
988                } else if *pin_label == pin::GAP {
989                    inputs.gap_length = value.as_float();
990                } else if *pin_label == pin::ANGLE {
991                    // Convert degrees from slider to radians for pattern angle
992                    inputs.pattern_angle = value.as_float().map(|deg| deg.to_radians());
993                } else if *pin_label == pin::SPEED {
994                    inputs.animation_speed = value.as_float();
995                // Stroke outline
996                } else if *pin_label == pin::STROKE_OL_THICK {
997                    inputs.stroke_outline_thickness = value.as_float();
998                } else if *pin_label == pin::STROKE_OL_COLOR {
999                    inputs.stroke_outline_color = value.as_color();
1000                // Border settings
1001                } else if *pin_label == pin::BORDER_WIDTH {
1002                    inputs.border_thickness = value.as_float();
1003                } else if *pin_label == pin::BORDER_GAP {
1004                    inputs.border_gap = value.as_float();
1005                } else if *pin_label == pin::BORDER_START_COLOR {
1006                    inputs.border_color = value.as_color();
1007                } else if *pin_label == pin::BORDER_END_COLOR {
1008                    inputs.border_color_end = value.as_color();
1009                } else if *pin_label == pin::BORDER_BG {
1010                    inputs.border_background = value.as_color();
1011                } else if *pin_label == pin::BORDER_BG_END {
1012                    inputs.border_background_end = value.as_color();
1013                } else if *pin_label == pin::BORDER_OL_THICK {
1014                    inputs.border_outline_thickness = value.as_float();
1015                } else if *pin_label == pin::BORDER_OL_COLOR {
1016                    inputs.border_outline_color = value.as_color();
1017                // Shadow settings
1018                } else if *pin_label == pin::SHADOW_BLUR {
1019                    inputs.shadow_blur = value.as_float();
1020                } else if *pin_label == pin::SHADOW_EXPAND {
1021                    inputs.shadow_expand = value.as_float();
1022                } else if *pin_label == pin::SHADOW_COLOR {
1023                    inputs.shadow_color = value.as_color();
1024                } else if *pin_label == pin::SHADOW_END_COLOR {
1025                    inputs.shadow_color_end = value.as_color();
1026                } else if *pin_label == pin::SHADOW_OFFSET_X {
1027                    inputs.shadow_offset_x = value.as_float();
1028                } else if *pin_label == pin::SHADOW_OFFSET_Y {
1029                    inputs.shadow_offset_y = value.as_float();
1030                }
1031            }
1032            ConfigNodeType::ShadowConfig(inputs) => {
1033                // ShadowConfig pin labels
1034                if *pin_label == pin::SHADOW_OFFSET {
1035                    // For shadow config, offset sets both x and y
1036                    inputs.offset_x = value.as_float();
1037                    inputs.offset_y = value.as_float();
1038                } else if *pin_label == pin::SHADOW_OFFSET_X {
1039                    inputs.offset_x = value.as_float();
1040                } else if *pin_label == pin::SHADOW_OFFSET_Y {
1041                    inputs.offset_y = value.as_float();
1042                } else if *pin_label == pin::SHADOW_BLUR {
1043                    inputs.blur_radius = value.as_float();
1044                } else if *pin_label == pin::SHADOW_COLOR {
1045                    inputs.color = value.as_color();
1046                } else if *pin_label == pin::ON {
1047                    inputs.enabled = value.as_bool();
1048                }
1049            }
1050            ConfigNodeType::PinConfig(inputs) => {
1051                // PinConfig pin labels
1052                if *pin_label == pin::COLOR {
1053                    inputs.color = value.as_color();
1054                } else if *pin_label == pin::SIZE {
1055                    inputs.radius = value.as_float();
1056                } else if *pin_label == pin::SHAPE {
1057                    inputs.shape = value.as_pin_shape();
1058                } else if *pin_label == pin::GLOW {
1059                    inputs.border_color = value.as_color();
1060                } else if *pin_label == pin::PULSE {
1061                    inputs.border_width = value.as_float();
1062                }
1063            }
1064            ConfigNodeType::ApplyToNode { target_id, .. } => {
1065                if *pin_label == pin::TARGET {
1066                    *target_id = value.as_int();
1067                }
1068            }
1069            _ => {}
1070        }
1071    }
1072
1073    /// Connects a config node's output to an ApplyToGraph node
1074    fn connect_config_to_apply(
1075        &mut self,
1076        config_node_id: &NodeId,
1077        _config_type: &ConfigNodeType, // Ignored - we read from current state
1078        apply_node_id: &NodeId,
1079        apply_pin_label: &PinLabel,
1080    ) {
1081        use nodes::pins::config as pin;
1082
1083        // Build the config from the CURRENT state of the config node (not the snapshot)
1084        let built_config = match self.nodes.get(config_node_id) {
1085            Some((_, NodeType::Config(ConfigNodeType::NodeConfig(inputs)))) => {
1086                Some(ConfigOutput::Node(inputs.build()))
1087            }
1088            Some((_, NodeType::Config(ConfigNodeType::EdgeConfig(inputs)))) => {
1089                Some(ConfigOutput::Edge(inputs.build()))
1090            }
1091            Some((_, NodeType::Config(ConfigNodeType::PinConfig(inputs)))) => {
1092                Some(ConfigOutput::Pin(inputs.build()))
1093            }
1094            _ => None,
1095        };
1096
1097        let Some((_, node_type)) = self.nodes.get_mut(apply_node_id) else {
1098            return;
1099        };
1100
1101        if let NodeType::Config(ConfigNodeType::ApplyToGraph {
1102            has_node_config,
1103            has_edge_config,
1104            has_pin_config,
1105        }) = node_type
1106        {
1107            if *apply_pin_label == pin::NODE_CONFIG {
1108                if matches!(&built_config, Some(ConfigOutput::Node(_))) {
1109                    *has_node_config = true;
1110                }
1111            } else if *apply_pin_label == pin::EDGE_CONFIG {
1112                if matches!(&built_config, Some(ConfigOutput::Edge(_))) {
1113                    *has_edge_config = true;
1114                }
1115            } else if *apply_pin_label == pin::PIN_CONFIG
1116                && matches!(&built_config, Some(ConfigOutput::Pin(_)))
1117            {
1118                *has_pin_config = true;
1119            }
1120        }
1121
1122        // Store the config for later application
1123        if let Some(config) = built_config {
1124            self.pending_configs
1125                .entry(apply_node_id.clone())
1126                .or_default()
1127                .push((*apply_pin_label, config));
1128        }
1129    }
1130
1131    /// Applies configs from ApplyToGraph nodes to the computed style
1132    fn apply_graph_configs(&mut self, computed: &mut ComputedStyle) {
1133        // Find ApplyToGraph nodes and apply their connected configs
1134        for (node_id, (_, node_type)) in &self.nodes {
1135            if let NodeType::Config(ConfigNodeType::ApplyToGraph {
1136                has_node_config,
1137                has_edge_config,
1138                has_pin_config,
1139            }) = node_type
1140                && let Some(configs) = self.pending_configs.get(node_id) {
1141                    for (_, config) in configs {
1142                        match config {
1143                            ConfigOutput::Node(node_config) => {
1144                                if *has_node_config {
1145                                    if let Some(r) = node_config.corner_radius {
1146                                        computed.corner_radius = Some(r);
1147                                    }
1148                                    if let Some(o) = node_config.opacity {
1149                                        computed.opacity = Some(o);
1150                                    }
1151                                    if let Some(ref b) = node_config.border {
1152                                        computed.border_width = Some(b.pattern.thickness);
1153                                    }
1154                                    if let Some(c) = node_config.fill_color {
1155                                        computed.fill_color = Some(c);
1156                                    }
1157                                    if node_config.shadow.is_some() {
1158                                        computed.shadow = node_config.shadow.clone();
1159                                    }
1160                                }
1161                            }
1162                            ConfigOutput::Edge(edge_config) => {
1163                                if *has_edge_config {
1164                                    // Merge with existing or set as new
1165                                    computed.edge_config = Some(match &computed.edge_config {
1166                                        Some(existing) => edge_config.merge(existing),
1167                                        None => edge_config.clone(),
1168                                    });
1169                                }
1170                            }
1171                            ConfigOutput::Pin(pin_config) => {
1172                                if *has_pin_config {
1173                                    if let Some(c) = pin_config.color {
1174                                        computed.pin_color = Some(c);
1175                                    }
1176                                    if let Some(r) = pin_config.radius {
1177                                        computed.pin_radius = Some(r);
1178                                    }
1179                                    if let Some(s) = pin_config.shape {
1180                                        computed.pin_shape = Some(s);
1181                                    }
1182                                    if let Some(c) = pin_config.border_color {
1183                                        computed.pin_border_color = Some(c);
1184                                    }
1185                                    if let Some(w) = pin_config.border_width {
1186                                        computed.pin_border_width = Some(w);
1187                                    }
1188                                }
1189                            }
1190                        }
1191                    }
1192                }
1193        }
1194        // Clear pending configs after application
1195        self.pending_configs.clear();
1196    }
1197
1198    fn update(&mut self, message: ApplicationMessage) -> Task<ApplicationMessage> {
1199        match message {
1200            ApplicationMessage::Noop => Task::none(),
1201            ApplicationMessage::EdgeConnected { from, to } => {
1202                let edge_id = generate_edge_id();
1203                self.edges.insert(
1204                    edge_id.clone(),
1205                    EdgeData {
1206                        from_node: from.node_id,
1207                        from_pin: from.pin_id,
1208                        to_node: to.node_id,
1209                        to_pin: to.pin_id,
1210                    },
1211                );
1212                self.edge_order.push(edge_id);
1213                self.propagate_values();
1214                self.save_state();
1215                Task::none()
1216            }
1217            ApplicationMessage::NodeMoved {
1218                node_id,
1219                new_position,
1220            } => {
1221                if let Some((position, _)) = self.nodes.get_mut(&node_id) {
1222                    *position = new_position;
1223                }
1224                self.save_state();
1225                Task::none()
1226            }
1227            ApplicationMessage::EdgeDisconnected { from, to } => {
1228                // Find and remove the edge by matching from/to
1229                let edge_to_remove: Option<EdgeId> = self
1230                    .edges
1231                    .iter()
1232                    .find(|(_, e)| {
1233                        e.from_node == from.node_id
1234                            && e.from_pin == from.pin_id
1235                            && e.to_node == to.node_id
1236                            && e.to_pin == to.pin_id
1237                    })
1238                    .map(|(id, _)| id.clone());
1239
1240                if let Some(edge_id) = edge_to_remove {
1241                    self.edges.remove(&edge_id);
1242                    self.edge_order.retain(|id| id != &edge_id);
1243                }
1244                self.propagate_values();
1245                self.save_state();
1246                Task::none()
1247            }
1248            ApplicationMessage::ToggleCommandPalette => {
1249                self.command_palette_open = !self.command_palette_open;
1250                if !self.command_palette_open {
1251                    if let Some(original) = self.palette_original_theme.take() {
1252                        self.current_theme = original;
1253                    }
1254                    self.palette_preview_theme = None;
1255                    self.command_input.clear();
1256                    self.palette_view = PaletteView::Main;
1257                    self.palette_selected_index = 0;
1258                    Task::none()
1259                } else {
1260                    self.palette_original_theme = Some(self.current_theme.clone());
1261                    self.palette_view = PaletteView::Main;
1262                    self.palette_selected_index = 0;
1263                    focus_input()
1264                }
1265            }
1266            ApplicationMessage::CommandPaletteInput(input) => {
1267                self.command_input = input;
1268                self.palette_selected_index = 0;
1269                Task::none()
1270            }
1271            ApplicationMessage::ExecuteShortcut(cmd_id) => match cmd_id.as_str() {
1272                "add_node" => {
1273                    self.command_palette_open = true;
1274                    self.palette_original_theme = Some(self.current_theme.clone());
1275                    self.palette_view = PaletteView::Submenu("nodes".to_string());
1276                    self.palette_selected_index = 0;
1277                    self.command_input.clear();
1278                    focus_input()
1279                }
1280                "change_theme" => {
1281                    self.command_palette_open = true;
1282                    self.palette_original_theme = Some(self.current_theme.clone());
1283                    self.palette_view = PaletteView::Submenu("themes".to_string());
1284                    self.palette_selected_index = 0;
1285                    self.command_input.clear();
1286                    focus_input()
1287                }
1288                "export_state" => {
1289                    self.export_state_to_file();
1290                    Task::none()
1291                }
1292                _ => Task::none(),
1293            },
1294            ApplicationMessage::CommandPaletteNavigate(new_index) => {
1295                if !self.command_palette_open {
1296                    return Task::none();
1297                }
1298                self.palette_selected_index = new_index;
1299
1300                if let PaletteView::Submenu(ref submenu) = self.palette_view
1301                    && submenu == "themes" {
1302                        let (_, commands) = self.build_palette_commands();
1303                        if let Some(original_idx) = get_filtered_command_index(
1304                            &self.command_input,
1305                            &commands,
1306                            self.palette_selected_index,
1307                        ) {
1308                            let themes = Self::get_available_themes();
1309                            if original_idx < themes.len() {
1310                                self.palette_preview_theme = Some(themes[original_idx].clone());
1311                            }
1312                        }
1313                    }
1314                Task::none()
1315            }
1316            ApplicationMessage::CommandPaletteNavigateUp => {
1317                if !self.command_palette_open {
1318                    return Task::none();
1319                }
1320                let (_, commands) = self.build_palette_commands();
1321                let filtered_count = get_filtered_count(&self.command_input, &commands);
1322                let new_index = navigate_up(self.palette_selected_index, filtered_count);
1323                self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1324            }
1325            ApplicationMessage::CommandPaletteNavigateDown => {
1326                if !self.command_palette_open {
1327                    return Task::none();
1328                }
1329                let (_, commands) = self.build_palette_commands();
1330                let filtered_count = get_filtered_count(&self.command_input, &commands);
1331                let new_index = navigate_down(self.palette_selected_index, filtered_count);
1332                self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1333            }
1334            ApplicationMessage::CommandPaletteSelect(index) => {
1335                if !self.command_palette_open {
1336                    return Task::none();
1337                }
1338                self.palette_selected_index = index;
1339                self.update(ApplicationMessage::CommandPaletteConfirm)
1340            }
1341            ApplicationMessage::CommandPaletteConfirm => {
1342                if !self.command_palette_open {
1343                    return Task::none();
1344                }
1345                let (_, commands) = self.build_palette_commands();
1346                let Some(original_idx) = get_filtered_command_index(
1347                    &self.command_input,
1348                    &commands,
1349                    self.palette_selected_index,
1350                ) else {
1351                    return Task::none();
1352                };
1353
1354                use iced_palette::CommandAction;
1355                let cmd = &commands[original_idx];
1356                match &cmd.action {
1357                    CommandAction::Message(msg) => {
1358                        let msg = msg.clone();
1359                        self.command_input.clear();
1360                        self.palette_selected_index = 0;
1361                        match msg {
1362                            ApplicationMessage::NavigateToSubmenu(submenu) => {
1363                                self.palette_view = PaletteView::Submenu(submenu);
1364                                focus_input()
1365                            }
1366                            ApplicationMessage::SpawnNode { node_type } => {
1367                                let new_id = generate_node_id();
1368                                let pos = self.spawn_position();
1369                                self.nodes.insert(new_id.clone(), (pos, node_type));
1370                                self.node_order.push(new_id.clone());
1371                                self.selected_nodes = HashSet::from([new_id]);
1372                                self.command_palette_open = false;
1373                                self.palette_view = PaletteView::Main;
1374                                Task::none()
1375                            }
1376                            ApplicationMessage::ChangeTheme(theme) => {
1377                                self.current_theme = theme;
1378                                self.palette_preview_theme = None;
1379                                self.palette_original_theme = None;
1380                                self.command_palette_open = false;
1381                                self.palette_view = PaletteView::Main;
1382                                Task::none()
1383                            }
1384                            ApplicationMessage::ExportState => {
1385                                self.command_palette_open = false;
1386                                self.palette_view = PaletteView::Main;
1387                                self.export_state_to_file();
1388                                Task::none()
1389                            }
1390                            _ => Task::none(),
1391                        }
1392                    }
1393                    _ => Task::none(),
1394                }
1395            }
1396            ApplicationMessage::CommandPaletteCancel => {
1397                if !self.command_palette_open {
1398                    return Task::none();
1399                }
1400                if let Some(original) = self.palette_original_theme.take() {
1401                    self.current_theme = original;
1402                }
1403                self.palette_preview_theme = None;
1404                self.command_palette_open = false;
1405                self.command_input.clear();
1406                self.palette_view = PaletteView::Main;
1407                self.palette_selected_index = 0;
1408                Task::none()
1409            }
1410            ApplicationMessage::SpawnNode { node_type } => {
1411                let new_id = generate_node_id();
1412                let pos = self.spawn_position();
1413                self.nodes.insert(new_id.clone(), (pos, node_type));
1414                self.node_order.push(new_id.clone());
1415                self.selected_nodes = HashSet::from([new_id]);
1416                self.command_palette_open = false;
1417                self.command_input.clear();
1418                self.palette_view = PaletteView::Main;
1419                self.save_state();
1420                Task::none()
1421            }
1422            ApplicationMessage::CameraChanged { position, zoom } => {
1423                self.camera_position = position;
1424                self.camera_zoom = zoom;
1425                self.save_state();
1426                Task::none()
1427            }
1428            ApplicationMessage::WindowResized(size) => {
1429                self.viewport_size = size;
1430                self.window_size = Some((size.width as u32, size.height as u32));
1431                // Query maximize state on resize - it may have changed
1432                window::oldest()
1433                    .and_then(window::is_maximized)
1434                    .map(ApplicationMessage::WindowMaximizedChanged)
1435            }
1436            ApplicationMessage::WindowMoved(position) => {
1437                self.window_position = Some((position.x as i32, position.y as i32));
1438                self.save_state();
1439                Task::none()
1440            }
1441            ApplicationMessage::WindowMaximizedChanged(maximized) => {
1442                self.window_maximized = Some(maximized);
1443                self.save_state();
1444                Task::none()
1445            }
1446            ApplicationMessage::ChangeTheme(theme) => {
1447                self.current_theme = theme;
1448                self.command_palette_open = false;
1449                self.command_input.clear();
1450                self.palette_view = PaletteView::Main;
1451                self.save_state();
1452                Task::none()
1453            }
1454            ApplicationMessage::NavigateToSubmenu(submenu) => {
1455                self.palette_view = PaletteView::Submenu(submenu);
1456                self.command_input.clear();
1457                focus_input()
1458            }
1459            ApplicationMessage::NavigateBack => {
1460                self.palette_view = PaletteView::Main;
1461                self.command_input.clear();
1462                focus_input()
1463            }
1464            ApplicationMessage::Tick => Task::none(),
1465            ApplicationMessage::ExportState => {
1466                self.export_state_to_file();
1467                Task::none()
1468            }
1469            ApplicationMessage::SelectionChanged(node_ids) => {
1470                self.selected_nodes = node_ids.into_iter().collect();
1471                Task::none()
1472            }
1473            ApplicationMessage::CloneNodes(node_ids) => {
1474                let offset = Vector::new(50.0, 50.0);
1475                let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
1476                let mut new_ids = Vec::new();
1477
1478                // Clone selected nodes
1479                for old_id in &node_ids {
1480                    if let Some((pos, node_type)) = self.nodes.get(old_id) {
1481                        let new_id = generate_node_id();
1482                        let new_pos = Point::new(pos.x + offset.x, pos.y + offset.y);
1483                        self.nodes
1484                            .insert(new_id.clone(), (new_pos, node_type.clone()));
1485                        self.node_order.push(new_id.clone());
1486                        id_map.insert(old_id.clone(), new_id.clone());
1487                        new_ids.push(new_id);
1488                    }
1489                }
1490
1491                // Clone edges between selected nodes
1492                let edges_to_clone: Vec<_> = self
1493                    .edges
1494                    .iter()
1495                    .filter(|(_, e)| {
1496                        node_ids.contains(&e.from_node) && node_ids.contains(&e.to_node)
1497                    })
1498                    .map(|(_, e)| e.clone())
1499                    .collect();
1500
1501                for edge in edges_to_clone {
1502                    if let (Some(new_from), Some(new_to)) =
1503                        (id_map.get(&edge.from_node), id_map.get(&edge.to_node))
1504                    {
1505                        let new_edge_id = generate_edge_id();
1506                        self.edges.insert(
1507                            new_edge_id.clone(),
1508                            EdgeData {
1509                                from_node: new_from.clone(),
1510                                from_pin: edge.from_pin,
1511                                to_node: new_to.clone(),
1512                                to_pin: edge.to_pin,
1513                            },
1514                        );
1515                        self.edge_order.push(new_edge_id);
1516                    }
1517                }
1518
1519                self.selected_nodes = new_ids.into_iter().collect();
1520                self.propagate_values();
1521                self.save_state();
1522                Task::none()
1523            }
1524            ApplicationMessage::DeleteNodes(node_ids) => {
1525                // With NanoIDs, deletion is simple - no index remapping needed!
1526                for node_id in &node_ids {
1527                    // Remove node
1528                    self.nodes.remove(node_id);
1529                    self.node_order.retain(|id| id != node_id);
1530
1531                    // Remove edges connected to this node
1532                    let edges_to_remove: Vec<_> = self
1533                        .edges
1534                        .iter()
1535                        .filter(|(_, e)| &e.from_node == node_id || &e.to_node == node_id)
1536                        .map(|(id, _)| id.clone())
1537                        .collect();
1538
1539                    for edge_id in edges_to_remove {
1540                        self.edges.remove(&edge_id);
1541                        self.edge_order.retain(|id| id != &edge_id);
1542                    }
1543                }
1544
1545                self.selected_nodes.clear();
1546                self.propagate_values();
1547                self.save_state();
1548                Task::none()
1549            }
1550            ApplicationMessage::GroupMoved { node_ids, delta } => {
1551                for node_id in node_ids {
1552                    if let Some((pos, _)) = self.nodes.get_mut(&node_id) {
1553                        pos.x += delta.x;
1554                        pos.y += delta.y;
1555                    }
1556                }
1557                self.save_state();
1558                Task::none()
1559            }
1560            ApplicationMessage::SliderChanged { node_id, value } => {
1561                if let Some((_, NodeType::Input(InputNodeType::FloatSlider { value: v, .. }))) =
1562                    self.nodes.get_mut(&node_id)
1563                {
1564                    *v = value;
1565                    self.propagate_values();
1566                }
1567                Task::none()
1568            }
1569            ApplicationMessage::IntSliderChanged { node_id, value } => {
1570                if let Some((_, NodeType::Input(InputNodeType::IntSlider { value: v, .. }))) =
1571                    self.nodes.get_mut(&node_id)
1572                {
1573                    *v = value;
1574                    self.propagate_values();
1575                }
1576                Task::none()
1577            }
1578            ApplicationMessage::BoolChanged { node_id, value } => {
1579                if let Some((_, NodeType::Input(InputNodeType::BoolToggle { value: v, .. }))) =
1580                    self.nodes.get_mut(&node_id)
1581                {
1582                    *v = value;
1583                    self.propagate_values();
1584                }
1585                Task::none()
1586            }
1587            ApplicationMessage::EdgeCurveChanged { node_id, value } => {
1588                if let Some((_, NodeType::Input(InputNodeType::EdgeCurveSelector { value: v }))) =
1589                    self.nodes.get_mut(&node_id)
1590                {
1591                    *v = value;
1592                    self.propagate_values();
1593                }
1594                Task::none()
1595            }
1596            ApplicationMessage::PinShapeChanged { node_id, value } => {
1597                if let Some((_, NodeType::Input(InputNodeType::PinShapeSelector { value: v }))) =
1598                    self.nodes.get_mut(&node_id)
1599                {
1600                    *v = value;
1601                    self.propagate_values();
1602                }
1603                Task::none()
1604            }
1605            ApplicationMessage::PatternTypeChanged { node_id, value } => {
1606                if let Some((_, NodeType::Input(InputNodeType::PatternTypeSelector { value: v }))) =
1607                    self.nodes.get_mut(&node_id)
1608                {
1609                    *v = value;
1610                    self.propagate_values();
1611                }
1612                Task::none()
1613            }
1614            ApplicationMessage::ColorChanged { node_id, color } => {
1615                if let Some((_, node_type)) = self.nodes.get_mut(&node_id) {
1616                    match node_type {
1617                        NodeType::Input(InputNodeType::ColorPicker { color: c }) => {
1618                            *c = color;
1619                            self.propagate_values();
1620                        }
1621                        NodeType::Input(InputNodeType::ColorPreset { color: c }) => {
1622                            *c = color;
1623                            self.propagate_values();
1624                        }
1625                        _ => {}
1626                    }
1627                }
1628                Task::none()
1629            }
1630            ApplicationMessage::ToggleNodeExpanded { node_id } => {
1631                if self.expanded_nodes.contains(&node_id) {
1632                    self.expanded_nodes.remove(&node_id);
1633                } else {
1634                    self.expanded_nodes.insert(node_id);
1635                }
1636                Task::none()
1637            }
1638            ApplicationMessage::UpdateFloatSliderConfig { node_id, config } => {
1639                if let Some((_, NodeType::Input(InputNodeType::FloatSlider { config: c, value }))) =
1640                    self.nodes.get_mut(&node_id)
1641                {
1642                    // Clamp value to new range if needed
1643                    *value = value.clamp(config.min, config.max);
1644                    *c = config;
1645                }
1646                Task::none()
1647            }
1648            ApplicationMessage::UpdateIntSliderConfig { node_id, config } => {
1649                if let Some((_, NodeType::Input(InputNodeType::IntSlider { config: c, value }))) =
1650                    self.nodes.get_mut(&node_id)
1651                {
1652                    // Clamp value to new range if needed
1653                    *value = (*value).clamp(config.min, config.max);
1654                    *c = config;
1655                }
1656                Task::none()
1657            }
1658            ApplicationMessage::ToggleEdgeSection { node_id, section } => {
1659                if section == EdgeSection::Debug {
1660                    // Toggle tile_debug on all EdgeConfig input states
1661                    for (_, (_, node_type)) in self.nodes.iter_mut() {
1662                        if let NodeType::Config(ConfigNodeType::EdgeConfig(inputs)) = node_type {
1663                            inputs.tile_debug = !inputs.tile_debug;
1664                        }
1665                    }
1666                } else {
1667                    let sections = self.edge_config_sections
1668                        .entry(node_id)
1669                        .or_insert_with(EdgeSections::new_all_expanded);
1670                    match section {
1671                        EdgeSection::Stroke => sections.stroke = !sections.stroke,
1672                        EdgeSection::Pattern => sections.pattern = !sections.pattern,
1673                        EdgeSection::Border => sections.border = !sections.border,
1674                        EdgeSection::Shadow => sections.shadow = !sections.shadow,
1675                        EdgeSection::Debug => unreachable!(),
1676                    }
1677                }
1678                Task::none()
1679            }
1680            ApplicationMessage::ToggleNodeSection { node_id, section } => {
1681                let sections = self.node_config_sections
1682                    .entry(node_id)
1683                    .or_insert_with(NodeSections::new_all_expanded);
1684                match section {
1685                    NodeSection::Fill => sections.fill = !sections.fill,
1686                    NodeSection::Border => sections.border = !sections.border,
1687                    NodeSection::Shadow => sections.shadow = !sections.shadow,
1688                }
1689                Task::none()
1690            }
1691        }
1692    }
1693
1694    fn theme(&self) -> Theme {
1695        self.palette_preview_theme
1696            .as_ref()
1697            .unwrap_or(&self.current_theme)
1698            .clone()
1699    }
1700
1701    fn get_main_commands_with_shortcuts() -> Vec<Command<ApplicationMessage>> {
1702        vec![
1703            command("add_node", "Add Node")
1704                .description("Add a new node to the graph")
1705                .shortcut(Shortcut::cmd('n'))
1706                .action(ApplicationMessage::ExecuteShortcut("add_node".to_string())),
1707            command("change_theme", "Change Theme")
1708                .description("Switch to a different color theme")
1709                .shortcut(Shortcut::cmd('t'))
1710                .action(ApplicationMessage::ExecuteShortcut(
1711                    "change_theme".to_string(),
1712                )),
1713            command("export_state", "Export State")
1714                .description("Export graph state to file for Claude")
1715                .shortcut(Shortcut::cmd('e'))
1716                .action(ApplicationMessage::ExecuteShortcut(
1717                    "export_state".to_string(),
1718                )),
1719        ]
1720    }
1721
1722    fn get_available_themes() -> Vec<Theme> {
1723        vec![
1724            Theme::Dark,
1725            Theme::Light,
1726            Theme::Dracula,
1727            Theme::Nord,
1728            Theme::SolarizedLight,
1729            Theme::SolarizedDark,
1730            Theme::GruvboxLight,
1731            Theme::GruvboxDark,
1732            Theme::CatppuccinLatte,
1733            Theme::CatppuccinFrappe,
1734            Theme::CatppuccinMacchiato,
1735            Theme::CatppuccinMocha,
1736            Theme::TokyoNight,
1737            Theme::TokyoNightStorm,
1738            Theme::TokyoNightLight,
1739            Theme::KanagawaWave,
1740            Theme::KanagawaDragon,
1741            Theme::KanagawaLotus,
1742            Theme::Moonfly,
1743            Theme::Nightfly,
1744            Theme::Oxocarbon,
1745            Theme::Ferra,
1746        ]
1747    }
1748
1749    fn get_theme_name(theme: &Theme) -> &'static str {
1750        match theme {
1751            Theme::Dark => "Dark",
1752            Theme::Light => "Light",
1753            Theme::Dracula => "Dracula",
1754            Theme::Nord => "Nord",
1755            Theme::SolarizedLight => "Solarized Light",
1756            Theme::SolarizedDark => "Solarized Dark",
1757            Theme::GruvboxLight => "Gruvbox Light",
1758            Theme::GruvboxDark => "Gruvbox Dark",
1759            Theme::CatppuccinLatte => "Catppuccin Latte",
1760            Theme::CatppuccinFrappe => "Catppuccin Frappe",
1761            Theme::CatppuccinMacchiato => "Catppuccin Macchiato",
1762            Theme::CatppuccinMocha => "Catppuccin Mocha",
1763            Theme::TokyoNight => "Tokyo Night",
1764            Theme::TokyoNightStorm => "Tokyo Night Storm",
1765            Theme::TokyoNightLight => "Tokyo Night Light",
1766            Theme::KanagawaWave => "Kanagawa Wave",
1767            Theme::KanagawaDragon => "Kanagawa Dragon",
1768            Theme::KanagawaLotus => "Kanagawa Lotus",
1769            Theme::Moonfly => "Moonfly",
1770            Theme::Nightfly => "Nightfly",
1771            Theme::Oxocarbon => "Oxocarbon",
1772            Theme::Ferra => "Ferra",
1773            _ => "Unknown",
1774        }
1775    }
1776
1777    fn view(&self) -> iced::Element<'_, ApplicationMessage> {
1778        use iced_nodegraph::{NodeGraph, PinRef};
1779
1780        // Use preview theme if active (for theme selection), otherwise current theme
1781        let theme = self
1782            .palette_preview_theme
1783            .as_ref()
1784            .unwrap_or(&self.current_theme);
1785
1786        // Graph-wide node defaults - combine with per-node configs using merge()
1787        let node_defaults = NodeConfig::new().corner_radius(8.0).opacity(0.88);
1788
1789        // Pin defaults from connected config nodes
1790        let pin_defaults = self.computed_style.to_pin_config();
1791
1792        let mut ng: NodeGraph<
1793            '_,
1794            NodeId,
1795            PinLabel,
1796            EdgeId,
1797            ApplicationMessage,
1798            Theme,
1799            iced::Renderer,
1800        > = NodeGraph::default();
1801
1802        ng = ng
1803            .on_connect(
1804                |from: PinRef<NodeId, PinLabel>, to: PinRef<NodeId, PinLabel>| {
1805                    ApplicationMessage::EdgeConnected { from, to }
1806                },
1807            )
1808            .on_disconnect(
1809                |from: PinRef<NodeId, PinLabel>, to: PinRef<NodeId, PinLabel>| {
1810                    ApplicationMessage::EdgeDisconnected { from, to }
1811                },
1812            )
1813            .on_move(|node_id, new_position| ApplicationMessage::NodeMoved {
1814                node_id,
1815                new_position,
1816            })
1817            .on_select(ApplicationMessage::SelectionChanged)
1818            .on_clone(ApplicationMessage::CloneNodes)
1819            .on_delete(ApplicationMessage::DeleteNodes)
1820            .on_group_move(|node_ids, delta| ApplicationMessage::GroupMoved { node_ids, delta })
1821            .on_camera_change(|position, zoom| ApplicationMessage::CameraChanged { position, zoom })
1822            .initial_camera(self.camera_position, self.camera_zoom)
1823            .pin_defaults(pin_defaults)
1824            // Style callbacks - user controls appearance based on status
1825            // The base style comes from per-element config or theme defaults
1826            .node_style(|_theme, status, base| {
1827                use iced_nodegraph::NodeStatus;
1828                match status {
1829                    NodeStatus::Selected => base
1830                        .border_color(iced::Color::from_rgb(0.3, 0.6, 1.0))
1831                        .border_width(2.5),
1832                    NodeStatus::Idle => base,
1833                }
1834            })
1835            .pin_style(|_theme, _status, base| {
1836                // Pin animation (pulsing) is handled internally via scaled_radius()
1837                base
1838            })
1839            .edge_style(|_theme, status, base| {
1840                use iced_nodegraph::EdgeStatus;
1841                match status {
1842                    EdgeStatus::PendingCut => {
1843                        base.solid_color(iced::Color::from_rgb(1.0, 0.3, 0.3))
1844                    }
1845                    EdgeStatus::Idle => base,
1846                }
1847            })
1848            .box_select_style(|_theme| {
1849                (
1850                    iced::Color::from_rgba(0.3, 0.6, 1.0, 0.15), // fill
1851                    iced::Color::from_rgb(0.3, 0.6, 1.0),        // border
1852                )
1853            })
1854            .cutting_tool_style(|_theme| iced::Color::from_rgb(1.0, 0.3, 0.3));
1855
1856        // Add all nodes from state (in order)
1857        for node_id in &self.node_order {
1858            let Some((position, node_type)) = self.nodes.get(node_id) else {
1859                continue;
1860            };
1861            let node_id_clone = node_id.clone();
1862            let element: iced::Element<'_, ApplicationMessage> = match node_type {
1863                NodeType::Workflow(name) => node(name.as_str(), theme),
1864                NodeType::Input(input) => match input {
1865                    InputNodeType::FloatSlider { config, value } => {
1866                        let id = node_id_clone.clone();
1867                        let expanded = self.expanded_nodes.contains(node_id);
1868                        float_slider_node(
1869                            theme,
1870                            *value,
1871                            config,
1872                            expanded,
1873                            {
1874                                let id = id.clone();
1875                                move |v| ApplicationMessage::SliderChanged {
1876                                    node_id: id.clone(),
1877                                    value: v,
1878                                }
1879                            },
1880                            {
1881                                let id = id.clone();
1882                                move |cfg| ApplicationMessage::UpdateFloatSliderConfig {
1883                                    node_id: id.clone(),
1884                                    config: cfg,
1885                                }
1886                            },
1887                            ApplicationMessage::ToggleNodeExpanded { node_id: id },
1888                        )
1889                    }
1890                    InputNodeType::IntSlider { config, value } => {
1891                        let id = node_id_clone.clone();
1892                        let expanded = self.expanded_nodes.contains(node_id);
1893                        int_slider_node(
1894                            theme,
1895                            *value,
1896                            config,
1897                            expanded,
1898                            {
1899                                let id = id.clone();
1900                                move |v| ApplicationMessage::IntSliderChanged {
1901                                    node_id: id.clone(),
1902                                    value: v,
1903                                }
1904                            },
1905                            {
1906                                let id = id.clone();
1907                                move |cfg| ApplicationMessage::UpdateIntSliderConfig {
1908                                    node_id: id.clone(),
1909                                    config: cfg,
1910                                }
1911                            },
1912                            ApplicationMessage::ToggleNodeExpanded { node_id: id },
1913                        )
1914                    }
1915                    InputNodeType::BoolToggle { config, value } => {
1916                        let id = node_id_clone.clone();
1917                        bool_toggle_node(theme, *value, config, move |v| {
1918                            ApplicationMessage::BoolChanged {
1919                                node_id: id.clone(),
1920                                value: v,
1921                            }
1922                        })
1923                    }
1924                    InputNodeType::EdgeCurveSelector { value } => {
1925                        let id = node_id_clone.clone();
1926                        edge_curve_selector_node(theme, *value, move |v| {
1927                            ApplicationMessage::EdgeCurveChanged {
1928                                node_id: id.clone(),
1929                                value: v,
1930                            }
1931                        })
1932                    }
1933                    InputNodeType::PinShapeSelector { value } => {
1934                        let id = node_id_clone.clone();
1935                        pin_shape_selector_node(theme, *value, move |v| {
1936                            ApplicationMessage::PinShapeChanged {
1937                                node_id: id.clone(),
1938                                value: v,
1939                            }
1940                        })
1941                    }
1942                    InputNodeType::PatternTypeSelector { value } => {
1943                        let id = node_id_clone.clone();
1944                        pattern_type_selector_node(theme, *value, move |v| {
1945                            ApplicationMessage::PatternTypeChanged {
1946                                node_id: id.clone(),
1947                                value: v,
1948                            }
1949                        })
1950                    }
1951                    InputNodeType::ColorPicker { color } => {
1952                        let id = node_id_clone.clone();
1953                        color_picker_node(theme, *color, move |c| {
1954                            ApplicationMessage::ColorChanged {
1955                                node_id: id.clone(),
1956                                color: c,
1957                            }
1958                        })
1959                    }
1960                    InputNodeType::ColorPreset { color } => {
1961                        let id = node_id_clone.clone();
1962                        color_preset_node(theme, *color, move |c| {
1963                            ApplicationMessage::ColorChanged {
1964                                node_id: id.clone(),
1965                                color: c,
1966                            }
1967                        })
1968                    }
1969                },
1970                NodeType::Config(config) => match config {
1971                    ConfigNodeType::NodeConfig(inputs) => {
1972                        let id = node_id_clone.clone();
1973                        let sections = self.node_config_sections
1974                            .get(&id)
1975                            .cloned()
1976                            .unwrap_or_else(NodeSections::new_all_expanded);
1977                        node_config_node(theme, inputs, &sections, move |section| {
1978                            ApplicationMessage::ToggleNodeSection { node_id: id.clone(), section }
1979                        })
1980                    }
1981                    ConfigNodeType::EdgeConfig(inputs) => {
1982                        let id = node_id_clone.clone();
1983                        let sections = self.edge_config_sections
1984                            .get(&id)
1985                            .cloned()
1986                            .unwrap_or_else(EdgeSections::new_all_expanded);
1987                        edge_config_node(theme, inputs, &sections, move |section| {
1988                            ApplicationMessage::ToggleEdgeSection { node_id: id.clone(), section }
1989                        })
1990                    }
1991                    ConfigNodeType::ShadowConfig(inputs) => shadow_config_node(theme, inputs),
1992                    ConfigNodeType::PinConfig(inputs) => pin_config_node(theme, inputs),
1993                    ConfigNodeType::ApplyToGraph {
1994                        has_node_config,
1995                        has_edge_config,
1996                        has_pin_config,
1997                    } => apply_to_graph_node(
1998                        theme,
1999                        *has_node_config,
2000                        *has_edge_config,
2001                        *has_pin_config,
2002                    ),
2003                    ConfigNodeType::ApplyToNode {
2004                        has_node_config,
2005                        target_id,
2006                    } => apply_to_node_node(theme, *has_node_config, *target_id),
2007                },
2008                NodeType::Math(state) => math_node(theme, state),
2009            };
2010
2011            // Apply computed style to workflow nodes only (not to input/config nodes)
2012            // Merge per-node config with defaults (per-node takes priority)
2013            if matches!(node_type, NodeType::Workflow(_)) {
2014                let config = self.computed_style.to_node_config().merge(&node_defaults);
2015                ng.push_node_styled(node_id.clone(), *position, element, config);
2016            } else {
2017                ng.push_node_styled(node_id.clone(), *position, element, node_defaults.clone());
2018            }
2019        }
2020
2021        // Add stored edges with computed config
2022        let edge_config = self.computed_style.to_edge_config();
2023        for edge_id in &self.edge_order {
2024            if let Some(edge) = self.edges.get(edge_id) {
2025                let from = PinRef::new(edge.from_node.clone(), edge.from_pin);
2026                let to = PinRef::new(edge.to_node.clone(), edge.to_pin);
2027                ng.push_edge_styled(from, to, edge_config.clone());
2028            }
2029        }
2030
2031        // Set edge defaults for dragging edge preview
2032        ng = ng.edge_defaults(edge_config);
2033
2034        // Enable tile debug if any EdgeConfig node has it toggled on
2035        let tile_debug = self.nodes.values().any(|(_, nt)| {
2036            matches!(nt, NodeType::Config(ConfigNodeType::EdgeConfig(inputs)) if inputs.tile_debug)
2037        });
2038        if tile_debug {
2039            ng = ng.sdf_debug(iced_nodegraph::SdfDebug {
2040                edges: true,
2041                shadows: false,
2042                node_fill: false,
2043                node_foreground: false,
2044            });
2045        }
2046
2047        let graph_view: iced::Element<'_, ApplicationMessage> = ng.into();
2048
2049        // Always use the same widget structure to preserve NodeGraph state
2050        // The command palette is conditionally shown as an overlay
2051        let overlay: iced::Element<'_, ApplicationMessage> = if self.command_palette_open {
2052            let (_, commands) = self.build_palette_commands();
2053            command_palette(
2054                &self.command_input,
2055                &commands,
2056                self.palette_selected_index,
2057                ApplicationMessage::CommandPaletteInput,
2058                ApplicationMessage::CommandPaletteSelect,
2059                ApplicationMessage::CommandPaletteNavigate,
2060                || ApplicationMessage::CommandPaletteCancel,
2061            )
2062        } else {
2063            // Invisible placeholder to maintain widget tree structure
2064            container(text("")).width(0).height(0).into()
2065        };
2066
2067        stack!(graph_view, overlay)
2068            .width(Length::Fill)
2069            .height(Length::Fill)
2070            .into()
2071    }
2072
2073    fn build_palette_commands(&self) -> (&'static str, Vec<Command<ApplicationMessage>>) {
2074        match &self.palette_view {
2075            PaletteView::Main => {
2076                let commands = vec![
2077                    command("add_node", "Add Node")
2078                        .description("Add a new node to the graph")
2079                        .shortcut(Shortcut::cmd('n'))
2080                        .action(ApplicationMessage::NavigateToSubmenu("nodes".to_string())),
2081                    command("change_theme", "Change Theme")
2082                        .description("Switch to a different color theme")
2083                        .shortcut(Shortcut::cmd('t'))
2084                        .action(ApplicationMessage::NavigateToSubmenu("themes".to_string())),
2085                    command("export_state", "Export State")
2086                        .description("Export graph state to file for Claude")
2087                        .shortcut(Shortcut::cmd('e'))
2088                        .action(ApplicationMessage::ExportState),
2089                ];
2090                ("Command Palette", commands)
2091            }
2092            PaletteView::Submenu(submenu) if submenu == "nodes" => {
2093                let commands = vec![
2094                    // Workflow nodes
2095                    command("workflow", "Workflow Nodes")
2096                        .description("Original demo nodes")
2097                        .action(ApplicationMessage::NavigateToSubmenu(
2098                            "workflow_nodes".to_string(),
2099                        )),
2100                    // Input nodes
2101                    command("inputs", "Input Nodes")
2102                        .description("Sliders, color pickers, etc.")
2103                        .action(ApplicationMessage::NavigateToSubmenu(
2104                            "input_nodes".to_string(),
2105                        )),
2106                    // Math nodes
2107                    command("math", "Math Nodes")
2108                        .description("Add, Subtract, Multiply, Divide")
2109                        .action(ApplicationMessage::NavigateToSubmenu(
2110                            "math_nodes".to_string(),
2111                        )),
2112                    // Config nodes
2113                    command("config", "Style Config Nodes")
2114                        .description("Configure node and edge styling")
2115                        .action(ApplicationMessage::NavigateToSubmenu(
2116                            "config_nodes".to_string(),
2117                        )),
2118                ];
2119                ("Add Node", commands)
2120            }
2121            PaletteView::Submenu(submenu) if submenu == "workflow_nodes" => {
2122                let workflow_nodes = vec!["email_trigger", "email_parser", "filter", "calendar"];
2123                let commands = workflow_nodes
2124                    .into_iter()
2125                    .map(|name| {
2126                        command(name, name).action(ApplicationMessage::SpawnNode {
2127                            node_type: NodeType::Workflow(name.to_string()),
2128                        })
2129                    })
2130                    .collect();
2131                ("Workflow Nodes", commands)
2132            }
2133            PaletteView::Submenu(submenu) if submenu == "input_nodes" => {
2134                let commands = vec![
2135                    command("float_slider", "Float Slider")
2136                        .description("Generic float slider (0-20)")
2137                        .action(ApplicationMessage::SpawnNode {
2138                            node_type: NodeType::Input(InputNodeType::FloatSlider {
2139                                config: FloatSliderConfig::default(),
2140                                value: 5.0,
2141                            }),
2142                        }),
2143                    command("pattern_angle", "Pattern Angle")
2144                        .description("Angle for Arrowed/Angled patterns (-90 to 90 degrees)")
2145                        .action(ApplicationMessage::SpawnNode {
2146                            node_type: NodeType::Input(InputNodeType::FloatSlider {
2147                                config: FloatSliderConfig::pattern_angle(),
2148                                value: 45.0,
2149                            }),
2150                        }),
2151                    command("color_picker", "Color Picker (RGB)")
2152                        .description("Full RGB color picker with sliders")
2153                        .action(ApplicationMessage::SpawnNode {
2154                            node_type: NodeType::Input(InputNodeType::ColorPicker {
2155                                color: Color::from_rgb(0.5, 0.5, 0.5),
2156                            }),
2157                        }),
2158                    command("color_preset", "Color Presets")
2159                        .description("Quick color selection from presets")
2160                        .action(ApplicationMessage::SpawnNode {
2161                            node_type: NodeType::Input(InputNodeType::ColorPreset {
2162                                color: Color::from_rgb(0.5, 0.5, 0.5),
2163                            }),
2164                        }),
2165                    command("int_slider", "Int Slider")
2166                        .description("Integer slider (0-100)")
2167                        .action(ApplicationMessage::SpawnNode {
2168                            node_type: NodeType::Input(InputNodeType::IntSlider {
2169                                config: IntSliderConfig::default(),
2170                                value: 50,
2171                            }),
2172                        }),
2173                    command("bool_toggle", "Boolean Toggle")
2174                        .description("Toggle for boolean values")
2175                        .action(ApplicationMessage::SpawnNode {
2176                            node_type: NodeType::Input(InputNodeType::BoolToggle {
2177                                config: BoolToggleConfig::default(),
2178                                value: true,
2179                            }),
2180                        }),
2181                    command("edge_curve", "Edge Curve Selector")
2182                        .description("Select edge curve (Bezier, Line, Orthogonal)")
2183                        .action(ApplicationMessage::SpawnNode {
2184                            node_type: NodeType::Input(InputNodeType::EdgeCurveSelector {
2185                                value: EdgeCurve::BezierCubic,
2186                            }),
2187                        }),
2188                    command("pin_shape", "Pin Shape Selector")
2189                        .description("Select pin shape (Circle, Square, Diamond)")
2190                        .action(ApplicationMessage::SpawnNode {
2191                            node_type: NodeType::Input(InputNodeType::PinShapeSelector {
2192                                value: PinShape::Circle,
2193                            }),
2194                        }),
2195                    command("pattern_type", "Pattern Type Selector")
2196                        .description("Select edge pattern (Solid, Dashed, Dotted)")
2197                        .action(ApplicationMessage::SpawnNode {
2198                            node_type: NodeType::Input(InputNodeType::PatternTypeSelector {
2199                                value: PatternType::Solid,
2200                            }),
2201                        }),
2202                ];
2203                ("Input Nodes", commands)
2204            }
2205            PaletteView::Submenu(submenu) if submenu == "math_nodes" => {
2206                let commands = vec![
2207                    command("add", "Add").description("A + B").action(
2208                        ApplicationMessage::SpawnNode {
2209                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Add)),
2210                        },
2211                    ),
2212                    command("subtract", "Subtract").description("A - B").action(
2213                        ApplicationMessage::SpawnNode {
2214                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Subtract)),
2215                        },
2216                    ),
2217                    command("multiply", "Multiply").description("A * B").action(
2218                        ApplicationMessage::SpawnNode {
2219                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Multiply)),
2220                        },
2221                    ),
2222                    command("divide", "Divide").description("A / B").action(
2223                        ApplicationMessage::SpawnNode {
2224                            node_type: NodeType::Math(MathNodeState::new(MathOperation::Divide)),
2225                        },
2226                    ),
2227                ];
2228                ("Math Nodes", commands)
2229            }
2230            PaletteView::Submenu(submenu) if submenu == "config_nodes" => {
2231                let commands = vec![
2232                    command("node_config", "Node Config")
2233                        .description("Node config with all fields and inheritance")
2234                        .action(ApplicationMessage::SpawnNode {
2235                            node_type: NodeType::Config(ConfigNodeType::NodeConfig(
2236                                NodeConfigInputs::default(),
2237                            )),
2238                        }),
2239                    command("edge_config", "Edge Config")
2240                        .description("Edge config with colors, thickness, type")
2241                        .action(ApplicationMessage::SpawnNode {
2242                            node_type: NodeType::Config(ConfigNodeType::EdgeConfig(
2243                                EdgeConfigInputs::default(),
2244                            )),
2245                        }),
2246                    command("shadow_config", "Shadow Config")
2247                        .description("Shadow configuration with offset, blur, color")
2248                        .action(ApplicationMessage::SpawnNode {
2249                            node_type: NodeType::Config(ConfigNodeType::ShadowConfig(
2250                                ShadowConfigInputs::default(),
2251                            )),
2252                        }),
2253                    command("pin_config", "Pin Config")
2254                        .description("Pin configuration with shape, color, radius")
2255                        .action(ApplicationMessage::SpawnNode {
2256                            node_type: NodeType::Config(ConfigNodeType::PinConfig(
2257                                PinConfigInputs::default(),
2258                            )),
2259                        }),
2260                    // Apply nodes
2261                    command("apply_to_graph", "Apply to Graph")
2262                        .description("Apply configs to all nodes/edges in graph")
2263                        .action(ApplicationMessage::SpawnNode {
2264                            node_type: NodeType::Config(ConfigNodeType::ApplyToGraph {
2265                                has_node_config: false,
2266                                has_edge_config: false,
2267                                has_pin_config: false,
2268                            }),
2269                        }),
2270                    command("apply_to_node", "Apply to Node")
2271                        .description("Apply config to a specific node by ID")
2272                        .action(ApplicationMessage::SpawnNode {
2273                            node_type: NodeType::Config(ConfigNodeType::ApplyToNode {
2274                                has_node_config: false,
2275                                target_id: None,
2276                            }),
2277                        }),
2278                ];
2279                ("Style Config Nodes", commands)
2280            }
2281            PaletteView::Submenu(submenu) if submenu == "themes" => {
2282                let commands = Self::get_available_themes()
2283                    .iter()
2284                    .map(|theme| {
2285                        let name = Self::get_theme_name(theme);
2286                        command(name, name).action(ApplicationMessage::ChangeTheme(theme.clone()))
2287                    })
2288                    .collect();
2289                ("Choose Theme", commands)
2290            }
2291            _ => ("Command Palette", vec![]),
2292        }
2293    }
2294
2295    fn subscription(&self) -> Subscription<ApplicationMessage> {
2296        Subscription::batch(vec![
2297            event::listen_with(handle_keyboard_event),
2298            window::frames().map(|_| ApplicationMessage::Tick),
2299            event::listen_with(|event, _, _| match event {
2300                Event::Window(window::Event::Resized(size)) => {
2301                    Some(ApplicationMessage::WindowResized(size))
2302                }
2303                Event::Window(window::Event::Moved(position)) => {
2304                    Some(ApplicationMessage::WindowMoved(position))
2305                }
2306                _ => None,
2307            }),
2308        ])
2309    }
2310}
2311
2312fn handle_keyboard_event(
2313    event: Event,
2314    _status: iced::event::Status,
2315    _window: iced::window::Id,
2316) -> Option<ApplicationMessage> {
2317    match event {
2318        Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
2319            if is_toggle_shortcut(&key, modifiers) {
2320                return Some(ApplicationMessage::ToggleCommandPalette);
2321            }
2322
2323            if modifiers.command() {
2324                let main_commands = Application::get_main_commands_with_shortcuts();
2325                if let Some(cmd_id) = find_matching_shortcut(&main_commands, &key, modifiers) {
2326                    return Some(ApplicationMessage::ExecuteShortcut(cmd_id.to_string()));
2327                }
2328            }
2329
2330            match key {
2331                keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
2332                    Some(ApplicationMessage::CommandPaletteNavigateUp)
2333                }
2334                keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
2335                    Some(ApplicationMessage::CommandPaletteNavigateDown)
2336                }
2337                keyboard::Key::Named(keyboard::key::Named::Enter) => {
2338                    Some(ApplicationMessage::CommandPaletteConfirm)
2339                }
2340                keyboard::Key::Named(keyboard::key::Named::Escape) => {
2341                    Some(ApplicationMessage::CommandPaletteCancel)
2342                }
2343                _ => None,
2344            }
2345        }
2346        _ => None,
2347    }
2348}
2349
2350#[cfg(test)]
2351mod tests {
2352    use super::*;
2353    use nodes::{MathNodeState, MathOperation, NodeType};
2354
2355    // === Math Operation Tests ===
2356
2357    #[test]
2358    fn test_math_add() {
2359        let op = MathOperation::Add;
2360        assert_eq!(op.compute(5.0, 3.0), 8.0);
2361        assert_eq!(op.symbol(), "+");
2362        assert_eq!(op.name(), "Add");
2363    }
2364
2365    #[test]
2366    fn test_math_subtract() {
2367        let op = MathOperation::Subtract;
2368        assert_eq!(op.compute(5.0, 3.0), 2.0);
2369        assert_eq!(op.compute(3.0, 5.0), -2.0);
2370        assert_eq!(op.symbol(), "-");
2371    }
2372
2373    #[test]
2374    fn test_math_multiply() {
2375        let op = MathOperation::Multiply;
2376        assert_eq!(op.compute(5.0, 3.0), 15.0);
2377        assert_eq!(op.compute(0.0, 100.0), 0.0);
2378        assert_eq!(op.symbol(), "*");
2379    }
2380
2381    #[test]
2382    fn test_math_divide() {
2383        let op = MathOperation::Divide;
2384        assert_eq!(op.compute(6.0, 2.0), 3.0);
2385        assert_eq!(op.symbol(), "/");
2386    }
2387
2388    #[test]
2389    fn test_math_divide_by_zero() {
2390        let op = MathOperation::Divide;
2391        let result = op.compute(5.0, 0.0);
2392        assert!(result.is_infinite());
2393    }
2394
2395    // === MathNodeState Tests ===
2396
2397    #[test]
2398    fn test_math_node_result_with_both_inputs() {
2399        let mut state = MathNodeState::new(MathOperation::Add);
2400        state.input_a = Some(10.0);
2401        state.input_b = Some(5.0);
2402        assert_eq!(state.result(), Some(15.0));
2403    }
2404
2405    #[test]
2406    fn test_math_node_result_with_missing_a() {
2407        let mut state = MathNodeState::new(MathOperation::Add);
2408        state.input_a = None;
2409        state.input_b = Some(5.0);
2410        assert_eq!(state.result(), None);
2411    }
2412
2413    #[test]
2414    fn test_math_node_result_with_missing_b() {
2415        let mut state = MathNodeState::new(MathOperation::Add);
2416        state.input_a = Some(10.0);
2417        state.input_b = None;
2418        assert_eq!(state.result(), None);
2419    }
2420
2421    // === NodeType Output Value Tests ===
2422
2423    #[test]
2424    fn test_math_node_output_value() {
2425        let mut state = MathNodeState::new(MathOperation::Multiply);
2426        state.input_a = Some(4.0);
2427        state.input_b = Some(3.0);
2428        let node_type = NodeType::Math(state);
2429
2430        let output = node_type.output_value();
2431        assert!(output.is_some());
2432        if let Some(NodeValue::Float(f)) = output {
2433            assert_eq!(f, 12.0);
2434        } else {
2435            panic!("Expected Float value");
2436        }
2437    }
2438
2439    #[test]
2440    fn test_math_node_output_value_no_result() {
2441        let state = MathNodeState::new(MathOperation::Add); // No inputs
2442        let node_type = NodeType::Math(state);
2443        assert!(node_type.output_value().is_none());
2444    }
2445
2446    #[test]
2447    fn test_input_node_output_value() {
2448        let input = InputNodeType::FloatSlider {
2449            config: FloatSliderConfig::default(),
2450            value: 7.5,
2451        };
2452        let node_type = NodeType::Input(input);
2453
2454        let output = node_type.output_value();
2455        assert!(output.is_some());
2456        if let Some(NodeValue::Float(f)) = output {
2457            assert!((f - 7.5).abs() < 0.001);
2458        } else {
2459            panic!("Expected Float value");
2460        }
2461    }
2462
2463    // === ComputedStyle Tests ===
2464
2465    #[test]
2466    fn test_computed_style_to_pin_config_empty() {
2467        let style = ComputedStyle::default();
2468        let config = style.to_pin_config();
2469        // Empty style should produce empty config
2470        assert!(config.color.is_none());
2471        assert!(config.radius.is_none());
2472        assert!(config.shape.is_none());
2473    }
2474
2475    #[test]
2476    fn test_computed_style_to_pin_config_with_values() {
2477        let mut style = ComputedStyle::default();
2478        style.pin_color = Some(Color::from_rgb(1.0, 0.0, 0.0));
2479        style.pin_radius = Some(10.0);
2480        style.pin_shape = Some(PinShape::Diamond);
2481
2482        let config = style.to_pin_config();
2483        assert_eq!(config.color, Some(Color::from_rgb(1.0, 0.0, 0.0)));
2484        assert_eq!(config.radius, Some(10.0));
2485        assert_eq!(config.shape, Some(PinShape::Diamond));
2486    }
2487
2488    #[test]
2489    fn test_computed_style_to_node_config() {
2490        let mut style = ComputedStyle::default();
2491        style.corner_radius = Some(12.0);
2492        style.opacity = Some(0.8);
2493        style.fill_color = Some(Color::from_rgb(0.2, 0.3, 0.4));
2494
2495        let config = style.to_node_config();
2496        assert_eq!(config.corner_radius, Some(12.0));
2497        assert_eq!(config.opacity, Some(0.8));
2498        assert_eq!(config.fill_color, Some(Color::from_rgb(0.2, 0.3, 0.4)));
2499    }
2500
2501    #[test]
2502    fn test_computed_style_edge_config_passthrough() {
2503        // EdgeConfig should be stored and returned as-is
2504        let mut style = ComputedStyle::default();
2505        assert!(style.to_edge_config().pattern.is_none());
2506
2507        let ec = EdgeConfig::new()
2508            .thickness(5.0)
2509            .solid_color(Color::from_rgb(1.0, 0.0, 0.0))
2510            .curve(EdgeCurve::Line);
2511        style.edge_config = Some(ec);
2512
2513        let result = style.to_edge_config();
2514        assert_eq!(result.pattern.unwrap().thickness, 5.0);
2515        assert_eq!(result.start_color, Some(Color::from_rgb(1.0, 0.0, 0.0)));
2516        assert_eq!(result.curve, Some(EdgeCurve::Line));
2517    }
2518
2519    #[test]
2520    fn test_edge_config_inputs_pattern_type_dashed() {
2521        use iced_nodegraph::SdfPatternType;
2522
2523        let mut inputs = EdgeConfigInputs::default();
2524        inputs.pattern_type = Some(PatternType::Dashed);
2525        inputs.thickness = Some(3.0);
2526        inputs.dash_length = Some(10.0);
2527        inputs.gap_length = Some(5.0);
2528
2529        let config = inputs.build();
2530        let pattern = config.pattern.expect("pattern should be Some");
2531        assert_eq!(pattern.thickness, 3.0);
2532        assert!(
2533            matches!(pattern.pattern_type, SdfPatternType::Dashed { dash, gap, .. } if (dash - 10.0).abs() < 0.01 && (gap - 5.0).abs() < 0.01),
2534            "Expected Dashed pattern, got {:?}",
2535            pattern.pattern_type
2536        );
2537    }
2538
2539    #[test]
2540    fn test_edge_config_inputs_pattern_type_arrowed() {
2541        use iced_nodegraph::SdfPatternType;
2542
2543        let mut inputs = EdgeConfigInputs::default();
2544        inputs.pattern_type = Some(PatternType::Arrowed);
2545
2546        let config = inputs.build();
2547        let pattern = config.pattern.expect("pattern should be Some");
2548        assert!(
2549            matches!(pattern.pattern_type, SdfPatternType::Arrowed { .. }),
2550            "Expected Arrowed pattern, got {:?}",
2551            pattern.pattern_type
2552        );
2553    }
2554
2555    #[test]
2556    fn test_edge_config_inputs_pattern_type_dotted() {
2557        use iced_nodegraph::SdfPatternType;
2558
2559        let mut inputs = EdgeConfigInputs::default();
2560        inputs.pattern_type = Some(PatternType::Dotted);
2561        inputs.dot_radius = Some(3.0);
2562        inputs.gap_length = Some(4.0);
2563
2564        let config = inputs.build();
2565        let pattern = config.pattern.expect("pattern should be Some");
2566        assert!(
2567            matches!(pattern.pattern_type, SdfPatternType::Dotted { .. }),
2568            "Expected Dotted pattern, got {:?}",
2569            pattern.pattern_type
2570        );
2571    }
2572
2573    #[test]
2574    fn test_edge_config_inputs_pattern_type_dash_capped() {
2575        use iced_nodegraph::SdfPatternType;
2576
2577        let mut inputs = EdgeConfigInputs::default();
2578        inputs.pattern_type = Some(PatternType::DashCapped);
2579
2580        let config = inputs.build();
2581        let pattern = config.pattern.expect("pattern should be Some");
2582        assert!(
2583            matches!(pattern.pattern_type, SdfPatternType::DashCapped { .. }),
2584            "Expected DashCapped pattern, got {:?}",
2585            pattern.pattern_type
2586        );
2587    }
2588
2589    #[test]
2590    fn test_edge_config_inputs_pattern_type_dash_dotted() {
2591        use iced_nodegraph::SdfPatternType;
2592
2593        let mut inputs = EdgeConfigInputs::default();
2594        inputs.pattern_type = Some(PatternType::DashDotted);
2595
2596        let config = inputs.build();
2597        let pattern = config.pattern.expect("pattern should be Some");
2598        assert!(
2599            matches!(pattern.pattern_type, SdfPatternType::DashDotted { .. }),
2600            "Expected DashDotted pattern, got {:?}",
2601            pattern.pattern_type
2602        );
2603    }
2604
2605    #[test]
2606    fn test_edge_config_inputs_pattern_preserved_through_build() {
2607        // Verify the full pipeline: EdgeConfigInputs -> build() -> EdgeConfig
2608        // Pattern, border, shadow must all survive
2609        use iced_nodegraph::SdfPatternType;
2610
2611        let mut inputs = EdgeConfigInputs::default();
2612        inputs.pattern_type = Some(PatternType::Dashed);
2613        inputs.thickness = Some(4.0);
2614        inputs.dash_length = Some(8.0);
2615        inputs.gap_length = Some(4.0);
2616        inputs.animation_speed = Some(50.0);
2617        inputs.start_color = Some(Color::from_rgb(1.0, 0.0, 0.0));
2618        inputs.end_color = Some(Color::from_rgb(0.0, 0.0, 1.0));
2619        inputs.border_thickness = Some(2.0);
2620        inputs.border_gap = Some(1.0);
2621        inputs.shadow_blur = Some(6.0);
2622        inputs.shadow_expand = Some(3.0);
2623
2624        let config = inputs.build();
2625
2626        // Pattern
2627        let pattern = config.pattern.expect("pattern must be present");
2628        assert_eq!(pattern.thickness, 4.0);
2629        assert!(matches!(pattern.pattern_type, SdfPatternType::Dashed { .. }));
2630        assert!((pattern.flow_speed - 50.0).abs() < 0.01);
2631
2632        // Colors
2633        assert_eq!(config.start_color, Some(Color::from_rgb(1.0, 0.0, 0.0)));
2634        assert_eq!(config.end_color, Some(Color::from_rgb(0.0, 0.0, 1.0)));
2635
2636        // Border
2637        let border = config.border.expect("border must be present");
2638        assert_eq!(border.width, 2.0);
2639        assert_eq!(border.gap, 1.0);
2640
2641        // Shadow
2642        let shadow = config.shadow.expect("shadow must be present");
2643        assert_eq!(shadow.blur, 6.0);
2644        assert_eq!(shadow.expand, 3.0);
2645    }
2646}