1#![allow(clippy::large_enum_variant)]
3
4mod 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#[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 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 SelectionChanged(Vec<NodeId>),
171 CloneNodes(Vec<NodeId>),
172 DeleteNodes(Vec<NodeId>),
173 GroupMoved {
174 node_ids: Vec<NodeId>,
175 delta: Vector,
176 },
177 ExportState,
179 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 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 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#[derive(Debug, Clone)]
239#[allow(dead_code)]
240enum ConfigOutput {
241 Node(NodeConfig),
242 Edge(EdgeConfig),
243 Pin(iced_nodegraph::PinConfig),
244}
245
246#[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 edge_config: Option<EdgeConfig>,
256 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 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 fn to_edge_config(&self) -> EdgeConfig {
289 self.edge_config.clone().unwrap_or_default()
290 }
291
292 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: HashMap<NodeId, (Point, NodeType)>,
317 node_order: Vec<NodeId>,
319 edges: HashMap<EdgeId, EdgeData>,
321 edge_order: Vec<EdgeId>,
323 selected_nodes: HashSet<NodeId>,
325 expanded_nodes: HashSet<NodeId>,
327 edge_config_sections: HashMap<NodeId, EdgeSections>,
329 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: ComputedStyle,
340 pending_configs: HashMap<NodeId, Vec<(PinLabel, ConfigOutput)>>,
342 viewport_size: iced::Size,
344 camera_position: Point,
346 camera_zoom: f32,
348 window_position: Option<(i32, i32)>,
350 window_size: Option<(u32, u32)>,
352 window_maximized: Option<bool>,
354}
355
356impl Default for Application {
357 fn default() -> Self {
358 use nodes::pins::workflow;
359
360 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 let mut edges = HashMap::new();
405 let mut edge_order = Vec::new();
406
407 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 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 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 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), 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 #[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 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 #[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 }
566
567 fn spawn_position(&self) -> Point {
569 let screen_center_x = self.viewport_size.width / 2.0;
571 let screen_center_y = self.viewport_size.height / 2.0;
572
573 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 Point::new(world_x - 50.0, world_y - 40.0)
579 }
580
581 #[cfg(not(target_arch = "wasm32"))]
584 fn export_state_to_file(&self) {
585 use std::io::Write;
586
587 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 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 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 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 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 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 #[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 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 }
730
731 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 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 let edges_snapshot: Vec<_> = self.edges.values().cloned().collect();
774
775 const MAX_ITERATIONS: usize = 10;
778 for _ in 0..MAX_ITERATIONS {
779 let mut changed = false;
780
781 for edge in &edges_snapshot {
782 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 if let Some((_, NodeType::Math(state))) = self.nodes.get_mut(&edge.to_node) {
791 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 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 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 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 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 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 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 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 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 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 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 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 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 self.apply_graph_configs(&mut new_computed);
935
936 self.computed_style = new_computed;
937 }
938
939 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 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 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 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 } 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 } 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 } 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 if *pin_label == pin::SHADOW_OFFSET {
1035 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 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 fn connect_config_to_apply(
1075 &mut self,
1076 config_node_id: &NodeId,
1077 _config_type: &ConfigNodeType, apply_node_id: &NodeId,
1079 apply_pin_label: &PinLabel,
1080 ) {
1081 use nodes::pins::config as pin;
1082
1083 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 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 fn apply_graph_configs(&mut self, computed: &mut ComputedStyle) {
1133 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 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 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 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 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 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 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 for node_id in &node_ids {
1527 self.nodes.remove(node_id);
1529 self.node_order.retain(|id| id != node_id);
1530
1531 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 *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 *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 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 let theme = self
1782 .palette_preview_theme
1783 .as_ref()
1784 .unwrap_or(&self.current_theme);
1785
1786 let node_defaults = NodeConfig::new().corner_radius(8.0).opacity(0.88);
1788
1789 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 .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 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), iced::Color::from_rgb(0.3, 0.6, 1.0), )
1853 })
1854 .cutting_tool_style(|_theme| iced::Color::from_rgb(1.0, 0.3, 0.3));
1855
1856 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, §ions, 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, §ions, 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 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 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 ng = ng.edge_defaults(edge_config);
2033
2034 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 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 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 command("workflow", "Workflow Nodes")
2096 .description("Original demo nodes")
2097 .action(ApplicationMessage::NavigateToSubmenu(
2098 "workflow_nodes".to_string(),
2099 )),
2100 command("inputs", "Input Nodes")
2102 .description("Sliders, color pickers, etc.")
2103 .action(ApplicationMessage::NavigateToSubmenu(
2104 "input_nodes".to_string(),
2105 )),
2106 command("math", "Math Nodes")
2108 .description("Add, Subtract, Multiply, Divide")
2109 .action(ApplicationMessage::NavigateToSubmenu(
2110 "math_nodes".to_string(),
2111 )),
2112 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 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 #[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 #[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 #[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); 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 #[test]
2466 fn test_computed_style_to_pin_config_empty() {
2467 let style = ComputedStyle::default();
2468 let config = style.to_pin_config();
2469 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 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 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 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 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 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 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}