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