1#![allow(clippy::large_enum_variant)]
4#![doc = include_str!("../README.md")]
5#![doc = r#"<link rel="stylesheet" href="../gallery/pkg/demo.css"><script type="module" src="../gallery/pkg/demo-loader.js"></script>"#]
6
7mod ids;
8mod nodes;
9#[cfg(not(target_arch = "wasm32"))]
10mod persistence;
11mod rig;
12mod style_overlay;
13
14#[cfg(not(target_arch = "wasm32"))]
16use demo_common::Demo;
17use iced::{
18 Color, Event, Length, Point, Size, Subscription, Task, Theme, Vector, event, keyboard,
19 widget::{container, opaque, stack, text},
20 window,
21};
22use iced_nodegraph::{
23 AnchorStatus, AnchorStyle, ColorQuad, CuttingToolStyle, EdgeStatus, EdgeStyle, FocusOptions,
24 FocusTarget, GraphStyle, Minimap, MinimapStyle, NodeStatus, NodeStyle, PinRef, PinStatus,
25 PinStyle, SelectionBoxStyle, anchor as ng_anchor, default_anchor_style,
26 default_cutting_tool_style, default_edge_style, default_graph_style, default_minimap_style,
27 default_node_style, default_pin_style, default_selection_box_style, edge as ng_edge,
28 node as ng_node,
29};
30use iced_nodegraph::{EdgeCurve, PinShape, TilingKind};
31use iced_palette::{
32 Command, Shortcut, command, command_palette, find_matching_shortcut, focus_input,
33 get_filtered_command_index, get_filtered_count, is_toggle_shortcut, navigate_down, navigate_up,
34};
35use ids::{EdgeData, EdgeId, HelloIds, NodeId, PinLabel, generate_edge_id, generate_node_id};
36use nodes::{
37 AlphaNode, AnchorConfigInputs, BoolToggleConfig, ClassCandidate, ColorQuadNode, ConfigNodeType,
38 CuttingToolConfigInputs, EdgeConfigInputs, EdgeSection, EdgeSections, FloatSliderConfig,
39 GraphConfigInputs, InputNodeType, IntSliderConfig, MathNodeState, MathOperation,
40 MinimapConfigInputs, NodeConfigInputs, NodeSection, NodeSections, NodeType, NodeValue,
41 PatternType, PinConfigInputs, SelectionBoxConfigInputs, Vec2Node, alpha_node,
42 anchor_config_node, bool_toggle_node, catalog_node, color_picker_node, color_preset_node,
43 color_quad_node, cutting_tool_config_node, edge_config_node, edge_curve_selector_node,
44 float_slider_node, frame_node, graph_config_node, int_slider_node, math_node,
45 minimap_config_node, node, node_class_node, node_config_node, pattern_type_selector_node,
46 pin_config_node, pin_shape_selector_node, selection_box_config_node, theme_extended_node,
47 theme_node, tiling_kind_selector_node, vec2_node,
48};
49use std::collections::{HashMap, HashSet};
50use style_overlay::{
51 AnchorOverlay, CuttingToolOverlay, EdgeOverlay, GraphOverlay, MinimapOverlay, NodeOverlay,
52 PinOverlay, SelectionBoxOverlay,
53};
54
55#[cfg(not(target_arch = "wasm32"))]
60pub fn main() -> iced::Result {
61 let window_settings = {
62 let (position, size, maximized) = persistence::load_state()
64 .map(|s| (s.window_position, s.window_size, s.window_maximized))
65 .unwrap_or((None, None, None));
66
67 iced::window::Settings {
68 position: position
69 .map(|(x, y)| iced::window::Position::Specific(Point::new(x as f32, y as f32)))
70 .unwrap_or(iced::window::Position::Centered),
71 size: size
72 .map(|(w, h)| iced::Size::new(w as f32, h as f32))
73 .unwrap_or(iced::Size::new(1280.0, 800.0)),
74 maximized: maximized.unwrap_or(false),
75 ..Default::default()
76 }
77 };
78
79 iced::application(
80 Application::new_persisted,
81 Application::update,
82 Application::view,
83 )
84 .subscription(Application::subscription)
85 .title("Hello World - iced_nodegraph Demo")
86 .theme(Application::theme)
87 .window(window_settings)
88 .run()
89}
90
91#[derive(Debug, Clone)]
92enum ApplicationMessage {
93 EdgeConnected {
94 from: PinRef<HelloIds>,
95 to: PinRef<HelloIds>,
96 },
97 EdgeDisconnected {
98 from: PinRef<HelloIds>,
99 to: PinRef<HelloIds>,
100 },
101 EdgesCut(Vec<EdgeId>),
103 AnchorCreated {
105 edge: EdgeId,
106 position: Point,
107 },
108 AnchorMoved {
109 anchor: usize,
110 position: Point,
111 },
112 AnchorDeleted(usize),
113 RouteAttached {
114 edge: EdgeId,
115 anchor: usize,
116 },
117 RouteDetached {
118 edge: EdgeId,
119 anchor: usize,
120 },
121 DragEnded,
125 ToggleCommandPalette,
126 CommandPaletteInput(String),
127 CommandPaletteNavigateUp,
128 CommandPaletteNavigateDown,
129 CommandPaletteSelect(usize),
130 CommandPaletteConfirm,
131 CommandPaletteCancel,
132 ExecuteShortcut(String),
133 CommandPaletteNavigate(usize),
134 SpawnNode {
135 node_type: NodeType,
136 },
137 ChangeTheme(Theme),
138 CameraChanged {
139 position: Point,
140 zoom: f32,
141 },
142 FocusNode,
144 WindowResized(iced::Size),
145 WindowMoved(Point),
146 WindowMaximizedChanged(bool),
147 NavigateToSubmenu(String),
148 SelectionChanged(Vec<NodeId>),
150 CloneNodes(Vec<NodeId>),
151 DeleteNodes(Vec<NodeId>),
152 NodesMoved {
153 delta: Vector,
154 node_ids: Vec<NodeId>,
155 },
156 ExportState,
158 Reset,
161 FocusNext,
164 FocusPrevious,
165 SliderChanged {
167 node_id: NodeId,
168 value: f32,
169 },
170 IntSliderChanged {
171 node_id: NodeId,
172 value: i32,
173 },
174 BoolChanged {
175 node_id: NodeId,
176 value: bool,
177 },
178 EdgeCurveChanged {
179 node_id: NodeId,
180 value: EdgeCurve,
181 },
182 PinShapeChanged {
183 node_id: NodeId,
184 value: PinShape,
185 },
186 PatternTypeChanged {
187 node_id: NodeId,
188 value: PatternType,
189 },
190 TilingKindChanged {
191 node_id: NodeId,
192 value: TilingKind,
193 },
194 ColorChanged {
195 node_id: NodeId,
196 color: Color,
197 },
198 ToggleNodeExpanded {
200 node_id: NodeId,
201 },
202 UpdateFloatSliderConfig {
203 node_id: NodeId,
204 config: FloatSliderConfig,
205 },
206 UpdateIntSliderConfig {
207 node_id: NodeId,
208 config: IntSliderConfig,
209 },
210 ToggleEdgeSection {
212 node_id: NodeId,
213 section: EdgeSection,
214 },
215 ToggleNodeSection {
216 node_id: NodeId,
217 section: NodeSection,
218 },
219 NodeClassTargetChanged {
221 node_id: NodeId,
222 target: Option<NodeId>,
223 },
224}
225
226#[derive(Debug, Clone, PartialEq)]
227enum PaletteView {
228 Main,
229 Submenu(String),
230}
231
232#[derive(Debug, Clone)]
234enum ConfigOutput {
235 Node(NodeOverlay),
236 Edge(EdgeOverlay),
237 Pin(PinOverlay),
238 Graph(GraphOverlay),
239 Anchor(AnchorOverlay),
240 SelectionBox(SelectionBoxOverlay),
241 CuttingTool(CuttingToolOverlay),
242 Minimap(MinimapOverlay),
243}
244
245const EDGE_GLOW_OPACITY: f32 = 0.45;
247
248fn pin_color_for(ty: std::any::TypeId) -> Color {
251 use nodes::{colors, pins};
252 use std::any::TypeId;
253 if ty == TypeId::of::<pins::ColorData>() {
254 colors::PIN_COLOR
255 } else if ty == TypeId::of::<pins::Float>() || ty == TypeId::of::<pins::Int>() {
256 colors::PIN_NUMBER
257 } else if ty == TypeId::of::<pins::Bool>() {
258 colors::PIN_BOOL
259 } else if ty == TypeId::of::<pins::StringData>() {
260 colors::PIN_STRING
261 } else if ty == TypeId::of::<pins::Email>() {
262 colors::PIN_EMAIL
263 } else if ty == TypeId::of::<pins::DateTime>() {
264 colors::PIN_DATETIME
265 } else if ty == TypeId::of::<pins::NodeConfigData>()
266 || ty == TypeId::of::<pins::EdgeConfigData>()
267 || ty == TypeId::of::<pins::PinConfigData>()
268 || ty == TypeId::of::<pins::GraphConfigData>()
269 || ty == TypeId::of::<pins::AnchorConfigData>()
270 || ty == TypeId::of::<pins::SelectionBoxConfigData>()
271 || ty == TypeId::of::<pins::CuttingToolConfigData>()
272 || ty == TypeId::of::<pins::MinimapConfigData>()
273 {
274 colors::PIN_CONFIG
275 } else {
276 colors::PIN_ANY
277 }
278}
279
280fn graph_id() -> iced::widget::Id {
283 iced::widget::Id::new("graph")
284}
285
286#[derive(Debug, Clone, Default)]
291struct ComputedCatalog {
292 node: NodeOverlay,
293 node_selected: NodeOverlay,
294 pin: PinOverlay,
295 pin_valid_target: PinOverlay,
296 edge: EdgeOverlay,
297 edge_pending_cut: EdgeOverlay,
298 drag_edge: EdgeOverlay,
299 anchor: AnchorOverlay,
300 anchor_hovered: AnchorOverlay,
301 anchor_valid_target: AnchorOverlay,
302 drag_anchor: AnchorOverlay,
303 graph: GraphOverlay,
304 selection_box: SelectionBoxOverlay,
305 cutting_tool: CuttingToolOverlay,
306 minimap: MinimapOverlay,
307 node_classes: HashMap<NodeId, NodeOverlay>,
309}
310
311impl ComputedCatalog {
312 fn node(&self, status: NodeStatus) -> NodeOverlay {
313 match status {
314 NodeStatus::Idle => self.node.clone(),
315 NodeStatus::Selected => self.node_selected.merge(&self.node),
316 }
317 }
318
319 fn pin(&self, status: PinStatus) -> PinOverlay {
320 match status {
321 PinStatus::Idle => self.pin.clone(),
322 PinStatus::ValidTarget => self.pin_valid_target.merge(&self.pin),
323 }
324 }
325
326 fn edge(&self, status: EdgeStatus) -> EdgeOverlay {
327 match status {
328 EdgeStatus::Idle => self.edge.clone(),
329 EdgeStatus::PendingCut => self.edge_pending_cut.merge(&self.edge),
330 }
331 }
332
333 fn anchor(&self, status: AnchorStatus) -> AnchorOverlay {
334 match status {
335 AnchorStatus::Idle => self.anchor.clone(),
336 AnchorStatus::Hovered => self.anchor_hovered.merge(&self.anchor),
337 AnchorStatus::ValidTarget => self.anchor_valid_target.merge(&self.anchor),
338 }
339 }
340}
341
342struct Application {
343 nodes: HashMap<NodeId, (Point, NodeType)>,
345 node_order: Vec<NodeId>,
347 edges: HashMap<EdgeId, EdgeData>,
349 edge_order: Vec<EdgeId>,
351 selected_nodes: HashSet<NodeId>,
353 expanded_nodes: HashSet<NodeId>,
355 edge_config_sections: HashMap<NodeId, EdgeSections>,
357 node_config_sections: HashMap<NodeId, NodeSections>,
359 command_palette_open: bool,
360 command_input: String,
361 current_theme: Theme,
362 palette_view: PaletteView,
363 palette_selected_index: usize,
364 palette_preview_theme: Option<Theme>,
365 palette_original_theme: Option<Theme>,
366 computed: ComputedCatalog,
368 pending_configs: HashMap<NodeId, Vec<(PinLabel, ConfigOutput)>>,
370 anchors: Vec<(usize, Point)>,
372 next_anchor: usize,
375 viewport_size: iced::Size,
377 camera_position: Point,
379 camera_zoom: f32,
381 window_position: Option<(i32, i32)>,
383 window_size: Option<(u32, u32)>,
385 window_maximized: Option<bool>,
387}
388
389impl Default for Application {
390 fn default() -> Self {
391 use nodes::pins::workflow;
392
393 let node0_id = generate_node_id();
395 let node1_id = generate_node_id();
396 let node2_id = generate_node_id();
397 let node3_id = generate_node_id();
398
399 let mut nodes = HashMap::new();
400 nodes.insert(
401 node0_id.clone(),
402 (
403 Point::new(45.5, 149.0),
404 NodeType::Workflow("email_trigger".to_string()),
405 ),
406 );
407 nodes.insert(
408 node1_id.clone(),
409 (
410 Point::new(274.5, 227.5),
411 NodeType::Workflow("email_parser".to_string()),
412 ),
413 );
414 nodes.insert(
415 node2_id.clone(),
416 (
417 Point::new(459.5, 432.5),
418 NodeType::Workflow("filter".to_string()),
419 ),
420 );
421 nodes.insert(
422 node3_id.clone(),
423 (
424 Point::new(679.0, 252.5),
425 NodeType::Workflow("calendar".to_string()),
426 ),
427 );
428
429 let mut node_order = vec![
430 node0_id.clone(),
431 node1_id.clone(),
432 node2_id.clone(),
433 node3_id.clone(),
434 ];
435
436 let mut edges = HashMap::new();
438 let mut edge_order = Vec::new();
439
440 let edge0_id = generate_edge_id();
442 edges.insert(
443 edge0_id.clone(),
444 EdgeData::new(
445 node0_id.clone(),
446 workflow::ON_EMAIL,
447 node1_id.clone(),
448 workflow::EMAIL,
449 ),
450 );
451 edge_order.push(edge0_id);
452
453 let edge1_id = generate_edge_id();
455 edges.insert(
456 edge1_id.clone(),
457 EdgeData::new(
458 node1_id.clone(),
459 workflow::SUBJECT,
460 node2_id.clone(),
461 workflow::INPUT,
462 ),
463 );
464 edge_order.push(edge1_id);
465
466 let edge2_id = generate_edge_id();
469 edges.insert(
470 edge2_id.clone(),
471 EdgeData {
472 route: vec![0],
473 ..EdgeData::new(
474 node1_id.clone(),
475 workflow::DATETIME,
476 node3_id.clone(),
477 workflow::DATETIME,
478 )
479 },
480 );
481 edge_order.push(edge2_id);
482
483 let edge3_id = generate_edge_id();
485 edges.insert(
486 edge3_id.clone(),
487 EdgeData::new(
488 node2_id.clone(),
489 workflow::MATCHES,
490 node3_id.clone(),
491 workflow::TITLE,
492 ),
493 );
494 edge_order.push(edge3_id);
495
496 let rig = rig::build(Point::new(1000.0, 0.0), node3_id);
499 for (id, position, node_type) in rig.nodes {
500 nodes.insert(id.clone(), (position, node_type));
501 node_order.push(id);
502 }
503 for (id, edge) in rig.edges {
504 edges.insert(id.clone(), edge);
505 edge_order.push(id);
506 }
507
508 let mut app = Self {
509 nodes,
510 node_order,
511 edges,
512 edge_order,
513 selected_nodes: HashSet::new(),
514 expanded_nodes: HashSet::new(),
515 edge_config_sections: HashMap::new(),
516 node_config_sections: HashMap::new(),
517 command_palette_open: false,
518 command_input: String::new(),
519 current_theme: Theme::CatppuccinFrappe,
520 palette_view: PaletteView::Main,
521 palette_selected_index: 0,
522 palette_preview_theme: None,
523 palette_original_theme: None,
524 computed: ComputedCatalog::default(),
525 pending_configs: HashMap::new(),
526 anchors: vec![(0, Point::new(560.0, 160.0))],
527 next_anchor: 1,
528 viewport_size: iced::Size::new(800.0, 600.0), camera_position: Point::ORIGIN,
530 camera_zoom: 1.0,
531 window_position: None,
532 window_size: None,
533 window_maximized: None,
534 };
535 app.propagate_values();
537 app
538 }
539}
540
541impl Application {
542 #[cfg(not(target_arch = "wasm32"))]
543 fn new_persisted() -> Self {
544 {
546 match persistence::load_state() {
547 Ok(saved) => {
548 let (
549 nodes,
550 node_order,
551 edges,
552 edge_order,
553 theme,
554 camera_pos,
555 camera_zoom,
556 window_pos,
557 window_size,
558 edge_config_sections,
559 node_config_sections,
560 window_maximized,
561 anchors,
562 next_anchor,
563 ) = saved.to_app();
564 println!(
565 "Loaded saved state: {} nodes, {} edges",
566 nodes.len(),
567 edges.len()
568 );
569 let mut app = Self {
570 nodes,
571 node_order,
572 edges,
573 edge_order,
574 current_theme: theme,
575 camera_position: camera_pos,
576 camera_zoom,
577 window_position: window_pos,
578 window_size,
579 edge_config_sections,
580 node_config_sections,
581 window_maximized,
582 anchors,
583 next_anchor,
584 ..Self::default()
585 };
586 app.propagate_values();
588 return app;
589 }
590 Err(e) => {
591 println!("No saved state found: {}", e);
592 }
593 }
594 }
595 Self::default()
596 }
597
598 fn reset_to_default(&mut self) {
602 let viewport_size = self.viewport_size;
603 let window_position = self.window_position;
604 let window_size = self.window_size;
605 let window_maximized = self.window_maximized;
606 *self = Self {
607 viewport_size,
608 window_position,
609 window_size,
610 window_maximized,
611 ..Self::default()
612 };
613 self.propagate_values();
614 self.save_state();
615 }
616
617 #[cfg(all(not(target_arch = "wasm32"), not(test)))]
620 fn save_state(&self) {
621 let saved = persistence::SavedState::from_app(
622 &self.nodes,
623 &self.node_order,
624 &self.edges,
625 &self.edge_order,
626 &self.current_theme,
627 self.camera_position,
628 self.camera_zoom,
629 self.window_position,
630 self.window_size,
631 &self.edge_config_sections,
632 &self.node_config_sections,
633 self.window_maximized,
634 &self.anchors,
635 self.next_anchor,
636 );
637 if let Err(e) = persistence::save_state(&saved) {
638 eprintln!("Failed to save state: {}", e);
639 }
640 }
641
642 #[cfg(any(target_arch = "wasm32", test))]
643 fn save_state(&self) {}
644
645 fn spawn_position(&self) -> Point {
647 let screen_center_x = self.viewport_size.width / 2.0;
649 let screen_center_y = self.viewport_size.height / 2.0;
650
651 let world_x = screen_center_x / self.camera_zoom - self.camera_position.x;
653 let world_y = screen_center_y / self.camera_zoom - self.camera_position.y;
654
655 Point::new(world_x - 50.0, world_y - 40.0)
657 }
658
659 #[cfg(not(target_arch = "wasm32"))]
662 fn export_state_to_file(&self) {
663 use std::io::Write;
664
665 let out_dir = std::path::Path::new("out");
667 if !out_dir.exists()
668 && let Err(e) = std::fs::create_dir(out_dir)
669 {
670 eprintln!("Failed to create out/ directory: {}", e);
671 return;
672 }
673
674 let filename = Self::generate_random_name();
676 let path = out_dir.join(format!("{}.txt", filename));
677
678 let mut output = String::new();
679 output.push_str("# Graph State Export\n");
680 output.push_str(
681 "# Generated by hello_world demo - use this to update demo initial state\n\n",
682 );
683
684 output.push_str("## Nodes\n");
686 output.push_str(&format!("# Total: {} nodes\n\n", self.nodes.len()));
687
688 for node_id in &self.node_order {
689 if let Some((pos, node_type)) = self.nodes.get(node_id) {
690 output.push_str(&format!("Node {}: ({:.1}, {:.1})\n", node_id, pos.x, pos.y));
691 match node_type {
692 NodeType::Workflow(name) => {
693 output.push_str(&format!(" Type: Workflow(\"{}\")\n", name));
694 }
695 NodeType::Input(input) => {
696 output.push_str(&format!(" Type: Input({:?})\n", input));
697 }
698 NodeType::Config(config) => {
699 output.push_str(&format!(" Type: Config({:?})\n", config));
700 }
701 NodeType::Math(state) => {
702 output.push_str(&format!(" Type: Math({:?})\n", state));
703 }
704 NodeType::ColorQuad(state) => {
705 output.push_str(&format!(" Type: ColorQuad({:?})\n", state));
706 }
707 NodeType::Vec2(state) => {
708 output.push_str(&format!(" Type: Vec2({:?})\n", state));
709 }
710 NodeType::Alpha(state) => {
711 output.push_str(&format!(" Type: Alpha({:?})\n", state));
712 }
713 NodeType::Theme => {
714 output.push_str(" Type: Theme\n");
715 }
716 NodeType::ThemeExtended => {
717 output.push_str(" Type: ThemeExtended\n");
718 }
719 NodeType::Frame { label, size } => {
720 output.push_str(&format!(
721 " Type: Frame(\"{}\", {:.0}x{:.0})\n",
722 label, size.width, size.height
723 ));
724 }
725 }
726 output.push('\n');
727 }
728 }
729
730 output.push_str("## Edges\n");
732 output.push_str(&format!("# Total: {} edges\n\n", self.edges.len()));
733
734 for edge_id in &self.edge_order {
735 if let Some(edge) = self.edges.get(edge_id) {
736 output.push_str(&format!(
737 "Edge {}: Node {}.Pin \"{}\" -> Node {}.Pin \"{}\"\n",
738 edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin
739 ));
740 }
741 }
742
743 output.push_str("\n## JSON Format (for state.json)\n\n");
745 output.push_str("```json\n");
746 output.push_str("{\n \"nodes\": [\n");
747 for (i, node_id) in self.node_order.iter().enumerate() {
748 if let Some((pos, node_type)) = self.nodes.get(node_id) {
749 let type_str = match node_type {
750 NodeType::Workflow(name) => {
751 format!("{{\"type\": \"Workflow\", \"name\": \"{}\"}}", name)
752 }
753 _ => format!("{:?}", node_type),
754 };
755 let comma = if i < self.node_order.len() - 1 {
756 ","
757 } else {
758 ""
759 };
760 output.push_str(&format!(
761 " {{\"id\": \"{}\", \"x\": {:.1}, \"y\": {:.1}, \"node_type\": {}}}{}\n",
762 node_id, pos.x, pos.y, type_str, comma
763 ));
764 }
765 }
766 output.push_str(" ],\n \"edges\": [\n");
767 for (i, edge_id) in self.edge_order.iter().enumerate() {
768 if let Some(edge) = self.edges.get(edge_id) {
769 let comma = if i < self.edge_order.len() - 1 {
770 ","
771 } else {
772 ""
773 };
774 output.push_str(&format!(
775 " {{\"id\": \"{}\", \"from_node\": \"{}\", \"from_pin\": \"{}\", \"to_node\": \"{}\", \"to_pin\": \"{}\"}}{}\n",
776 edge_id, edge.from_node, edge.from_pin, edge.to_node, edge.to_pin, comma
777 ));
778 }
779 }
780 output.push_str(" ]\n}\n");
781 output.push_str("```\n");
782
783 match std::fs::File::create(&path) {
785 Ok(mut file) => {
786 if let Err(e) = file.write_all(output.as_bytes()) {
787 eprintln!("Failed to write state export: {}", e);
788 } else {
789 println!("State exported to: {}", path.display());
790 }
791 }
792 Err(e) => {
793 eprintln!("Failed to create export file: {}", e);
794 }
795 }
796 }
797
798 #[cfg(not(target_arch = "wasm32"))]
800 fn generate_random_name() -> String {
801 use std::time::{SystemTime, UNIX_EPOCH};
802
803 const ADJECTIVES: &[&str] = &[
804 "swift", "bright", "calm", "bold", "keen", "warm", "cool", "wild", "soft", "sharp",
805 "quick", "slow", "deep", "wide", "tall", "tiny", "grand", "pure", "rare", "wise",
806 "fair", "dark", "light", "fresh",
807 ];
808 const NOUNS: &[&str] = &[
809 "river", "mountain", "forest", "ocean", "meadow", "valley", "canyon", "island",
810 "sunset", "sunrise", "thunder", "breeze", "garden", "crystal", "shadow", "ember",
811 "falcon", "phoenix", "dragon", "tiger", "wolf", "eagle", "raven", "fox",
812 ];
813
814 let nanos = SystemTime::now()
816 .duration_since(UNIX_EPOCH)
817 .map(|d| d.as_nanos())
818 .unwrap_or(0);
819
820 let adj_idx = (nanos % ADJECTIVES.len() as u128) as usize;
821 let noun_idx = ((nanos / 7) % NOUNS.len() as u128) as usize;
822
823 format!("{}-{}", ADJECTIVES[adj_idx], NOUNS[noun_idx])
824 }
825
826 #[cfg(target_arch = "wasm32")]
827 fn export_state_to_file(&self) {
828 }
830
831 fn effective_theme(&self) -> &Theme {
834 self.palette_preview_theme
835 .as_ref()
836 .unwrap_or(&self.current_theme)
837 }
838
839 fn pin_output_value(&self, node_id: &NodeId, pin: &PinLabel) -> Option<NodeValue> {
843 match self.nodes.get(node_id) {
844 Some((_, NodeType::Theme | NodeType::ThemeExtended)) => {
845 theme_color(self.effective_theme(), pin).map(NodeValue::Color)
846 }
847 Some((_, node_type)) => node_type.output_value(),
848 None => None,
849 }
850 }
851
852 fn propagate_values(&mut self) {
856 let mut new_computed = ComputedCatalog::default();
857 self.pending_configs.clear();
858
859 for (_, node_type) in self.nodes.values_mut() {
861 match node_type {
862 NodeType::Config(config) => match config {
863 ConfigNodeType::NodeConfig(inputs) => *inputs = NodeConfigInputs::default(),
864 ConfigNodeType::EdgeConfig(inputs) => *inputs = EdgeConfigInputs::default(),
865 ConfigNodeType::PinConfig(inputs) => *inputs = PinConfigInputs::default(),
866 ConfigNodeType::GraphConfig(inputs) => *inputs = GraphConfigInputs::default(),
867 ConfigNodeType::AnchorConfig(inputs) => *inputs = AnchorConfigInputs::default(),
868 ConfigNodeType::SelectionBoxConfig(inputs) => {
869 *inputs = SelectionBoxConfigInputs::default()
870 }
871 ConfigNodeType::CuttingToolConfig(inputs) => {
872 *inputs = CuttingToolConfigInputs::default()
873 }
874 ConfigNodeType::MinimapConfig(inputs) => {
875 *inputs = MinimapConfigInputs::default()
876 }
877 ConfigNodeType::Catalog { connected } => connected.clear(),
878 ConfigNodeType::NodeClass {
881 has_node_config, ..
882 } => *has_node_config = false,
883 },
884 NodeType::Math(state) => {
885 state.input_a = None;
886 state.input_b = None;
887 }
888 NodeType::ColorQuad(state) => *state = ColorQuadNode::default(),
889 NodeType::Vec2(state) => *state = Vec2Node::default(),
890 NodeType::Alpha(state) => *state = AlphaNode::default(),
891 _ => {}
892 }
893 }
894
895 let edges_snapshot: Vec<_> = self.edges.values().cloned().collect();
899
900 const MAX_ITERATIONS: usize = 10;
903 for _ in 0..MAX_ITERATIONS {
904 let mut changed = false;
905
906 for edge in &edges_snapshot {
907 let forward = self.pin_output_value(&edge.from_node, &edge.from_pin);
910 if let Some(value) = forward
911 && let Some((_, node)) = self.nodes.get_mut(&edge.to_node)
912 && feed_combiner_input(node, &edge.to_pin, &value)
913 {
914 changed = true;
915 }
916
917 let reverse = self.pin_output_value(&edge.to_node, &edge.to_pin);
918 if let Some(value) = reverse
919 && let Some((_, node)) = self.nodes.get_mut(&edge.from_node)
920 && feed_combiner_input(node, &edge.from_pin, &value)
921 {
922 changed = true;
923 }
924 }
925
926 if !changed {
927 break;
928 }
929 }
930
931 for edge in &edges_snapshot {
935 if let Some(value) = self.pin_output_value(&edge.from_node, &edge.from_pin) {
936 self.apply_value_to_config_node(&edge.to_node, &edge.to_pin, &value);
937 }
938 if let Some(value) = self.pin_output_value(&edge.to_node, &edge.to_pin) {
939 self.apply_value_to_config_node(&edge.from_node, &edge.from_pin, &value);
940 }
941 }
942
943 for edge in &edges_snapshot {
946 if self.is_sink(&edge.to_node) {
947 self.connect_config_to_sink(&edge.from_node, &edge.to_node, &edge.to_pin);
948 }
949 if self.is_sink(&edge.from_node) {
950 self.connect_config_to_sink(&edge.to_node, &edge.from_node, &edge.from_pin);
951 }
952 }
953
954 self.apply_sinks(&mut new_computed);
956
957 self.computed = new_computed;
958 }
959
960 fn is_sink(&self, id: &NodeId) -> bool {
962 matches!(
963 self.nodes.get(id),
964 Some((
965 _,
966 NodeType::Config(ConfigNodeType::Catalog { .. } | ConfigNodeType::NodeClass { .. })
967 ))
968 )
969 }
970
971 fn apply_value_to_config_node(
973 &mut self,
974 node_id: &NodeId,
975 pin_label: &PinLabel,
976 value: &NodeValue,
977 ) {
978 use nodes::pins::{
979 anchor as apin, cutting_tool as cpin, edge as epin, graph as gpin, minimap as mpin,
980 node as npin, pin as ppin, selection_box as spin,
981 };
982
983 let Some((_, node_type)) = self.nodes.get_mut(node_id) else {
984 return;
985 };
986
987 let NodeType::Config(config) = node_type else {
988 return;
989 };
990
991 match config {
992 ConfigNodeType::NodeConfig(inputs) => {
993 if *pin_label == npin::FILL_COLOR {
995 inputs.fill_color = value.as_color_quad();
996 } else if *pin_label == npin::CORNER_RADIUS {
997 inputs.corner_radius = value.as_float();
998 } else if *pin_label == npin::OPACITY {
999 inputs.opacity = value.as_float();
1000 } else if *pin_label == npin::BORDER_COLOR {
1001 inputs.border_color = value.as_color_quad();
1002 } else if *pin_label == npin::BORDER_WIDTH {
1003 inputs.border_width = value.as_float();
1004 } else if *pin_label == npin::BORDER_OUTLINE_WIDTH {
1005 inputs.border_outline_width = value.as_float();
1006 } else if *pin_label == npin::BORDER_OUTLINE_COLOR {
1007 inputs.border_outline_color = value.as_color_quad();
1008 } else if *pin_label == npin::PATTERN {
1009 inputs.pattern_type = value.as_pattern_type();
1010 } else if *pin_label == npin::DASH {
1011 inputs.dash_length = value.as_float();
1012 } else if *pin_label == npin::GAP {
1013 inputs.gap_length = value.as_float();
1014 } else if *pin_label == npin::ANGLE {
1015 inputs.pattern_angle = value.as_float().map(|deg| deg.to_radians());
1017 } else if *pin_label == npin::SPEED {
1018 inputs.animation_speed = value.as_float();
1019 } else if *pin_label == npin::SHADOW_COLOR {
1020 inputs.shadow_color = value.as_color_quad();
1021 } else if *pin_label == npin::SHADOW_DISTANCE {
1022 inputs.shadow_distance = value.as_float();
1023 } else if *pin_label == npin::SHADOW_OFFSET {
1024 inputs.shadow_offset = value.as_vec2();
1025 }
1026 }
1027 ConfigNodeType::EdgeConfig(inputs) => {
1028 if *pin_label == epin::STROKE_COLOR {
1030 inputs.stroke_color = value.as_color_quad();
1031 } else if *pin_label == epin::THICKNESS {
1032 inputs.thickness = value.as_float();
1033 } else if *pin_label == epin::CURVE {
1034 inputs.curve = value.as_edge_curve();
1035 } else if *pin_label == epin::PATTERN {
1036 inputs.pattern_type = value.as_pattern_type();
1037 } else if *pin_label == epin::DASH {
1038 inputs.dash_length = value.as_float();
1039 } else if *pin_label == epin::GAP {
1040 inputs.gap_length = value.as_float();
1041 } else if *pin_label == epin::DOT_RADIUS {
1042 inputs.dot_radius = value.as_float();
1043 } else if *pin_label == epin::ANGLE {
1044 inputs.pattern_angle = value.as_float().map(|deg| deg.to_radians());
1046 } else if *pin_label == epin::SPEED {
1047 inputs.animation_speed = value.as_float();
1048 } else if *pin_label == epin::STROKE_OUTLINE_WIDTH {
1050 inputs.stroke_outline_width = value.as_float();
1051 } else if *pin_label == epin::STROKE_OUTLINE_COLOR {
1052 inputs.stroke_outline_color = value.as_color_quad();
1053 } else if *pin_label == epin::BORDER_WIDTH {
1055 inputs.border_width = value.as_float();
1056 } else if *pin_label == epin::BORDER_GAP {
1057 inputs.border_gap = value.as_float();
1058 } else if *pin_label == epin::BORDER_COLOR {
1059 inputs.border_color = value.as_color_quad();
1060 } else if *pin_label == epin::BORDER_BACKGROUND {
1061 inputs.border_background = value.as_color_quad();
1062 } else if *pin_label == epin::BORDER_OUTLINE_WIDTH {
1063 inputs.border_outline_width = value.as_float();
1064 } else if *pin_label == epin::BORDER_OUTLINE_COLOR {
1065 inputs.border_outline_color = value.as_color_quad();
1066 } else if *pin_label == epin::SHADOW_BLUR {
1068 inputs.shadow_blur = value.as_float();
1069 } else if *pin_label == epin::SHADOW_EXPAND {
1070 inputs.shadow_expand = value.as_float();
1071 } else if *pin_label == epin::SHADOW_COLOR {
1072 inputs.shadow_color = value.as_color_quad();
1073 } else if *pin_label == epin::SHADOW_OFFSET {
1074 inputs.shadow_offset = value.as_vec2();
1075 } else if *pin_label == epin::GLOW_COLOR {
1077 inputs.glow_color = value.as_color_quad();
1078 } else if *pin_label == epin::GLOW_WIDTH {
1079 inputs.glow_width = value.as_float();
1080 }
1081 }
1082 ConfigNodeType::PinConfig(inputs) => {
1083 if *pin_label == ppin::COLOR {
1085 inputs.color = value.as_color_quad();
1086 } else if *pin_label == ppin::RADIUS {
1087 inputs.radius = value.as_float();
1088 } else if *pin_label == ppin::CUTOUT_RADIUS {
1089 inputs.cutout_radius = value.as_float();
1090 } else if *pin_label == ppin::SHAPE {
1091 inputs.shape = value.as_pin_shape();
1092 } else if *pin_label == ppin::BORDER_COLOR {
1093 inputs.border_color = value.as_color_quad();
1094 } else if *pin_label == ppin::BORDER_WIDTH {
1095 inputs.border_width = value.as_float();
1096 }
1097 }
1098 ConfigNodeType::GraphConfig(inputs) => {
1099 if *pin_label == gpin::BACKGROUND {
1101 inputs.background_color = value.as_color_quad();
1102 } else if *pin_label == gpin::TILING_KIND {
1103 inputs.tiling_kind = value.as_tiling_kind();
1104 } else if *pin_label == gpin::SPACING {
1105 inputs.tiling_spacing = value.as_float();
1106 } else if *pin_label == gpin::THICKNESS {
1107 inputs.tiling_thickness = value.as_float();
1108 } else if *pin_label == gpin::LINE_COLOR {
1109 inputs.tiling_color = value.as_color_quad();
1110 }
1111 }
1112 ConfigNodeType::AnchorConfig(inputs) => {
1113 if *pin_label == apin::CORE_SIZE {
1114 inputs.core_size = value.as_float();
1115 } else if *pin_label == apin::CORE_RADIUS {
1116 inputs.core_radius = value.as_float();
1117 } else if *pin_label == apin::CORE_COLOR {
1118 inputs.core_color = value.as_color_quad();
1119 } else if *pin_label == apin::CORE_BORDER_COLOR {
1120 inputs.core_border_color = value.as_color_quad();
1121 } else if *pin_label == apin::CORE_BORDER_WIDTH {
1122 inputs.core_border_width = value.as_float();
1123 } else if *pin_label == apin::ORBIT_OFFSET {
1124 inputs.orbit_offset = value.as_float();
1125 } else if *pin_label == apin::ORBIT_SPACING {
1126 inputs.orbit_spacing = value.as_float();
1127 } else if *pin_label == apin::RING_COLOR {
1128 inputs.ring_color = value.as_color_quad();
1129 } else if *pin_label == apin::RING_WIDTH {
1130 inputs.ring_width = value.as_float();
1131 } else if *pin_label == apin::OFFERED_RING_COLOR {
1132 inputs.offered_ring_color = value.as_color_quad();
1133 }
1134 }
1135 ConfigNodeType::SelectionBoxConfig(inputs) => {
1136 if *pin_label == spin::FILL {
1137 inputs.fill = value.as_color_quad();
1138 } else if *pin_label == spin::BORDER_COLOR {
1139 inputs.border_color = value.as_color_quad();
1140 } else if *pin_label == spin::BORDER_WIDTH {
1141 inputs.border_width = value.as_float();
1142 }
1143 }
1144 ConfigNodeType::CuttingToolConfig(inputs) => {
1145 if *pin_label == cpin::COLOR {
1146 inputs.color = value.as_color_quad();
1147 } else if *pin_label == cpin::WIDTH {
1148 inputs.width = value.as_float();
1149 }
1150 }
1151 ConfigNodeType::MinimapConfig(inputs) => {
1152 if *pin_label == mpin::BACKGROUND {
1153 inputs.background = value.as_color_quad();
1154 } else if *pin_label == mpin::BORDER_COLOR {
1155 inputs.border_color = value.as_color_quad();
1156 } else if *pin_label == mpin::BORDER_WIDTH {
1157 inputs.border_width = value.as_float();
1158 } else if *pin_label == mpin::NODE_COLOR {
1159 inputs.node_color = value.as_color_quad();
1160 } else if *pin_label == mpin::SELECTED_NODE_COLOR {
1161 inputs.selected_node_color = value.as_color_quad();
1162 } else if *pin_label == mpin::VIEWPORT_FILL {
1163 inputs.viewport_fill = value.as_color_quad();
1164 } else if *pin_label == mpin::VIEWPORT_BORDER_COLOR {
1165 inputs.viewport_border_color = value.as_color_quad();
1166 } else if *pin_label == mpin::VIEWPORT_BORDER_WIDTH {
1167 inputs.viewport_border_width = value.as_float();
1168 }
1169 }
1170 ConfigNodeType::Catalog { .. } | ConfigNodeType::NodeClass { .. } => {}
1172 }
1173 }
1174
1175 fn catalog_input_accepts(pin: PinLabel, output: &ConfigOutput) -> bool {
1177 use nodes::pins::cfg;
1178 match output {
1179 ConfigOutput::Node(_) => pin == cfg::NODE_CONFIG || pin == cfg::NODE_SELECTED,
1180 ConfigOutput::Pin(_) => pin == cfg::PIN_CONFIG || pin == cfg::PIN_VALID_TARGET,
1181 ConfigOutput::Edge(_) => {
1182 pin == cfg::EDGE_CONFIG || pin == cfg::EDGE_PENDING_CUT || pin == cfg::DRAG_EDGE
1183 }
1184 ConfigOutput::Anchor(_) => {
1185 pin == cfg::ANCHOR
1186 || pin == cfg::ANCHOR_HOVERED
1187 || pin == cfg::ANCHOR_VALID_TARGET
1188 || pin == cfg::DRAG_ANCHOR
1189 }
1190 ConfigOutput::Graph(_) => pin == cfg::GRAPH_CONFIG,
1191 ConfigOutput::SelectionBox(_) => pin == cfg::SELECTION_BOX,
1192 ConfigOutput::CuttingTool(_) => pin == cfg::CUTTING_TOOL,
1193 ConfigOutput::Minimap(_) => pin == cfg::MINIMAP,
1194 }
1195 }
1196
1197 fn connect_config_to_sink(
1200 &mut self,
1201 config_node_id: &NodeId,
1202 sink_node_id: &NodeId,
1203 sink_pin: &PinLabel,
1204 ) {
1205 use nodes::pins::cfg;
1206
1207 let built = match self.nodes.get(config_node_id) {
1209 Some((_, NodeType::Config(config))) => match config {
1210 ConfigNodeType::NodeConfig(inputs) => Some(ConfigOutput::Node(inputs.build())),
1211 ConfigNodeType::EdgeConfig(inputs) => Some(ConfigOutput::Edge(inputs.build())),
1212 ConfigNodeType::PinConfig(inputs) => Some(ConfigOutput::Pin(inputs.build())),
1213 ConfigNodeType::GraphConfig(inputs) => Some(ConfigOutput::Graph(inputs.build())),
1214 ConfigNodeType::AnchorConfig(inputs) => Some(ConfigOutput::Anchor(inputs.build())),
1215 ConfigNodeType::SelectionBoxConfig(inputs) => {
1216 Some(ConfigOutput::SelectionBox(inputs.build()))
1217 }
1218 ConfigNodeType::CuttingToolConfig(inputs) => {
1219 Some(ConfigOutput::CuttingTool(inputs.build()))
1220 }
1221 ConfigNodeType::MinimapConfig(inputs) => {
1222 Some(ConfigOutput::Minimap(inputs.build()))
1223 }
1224 ConfigNodeType::Catalog { .. } | ConfigNodeType::NodeClass { .. } => None,
1225 },
1226 _ => None,
1227 };
1228 let Some(output) = built else {
1229 return;
1230 };
1231
1232 let accepted = match self.nodes.get_mut(sink_node_id) {
1233 Some((_, NodeType::Config(ConfigNodeType::Catalog { connected }))) => {
1234 let ok = Self::catalog_input_accepts(sink_pin, &output);
1235 if ok {
1236 connected.insert(sink_pin);
1237 }
1238 ok
1239 }
1240 Some((
1241 _,
1242 NodeType::Config(ConfigNodeType::NodeClass {
1243 has_node_config, ..
1244 }),
1245 )) => {
1246 let ok = *sink_pin == cfg::NODE_CONFIG && matches!(output, ConfigOutput::Node(_));
1247 if ok {
1248 *has_node_config = true;
1249 }
1250 ok
1251 }
1252 _ => false,
1253 };
1254
1255 if accepted {
1256 self.pending_configs
1257 .entry(sink_node_id.clone())
1258 .or_default()
1259 .push((*sink_pin, output));
1260 }
1261 }
1262
1263 fn apply_sinks(&mut self, computed: &mut ComputedCatalog) {
1267 use nodes::pins::cfg;
1268
1269 for (node_id, (_, node_type)) in &self.nodes {
1270 let Some(configs) = self.pending_configs.get(node_id) else {
1271 continue;
1272 };
1273 match node_type {
1274 NodeType::Config(ConfigNodeType::Catalog { .. }) => {
1275 for (pin, output) in configs {
1276 match output {
1277 ConfigOutput::Node(o) => {
1278 let slot = if *pin == cfg::NODE_SELECTED {
1279 &mut computed.node_selected
1280 } else {
1281 &mut computed.node
1282 };
1283 *slot = o.merge(slot);
1284 }
1285 ConfigOutput::Pin(o) => {
1286 let slot = if *pin == cfg::PIN_VALID_TARGET {
1287 &mut computed.pin_valid_target
1288 } else {
1289 &mut computed.pin
1290 };
1291 *slot = o.merge(slot);
1292 }
1293 ConfigOutput::Edge(o) => {
1294 let slot = if *pin == cfg::EDGE_PENDING_CUT {
1295 &mut computed.edge_pending_cut
1296 } else if *pin == cfg::DRAG_EDGE {
1297 &mut computed.drag_edge
1298 } else {
1299 &mut computed.edge
1300 };
1301 *slot = o.merge(slot);
1302 }
1303 ConfigOutput::Anchor(o) => {
1304 let slot = if *pin == cfg::ANCHOR_HOVERED {
1305 &mut computed.anchor_hovered
1306 } else if *pin == cfg::ANCHOR_VALID_TARGET {
1307 &mut computed.anchor_valid_target
1308 } else if *pin == cfg::DRAG_ANCHOR {
1309 &mut computed.drag_anchor
1310 } else {
1311 &mut computed.anchor
1312 };
1313 *slot = o.merge(slot);
1314 }
1315 ConfigOutput::Graph(o) => computed.graph = o.merge(&computed.graph),
1316 ConfigOutput::SelectionBox(o) => {
1317 computed.selection_box = o.merge(&computed.selection_box)
1318 }
1319 ConfigOutput::CuttingTool(o) => {
1320 computed.cutting_tool = o.merge(&computed.cutting_tool)
1321 }
1322 ConfigOutput::Minimap(o) => {
1323 computed.minimap = o.merge(&computed.minimap)
1324 }
1325 }
1326 }
1327 }
1328 NodeType::Config(ConfigNodeType::NodeClass {
1329 target: Some(target),
1330 has_node_config: true,
1331 }) => {
1332 for (_, output) in configs {
1333 if let ConfigOutput::Node(o) = output {
1334 computed
1335 .node_classes
1336 .entry(target.clone())
1337 .and_modify(|c| *c = o.merge(c))
1338 .or_insert_with(|| o.clone());
1339 }
1340 }
1341 }
1342 _ => {}
1343 }
1344 }
1345 self.pending_configs.clear();
1347 }
1348}
1349
1350impl demo_common::Demo for Application {
1351 type Message = ApplicationMessage;
1352
1353 fn boot() -> (Self, Task<ApplicationMessage>) {
1354 (Self::default(), Task::none())
1355 }
1356
1357 fn update(&mut self, message: ApplicationMessage) -> Task<ApplicationMessage> {
1358 match message {
1359 ApplicationMessage::EdgeConnected { from, to } => {
1360 let edge_id = generate_edge_id();
1361 self.edges.insert(
1362 edge_id.clone(),
1363 EdgeData::new(from.node_id, from.pin_id, to.node_id, to.pin_id),
1364 );
1365 self.edge_order.push(edge_id);
1366 self.propagate_values();
1367 self.save_state();
1368 Task::none()
1369 }
1370 ApplicationMessage::EdgesCut(ids) => {
1371 for id in &ids {
1373 self.edges.remove(id);
1374 }
1375 self.edge_order.retain(|id| !ids.contains(id));
1376 self.drop_unused_anchors();
1377 self.propagate_values();
1378 self.save_state();
1379 Task::none()
1380 }
1381 ApplicationMessage::AnchorCreated { edge, position } => {
1382 let id = self.next_anchor;
1383 self.next_anchor += 1;
1384 if let Some(data) = self.edges.get_mut(&edge) {
1388 data.route.push(id);
1389 self.anchors.push((id, position));
1390 }
1391 self.save_state();
1392 Task::none()
1393 }
1394 ApplicationMessage::AnchorMoved { anchor, position } => {
1395 if let Some((_, p)) = self.anchors.iter_mut().find(|(id, _)| *id == anchor) {
1396 *p = position;
1397 }
1398 self.save_state();
1399 Task::none()
1400 }
1401 ApplicationMessage::AnchorDeleted(anchor) => {
1402 self.anchors.retain(|(id, _)| *id != anchor);
1403 for data in self.edges.values_mut() {
1404 data.route.retain(|id| *id != anchor);
1405 }
1406 self.save_state();
1407 Task::none()
1408 }
1409 ApplicationMessage::RouteAttached { edge, anchor } => {
1410 if let Some(data) = self.edges.get_mut(&edge)
1411 && !data.route.contains(&anchor)
1412 {
1413 data.route.push(anchor);
1414 }
1415 self.save_state();
1416 Task::none()
1417 }
1418 ApplicationMessage::RouteDetached { edge, anchor } => {
1419 if let Some(data) = self.edges.get_mut(&edge) {
1420 data.route.retain(|id| *id != anchor);
1421 }
1422 self.save_state();
1427 Task::none()
1428 }
1429 ApplicationMessage::DragEnded => {
1430 self.drop_unused_anchors();
1431 self.save_state();
1432 Task::none()
1433 }
1434 ApplicationMessage::EdgeDisconnected { from, to } => {
1435 let edge_to_remove: Option<EdgeId> = self
1438 .edges
1439 .iter()
1440 .find(|(_, e)| {
1441 e.from_node == from.node_id
1442 && e.from_pin == from.pin_id
1443 && e.to_node == to.node_id
1444 && e.to_pin == to.pin_id
1445 })
1446 .map(|(id, _)| id.clone());
1447
1448 if let Some(edge_id) = edge_to_remove {
1449 self.edges.remove(&edge_id);
1450 self.edge_order.retain(|id| id != &edge_id);
1451 }
1452 self.drop_unused_anchors();
1453 self.propagate_values();
1454 self.save_state();
1455 Task::none()
1456 }
1457 ApplicationMessage::ToggleCommandPalette => {
1458 self.command_palette_open = !self.command_palette_open;
1459 if !self.command_palette_open {
1460 if let Some(original) = self.palette_original_theme.take() {
1461 self.current_theme = original;
1462 }
1463 self.palette_preview_theme = None;
1464 self.propagate_values();
1465 self.command_input.clear();
1466 self.palette_view = PaletteView::Main;
1467 self.palette_selected_index = 0;
1468 Task::none()
1469 } else {
1470 self.palette_original_theme = Some(self.current_theme.clone());
1471 self.palette_view = PaletteView::Main;
1472 self.palette_selected_index = 0;
1473 focus_input()
1474 }
1475 }
1476 ApplicationMessage::CommandPaletteInput(input) => {
1477 self.command_input = input;
1478 self.palette_selected_index = 0;
1479 Task::none()
1480 }
1481 ApplicationMessage::ExecuteShortcut(cmd_id) => match cmd_id.as_str() {
1482 "add_node" => {
1483 self.command_palette_open = true;
1484 self.palette_original_theme = Some(self.current_theme.clone());
1485 self.palette_view = PaletteView::Submenu("nodes".to_string());
1486 self.palette_selected_index = 0;
1487 self.command_input.clear();
1488 focus_input()
1489 }
1490 "change_theme" => {
1491 self.command_palette_open = true;
1492 self.palette_original_theme = Some(self.current_theme.clone());
1493 self.palette_view = PaletteView::Submenu("themes".to_string());
1494 self.palette_selected_index = 0;
1495 self.command_input.clear();
1496 focus_input()
1497 }
1498 "export_state" => {
1499 self.export_state_to_file();
1500 Task::none()
1501 }
1502 _ => Task::none(),
1503 },
1504 ApplicationMessage::CommandPaletteNavigate(new_index) => {
1505 if !self.command_palette_open {
1506 return Task::none();
1507 }
1508 self.palette_selected_index = new_index;
1509
1510 if let PaletteView::Submenu(ref submenu) = self.palette_view
1511 && submenu == "themes"
1512 {
1513 let (_, commands) = self.build_palette_commands();
1514 if let Some(original_idx) = get_filtered_command_index(
1515 &self.command_input,
1516 &commands,
1517 self.palette_selected_index,
1518 ) {
1519 let themes = Self::get_available_themes();
1520 if original_idx < themes.len() {
1521 self.palette_preview_theme = Some(themes[original_idx].clone());
1522 self.propagate_values();
1524 }
1525 }
1526 }
1527 Task::none()
1528 }
1529 ApplicationMessage::CommandPaletteNavigateUp => {
1530 if !self.command_palette_open {
1531 return Task::none();
1532 }
1533 let (_, commands) = self.build_palette_commands();
1534 let filtered_count = get_filtered_count(&self.command_input, &commands);
1535 let new_index = navigate_up(self.palette_selected_index, filtered_count);
1536 self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1537 }
1538 ApplicationMessage::CommandPaletteNavigateDown => {
1539 if !self.command_palette_open {
1540 return Task::none();
1541 }
1542 let (_, commands) = self.build_palette_commands();
1543 let filtered_count = get_filtered_count(&self.command_input, &commands);
1544 let new_index = navigate_down(self.palette_selected_index, filtered_count);
1545 self.update(ApplicationMessage::CommandPaletteNavigate(new_index))
1546 }
1547 ApplicationMessage::CommandPaletteSelect(index) => {
1548 if !self.command_palette_open {
1549 return Task::none();
1550 }
1551 self.palette_selected_index = index;
1552 self.update(ApplicationMessage::CommandPaletteConfirm)
1553 }
1554 ApplicationMessage::CommandPaletteConfirm => {
1555 if !self.command_palette_open {
1556 return Task::none();
1557 }
1558 let (_, commands) = self.build_palette_commands();
1559 let Some(original_idx) = get_filtered_command_index(
1560 &self.command_input,
1561 &commands,
1562 self.palette_selected_index,
1563 ) else {
1564 return Task::none();
1565 };
1566
1567 use iced_palette::CommandAction;
1568 let cmd = &commands[original_idx];
1569 match &cmd.action {
1570 CommandAction::Message(msg) => {
1571 let msg = msg.clone();
1572 self.command_input.clear();
1573 self.palette_selected_index = 0;
1574 match msg {
1575 ApplicationMessage::NavigateToSubmenu(submenu) => {
1576 self.palette_view = PaletteView::Submenu(submenu);
1577 focus_input()
1578 }
1579 ApplicationMessage::SpawnNode { node_type } => {
1580 let new_id = generate_node_id();
1581 let pos = self.spawn_position();
1582 self.nodes.insert(new_id.clone(), (pos, node_type));
1583 self.node_order.push(new_id.clone());
1584 self.selected_nodes = HashSet::from([new_id]);
1585 self.command_palette_open = false;
1586 self.palette_view = PaletteView::Main;
1587 self.save_state();
1588 Task::none()
1589 }
1590 ApplicationMessage::ChangeTheme(theme) => {
1591 self.current_theme = theme;
1592 self.palette_preview_theme = None;
1593 self.palette_original_theme = None;
1594 self.command_palette_open = false;
1595 self.palette_view = PaletteView::Main;
1596 self.propagate_values();
1599 self.save_state();
1600 Task::none()
1601 }
1602 ApplicationMessage::ExportState => {
1603 self.command_palette_open = false;
1604 self.palette_view = PaletteView::Main;
1605 self.export_state_to_file();
1606 Task::none()
1607 }
1608 ApplicationMessage::Reset => {
1609 self.reset_to_default();
1610 Task::none()
1611 }
1612 ApplicationMessage::FocusNode => {
1613 self.command_palette_open = false;
1614 self.palette_view = PaletteView::Main;
1615 self.focus_first_node()
1616 }
1617 _ => Task::none(),
1618 }
1619 }
1620 _ => Task::none(),
1621 }
1622 }
1623 ApplicationMessage::CommandPaletteCancel => {
1624 if !self.command_palette_open {
1625 return Task::none();
1626 }
1627 if let Some(original) = self.palette_original_theme.take() {
1628 self.current_theme = original;
1629 }
1630 self.palette_preview_theme = None;
1631 self.propagate_values();
1632 self.command_palette_open = false;
1633 self.command_input.clear();
1634 self.palette_view = PaletteView::Main;
1635 self.palette_selected_index = 0;
1636 Task::none()
1637 }
1638 ApplicationMessage::SpawnNode { node_type } => {
1639 let new_id = generate_node_id();
1640 let pos = self.spawn_position();
1641 self.nodes.insert(new_id.clone(), (pos, node_type));
1642 self.node_order.push(new_id.clone());
1643 self.selected_nodes = HashSet::from([new_id]);
1644 self.command_palette_open = false;
1645 self.command_input.clear();
1646 self.palette_view = PaletteView::Main;
1647 self.save_state();
1648 Task::none()
1649 }
1650 ApplicationMessage::CameraChanged { position, zoom } => {
1651 self.camera_position = position;
1652 self.camera_zoom = zoom;
1653 self.save_state();
1654 Task::none()
1655 }
1656 ApplicationMessage::FocusNode => self.focus_first_node(),
1657 ApplicationMessage::WindowResized(size) => {
1658 self.viewport_size = size;
1659 self.window_size = Some((size.width as u32, size.height as u32));
1660 window::oldest()
1662 .and_then(window::is_maximized)
1663 .map(ApplicationMessage::WindowMaximizedChanged)
1664 }
1665 ApplicationMessage::WindowMoved(position) => {
1666 self.window_position = Some((position.x as i32, position.y as i32));
1667 self.save_state();
1668 Task::none()
1669 }
1670 ApplicationMessage::WindowMaximizedChanged(maximized) => {
1671 self.window_maximized = Some(maximized);
1672 self.save_state();
1673 Task::none()
1674 }
1675 ApplicationMessage::ChangeTheme(theme) => {
1676 self.current_theme = theme;
1677 self.command_palette_open = false;
1678 self.command_input.clear();
1679 self.palette_view = PaletteView::Main;
1680 self.propagate_values();
1683 self.save_state();
1684 Task::none()
1685 }
1686 ApplicationMessage::NavigateToSubmenu(submenu) => {
1687 self.palette_view = PaletteView::Submenu(submenu);
1688 self.command_input.clear();
1689 focus_input()
1690 }
1691 ApplicationMessage::ExportState => {
1692 self.export_state_to_file();
1693 Task::none()
1694 }
1695 ApplicationMessage::Reset => {
1696 self.reset_to_default();
1697 Task::none()
1698 }
1699 ApplicationMessage::FocusNext => iced::widget::operation::focus_next(),
1700 ApplicationMessage::FocusPrevious => iced::widget::operation::focus_previous(),
1701 ApplicationMessage::SelectionChanged(node_ids) => {
1702 self.selected_nodes = node_ids.into_iter().collect();
1703 Task::none()
1704 }
1705 ApplicationMessage::CloneNodes(node_ids) => {
1706 let offset = Vector::new(50.0, 50.0);
1707 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
1708 let mut new_ids = Vec::new();
1709
1710 for old_id in &node_ids {
1712 if let Some((pos, node_type)) = self.nodes.get(old_id) {
1713 let new_id = generate_node_id();
1714 let new_pos = Point::new(pos.x + offset.x, pos.y + offset.y);
1715 self.nodes
1716 .insert(new_id.clone(), (new_pos, node_type.clone()));
1717 self.node_order.push(new_id.clone());
1718 id_map.insert(old_id.clone(), new_id.clone());
1719 new_ids.push(new_id);
1720 }
1721 }
1722
1723 let edges_to_clone: Vec<_> = self
1725 .edges
1726 .iter()
1727 .filter(|(_, e)| {
1728 node_ids.contains(&e.from_node) && node_ids.contains(&e.to_node)
1729 })
1730 .map(|(_, e)| e.clone())
1731 .collect();
1732
1733 for edge in edges_to_clone {
1734 if let (Some(new_from), Some(new_to)) =
1735 (id_map.get(&edge.from_node), id_map.get(&edge.to_node))
1736 {
1737 let new_edge_id = generate_edge_id();
1738 self.edges.insert(
1739 new_edge_id.clone(),
1740 EdgeData::new(
1741 new_from.clone(),
1742 edge.from_pin,
1743 new_to.clone(),
1744 edge.to_pin,
1745 ),
1746 );
1747 self.edge_order.push(new_edge_id);
1748 }
1749 }
1750
1751 self.selected_nodes = new_ids.into_iter().collect();
1752 self.propagate_values();
1753 self.save_state();
1754 Task::none()
1755 }
1756 ApplicationMessage::DeleteNodes(node_ids) => {
1757 self.delete_nodes(&node_ids);
1758 self.propagate_values();
1759 self.save_state();
1760 Task::none()
1761 }
1762 ApplicationMessage::NodesMoved { delta, node_ids } => {
1763 for node_id in node_ids {
1764 if let Some((pos, _)) = self.nodes.get_mut(&node_id) {
1765 pos.x += delta.x;
1766 pos.y += delta.y;
1767 }
1768 }
1769 self.save_state();
1770 Task::none()
1771 }
1772 ApplicationMessage::SliderChanged { node_id, value } => {
1773 if let Some((_, NodeType::Input(InputNodeType::FloatSlider { value: v, .. }))) =
1774 self.nodes.get_mut(&node_id)
1775 {
1776 *v = value;
1777 self.propagate_values();
1778 }
1779 Task::none()
1780 }
1781 ApplicationMessage::IntSliderChanged { node_id, value } => {
1782 if let Some((_, NodeType::Input(InputNodeType::IntSlider { value: v, .. }))) =
1783 self.nodes.get_mut(&node_id)
1784 {
1785 *v = value;
1786 self.propagate_values();
1787 }
1788 Task::none()
1789 }
1790 ApplicationMessage::BoolChanged { node_id, value } => {
1791 if let Some((_, NodeType::Input(InputNodeType::BoolToggle { value: v, .. }))) =
1792 self.nodes.get_mut(&node_id)
1793 {
1794 *v = value;
1795 self.propagate_values();
1796 }
1797 Task::none()
1798 }
1799 ApplicationMessage::EdgeCurveChanged { node_id, value } => {
1800 if let Some((_, NodeType::Input(InputNodeType::EdgeCurveSelector { value: v }))) =
1801 self.nodes.get_mut(&node_id)
1802 {
1803 *v = value;
1804 self.propagate_values();
1805 }
1806 Task::none()
1807 }
1808 ApplicationMessage::PinShapeChanged { node_id, value } => {
1809 if let Some((_, NodeType::Input(InputNodeType::PinShapeSelector { value: v }))) =
1810 self.nodes.get_mut(&node_id)
1811 {
1812 *v = value;
1813 self.propagate_values();
1814 }
1815 Task::none()
1816 }
1817 ApplicationMessage::PatternTypeChanged { node_id, value } => {
1818 if let Some((_, NodeType::Input(InputNodeType::PatternTypeSelector { value: v }))) =
1819 self.nodes.get_mut(&node_id)
1820 {
1821 *v = value;
1822 self.propagate_values();
1823 }
1824 Task::none()
1825 }
1826 ApplicationMessage::TilingKindChanged { node_id, value } => {
1827 if let Some((_, NodeType::Input(InputNodeType::TilingKindSelector { value: v }))) =
1828 self.nodes.get_mut(&node_id)
1829 {
1830 *v = value;
1831 self.propagate_values();
1832 }
1833 Task::none()
1834 }
1835 ApplicationMessage::ColorChanged { node_id, color } => {
1836 if let Some((_, node_type)) = self.nodes.get_mut(&node_id) {
1837 match node_type {
1838 NodeType::Input(InputNodeType::ColorPicker { color: c }) => {
1839 *c = color;
1840 self.propagate_values();
1841 }
1842 NodeType::Input(InputNodeType::ColorPreset { color: c }) => {
1843 *c = color;
1844 self.propagate_values();
1845 }
1846 _ => {}
1847 }
1848 }
1849 Task::none()
1850 }
1851 ApplicationMessage::ToggleNodeExpanded { node_id } => {
1852 if self.expanded_nodes.contains(&node_id) {
1853 self.expanded_nodes.remove(&node_id);
1854 } else {
1855 self.expanded_nodes.insert(node_id);
1856 }
1857 Task::none()
1858 }
1859 ApplicationMessage::UpdateFloatSliderConfig { node_id, config } => {
1860 if let Some((_, NodeType::Input(InputNodeType::FloatSlider { config: c, value }))) =
1861 self.nodes.get_mut(&node_id)
1862 {
1863 *value = value.clamp(config.min, config.max);
1865 *c = config;
1866 }
1867 Task::none()
1868 }
1869 ApplicationMessage::UpdateIntSliderConfig { node_id, config } => {
1870 if let Some((_, NodeType::Input(InputNodeType::IntSlider { config: c, value }))) =
1871 self.nodes.get_mut(&node_id)
1872 {
1873 *value = (*value).clamp(config.min, config.max);
1875 *c = config;
1876 }
1877 Task::none()
1878 }
1879 ApplicationMessage::ToggleEdgeSection { node_id, section } => {
1880 let sections = self
1881 .edge_config_sections
1882 .entry(node_id)
1883 .or_insert_with(EdgeSections::new_all_expanded);
1884 match section {
1885 EdgeSection::Stroke => sections.stroke = !sections.stroke,
1886 EdgeSection::Pattern => sections.pattern = !sections.pattern,
1887 EdgeSection::Border => sections.border = !sections.border,
1888 EdgeSection::Shadow => sections.shadow = !sections.shadow,
1889 }
1890 Task::none()
1891 }
1892 ApplicationMessage::ToggleNodeSection { node_id, section } => {
1893 let sections = self
1894 .node_config_sections
1895 .entry(node_id)
1896 .or_insert_with(NodeSections::new_all_expanded);
1897 match section {
1898 NodeSection::Fill => sections.fill = !sections.fill,
1899 NodeSection::Border => sections.border = !sections.border,
1900 NodeSection::Pattern => sections.pattern = !sections.pattern,
1901 NodeSection::Shadow => sections.shadow = !sections.shadow,
1902 }
1903 Task::none()
1904 }
1905 ApplicationMessage::NodeClassTargetChanged { node_id, target } => {
1906 if let Some((_, NodeType::Config(ConfigNodeType::NodeClass { target: t, .. }))) =
1907 self.nodes.get_mut(&node_id)
1908 {
1909 *t = target;
1910 self.propagate_values();
1911 self.save_state();
1912 }
1913 Task::none()
1914 }
1915 }
1916 }
1917
1918 fn theme(&self) -> Theme {
1919 self.effective_theme().clone()
1920 }
1921
1922 fn set_theme(&mut self, theme: Theme) {
1923 self.current_theme = theme;
1924 self.palette_preview_theme = None;
1925 self.propagate_values();
1927 }
1928
1929 fn view(&self) -> iced::Element<'_, ApplicationMessage> {
1930 use iced_nodegraph::NodeGraph;
1931
1932 let theme = self.effective_theme();
1933
1934 let candidates: Vec<ClassCandidate> = self
1937 .node_order
1938 .iter()
1939 .filter_map(|id| match self.nodes.get(id) {
1940 Some((_, NodeType::Workflow(name))) => Some(ClassCandidate {
1941 id: id.clone(),
1942 label: format!("{} {}", name, &id[..6.min(id.len())]),
1943 }),
1944 _ => None,
1945 })
1946 .collect();
1947
1948 let mut ng: NodeGraph<'_, HelloIds, ApplicationMessage, iced::Theme, iced::Renderer> =
1949 NodeGraph::new()
1950 .id(graph_id())
1951 .on_connect(|from: PinRef<HelloIds>, to: PinRef<HelloIds>| {
1952 ApplicationMessage::EdgeConnected { from, to }
1953 })
1954 .on_disconnect(|from: PinRef<HelloIds>, to: PinRef<HelloIds>| {
1955 ApplicationMessage::EdgeDisconnected { from, to }
1956 })
1957 .on_edge_delete(ApplicationMessage::EdgesCut)
1958 .on_move(|delta, node_ids| ApplicationMessage::NodesMoved { delta, node_ids })
1959 .on_select(ApplicationMessage::SelectionChanged)
1960 .on_clone(ApplicationMessage::CloneNodes)
1961 .on_delete(ApplicationMessage::DeleteNodes)
1962 .on_camera(|position, zoom| ApplicationMessage::CameraChanged { position, zoom })
1963 .on_anchor_move(|anchor, position| ApplicationMessage::AnchorMoved {
1964 anchor,
1965 position,
1966 })
1967 .on_anchor_create(|edge, position| ApplicationMessage::AnchorCreated {
1968 edge,
1969 position,
1970 })
1971 .on_anchor_delete(ApplicationMessage::AnchorDeleted)
1972 .on_route_attach(|edge, anchor| ApplicationMessage::RouteAttached { edge, anchor })
1973 .on_route_detach(|edge, anchor| ApplicationMessage::RouteDetached { edge, anchor })
1974 .on_drag_end(|| ApplicationMessage::DragEnded)
1975 .camera(self.camera_position, self.camera_zoom)
1976 .minimap(Minimap::default())
1977 .can_connect(|from, to| {
1982 from.direction() != to.direction() && from.info() == to.info()
1983 })
1984 .selection_box_style(|theme| self.selection_box_style(theme))
1988 .cutting_tool_style(|theme| self.cutting_tool_style(theme))
1989 .minimap_style(|theme| self.minimap_style(theme))
1990 .dragging_edge_style(|theme, source| self.drag_edge_style(theme, *source.info()))
1991 .dragging_anchor_style(|theme, status| {
1992 self.computed
1993 .drag_anchor
1994 .resolve_over(default_anchor_style(theme, status))
1995 })
1996 .graph_style(|theme| self.graph_style(theme))
1997 .anchors(self.anchors.iter().map(|&(id, position)| {
1998 ng_anchor(id, position).style(|theme, status| self.anchor_style(theme, status))
1999 }));
2000
2001 for node_id in &self.node_order {
2003 let Some((position, node_type)) = self.nodes.get(node_id) else {
2004 continue;
2005 };
2006 let node_id_clone = node_id.clone();
2007 let element: iced::Element<'_, ApplicationMessage> = match node_type {
2008 NodeType::Workflow(name) => node(name.as_str(), theme),
2009 NodeType::Input(input) => match input {
2010 InputNodeType::FloatSlider { config, value } => {
2011 let id = node_id_clone.clone();
2012 let expanded = self.expanded_nodes.contains(node_id);
2013 float_slider_node(
2014 theme,
2015 *value,
2016 config,
2017 expanded,
2018 {
2019 let id = id.clone();
2020 move |v| ApplicationMessage::SliderChanged {
2021 node_id: id.clone(),
2022 value: v,
2023 }
2024 },
2025 {
2026 let id = id.clone();
2027 move |cfg| ApplicationMessage::UpdateFloatSliderConfig {
2028 node_id: id.clone(),
2029 config: cfg,
2030 }
2031 },
2032 ApplicationMessage::ToggleNodeExpanded { node_id: id },
2033 )
2034 }
2035 InputNodeType::IntSlider { config, value } => {
2036 let id = node_id_clone.clone();
2037 let expanded = self.expanded_nodes.contains(node_id);
2038 int_slider_node(
2039 theme,
2040 *value,
2041 config,
2042 expanded,
2043 {
2044 let id = id.clone();
2045 move |v| ApplicationMessage::IntSliderChanged {
2046 node_id: id.clone(),
2047 value: v,
2048 }
2049 },
2050 {
2051 let id = id.clone();
2052 move |cfg| ApplicationMessage::UpdateIntSliderConfig {
2053 node_id: id.clone(),
2054 config: cfg,
2055 }
2056 },
2057 ApplicationMessage::ToggleNodeExpanded { node_id: id },
2058 )
2059 }
2060 InputNodeType::BoolToggle { config, value } => {
2061 let id = node_id_clone.clone();
2062 bool_toggle_node(theme, *value, config, move |v| {
2063 ApplicationMessage::BoolChanged {
2064 node_id: id.clone(),
2065 value: v,
2066 }
2067 })
2068 }
2069 InputNodeType::EdgeCurveSelector { value } => {
2070 let id = node_id_clone.clone();
2071 edge_curve_selector_node(theme, *value, move |v| {
2072 ApplicationMessage::EdgeCurveChanged {
2073 node_id: id.clone(),
2074 value: v,
2075 }
2076 })
2077 }
2078 InputNodeType::PinShapeSelector { value } => {
2079 let id = node_id_clone.clone();
2080 pin_shape_selector_node(theme, *value, move |v| {
2081 ApplicationMessage::PinShapeChanged {
2082 node_id: id.clone(),
2083 value: v,
2084 }
2085 })
2086 }
2087 InputNodeType::PatternTypeSelector { value } => {
2088 let id = node_id_clone.clone();
2089 pattern_type_selector_node(theme, *value, move |v| {
2090 ApplicationMessage::PatternTypeChanged {
2091 node_id: id.clone(),
2092 value: v,
2093 }
2094 })
2095 }
2096 InputNodeType::TilingKindSelector { value } => {
2097 let id = node_id_clone.clone();
2098 tiling_kind_selector_node(theme, *value, move |v| {
2099 ApplicationMessage::TilingKindChanged {
2100 node_id: id.clone(),
2101 value: v,
2102 }
2103 })
2104 }
2105 InputNodeType::ColorPicker { color } => {
2106 let id = node_id_clone.clone();
2107 color_picker_node(theme, *color, move |c| {
2108 ApplicationMessage::ColorChanged {
2109 node_id: id.clone(),
2110 color: c,
2111 }
2112 })
2113 }
2114 InputNodeType::ColorPreset { color } => {
2115 let id = node_id_clone.clone();
2116 color_preset_node(theme, *color, move |c| {
2117 ApplicationMessage::ColorChanged {
2118 node_id: id.clone(),
2119 color: c,
2120 }
2121 })
2122 }
2123 },
2124 NodeType::Config(config) => match config {
2125 ConfigNodeType::NodeConfig(inputs) => {
2126 let id = node_id_clone.clone();
2127 let sections = self
2128 .node_config_sections
2129 .get(&id)
2130 .cloned()
2131 .unwrap_or_else(NodeSections::new_all_expanded);
2132 node_config_node(theme, inputs, §ions, move |section| {
2133 ApplicationMessage::ToggleNodeSection {
2134 node_id: id.clone(),
2135 section,
2136 }
2137 })
2138 }
2139 ConfigNodeType::EdgeConfig(inputs) => {
2140 let id = node_id_clone.clone();
2141 let sections = self
2142 .edge_config_sections
2143 .get(&id)
2144 .cloned()
2145 .unwrap_or_else(EdgeSections::new_all_expanded);
2146 edge_config_node(theme, inputs, §ions, move |section| {
2147 ApplicationMessage::ToggleEdgeSection {
2148 node_id: id.clone(),
2149 section,
2150 }
2151 })
2152 }
2153 ConfigNodeType::PinConfig(inputs) => pin_config_node(theme, inputs),
2154 ConfigNodeType::GraphConfig(inputs) => graph_config_node(theme, inputs),
2155 ConfigNodeType::AnchorConfig(inputs) => anchor_config_node(theme, inputs),
2156 ConfigNodeType::SelectionBoxConfig(inputs) => {
2157 selection_box_config_node(theme, inputs)
2158 }
2159 ConfigNodeType::CuttingToolConfig(inputs) => {
2160 cutting_tool_config_node(theme, inputs)
2161 }
2162 ConfigNodeType::MinimapConfig(inputs) => minimap_config_node(theme, inputs),
2163 ConfigNodeType::Catalog { connected } => catalog_node(theme, connected),
2164 ConfigNodeType::NodeClass {
2165 target,
2166 has_node_config,
2167 } => {
2168 let id = node_id_clone.clone();
2169 node_class_node(
2170 theme,
2171 *has_node_config,
2172 target.as_ref(),
2173 candidates.clone(),
2174 move |target| ApplicationMessage::NodeClassTargetChanged {
2175 node_id: id.clone(),
2176 target,
2177 },
2178 )
2179 }
2180 },
2181 NodeType::Math(state) => math_node(theme, state),
2182 NodeType::ColorQuad(state) => color_quad_node(theme, state),
2183 NodeType::Vec2(state) => vec2_node(theme, state),
2184 NodeType::Alpha(state) => alpha_node(theme, state),
2185 NodeType::Theme => theme_node(theme),
2186 NodeType::ThemeExtended => theme_extended_node(theme),
2187 NodeType::Frame { label, size } => frame_node(theme, label, *size),
2188 };
2189
2190 let mut node = ng_node(node_id.clone(), *position, element)
2191 .style(|theme, status| self.node_style(node_id, theme, status))
2192 .pin_style(|theme, pin, _other, status| self.pin_style(*pin.info(), theme, status));
2193 if matches!(node_type, NodeType::Frame { .. }) {
2194 node = node.frame();
2195 }
2196 ng = ng.push_node(node);
2197 }
2198
2199 for edge_id in &self.edge_order {
2200 if let Some(edge_data) = self.edges.get(edge_id) {
2201 let from = PinRef::new(edge_data.from_node.clone(), edge_data.from_pin);
2202 let to = PinRef::new(edge_data.to_node.clone(), edge_data.to_pin);
2203 ng = ng.push_edge(
2204 ng_edge(edge_id.clone(), from, to)
2205 .route(edge_data.route.iter().copied())
2206 .style(|theme, status, start, end| {
2207 self.edge_style(theme, status, *start.info(), *end.info())
2208 }),
2209 );
2210 }
2211 }
2212
2213 let graph_view: iced::Element<'_, ApplicationMessage> = ng.into();
2214
2215 let overlay: iced::Element<'_, ApplicationMessage> = if self.command_palette_open {
2218 let (_, commands) = self.build_palette_commands();
2219 opaque(command_palette(
2223 &self.command_input,
2224 &commands,
2225 self.palette_selected_index,
2226 ApplicationMessage::CommandPaletteInput,
2227 ApplicationMessage::CommandPaletteSelect,
2228 ApplicationMessage::CommandPaletteNavigate,
2229 || ApplicationMessage::CommandPaletteCancel,
2230 ))
2231 } else {
2232 container(text("")).width(0).height(0).into()
2234 };
2235
2236 stack!(graph_view, overlay)
2237 .width(Length::Fill)
2238 .height(Length::Fill)
2239 .into()
2240 }
2241
2242 fn subscription(&self) -> Subscription<ApplicationMessage> {
2243 Subscription::batch(vec![
2244 event::listen_with(handle_keyboard_event),
2245 event::listen_with(|event, _, _| match event {
2246 Event::Window(window::Event::Resized(size)) => {
2247 Some(ApplicationMessage::WindowResized(size))
2248 }
2249 Event::Window(window::Event::Moved(position)) => {
2250 Some(ApplicationMessage::WindowMoved(position))
2251 }
2252 _ => None,
2253 }),
2254 ])
2255 }
2256}
2257
2258pub fn scene() -> (
2260 Box<dyn demo_common::Scene>,
2261 iced::Task<demo_common::SceneMessage>,
2262) {
2263 demo_common::erase::<Application>()
2264}
2265
2266impl Application {
2267 fn get_main_commands_with_shortcuts() -> Vec<Command<ApplicationMessage>> {
2268 vec![
2269 command("add_node", "Add Node")
2270 .description("Add a new node to the graph")
2271 .shortcut(Shortcut::cmd('n'))
2272 .action(ApplicationMessage::ExecuteShortcut("add_node".to_string())),
2273 command("change_theme", "Change Theme")
2274 .description("Switch to a different color theme")
2275 .shortcut(Shortcut::cmd('t'))
2276 .action(ApplicationMessage::ExecuteShortcut(
2277 "change_theme".to_string(),
2278 )),
2279 command("export_state", "Export State")
2280 .description("Export graph state to file for Claude")
2281 .shortcut(Shortcut::cmd('e'))
2282 .action(ApplicationMessage::ExecuteShortcut(
2283 "export_state".to_string(),
2284 )),
2285 ]
2286 }
2287
2288 fn get_available_themes() -> Vec<Theme> {
2289 vec![
2290 Theme::Dark,
2291 Theme::Light,
2292 Theme::Dracula,
2293 Theme::Nord,
2294 Theme::SolarizedLight,
2295 Theme::SolarizedDark,
2296 Theme::GruvboxLight,
2297 Theme::GruvboxDark,
2298 Theme::CatppuccinLatte,
2299 Theme::CatppuccinFrappe,
2300 Theme::CatppuccinMacchiato,
2301 Theme::CatppuccinMocha,
2302 Theme::TokyoNight,
2303 Theme::TokyoNightStorm,
2304 Theme::TokyoNightLight,
2305 Theme::KanagawaWave,
2306 Theme::KanagawaDragon,
2307 Theme::KanagawaLotus,
2308 Theme::Moonfly,
2309 Theme::Nightfly,
2310 Theme::Oxocarbon,
2311 Theme::Ferra,
2312 ]
2313 }
2314
2315 fn get_theme_name(theme: &Theme) -> &'static str {
2316 match theme {
2317 Theme::Dark => "Dark",
2318 Theme::Light => "Light",
2319 Theme::Dracula => "Dracula",
2320 Theme::Nord => "Nord",
2321 Theme::SolarizedLight => "Solarized Light",
2322 Theme::SolarizedDark => "Solarized Dark",
2323 Theme::GruvboxLight => "Gruvbox Light",
2324 Theme::GruvboxDark => "Gruvbox Dark",
2325 Theme::CatppuccinLatte => "Catppuccin Latte",
2326 Theme::CatppuccinFrappe => "Catppuccin Frappe",
2327 Theme::CatppuccinMacchiato => "Catppuccin Macchiato",
2328 Theme::CatppuccinMocha => "Catppuccin Mocha",
2329 Theme::TokyoNight => "Tokyo Night",
2330 Theme::TokyoNightStorm => "Tokyo Night Storm",
2331 Theme::TokyoNightLight => "Tokyo Night Light",
2332 Theme::KanagawaWave => "Kanagawa Wave",
2333 Theme::KanagawaDragon => "Kanagawa Dragon",
2334 Theme::KanagawaLotus => "Kanagawa Lotus",
2335 Theme::Moonfly => "Moonfly",
2336 Theme::Nightfly => "Nightfly",
2337 Theme::Oxocarbon => "Oxocarbon",
2338 Theme::Ferra => "Ferra",
2339 _ => "Unknown",
2340 }
2341 }
2342
2343 fn focus_first_node(&self) -> Task<ApplicationMessage> {
2347 match self.node_order.first() {
2348 Some(target) => iced_nodegraph::focus(
2349 graph_id(),
2350 FocusTarget::<HelloIds>::Node(target.clone()),
2351 FocusOptions::default(),
2352 ),
2353 None => Task::none(),
2354 }
2355 }
2356
2357 fn node_style(&self, node_id: &NodeId, theme: &Theme, status: NodeStatus) -> NodeStyle {
2367 let global = self.computed.node(status);
2368 let overlay = match self.computed.node_classes.get(node_id) {
2369 Some(class) => class.merge(&global),
2370 None => global,
2371 };
2372 overlay.resolve_over(default_node_style(theme, status))
2373 }
2374
2375 fn pin_style(&self, info: std::any::TypeId, theme: &Theme, status: PinStatus) -> PinStyle {
2378 self.computed
2379 .pin(status)
2380 .merge(&PinOverlay::new().color(pin_color_for(info)))
2381 .resolve_over(default_pin_style(theme, status))
2382 }
2383
2384 fn edge_style(
2388 &self,
2389 theme: &Theme,
2390 status: EdgeStatus,
2391 start: std::any::TypeId,
2392 end: std::any::TypeId,
2393 ) -> EdgeStyle {
2394 let type_colors = ColorQuad::arc(pin_color_for(start), pin_color_for(end));
2395 let base = EdgeStyle {
2396 stroke_color: type_colors,
2397 glow_color: type_colors.with_opacity(EDGE_GLOW_OPACITY),
2398 ..default_edge_style(theme, status)
2399 };
2400 self.computed.edge(status).resolve_over(base)
2401 }
2402
2403 fn drag_edge_style(&self, theme: &Theme, source: std::any::TypeId) -> EdgeStyle {
2406 let base = EdgeStyle {
2407 stroke_color: ColorQuad::solid(pin_color_for(source)),
2408 ..default_edge_style(theme, EdgeStatus::Idle)
2409 };
2410 self.computed.drag_edge.resolve_over(base)
2411 }
2412
2413 fn anchor_style(&self, theme: &Theme, status: AnchorStatus) -> AnchorStyle {
2414 self.computed
2415 .anchor(status)
2416 .resolve_over(default_anchor_style(theme, status))
2417 }
2418
2419 fn graph_style(&self, theme: &Theme) -> GraphStyle {
2420 self.computed.graph.resolve_over(default_graph_style(theme))
2421 }
2422
2423 fn selection_box_style(&self, theme: &Theme) -> SelectionBoxStyle {
2424 self.computed
2425 .selection_box
2426 .resolve_over(default_selection_box_style(theme))
2427 }
2428
2429 fn cutting_tool_style(&self, theme: &Theme) -> CuttingToolStyle {
2430 self.computed
2431 .cutting_tool
2432 .resolve_over(default_cutting_tool_style(theme))
2433 }
2434
2435 fn minimap_style(&self, theme: &Theme) -> MinimapStyle {
2436 self.computed
2437 .minimap
2438 .resolve_over(default_minimap_style(theme))
2439 }
2440
2441 fn drop_unused_anchors(&mut self) {
2447 let routed: HashSet<usize> = self
2448 .edges
2449 .values()
2450 .flat_map(|e| e.route.iter().copied())
2451 .collect();
2452 self.anchors.retain(|(id, _)| routed.contains(id));
2453 }
2454
2455 fn delete_nodes(&mut self, node_ids: &[NodeId]) {
2459 for node_id in node_ids {
2460 self.nodes.remove(node_id);
2461 self.node_order.retain(|id| id != node_id);
2462
2463 let edges_to_remove: Vec<_> = self
2464 .edges
2465 .iter()
2466 .filter(|(_, e)| &e.from_node == node_id || &e.to_node == node_id)
2467 .map(|(id, _)| id.clone())
2468 .collect();
2469 for edge_id in edges_to_remove {
2470 self.edges.remove(&edge_id);
2471 self.edge_order.retain(|id| id != &edge_id);
2472 }
2473 }
2474
2475 for (_, node_type) in self.nodes.values_mut() {
2476 if let NodeType::Config(ConfigNodeType::NodeClass {
2477 target: Some(target),
2478 ..
2479 }) = node_type
2480 && node_ids.contains(target)
2481 {
2482 *node_type = NodeType::Config(ConfigNodeType::NodeClass {
2483 target: None,
2484 has_node_config: false,
2485 });
2486 }
2487 }
2488
2489 self.drop_unused_anchors();
2490 self.selected_nodes.clear();
2491 }
2492
2493 fn build_palette_commands(&self) -> (&'static str, Vec<Command<ApplicationMessage>>) {
2494 match &self.palette_view {
2495 PaletteView::Main => {
2496 let commands = vec![
2497 command("add_node", "Add Node")
2498 .description("Add a new node to the graph")
2499 .shortcut(Shortcut::cmd('n'))
2500 .action(ApplicationMessage::NavigateToSubmenu("nodes".to_string())),
2501 command("change_theme", "Change Theme")
2502 .description("Switch to a different color theme")
2503 .shortcut(Shortcut::cmd('t'))
2504 .action(ApplicationMessage::NavigateToSubmenu("themes".to_string())),
2505 command("export_state", "Export State")
2506 .description("Export graph state to file for Claude")
2507 .shortcut(Shortcut::cmd('e'))
2508 .action(ApplicationMessage::ExportState),
2509 command("reset", "Reset")
2510 .description("Reset the app to its initial state")
2511 .action(ApplicationMessage::Reset),
2512 command("focus_node", "Focus node")
2513 .description("Fly the camera to a fixed demo node (eased tween)")
2514 .action(ApplicationMessage::FocusNode),
2515 ];
2516 ("Command Palette", commands)
2517 }
2518 PaletteView::Submenu(submenu) if submenu == "nodes" => {
2519 let commands = vec![
2520 command("workflow", "Workflow Nodes")
2522 .description("Original demo nodes")
2523 .action(ApplicationMessage::NavigateToSubmenu(
2524 "workflow_nodes".to_string(),
2525 )),
2526 command("inputs", "Input Nodes")
2528 .description("Sliders, color pickers, etc.")
2529 .action(ApplicationMessage::NavigateToSubmenu(
2530 "input_nodes".to_string(),
2531 )),
2532 command("math", "Math Nodes")
2534 .description("Add, Subtract, Multiply, Divide")
2535 .action(ApplicationMessage::NavigateToSubmenu(
2536 "math_nodes".to_string(),
2537 )),
2538 command("config", "Style Config Nodes")
2540 .description("Configure node and edge styling")
2541 .action(ApplicationMessage::NavigateToSubmenu(
2542 "config_nodes".to_string(),
2543 )),
2544 ];
2545 ("Add Node", commands)
2546 }
2547 PaletteView::Submenu(submenu) if submenu == "workflow_nodes" => {
2548 let workflow_nodes = vec!["email_trigger", "email_parser", "filter", "calendar"];
2549 let commands = workflow_nodes
2550 .into_iter()
2551 .map(|name| {
2552 command(name, name).action(ApplicationMessage::SpawnNode {
2553 node_type: NodeType::Workflow(name.to_string()),
2554 })
2555 })
2556 .collect();
2557 ("Workflow Nodes", commands)
2558 }
2559 PaletteView::Submenu(submenu) if submenu == "input_nodes" => {
2560 let commands = vec![
2561 command("float_slider", "Float Slider")
2562 .description("Generic float slider (0-20)")
2563 .action(ApplicationMessage::SpawnNode {
2564 node_type: NodeType::Input(InputNodeType::FloatSlider {
2565 config: FloatSliderConfig::default(),
2566 value: 5.0,
2567 }),
2568 }),
2569 command("pattern_angle", "Pattern Angle")
2570 .description("Angle for Arrowed/Angled patterns (-90 to 90 degrees)")
2571 .action(ApplicationMessage::SpawnNode {
2572 node_type: NodeType::Input(InputNodeType::FloatSlider {
2573 config: FloatSliderConfig::pattern_angle(),
2574 value: 45.0,
2575 }),
2576 }),
2577 command("color_picker", "Color Picker (RGB)")
2578 .description("Full RGB color picker with sliders")
2579 .action(ApplicationMessage::SpawnNode {
2580 node_type: NodeType::Input(InputNodeType::ColorPicker {
2581 color: Color::from_rgb(0.5, 0.5, 0.5),
2582 }),
2583 }),
2584 command("color_preset", "Color Presets")
2585 .description("Quick color selection from presets")
2586 .action(ApplicationMessage::SpawnNode {
2587 node_type: NodeType::Input(InputNodeType::ColorPreset {
2588 color: Color::from_rgb(0.5, 0.5, 0.5),
2589 }),
2590 }),
2591 command("int_slider", "Int Slider")
2592 .description("Integer slider (0-100)")
2593 .action(ApplicationMessage::SpawnNode {
2594 node_type: NodeType::Input(InputNodeType::IntSlider {
2595 config: IntSliderConfig::default(),
2596 value: 50,
2597 }),
2598 }),
2599 command("bool_toggle", "Boolean Toggle")
2600 .description("Toggle for boolean values")
2601 .action(ApplicationMessage::SpawnNode {
2602 node_type: NodeType::Input(InputNodeType::BoolToggle {
2603 config: BoolToggleConfig::default(),
2604 value: true,
2605 }),
2606 }),
2607 command("edge_curve", "Edge Curve Selector")
2608 .description("Select edge curve (Bezier, Line, Orthogonal)")
2609 .action(ApplicationMessage::SpawnNode {
2610 node_type: NodeType::Input(InputNodeType::EdgeCurveSelector {
2611 value: EdgeCurve::BezierCubic,
2612 }),
2613 }),
2614 command("pin_shape", "Pin Shape Selector")
2615 .description("Select pin shape (Circle, Square)")
2616 .action(ApplicationMessage::SpawnNode {
2617 node_type: NodeType::Input(InputNodeType::PinShapeSelector {
2618 value: PinShape::Circle,
2619 }),
2620 }),
2621 command("pattern_type", "Pattern Type Selector")
2622 .description("Select edge pattern (Solid, Dashed, Dotted)")
2623 .action(ApplicationMessage::SpawnNode {
2624 node_type: NodeType::Input(InputNodeType::PatternTypeSelector {
2625 value: PatternType::Solid,
2626 }),
2627 }),
2628 command("tiling_kind", "Tiling Kind Selector")
2629 .description("Select canvas tiling (Grid, Dots, Triangles, Hex)")
2630 .action(ApplicationMessage::SpawnNode {
2631 node_type: NodeType::Input(InputNodeType::TilingKindSelector {
2632 value: TilingKind::Grid,
2633 }),
2634 }),
2635 command("theme", "Theme")
2636 .description("Active theme's basic palette as color outputs")
2637 .action(ApplicationMessage::SpawnNode {
2638 node_type: NodeType::Theme,
2639 }),
2640 command("theme_extended", "Theme Extended")
2641 .description("Extended palette (base/weak/strong) as color outputs")
2642 .action(ApplicationMessage::SpawnNode {
2643 node_type: NodeType::ThemeExtended,
2644 }),
2645 ];
2646 ("Input Nodes", commands)
2647 }
2648 PaletteView::Submenu(submenu) if submenu == "math_nodes" => {
2649 let commands = vec![
2650 command("add", "Add").description("A + B").action(
2651 ApplicationMessage::SpawnNode {
2652 node_type: NodeType::Math(MathNodeState::new(MathOperation::Add)),
2653 },
2654 ),
2655 command("subtract", "Subtract").description("A - B").action(
2656 ApplicationMessage::SpawnNode {
2657 node_type: NodeType::Math(MathNodeState::new(MathOperation::Subtract)),
2658 },
2659 ),
2660 command("multiply", "Multiply").description("A * B").action(
2661 ApplicationMessage::SpawnNode {
2662 node_type: NodeType::Math(MathNodeState::new(MathOperation::Multiply)),
2663 },
2664 ),
2665 command("divide", "Divide").description("A / B").action(
2666 ApplicationMessage::SpawnNode {
2667 node_type: NodeType::Math(MathNodeState::new(MathOperation::Divide)),
2668 },
2669 ),
2670 ];
2671 ("Math Nodes", commands)
2672 }
2673 PaletteView::Submenu(submenu) if submenu == "config_nodes" => {
2674 let commands = vec![
2675 command("node_config", "Node Config")
2676 .description("Node config with all fields and inheritance")
2677 .action(ApplicationMessage::SpawnNode {
2678 node_type: NodeType::Config(ConfigNodeType::NodeConfig(
2679 NodeConfigInputs::default(),
2680 )),
2681 }),
2682 command("edge_config", "Edge Config")
2683 .description("Edge config with colors, thickness, type")
2684 .action(ApplicationMessage::SpawnNode {
2685 node_type: NodeType::Config(ConfigNodeType::EdgeConfig(
2686 EdgeConfigInputs::default(),
2687 )),
2688 }),
2689 command("pin_config", "Pin Config")
2690 .description("Pin configuration with shape, color, radius")
2691 .action(ApplicationMessage::SpawnNode {
2692 node_type: NodeType::Config(ConfigNodeType::PinConfig(
2693 PinConfigInputs::default(),
2694 )),
2695 }),
2696 command("graph_config", "Graph Config")
2697 .description("Canvas background and tiling (grid/dots/...)")
2698 .action(ApplicationMessage::SpawnNode {
2699 node_type: NodeType::Config(ConfigNodeType::GraphConfig(
2700 GraphConfigInputs::default(),
2701 )),
2702 }),
2703 command("anchor_config", "Anchor Config")
2704 .description("Anchor core, border and orbit ring")
2705 .action(ApplicationMessage::SpawnNode {
2706 node_type: NodeType::Config(ConfigNodeType::AnchorConfig(
2707 AnchorConfigInputs::default(),
2708 )),
2709 }),
2710 command("selection_box_config", "Selection Box Config")
2711 .description("Selection box fill and border")
2712 .action(ApplicationMessage::SpawnNode {
2713 node_type: NodeType::Config(ConfigNodeType::SelectionBoxConfig(
2714 SelectionBoxConfigInputs::default(),
2715 )),
2716 }),
2717 command("cutting_tool_config", "Cutting Tool Config")
2718 .description("Edge-cutting trail color and width")
2719 .action(ApplicationMessage::SpawnNode {
2720 node_type: NodeType::Config(ConfigNodeType::CuttingToolConfig(
2721 CuttingToolConfigInputs::default(),
2722 )),
2723 }),
2724 command("minimap_config", "Minimap Config")
2725 .description("Minimap background, marks and viewport")
2726 .action(ApplicationMessage::SpawnNode {
2727 node_type: NodeType::Config(ConfigNodeType::MinimapConfig(
2728 MinimapConfigInputs::default(),
2729 )),
2730 }),
2731 command("color_quad", "Color Quad")
2733 .description("Combine 4 corner colors into one ColorQuad")
2734 .action(ApplicationMessage::SpawnNode {
2735 node_type: NodeType::ColorQuad(ColorQuadNode::default()),
2736 }),
2737 command("vec2", "Vec2")
2738 .description("Combine x and y into a 2D vector (e.g. offset)")
2739 .action(ApplicationMessage::SpawnNode {
2740 node_type: NodeType::Vec2(Vec2Node::default()),
2741 }),
2742 command("alpha", "Alpha")
2743 .description("Replace a color's alpha")
2744 .action(ApplicationMessage::SpawnNode {
2745 node_type: NodeType::Alpha(AlphaNode::default()),
2746 }),
2747 command("catalog", "Catalog")
2749 .description("One input per style class and status")
2750 .action(ApplicationMessage::SpawnNode {
2751 node_type: NodeType::Config(ConfigNodeType::Catalog {
2752 connected: HashSet::new(),
2753 }),
2754 }),
2755 command("node_class", "Node Class")
2756 .description("Assign a node config to one node")
2757 .action(ApplicationMessage::SpawnNode {
2758 node_type: NodeType::Config(ConfigNodeType::NodeClass {
2759 target: None,
2760 has_node_config: false,
2761 }),
2762 }),
2763 command("frame", "Frame")
2765 .description("A titled region that moves the nodes laid over it")
2766 .action(ApplicationMessage::SpawnNode {
2767 node_type: NodeType::Frame {
2768 label: "Frame".to_string(),
2769 size: Size::new(400.0, 300.0),
2770 },
2771 }),
2772 ];
2773 ("Style Config Nodes", commands)
2774 }
2775 PaletteView::Submenu(submenu) if submenu == "themes" => {
2776 let commands = Self::get_available_themes()
2777 .iter()
2778 .map(|theme| {
2779 let name = Self::get_theme_name(theme);
2780 command(name, name).action(ApplicationMessage::ChangeTheme(theme.clone()))
2781 })
2782 .collect();
2783 ("Choose Theme", commands)
2784 }
2785 _ => ("Command Palette", vec![]),
2786 }
2787 }
2788}
2789
2790fn theme_color(theme: &Theme, pin: &PinLabel) -> Option<iced::Color> {
2798 use nodes::pins::{theme as t, theme_ext as x};
2799
2800 let pal = theme.palette();
2802 if *pin == t::BACKGROUND {
2803 return Some(pal.background);
2804 } else if *pin == t::TEXT {
2805 return Some(pal.text);
2806 } else if *pin == t::PRIMARY {
2807 return Some(pal.primary);
2808 } else if *pin == t::SUCCESS {
2809 return Some(pal.success);
2810 } else if *pin == t::WARNING {
2811 return Some(pal.warning);
2812 } else if *pin == t::DANGER {
2813 return Some(pal.danger);
2814 }
2815
2816 let p = theme.extended_palette();
2818 let color = if *pin == x::BACKGROUND_BASE {
2819 p.background.base.color
2820 } else if *pin == x::BACKGROUND_WEAK {
2821 p.background.weak.color
2822 } else if *pin == x::BACKGROUND_STRONG {
2823 p.background.strong.color
2824 } else if *pin == x::PRIMARY_BASE {
2825 p.primary.base.color
2826 } else if *pin == x::PRIMARY_WEAK {
2827 p.primary.weak.color
2828 } else if *pin == x::PRIMARY_STRONG {
2829 p.primary.strong.color
2830 } else if *pin == x::SECONDARY_BASE {
2831 p.secondary.base.color
2832 } else if *pin == x::SECONDARY_WEAK {
2833 p.secondary.weak.color
2834 } else if *pin == x::SECONDARY_STRONG {
2835 p.secondary.strong.color
2836 } else if *pin == x::SUCCESS_BASE {
2837 p.success.base.color
2838 } else if *pin == x::SUCCESS_WEAK {
2839 p.success.weak.color
2840 } else if *pin == x::SUCCESS_STRONG {
2841 p.success.strong.color
2842 } else if *pin == x::WARNING_BASE {
2843 p.warning.base.color
2844 } else if *pin == x::WARNING_WEAK {
2845 p.warning.weak.color
2846 } else if *pin == x::WARNING_STRONG {
2847 p.warning.strong.color
2848 } else if *pin == x::DANGER_BASE {
2849 p.danger.base.color
2850 } else if *pin == x::DANGER_WEAK {
2851 p.danger.weak.color
2852 } else if *pin == x::DANGER_STRONG {
2853 p.danger.strong.color
2854 } else {
2855 return None;
2856 };
2857 Some(color)
2858}
2859
2860fn feed_combiner_input(node: &mut NodeType, pin: &PinLabel, value: &NodeValue) -> bool {
2864 use nodes::pins::{build as pin_build, math as pin_math};
2865
2866 let mut changed = false;
2867 match node {
2868 NodeType::Math(state) => {
2869 if let Some(f) = value.as_float() {
2870 if *pin == pin_math::A && state.input_a != Some(f) {
2871 state.input_a = Some(f);
2872 changed = true;
2873 } else if *pin == pin_math::B && state.input_b != Some(f) {
2874 state.input_b = Some(f);
2875 changed = true;
2876 }
2877 }
2878 }
2879 NodeType::ColorQuad(state) => {
2880 if let Some(c) = value.as_color() {
2881 let slot = if *pin == pin_build::NEAR_START {
2882 Some(&mut state.near_start)
2883 } else if *pin == pin_build::NEAR_END {
2884 Some(&mut state.near_end)
2885 } else if *pin == pin_build::FAR_START {
2886 Some(&mut state.far_start)
2887 } else if *pin == pin_build::FAR_END {
2888 Some(&mut state.far_end)
2889 } else {
2890 None
2891 };
2892 if let Some(slot) = slot
2893 && *slot != Some(c)
2894 {
2895 *slot = Some(c);
2896 changed = true;
2897 }
2898 }
2899 }
2900 NodeType::Vec2(state) => {
2901 if let Some(f) = value.as_float() {
2902 if *pin == pin_build::X && state.x != Some(f) {
2903 state.x = Some(f);
2904 changed = true;
2905 } else if *pin == pin_build::Y && state.y != Some(f) {
2906 state.y = Some(f);
2907 changed = true;
2908 }
2909 }
2910 }
2911 NodeType::Alpha(state) => {
2912 if *pin == pin_build::ALPHA_COLOR {
2913 let c = value.as_color_quad().map(|q| q.near_start);
2914 if c.is_some() && state.color != c {
2915 state.color = c;
2916 changed = true;
2917 }
2918 } else if *pin == pin_build::ALPHA {
2919 let a = value.as_float();
2920 if a.is_some() && state.alpha != a {
2921 state.alpha = a;
2922 changed = true;
2923 }
2924 }
2925 }
2926 _ => {}
2927 }
2928 changed
2929}
2930
2931fn handle_keyboard_event(
2932 event: Event,
2933 _status: iced::event::Status,
2934 _window: iced::window::Id,
2935) -> Option<ApplicationMessage> {
2936 match event {
2937 Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
2938 if is_toggle_shortcut(&key, modifiers) {
2939 return Some(ApplicationMessage::ToggleCommandPalette);
2940 }
2941
2942 if modifiers.command() {
2943 let main_commands = Application::get_main_commands_with_shortcuts();
2944 if let Some(cmd_id) = find_matching_shortcut(&main_commands, &key, modifiers) {
2945 return Some(ApplicationMessage::ExecuteShortcut(cmd_id.to_string()));
2946 }
2947 }
2948
2949 match key {
2950 keyboard::Key::Named(keyboard::key::Named::Tab) => {
2951 if modifiers.shift() {
2952 Some(ApplicationMessage::FocusPrevious)
2953 } else {
2954 Some(ApplicationMessage::FocusNext)
2955 }
2956 }
2957 keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
2958 Some(ApplicationMessage::CommandPaletteNavigateUp)
2959 }
2960 keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
2961 Some(ApplicationMessage::CommandPaletteNavigateDown)
2962 }
2963 keyboard::Key::Named(keyboard::key::Named::Enter) => {
2964 Some(ApplicationMessage::CommandPaletteConfirm)
2965 }
2966 keyboard::Key::Named(keyboard::key::Named::Escape) => {
2967 Some(ApplicationMessage::CommandPaletteCancel)
2968 }
2969 _ => None,
2970 }
2971 }
2972 _ => None,
2973 }
2974}
2975
2976#[cfg(test)]
2977mod tests {
2978 use super::*;
2979 use nodes::{MathNodeState, MathOperation, NodeType};
2980
2981 #[test]
2984 fn test_math_add() {
2985 let op = MathOperation::Add;
2986 assert_eq!(op.compute(5.0, 3.0), 8.0);
2987 assert_eq!(op.symbol(), "+");
2988 assert_eq!(op.name(), "Add");
2989 }
2990
2991 #[test]
2992 fn test_math_subtract() {
2993 let op = MathOperation::Subtract;
2994 assert_eq!(op.compute(5.0, 3.0), 2.0);
2995 assert_eq!(op.compute(3.0, 5.0), -2.0);
2996 assert_eq!(op.symbol(), "-");
2997 }
2998
2999 #[test]
3000 fn test_math_multiply() {
3001 let op = MathOperation::Multiply;
3002 assert_eq!(op.compute(5.0, 3.0), 15.0);
3003 assert_eq!(op.compute(0.0, 100.0), 0.0);
3004 assert_eq!(op.symbol(), "*");
3005 }
3006
3007 #[test]
3008 fn test_math_divide() {
3009 let op = MathOperation::Divide;
3010 assert_eq!(op.compute(6.0, 2.0), 3.0);
3011 assert_eq!(op.symbol(), "/");
3012 }
3013
3014 #[test]
3015 fn test_math_divide_by_zero() {
3016 let op = MathOperation::Divide;
3017 let result = op.compute(5.0, 0.0);
3018 assert!(result.is_infinite());
3019 }
3020
3021 #[test]
3024 fn test_math_node_result_with_both_inputs() {
3025 let mut state = MathNodeState::new(MathOperation::Add);
3026 state.input_a = Some(10.0);
3027 state.input_b = Some(5.0);
3028 assert_eq!(state.result(), Some(15.0));
3029 }
3030
3031 #[test]
3032 fn test_math_node_result_with_missing_a() {
3033 let mut state = MathNodeState::new(MathOperation::Add);
3034 state.input_a = None;
3035 state.input_b = Some(5.0);
3036 assert_eq!(state.result(), None);
3037 }
3038
3039 #[test]
3040 fn test_math_node_result_with_missing_b() {
3041 let mut state = MathNodeState::new(MathOperation::Add);
3042 state.input_a = Some(10.0);
3043 state.input_b = None;
3044 assert_eq!(state.result(), None);
3045 }
3046
3047 #[test]
3050 fn test_math_node_output_value() {
3051 let mut state = MathNodeState::new(MathOperation::Multiply);
3052 state.input_a = Some(4.0);
3053 state.input_b = Some(3.0);
3054 let node_type = NodeType::Math(state);
3055
3056 let output = node_type.output_value();
3057 assert!(output.is_some());
3058 if let Some(NodeValue::Float(f)) = output {
3059 assert_eq!(f, 12.0);
3060 } else {
3061 panic!("Expected Float value");
3062 }
3063 }
3064
3065 #[test]
3066 fn test_math_node_output_value_no_result() {
3067 let state = MathNodeState::new(MathOperation::Add); let node_type = NodeType::Math(state);
3069 assert!(node_type.output_value().is_none());
3070 }
3071
3072 #[test]
3073 fn test_input_node_output_value() {
3074 let input = InputNodeType::FloatSlider {
3075 config: FloatSliderConfig::default(),
3076 value: 7.5,
3077 };
3078 let node_type = NodeType::Input(input);
3079
3080 let output = node_type.output_value();
3081 assert!(output.is_some());
3082 if let Some(NodeValue::Float(f)) = output {
3083 assert!((f - 7.5).abs() < 0.001);
3084 } else {
3085 panic!("Expected Float value");
3086 }
3087 }
3088
3089 fn empty_app() -> Application {
3093 let mut app = Application::default();
3094 app.nodes.clear();
3095 app.node_order.clear();
3096 app.edges.clear();
3097 app.edge_order.clear();
3098 app.anchors.clear();
3099 app
3100 }
3101
3102 fn add(app: &mut Application, node_type: NodeType) -> NodeId {
3103 let id = generate_node_id();
3104 app.nodes.insert(id.clone(), (Point::ORIGIN, node_type));
3105 app.node_order.push(id.clone());
3106 id
3107 }
3108
3109 fn wire(app: &mut Application, from: &NodeId, fp: PinLabel, to: &NodeId, tp: PinLabel) {
3110 let e = generate_edge_id();
3111 app.edges
3112 .insert(e.clone(), EdgeData::new(from.clone(), fp, to.clone(), tp));
3113 app.edge_order.push(e);
3114 }
3115
3116 fn catalog(app: &mut Application) -> NodeId {
3117 add(
3118 app,
3119 NodeType::Config(ConfigNodeType::Catalog {
3120 connected: HashSet::new(),
3121 }),
3122 )
3123 }
3124
3125 fn node_config(app: &mut Application) -> NodeId {
3126 add(
3127 app,
3128 NodeType::Config(ConfigNodeType::NodeConfig(NodeConfigInputs::default())),
3129 )
3130 }
3131
3132 fn slider(app: &mut Application, value: f32) -> NodeId {
3133 add(
3134 app,
3135 NodeType::Input(InputNodeType::FloatSlider {
3136 config: FloatSliderConfig::default(),
3137 value,
3138 }),
3139 )
3140 }
3141
3142 fn picker(app: &mut Application, color: Color) -> NodeId {
3143 add(app, NodeType::Input(InputNodeType::ColorPicker { color }))
3144 }
3145
3146 fn connected(app: &Application, catalog: &NodeId) -> HashSet<PinLabel> {
3148 match app.nodes.get(catalog) {
3149 Some((_, NodeType::Config(ConfigNodeType::Catalog { connected }))) => connected.clone(),
3150 _ => panic!("catalog node missing"),
3151 }
3152 }
3153
3154 #[test]
3155 fn test_node_config_chain_applies_to_catalog() {
3156 use nodes::pins;
3158 let mut app = empty_app();
3159 let red = Color::from_rgb(1.0, 0.0, 0.0);
3160 let picker = picker(&mut app, red);
3161 let cfg = node_config(&mut app);
3162 let catalog = catalog(&mut app);
3163 wire(
3164 &mut app,
3165 &picker,
3166 pins::input::COLOR,
3167 &cfg,
3168 pins::node::FILL_COLOR,
3169 );
3170 wire(
3171 &mut app,
3172 &cfg,
3173 pins::cfg::NODE_OUT,
3174 &catalog,
3175 pins::cfg::NODE_CONFIG,
3176 );
3177
3178 app.propagate_values();
3179
3180 assert!(connected(&app, &catalog).contains(pins::cfg::NODE_CONFIG));
3181 assert_eq!(
3182 app.computed.node.fill_color.map(|q| q.near_start),
3183 Some(red),
3184 "computed node class did not receive the config fill color",
3185 );
3186 }
3187
3188 #[test]
3189 fn test_catalog_rejects_mismatched_kind() {
3190 use nodes::pins;
3193 let mut app = empty_app();
3194 let picker = picker(&mut app, Color::WHITE);
3195 let cfg = node_config(&mut app);
3196 let catalog = catalog(&mut app);
3197 wire(
3198 &mut app,
3199 &picker,
3200 pins::input::COLOR,
3201 &cfg,
3202 pins::node::FILL_COLOR,
3203 );
3204 wire(
3205 &mut app,
3206 &cfg,
3207 pins::cfg::NODE_OUT,
3208 &catalog,
3209 pins::cfg::GRAPH_CONFIG,
3210 );
3211
3212 app.propagate_values();
3213
3214 assert!(connected(&app, &catalog).is_empty());
3215 assert!(app.computed.node.fill_color.is_none());
3216 assert!(app.computed.graph.background_color.is_none());
3217 }
3218
3219 #[test]
3220 fn test_graph_config_chain_applies_to_catalog() {
3221 use nodes::pins;
3225 let mut app = empty_app();
3226 let blue = Color::from_rgb(0.0, 0.0, 1.0);
3227 let picker = picker(&mut app, blue);
3228 let kind = add(
3229 &mut app,
3230 NodeType::Input(InputNodeType::TilingKindSelector {
3231 value: TilingKind::Dots,
3232 }),
3233 );
3234 let cfg = add(
3235 &mut app,
3236 NodeType::Config(ConfigNodeType::GraphConfig(GraphConfigInputs::default())),
3237 );
3238 let catalog = catalog(&mut app);
3239 wire(
3240 &mut app,
3241 &picker,
3242 pins::input::COLOR,
3243 &cfg,
3244 pins::graph::BACKGROUND,
3245 );
3246 wire(
3247 &mut app,
3248 &kind,
3249 pins::input::VALUE,
3250 &cfg,
3251 pins::graph::TILING_KIND,
3252 );
3253 wire(
3254 &mut app,
3255 &cfg,
3256 pins::cfg::GRAPH_OUT,
3257 &catalog,
3258 pins::cfg::GRAPH_CONFIG,
3259 );
3260
3261 app.propagate_values();
3262
3263 assert!(connected(&app, &catalog).contains(pins::cfg::GRAPH_CONFIG));
3264 assert_eq!(app.computed.graph.background_color, Some(blue));
3265 assert_eq!(app.computed.graph.tiling_kind, Some(TilingKind::Dots));
3266 let resolved = app
3267 .computed
3268 .graph
3269 .resolve_over(default_graph_style(&app.current_theme));
3270 assert_eq!(resolved.background_color, blue);
3271 assert_eq!(resolved.tiling.map(|t| t.kind), Some(TilingKind::Dots));
3272 }
3273
3274 #[test]
3275 fn test_node_config_shadow_chain_with_vec2() {
3276 use nodes::pins;
3279 let mut app = empty_app();
3280 let red = Color::from_rgb(1.0, 0.0, 0.0);
3281 let picker = picker(&mut app, red);
3282 let dist = slider(&mut app, 8.0);
3283 let sx = slider(&mut app, 5.0);
3284 let sy = slider(&mut app, 7.0);
3285 let vec2 = add(&mut app, NodeType::Vec2(Vec2Node::default()));
3286 let cfg = node_config(&mut app);
3287 let catalog = catalog(&mut app);
3288 wire(
3289 &mut app,
3290 &picker,
3291 pins::input::COLOR,
3292 &cfg,
3293 pins::node::SHADOW_COLOR,
3294 );
3295 wire(
3296 &mut app,
3297 &dist,
3298 pins::input::VALUE,
3299 &cfg,
3300 pins::node::SHADOW_DISTANCE,
3301 );
3302 wire(&mut app, &sx, pins::input::VALUE, &vec2, pins::build::X);
3303 wire(&mut app, &sy, pins::input::VALUE, &vec2, pins::build::Y);
3304 wire(
3305 &mut app,
3306 &vec2,
3307 pins::build::VEC2_OUT,
3308 &cfg,
3309 pins::node::SHADOW_OFFSET,
3310 );
3311 wire(
3312 &mut app,
3313 &cfg,
3314 pins::cfg::NODE_OUT,
3315 &catalog,
3316 pins::cfg::NODE_CONFIG,
3317 );
3318
3319 app.propagate_values();
3320
3321 let node = &app.computed.node;
3322 assert_eq!(node.shadow_color, Some(red));
3323 assert_eq!(node.shadow_distance, Some(8.0));
3324 assert_eq!(node.shadow_offset, Some((5.0, 7.0)));
3325 }
3326
3327 #[test]
3328 fn test_theme_node_feeds_config() {
3329 use nodes::pins;
3332 let mut app = empty_app();
3333 let theme = add(&mut app, NodeType::Theme);
3334 let cfg = node_config(&mut app);
3335 let catalog = catalog(&mut app);
3336 wire(
3337 &mut app,
3338 &theme,
3339 pins::theme::PRIMARY,
3340 &cfg,
3341 pins::node::FILL_COLOR,
3342 );
3343 wire(
3344 &mut app,
3345 &cfg,
3346 pins::cfg::NODE_OUT,
3347 &catalog,
3348 pins::cfg::NODE_CONFIG,
3349 );
3350
3351 let expected = app.current_theme.palette().primary;
3352 app.propagate_values();
3353 assert_eq!(
3354 app.computed.node.fill_color.map(|q| q.near_start),
3355 Some(expected),
3356 );
3357 }
3358
3359 #[test]
3360 fn test_theme_extended_node_feeds_config() {
3361 use nodes::pins;
3364 let mut app = empty_app();
3365 let theme = add(&mut app, NodeType::ThemeExtended);
3366 let cfg = node_config(&mut app);
3367 let catalog = catalog(&mut app);
3368 wire(
3369 &mut app,
3370 &theme,
3371 pins::theme_ext::PRIMARY_STRONG,
3372 &cfg,
3373 pins::node::FILL_COLOR,
3374 );
3375 wire(
3376 &mut app,
3377 &cfg,
3378 pins::cfg::NODE_OUT,
3379 &catalog,
3380 pins::cfg::NODE_CONFIG,
3381 );
3382
3383 let expected = app.current_theme.extended_palette().primary.strong.color;
3384 app.propagate_values();
3385 assert_eq!(
3386 app.computed.node.fill_color.map(|q| q.near_start),
3387 Some(expected),
3388 );
3389 }
3390
3391 #[test]
3392 fn test_palette_preview_recolors_rig() {
3393 use nodes::pins;
3396 let mut app = empty_app();
3397 let theme = add(&mut app, NodeType::Theme);
3398 let cfg = node_config(&mut app);
3399 let catalog = catalog(&mut app);
3400 wire(
3401 &mut app,
3402 &theme,
3403 pins::theme::PRIMARY,
3404 &cfg,
3405 pins::node::FILL_COLOR,
3406 );
3407 wire(
3408 &mut app,
3409 &cfg,
3410 pins::cfg::NODE_OUT,
3411 &catalog,
3412 pins::cfg::NODE_CONFIG,
3413 );
3414
3415 app.current_theme = Theme::Dark;
3416 app.palette_preview_theme = Some(Theme::Light);
3417 app.propagate_values();
3418 assert_eq!(
3419 app.computed.node.fill_color.map(|q| q.near_start),
3420 Some(Theme::Light.palette().primary),
3421 );
3422 }
3423
3424 #[test]
3425 fn catalog_status_input_layers_over_idle() {
3426 use nodes::pins;
3429 let mut app = empty_app();
3430 let red = Color::from_rgb(1.0, 0.0, 0.0);
3431 let blue = Color::from_rgb(0.0, 0.0, 1.0);
3432 let red_picker = picker(&mut app, red);
3433 let blue_picker = picker(&mut app, blue);
3434 let idle_cfg = node_config(&mut app);
3435 let selected_cfg = node_config(&mut app);
3436 let catalog = catalog(&mut app);
3437 wire(
3438 &mut app,
3439 &red_picker,
3440 pins::input::COLOR,
3441 &idle_cfg,
3442 pins::node::FILL_COLOR,
3443 );
3444 wire(
3445 &mut app,
3446 &blue_picker,
3447 pins::input::COLOR,
3448 &selected_cfg,
3449 pins::node::BORDER_COLOR,
3450 );
3451 wire(
3452 &mut app,
3453 &idle_cfg,
3454 pins::cfg::NODE_OUT,
3455 &catalog,
3456 pins::cfg::NODE_CONFIG,
3457 );
3458 wire(
3459 &mut app,
3460 &selected_cfg,
3461 pins::cfg::NODE_OUT,
3462 &catalog,
3463 pins::cfg::NODE_SELECTED,
3464 );
3465
3466 app.propagate_values();
3467
3468 let selected = app.computed.node(NodeStatus::Selected);
3469 assert_eq!(selected.fill_color.map(|q| q.near_start), Some(red));
3470 assert_eq!(selected.border_color.map(|q| q.near_start), Some(blue));
3471 let idle = app.computed.node(NodeStatus::Idle);
3472 assert_eq!(idle.fill_color.map(|q| q.near_start), Some(red));
3473 assert!(idle.border_color.is_none());
3474 }
3475
3476 #[test]
3477 fn node_class_targets_one_node() {
3478 use nodes::pins;
3481 let mut app = empty_app();
3482 let green = Color::from_rgb(0.0, 1.0, 0.0);
3483 let workflow = add(&mut app, NodeType::Workflow("filter".to_string()));
3484 let picker = picker(&mut app, green);
3485 let cfg = node_config(&mut app);
3486 let class = add(
3487 &mut app,
3488 NodeType::Config(ConfigNodeType::NodeClass {
3489 target: Some(workflow.clone()),
3490 has_node_config: false,
3491 }),
3492 );
3493 wire(
3494 &mut app,
3495 &picker,
3496 pins::input::COLOR,
3497 &cfg,
3498 pins::node::FILL_COLOR,
3499 );
3500 wire(
3501 &mut app,
3502 &cfg,
3503 pins::cfg::NODE_OUT,
3504 &class,
3505 pins::cfg::NODE_CONFIG,
3506 );
3507
3508 app.propagate_values();
3509
3510 assert_eq!(
3511 app.computed.node_classes[&workflow]
3512 .fill_color
3513 .map(|q| q.near_start),
3514 Some(green),
3515 );
3516 assert!(app.computed.node.fill_color.is_none());
3517 assert!(matches!(
3518 app.nodes.get(&class),
3519 Some((
3520 _,
3521 NodeType::Config(ConfigNodeType::NodeClass {
3522 has_node_config: true,
3523 ..
3524 })
3525 ))
3526 ));
3527
3528 if let Some((_, NodeType::Config(ConfigNodeType::NodeClass { target, .. }))) =
3529 app.nodes.get_mut(&class)
3530 {
3531 *target = None;
3532 }
3533 app.propagate_values();
3534 assert!(app.computed.node_classes.is_empty());
3535 }
3536
3537 #[test]
3538 fn deleting_the_target_clears_the_node_class() {
3539 let mut app = empty_app();
3540 let workflow = add(&mut app, NodeType::Workflow("filter".to_string()));
3541 let class = add(
3542 &mut app,
3543 NodeType::Config(ConfigNodeType::NodeClass {
3544 target: Some(workflow.clone()),
3545 has_node_config: false,
3546 }),
3547 );
3548
3549 app.delete_nodes(&[workflow]);
3550
3551 assert!(matches!(
3552 app.nodes.get(&class),
3553 Some((
3554 _,
3555 NodeType::Config(ConfigNodeType::NodeClass { target: None, .. })
3556 ))
3557 ));
3558 }
3559
3560 #[test]
3561 fn alpha_builder_applies_alpha_to_palette_color() {
3562 use nodes::pins;
3565 let mut app = empty_app();
3566 let theme = add(&mut app, NodeType::ThemeExtended);
3567 let alpha_slider = slider(&mut app, 0.25);
3568 let alpha = add(&mut app, NodeType::Alpha(AlphaNode::default()));
3569 let cfg = add(
3570 &mut app,
3571 NodeType::Config(ConfigNodeType::SelectionBoxConfig(
3572 SelectionBoxConfigInputs::default(),
3573 )),
3574 );
3575 let catalog = catalog(&mut app);
3576 wire(
3577 &mut app,
3578 &theme,
3579 pins::theme_ext::PRIMARY_BASE,
3580 &alpha,
3581 pins::build::ALPHA_COLOR,
3582 );
3583 wire(
3584 &mut app,
3585 &alpha_slider,
3586 pins::input::VALUE,
3587 &alpha,
3588 pins::build::ALPHA,
3589 );
3590 wire(
3591 &mut app,
3592 &alpha,
3593 pins::build::ALPHA_OUT,
3594 &cfg,
3595 pins::selection_box::FILL,
3596 );
3597 wire(
3598 &mut app,
3599 &cfg,
3600 pins::cfg::SELECTION_BOX_OUT,
3601 &catalog,
3602 pins::cfg::SELECTION_BOX,
3603 );
3604
3605 app.propagate_values();
3606
3607 let fill = app.computed.selection_box.fill.expect("fill not computed");
3608 let base = app.current_theme.extended_palette().primary.base.color;
3609 assert_eq!(fill.a, 0.25);
3610 assert_eq!((fill.r, fill.g, fill.b), (base.r, base.g, base.b));
3611 }
3612
3613 fn assert_rig_complete(app: &Application) {
3615 use nodes::pins;
3616 let catalog_id = app
3617 .node_order
3618 .iter()
3619 .find(|id| {
3620 matches!(
3621 app.nodes.get(*id),
3622 Some((_, NodeType::Config(ConfigNodeType::Catalog { .. })))
3623 )
3624 })
3625 .expect("boot scene has no Catalog node");
3626 let connected = connected(app, catalog_id);
3627 for input in pins::cfg::CATALOG_INPUTS {
3628 assert!(connected.contains(input), "Catalog input {input} unwired");
3629 }
3630 assert_eq!(app.computed.node.corner_radius, Some(8.0));
3631 assert_eq!(app.computed.node.opacity, Some(0.88));
3632 assert_eq!(app.computed.graph.tiling_spacing, Some(40.0));
3633 assert_eq!(app.computed.node_classes.len(), 1);
3634 }
3635
3636 #[test]
3637 fn boot_rig_wires_every_catalog_input() {
3638 let mut app = Application::default();
3639 app.propagate_values();
3640 assert_rig_complete(&app);
3641 }
3642
3643 #[test]
3644 fn boot_rig_feeds_every_config_field() {
3645 let mut app = Application::default();
3648 app.propagate_values();
3649
3650 let optional: &[(&str, PinLabel)] = &[
3652 ("Pin", nodes::pins::pin::COLOR),
3653 ("Edge", nodes::pins::edge::STROKE_COLOR),
3654 ("Edge", nodes::pins::edge::GLOW_COLOR),
3655 ];
3656 let unwired = |title: &str, label: PinLabel, is_none: bool| {
3657 assert!(
3658 !is_none || optional.contains(&(title, label)),
3659 "{title} config: {label} has no source"
3660 );
3661 };
3662
3663 let node_inputs: Vec<&NodeConfigInputs> = app
3666 .nodes
3667 .values()
3668 .filter_map(|(_, t)| match t {
3669 NodeType::Config(ConfigNodeType::NodeConfig(i)) => Some(i),
3670 _ => None,
3671 })
3672 .collect();
3673 let idle = node_inputs
3674 .iter()
3675 .find(|i| i.corner_radius.is_some())
3676 .expect("idle node config");
3677 for (label, none) in [
3678 (nodes::pins::node::FILL_COLOR, idle.fill_color.is_none()),
3679 (nodes::pins::node::OPACITY, idle.opacity.is_none()),
3680 (nodes::pins::node::BORDER_COLOR, idle.border_color.is_none()),
3681 (nodes::pins::node::BORDER_WIDTH, idle.border_width.is_none()),
3682 (
3683 nodes::pins::node::BORDER_OUTLINE_WIDTH,
3684 idle.border_outline_width.is_none(),
3685 ),
3686 (
3687 nodes::pins::node::BORDER_OUTLINE_COLOR,
3688 idle.border_outline_color.is_none(),
3689 ),
3690 (nodes::pins::node::PATTERN, idle.pattern_type.is_none()),
3691 (nodes::pins::node::DASH, idle.dash_length.is_none()),
3692 (nodes::pins::node::GAP, idle.gap_length.is_none()),
3693 (nodes::pins::node::ANGLE, idle.pattern_angle.is_none()),
3694 (nodes::pins::node::SPEED, idle.animation_speed.is_none()),
3695 (nodes::pins::node::SHADOW_COLOR, idle.shadow_color.is_none()),
3696 (
3697 nodes::pins::node::SHADOW_DISTANCE,
3698 idle.shadow_distance.is_none(),
3699 ),
3700 (
3701 nodes::pins::node::SHADOW_OFFSET,
3702 idle.shadow_offset.is_none(),
3703 ),
3704 ] {
3705 unwired("Node", label, none);
3706 }
3707
3708 let edge = app
3709 .nodes
3710 .values()
3711 .find_map(|(_, t)| match t {
3712 NodeType::Config(ConfigNodeType::EdgeConfig(i)) if i.curve.is_some() => Some(i),
3713 _ => None,
3714 })
3715 .expect("idle edge config");
3716 for (label, none) in [
3717 (nodes::pins::edge::STROKE_COLOR, edge.stroke_color.is_none()),
3718 (nodes::pins::edge::THICKNESS, edge.thickness.is_none()),
3719 (
3720 nodes::pins::edge::STROKE_OUTLINE_WIDTH,
3721 edge.stroke_outline_width.is_none(),
3722 ),
3723 (
3724 nodes::pins::edge::STROKE_OUTLINE_COLOR,
3725 edge.stroke_outline_color.is_none(),
3726 ),
3727 (nodes::pins::edge::PATTERN, edge.pattern_type.is_none()),
3728 (nodes::pins::edge::DASH, edge.dash_length.is_none()),
3729 (nodes::pins::edge::GAP, edge.gap_length.is_none()),
3730 (nodes::pins::edge::DOT_RADIUS, edge.dot_radius.is_none()),
3731 (nodes::pins::edge::ANGLE, edge.pattern_angle.is_none()),
3732 (nodes::pins::edge::SPEED, edge.animation_speed.is_none()),
3733 (nodes::pins::edge::BORDER_WIDTH, edge.border_width.is_none()),
3734 (nodes::pins::edge::BORDER_GAP, edge.border_gap.is_none()),
3735 (nodes::pins::edge::BORDER_COLOR, edge.border_color.is_none()),
3736 (
3737 nodes::pins::edge::BORDER_BACKGROUND,
3738 edge.border_background.is_none(),
3739 ),
3740 (
3741 nodes::pins::edge::BORDER_OUTLINE_WIDTH,
3742 edge.border_outline_width.is_none(),
3743 ),
3744 (
3745 nodes::pins::edge::BORDER_OUTLINE_COLOR,
3746 edge.border_outline_color.is_none(),
3747 ),
3748 (nodes::pins::edge::SHADOW_COLOR, edge.shadow_color.is_none()),
3749 (nodes::pins::edge::SHADOW_BLUR, edge.shadow_blur.is_none()),
3750 (
3751 nodes::pins::edge::SHADOW_EXPAND,
3752 edge.shadow_expand.is_none(),
3753 ),
3754 (
3755 nodes::pins::edge::SHADOW_OFFSET,
3756 edge.shadow_offset.is_none(),
3757 ),
3758 (nodes::pins::edge::GLOW_COLOR, edge.glow_color.is_none()),
3759 (nodes::pins::edge::GLOW_WIDTH, edge.glow_width.is_none()),
3760 ] {
3761 unwired("Edge", label, none);
3762 }
3763
3764 let pin = app
3765 .nodes
3766 .values()
3767 .find_map(|(_, t)| match t {
3768 NodeType::Config(ConfigNodeType::PinConfig(i)) if i.radius.is_some() => Some(i),
3769 _ => None,
3770 })
3771 .expect("idle pin config");
3772 for (label, none) in [
3773 (nodes::pins::pin::COLOR, pin.color.is_none()),
3774 (nodes::pins::pin::CUTOUT_RADIUS, pin.cutout_radius.is_none()),
3775 (nodes::pins::pin::SHAPE, pin.shape.is_none()),
3776 (nodes::pins::pin::BORDER_COLOR, pin.border_color.is_none()),
3777 (nodes::pins::pin::BORDER_WIDTH, pin.border_width.is_none()),
3778 ] {
3779 unwired("Pin", label, none);
3780 }
3781
3782 let anchor = app.computed.anchor(AnchorStatus::Idle);
3784 assert!(anchor.core_size.is_some() && anchor.ring_width.is_some());
3785 assert!(anchor.core_color.is_some() && anchor.ring_color.is_some());
3786 assert!(anchor.core_border_color.is_some() && anchor.core_border_width.is_some());
3787 assert!(anchor.orbit_offset.is_some() && anchor.orbit_spacing.is_some());
3788 assert!(anchor.core_radius.is_some());
3789 assert!(anchor.offered_ring_color.is_some());
3790
3791 let g = &app.computed.graph;
3792 assert!(g.background_color.is_some() && g.tiling_kind.is_some());
3793 assert!(g.tiling_thickness.is_some() && g.tiling_color.is_some());
3794
3795 let s = &app.computed.selection_box;
3796 assert!(s.fill.is_some() && s.border_color.is_some() && s.border_width.is_some());
3797
3798 let c = &app.computed.cutting_tool;
3799 assert!(c.color.is_some() && c.width.is_some());
3800
3801 let m = &app.computed.minimap;
3802 assert!(m.background.is_some() && m.border_color.is_some());
3803 assert!(m.border_width.is_some() && m.node_color.is_some());
3804 assert!(m.selected_node_color.is_some() && m.viewport_fill.is_some());
3805 assert!(m.viewport_border_color.is_some() && m.viewport_border_width.is_some());
3806
3807 assert!(s.fill.unwrap().a < 1.0);
3809 assert!(m.background.unwrap().a < 1.0);
3810 }
3811
3812 #[cfg(not(target_arch = "wasm32"))]
3813 #[test]
3814 fn persistence_round_trips_the_rig() {
3815 let app = Application::default();
3816 let saved = persistence::SavedState::from_app(
3817 &app.nodes,
3818 &app.node_order,
3819 &app.edges,
3820 &app.edge_order,
3821 &app.current_theme,
3822 app.camera_position,
3823 app.camera_zoom,
3824 None,
3825 None,
3826 &app.edge_config_sections,
3827 &app.node_config_sections,
3828 None,
3829 &app.anchors,
3830 app.next_anchor,
3831 );
3832 let json = serde_json::to_string(&saved).expect("serialize");
3833 let loaded: persistence::SavedState = serde_json::from_str(&json).expect("parse");
3834 let (nodes, node_order, edges, edge_order, _, _, _, _, _, _, _, _, anchors, next_anchor) =
3835 loaded.to_app();
3836
3837 let mut restored = Application {
3838 nodes,
3839 node_order,
3840 edges,
3841 edge_order,
3842 anchors,
3843 next_anchor,
3844 ..Application::default()
3845 };
3846 restored.propagate_values();
3847 assert_rig_complete(&restored);
3848 assert_eq!(restored.anchors, app.anchors);
3849 assert_eq!(restored.next_anchor, 1);
3850 let routed: Vec<_> = restored
3851 .edges
3852 .values()
3853 .filter(|e| !e.route.is_empty())
3854 .collect();
3855 assert_eq!(routed.len(), 1);
3856 assert_eq!(routed[0].route, vec![0]);
3857 }
3858
3859 #[test]
3860 fn test_node_config_shadow_resolves() {
3861 use iced_nodegraph::{ColorQuad, NodeStatus, NodeStyle};
3862
3863 let inputs = NodeConfigInputs {
3866 shadow_color: Some(ColorQuad::solid(Color::from_rgb(1.0, 0.0, 0.0))),
3867 shadow_distance: Some(12.0),
3868 shadow_offset: Some((4.0, 6.0)),
3869 ..Default::default()
3870 };
3871 let overlay = inputs.build();
3872 assert_eq!(overlay.shadow_color, Some(Color::from_rgb(1.0, 0.0, 0.0)));
3873 assert_eq!(overlay.shadow_distance, Some(12.0));
3874 assert_eq!(overlay.shadow_offset, Some((4.0, 6.0)));
3875
3876 let resolved = overlay.resolve_over(NodeStyle::comment(&Theme::Dark, NodeStatus::Idle));
3878 assert_eq!(resolved.shadow_color, Color::from_rgb(1.0, 0.0, 0.0));
3879 assert_eq!(resolved.shadow_distance, 12.0);
3880 assert_eq!(resolved.shadow_offset, (4.0, 6.0));
3881 }
3882
3883 fn class_target(app: &Application) -> NodeId {
3887 app.nodes
3888 .values()
3889 .find_map(|(_, t)| match t {
3890 NodeType::Config(ConfigNodeType::NodeClass {
3891 target: Some(id), ..
3892 }) => Some(id.clone()),
3893 _ => None,
3894 })
3895 .expect("boot rig has a targeted Node Class")
3896 }
3897
3898 #[test]
3899 fn classed_node_keeps_its_tint_while_selected() {
3900 let app = Application::default();
3901 let theme = app.effective_theme().clone();
3902 let palette = theme.extended_palette();
3903 let calendar = class_target(&app);
3904
3905 let idle = app.node_style(&calendar, &theme, NodeStatus::Idle);
3906 let selected = app.node_style(&calendar, &theme, NodeStatus::Selected);
3907 assert_eq!(idle.fill_color.near_start, palette.warning.weak.color);
3909 assert_eq!(selected.fill_color.near_start, palette.warning.weak.color);
3910 assert_eq!(selected.border_color.near_start, palette.warning.base.color);
3911 assert_eq!(selected.border_pattern.thickness, 2.0);
3913 assert_eq!(selected.border_outline_width, 3.0);
3914 assert_eq!(selected.shadow_distance, 11.0);
3915 assert_eq!(idle.border_pattern.thickness, 1.0);
3916
3917 let other = app.node_order[0].clone();
3919 let plain = app.node_style(&other, &theme, NodeStatus::Selected);
3920 assert_eq!(plain.fill_color.near_start, palette.primary.weak.color);
3921 assert_eq!(plain.border_color.near_start, palette.primary.base.color);
3922 }
3923
3924 #[test]
3925 fn hovered_anchor_takes_the_accent() {
3926 let app = Application::default();
3927 let theme = app.effective_theme().clone();
3928 let primary = theme.extended_palette().primary.base.color;
3929
3930 let idle = app.anchor_style(&theme, AnchorStatus::Idle);
3931 let hovered = app.anchor_style(&theme, AnchorStatus::Hovered);
3932 assert_eq!(idle.core_border_width, 1.0);
3933 assert_eq!(hovered.core_border_width, 1.5);
3934 let accent = hovered.core_border_color.near_start;
3935 assert_eq!(
3936 (accent.r, accent.g, accent.b),
3937 (primary.r, primary.g, primary.b)
3938 );
3939 assert_eq!(accent.a, 0.6);
3940 assert_eq!(hovered.orbit_offset, idle.orbit_offset);
3942 assert_eq!(hovered.core_size, 6.0);
3943 }
3944
3945 #[test]
3946 fn chrome_resolves_from_the_rig() {
3947 let app = Application::default();
3948 let theme = app.effective_theme().clone();
3949 let palette = theme.extended_palette();
3950
3951 assert_eq!(app.selection_box_style(&theme).fill.a, 0.15);
3952 assert_eq!(app.cutting_tool_style(&theme).width, 3.0);
3953 assert_eq!(
3954 app.cutting_tool_style(&theme).color,
3955 palette.danger.base.color
3956 );
3957 assert_eq!(app.minimap_style(&theme).background.a, 0.9);
3958 let graph = app.graph_style(&theme);
3959 assert_eq!(graph.background_color, palette.background.base.color);
3960 assert_eq!(graph.tiling.map(|t| t.spacing), Some(40.0));
3961 let float = TypeId::of::<nodes::pins::Float>();
3962 assert_eq!(app.pin_style(float, &theme, PinStatus::Idle).radius, 5.0);
3963 assert_eq!(
3964 app.edge_style(&theme, EdgeStatus::PendingCut, float, float)
3965 .stroke_color
3966 .near_start,
3967 palette.danger.base.color
3968 );
3969 }
3970
3971 use iced::mouse;
3974 use iced_test::Simulator;
3975 use std::any::TypeId;
3976
3977 type Sim<'a> = Simulator<'a, ApplicationMessage, Theme, iced::Renderer>;
3978
3979 fn simulator(app: &Application) -> Sim<'_> {
3982 Simulator::with_size(
3983 iced::Settings::default(),
3984 Size::new(2000.0, 1600.0),
3985 app.view(),
3986 )
3987 }
3988
3989 fn apply(app: &mut Application, msgs: Vec<ApplicationMessage>) {
3992 for m in msgs {
3993 let _ = app.update(m);
3994 }
3995 }
3996
3997 fn moved(p: Point) -> iced::Event {
3998 iced::Event::Mouse(mouse::Event::CursorMoved { position: p })
3999 }
4000
4001 fn click(ui: &mut Sim<'_>, at: Point) {
4002 ui.point_at(at);
4003 ui.simulate([
4004 moved(at),
4005 iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
4006 iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
4007 ]);
4008 }
4009
4010 #[test]
4011 fn clicking_the_corner_radius_slider_reshapes_every_node() {
4012 let mut app = Application::default();
4013 let theme = app.effective_theme().clone();
4014 let calendar = class_target(&app);
4015 assert_eq!(
4016 app.node_style(&calendar, &theme, NodeStatus::Idle)
4017 .corner_radius,
4018 8.0
4019 );
4020
4021 let msgs: Vec<_> = {
4022 let mut ui = simulator(&app);
4023 let title = ui.find("corner_radius").expect("corner_radius slider node");
4026 let bounds = title.bounds();
4027 let slider_left = bounds.x + 4.0 + 1.0;
4028 let slider_y = bounds.y + bounds.height + 4.0 + 10.0 + 8.0;
4029 click(&mut ui, Point::new(slider_left + 75.0, slider_y));
4030 ui.into_messages().collect()
4031 };
4032 apply(&mut app, msgs.clone());
4033 let moved_to = msgs
4034 .iter()
4035 .find_map(|m| match m {
4036 ApplicationMessage::SliderChanged { value, .. } => Some(*value),
4037 _ => None,
4038 })
4039 .expect("the click reached the slider");
4040 assert!((20.0..=28.0).contains(&moved_to), "slider value {moved_to}");
4041 assert_eq!(app.computed.node.corner_radius, Some(moved_to));
4042 for id in &app.node_order {
4043 if matches!(app.nodes[id].1, NodeType::Workflow(_)) {
4044 assert_eq!(
4045 app.node_style(id, &theme, NodeStatus::Idle).corner_radius,
4046 moved_to
4047 );
4048 }
4049 }
4050 }
4051
4052 #[test]
4053 fn selection_box_alpha_slider_drives_its_opacity() {
4054 use nodes::pins;
4055 let mut app = Application::default();
4056 let theme = app.effective_theme().clone();
4057 assert_eq!(app.selection_box_style(&theme).fill.a, 0.15);
4058
4059 let cfg = app
4061 .nodes
4062 .iter()
4063 .find_map(|(id, (_, t))| {
4064 matches!(t, NodeType::Config(ConfigNodeType::SelectionBoxConfig(_)))
4065 .then(|| id.clone())
4066 })
4067 .expect("selection box config");
4068 let alpha = app
4069 .edges
4070 .values()
4071 .find(|e| e.to_node == cfg && e.to_pin == pins::selection_box::FILL)
4072 .map(|e| e.from_node.clone())
4073 .expect("alpha node feeding fill");
4074 let slider = app
4075 .edges
4076 .values()
4077 .find(|e| e.to_node == alpha && e.to_pin == pins::build::ALPHA)
4078 .map(|e| e.from_node.clone())
4079 .expect("slider feeding alpha");
4080
4081 let _ = app.update(ApplicationMessage::SliderChanged {
4082 node_id: slider,
4083 value: 0.6,
4084 });
4085 assert_eq!(app.selection_box_style(&theme).fill.a, 0.6);
4086 }
4087
4088 #[test]
4089 fn node_class_pick_list_retargets_through_the_graph() {
4090 let mut app = Application::default();
4091 let theme = app.effective_theme().clone();
4092 let before = class_target(&app);
4093 let first_workflow = app.node_order[0].clone();
4094 assert_ne!(before, first_workflow);
4095
4096 app.camera_position = Point::new(-1500.0, -400.0);
4100 app.camera_zoom = 1.0;
4101 let msgs: Vec<_> = {
4102 let mut ui = Simulator::new(app.view());
4103 let label = ui.find("target").expect("Node Class target row");
4107 let bounds = label.bounds();
4108 let pan = Vector::new(app.camera_position.x, app.camera_position.y);
4109 let picker = Point::new(bounds.x + bounds.width + 6.0 + 30.0, bounds.center_y()) + pan;
4110 assert!(picker.x > 0.0 && picker.x < 1024.0 && picker.y > 0.0 && picker.y < 768.0);
4111 click(&mut ui, picker);
4112 click(&mut ui, Point::new(picker.x, picker.y + 24.0));
4113 ui.into_messages().collect()
4114 };
4115 apply(&mut app, msgs.clone());
4116 assert!(
4117 msgs.iter().any(|m| matches!(
4118 m,
4119 ApplicationMessage::NodeClassTargetChanged { target: Some(t), .. } if *t == first_workflow
4120 )),
4121 "pick list did not report the new target: {msgs:?}"
4122 );
4123 assert_eq!(class_target(&app), first_workflow);
4124 let palette = theme.extended_palette();
4125 assert_eq!(
4126 app.node_style(&first_workflow, &theme, NodeStatus::Idle)
4127 .fill_color
4128 .near_start,
4129 palette.warning.weak.color
4130 );
4131 assert_ne!(
4132 app.node_style(&before, &theme, NodeStatus::Idle)
4133 .fill_color
4134 .near_start,
4135 palette.warning.weak.color
4136 );
4137 }
4138
4139 #[test]
4140 fn test_edge_config_inputs_pattern_type_dashed() {
4141 let inputs = EdgeConfigInputs {
4142 pattern_type: Some(PatternType::Dashed),
4143 thickness: Some(3.0),
4144 dash_length: Some(10.0),
4145 gap_length: Some(5.0),
4146 ..Default::default()
4147 };
4148
4149 let config = inputs.build();
4150 let pattern = config.pattern.expect("pattern should be Some");
4151 assert_eq!(pattern.thickness, 3.0);
4152 assert!(
4153 matches!(pattern.pattern_type, iced_nodegraph::PatternType::Dashed { dash, gap, .. } if (dash - 10.0).abs() < 0.01 && (gap - 5.0).abs() < 0.01),
4154 "Expected Dashed pattern, got {:?}",
4155 pattern.pattern_type
4156 );
4157 }
4158
4159 #[test]
4160 fn test_edge_config_inputs_pattern_type_arrowed() {
4161 let inputs = EdgeConfigInputs {
4162 pattern_type: Some(PatternType::Arrowed),
4163 ..Default::default()
4164 };
4165
4166 let config = inputs.build();
4167 let pattern = config.pattern.expect("pattern should be Some");
4168 assert!(
4169 matches!(
4170 pattern.pattern_type,
4171 iced_nodegraph::PatternType::Arrowed { .. }
4172 ),
4173 "Expected Arrowed pattern, got {:?}",
4174 pattern.pattern_type
4175 );
4176 }
4177
4178 #[test]
4179 fn test_edge_config_inputs_pattern_type_dotted() {
4180 let inputs = EdgeConfigInputs {
4181 pattern_type: Some(PatternType::Dotted),
4182 dot_radius: Some(3.0),
4183 gap_length: Some(4.0),
4184 ..Default::default()
4185 };
4186
4187 let config = inputs.build();
4188 let pattern = config.pattern.expect("pattern should be Some");
4189 assert!(
4190 matches!(
4191 pattern.pattern_type,
4192 iced_nodegraph::PatternType::Dotted { .. }
4193 ),
4194 "Expected Dotted pattern, got {:?}",
4195 pattern.pattern_type
4196 );
4197 }
4198
4199 #[test]
4200 fn test_edge_config_inputs_pattern_type_dash_dotted() {
4201 let inputs = EdgeConfigInputs {
4202 pattern_type: Some(PatternType::DashDotted),
4203 ..Default::default()
4204 };
4205
4206 let config = inputs.build();
4207 let pattern = config.pattern.expect("pattern should be Some");
4208 assert!(
4209 matches!(
4210 pattern.pattern_type,
4211 iced_nodegraph::PatternType::DashDotted { .. }
4212 ),
4213 "Expected DashDotted pattern, got {:?}",
4214 pattern.pattern_type
4215 );
4216 }
4217
4218 #[test]
4219 fn test_edge_config_inputs_pattern_preserved_through_build() {
4220 let inputs = EdgeConfigInputs {
4223 pattern_type: Some(PatternType::Dashed),
4224 thickness: Some(4.0),
4225 dash_length: Some(8.0),
4226 gap_length: Some(4.0),
4227 animation_speed: Some(50.0),
4228 stroke_color: Some(iced_nodegraph::ColorQuad::arc(
4229 Color::from_rgb(1.0, 0.0, 0.0),
4230 Color::from_rgb(0.0, 0.0, 1.0),
4231 )),
4232 border_width: Some(2.0),
4233 border_gap: Some(1.0),
4234 shadow_blur: Some(6.0),
4235 shadow_expand: Some(3.0),
4236 ..Default::default()
4237 };
4238
4239 let config = inputs.build();
4240
4241 let pattern = config.pattern.expect("pattern must be present");
4243 assert_eq!(pattern.thickness, 4.0);
4244 assert!(matches!(
4245 pattern.pattern_type,
4246 iced_nodegraph::PatternType::Dashed { .. }
4247 ));
4248 assert!((pattern.flow_speed - 50.0).abs() < 0.01);
4249
4250 let stroke = config.stroke_color.expect("stroke color present");
4252 assert_eq!(stroke.near_start, Color::from_rgb(1.0, 0.0, 0.0));
4253 assert_eq!(stroke.near_end, Color::from_rgb(0.0, 0.0, 1.0));
4254
4255 assert_eq!(config.border_width, Some(2.0));
4257 assert_eq!(config.border_gap, Some(1.0));
4258
4259 assert_eq!(config.shadow_blur, Some(6.0));
4261 assert_eq!(config.shadow_expand, Some(3.0));
4262 }
4263}