1use demo_common::{NodeContentStyle, simple_node};
38use demo_common::{ScreenshotHelper, ScreenshotMessage};
39use iced::{
40 Color, Element, Length, Point, Subscription, Theme, Vector,
41 alignment::Horizontal,
42 widget::{Space, button, column, container, row, scrollable, text},
43};
44use iced_nodegraph::{
45 KeyCombo, Keymap, PinInfo as NgPinInfo, PinRef, PinStatus, PinStyle, default_pin_style, edge,
46 node, pin,
47};
48use std::collections::{HashMap, HashSet};
49
50#[cfg(feature = "wasm")]
51use wasm_bindgen::prelude::*;
52
53#[cfg(feature = "wasm")]
54#[wasm_bindgen(start)]
55pub fn wasm_init() {
56 console_error_panic_hook::set_once();
57}
58
59struct Integer;
62struct Float;
63struct StringType;
64struct AnyType;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum PinType {
70 Integer,
71 Float,
72 String,
73 #[allow(dead_code)]
74 Boolean,
75 Any,
76}
77
78impl PinType {
79 fn name(self) -> &'static str {
80 match self {
81 PinType::Integer => "Integer",
82 PinType::Float => "Float",
83 PinType::String => "String",
84 PinType::Boolean => "Boolean",
85 PinType::Any => "Any",
86 }
87 }
88
89 fn color(self) -> Color {
90 match self {
91 PinType::Integer => Color::from_rgb(0.2, 0.6, 1.0),
92 PinType::Float => Color::from_rgb(0.2, 0.9, 0.4),
93 PinType::String => Color::from_rgb(1.0, 0.8, 0.2),
94 PinType::Boolean => Color::from_rgb(0.9, 0.3, 0.3),
95 PinType::Any => Color::from_rgb(0.7, 0.7, 0.7),
96 }
97 }
98
99 fn is_compatible(self, other: PinType) -> bool {
100 if self == PinType::Any || other == PinType::Any {
101 return true;
102 }
103 if self == other {
104 return true;
105 }
106 if self == PinType::Integer && other == PinType::Float {
108 return true;
109 }
110 if self == PinType::Float && other == PinType::Integer {
111 return true;
112 }
113 false
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118enum PinDir {
119 Input,
120 Output,
121 Bidirectional,
122}
123
124fn pin_style(
127 theme: &Theme,
128 pin: &NgPinInfo<'_, usize, ::std::any::TypeId>,
129 _other: Option<&NgPinInfo<'_, usize, ::std::any::TypeId>>,
130 status: PinStatus,
131) -> PinStyle {
132 use std::any::TypeId;
133 let ty = *pin.info();
134 let color = if ty == TypeId::of::<Integer>() {
135 PinType::Integer.color()
136 } else if ty == TypeId::of::<Float>() {
137 PinType::Float.color()
138 } else if ty == TypeId::of::<StringType>() {
139 PinType::String.color()
140 } else {
141 PinType::Any.color()
142 };
143 PinStyle {
144 color: color.into(),
145 ..default_pin_style(theme, status)
146 }
147}
148
149#[derive(Debug, Clone)]
150struct PinInfo {
151 pin_type: PinType,
152 direction: PinDir,
153 single_connection: bool,
154 label: &'static str,
155}
156
157#[derive(Debug, Clone)]
160enum Message {
161 EdgeConnected {
162 from: PinRef<usize, usize>,
163 to: PinRef<usize, usize>,
164 },
165 EdgeDisconnected {
166 from: PinRef<usize, usize>,
167 to: PinRef<usize, usize>,
168 },
169 SelectionChanged(Vec<usize>),
170 NodesMoved {
171 delta: Vector,
172 node_ids: Vec<usize>,
173 },
174 ClearAll,
175 Reset,
176 ToggleRules,
177 Screenshot(ScreenshotMessage),
178}
179
180impl From<ScreenshotMessage> for Message {
181 fn from(msg: ScreenshotMessage) -> Self {
182 Message::Screenshot(msg)
183 }
184}
185
186struct App {
187 edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)>,
188 node_positions: HashMap<usize, Point>,
189 pin_registry: HashMap<(usize, usize), PinInfo>,
190 selected_nodes: HashSet<usize>,
191 feedback: Vec<String>,
192 show_rules: bool,
193 theme: Theme,
194 screenshot: ScreenshotHelper,
195}
196
197impl App {
198 fn new() -> (Self, iced::Task<Message>) {
199 let mut app = Self {
200 edges: Vec::new(),
201 node_positions: Self::default_positions(),
202 pin_registry: HashMap::new(),
203 selected_nodes: HashSet::new(),
204 feedback: vec!["Drag between pins to connect nodes.".into()],
205 show_rules: false,
206 theme: Theme::Dark,
207 screenshot: ScreenshotHelper::from_args(),
208 };
209 app.register_pins();
210 (app, iced::Task::none())
211 }
212
213 fn default_positions() -> HashMap<usize, Point> {
214 let mut m = HashMap::new();
215 m.insert(0, Point::new(50.0, 120.0));
216 m.insert(1, Point::new(350.0, 50.0));
217 m.insert(2, Point::new(350.0, 250.0));
218 m.insert(3, Point::new(650.0, 120.0));
219 m.insert(4, Point::new(350.0, 440.0));
220 m
221 }
222
223 fn register_pins(&mut self) {
224 self.pin_registry.clear();
225
226 self.register(0, 0, PinType::Integer, PinDir::Output, false, "Int Out");
228 self.register(0, 1, PinType::Float, PinDir::Output, false, "Float Out");
229
230 self.register(1, 0, PinType::Float, PinDir::Input, true, "A");
232 self.register(1, 1, PinType::Float, PinDir::Input, true, "B");
233 self.register(1, 2, PinType::Float, PinDir::Output, false, "Result");
234
235 self.register(2, 0, PinType::Any, PinDir::Input, false, "In");
237 self.register(2, 1, PinType::Integer, PinDir::Output, false, "Int");
238 self.register(2, 2, PinType::Float, PinDir::Output, false, "Float");
239 self.register(2, 3, PinType::String, PinDir::Output, false, "String");
240
241 self.register(3, 0, PinType::Any, PinDir::Input, false, "Value");
243 self.register(3, 1, PinType::String, PinDir::Input, false, "Label");
244
245 self.register(4, 0, PinType::Float, PinDir::Bidirectional, false, "Float");
247 self.register(4, 1, PinType::Integer, PinDir::Bidirectional, false, "Int");
248 self.register(4, 2, PinType::Any, PinDir::Bidirectional, false, "Any");
249 self.register(4, 3, PinType::String, PinDir::Bidirectional, false, "Str");
250 }
251
252 fn register(
253 &mut self,
254 node: usize,
255 pin: usize,
256 pin_type: PinType,
257 direction: PinDir,
258 single_connection: bool,
259 label: &'static str,
260 ) {
261 self.pin_registry.insert(
262 (node, pin),
263 PinInfo {
264 pin_type,
265 direction,
266 single_connection,
267 label,
268 },
269 );
270 }
271
272 fn validate_connection(
273 &self,
274 from: &PinRef<usize, usize>,
275 to: &PinRef<usize, usize>,
276 ) -> Result<String, String> {
277 let from_info = self
278 .pin_registry
279 .get(&(from.node_id, from.pin_id))
280 .ok_or("Unknown source pin")?;
281 let to_info = self
282 .pin_registry
283 .get(&(to.node_id, to.pin_id))
284 .ok_or("Unknown target pin")?;
285
286 if from.node_id == to.node_id && from.pin_id == to.pin_id {
288 return Err("Cannot connect a pin to itself".into());
289 }
290
291 let dir_ok = !matches!(
293 (from_info.direction, to_info.direction),
294 (PinDir::Input, PinDir::Input) | (PinDir::Output, PinDir::Output)
295 );
296 if !dir_ok {
297 return Err(format!(
298 "Direction mismatch: cannot connect {:?} to {:?}",
299 from_info.direction, to_info.direction
300 ));
301 }
302
303 if !from_info.pin_type.is_compatible(to_info.pin_type) {
305 return Err(format!(
306 "Type mismatch: {} is not compatible with {}",
307 from_info.pin_type.name(),
308 to_info.pin_type.name()
309 ));
310 }
311
312 if to_info.single_connection {
314 let already_connected = self
315 .edges
316 .iter()
317 .any(|(_, t)| t.node_id == to.node_id && t.pin_id == to.pin_id);
318 if already_connected {
319 return Err(format!(
320 "Pin '{}' only accepts a single connection",
321 to_info.label
322 ));
323 }
324 }
325
326 if from_info.single_connection {
328 let already_connected = self
329 .edges
330 .iter()
331 .any(|(f, _)| f.node_id == from.node_id && f.pin_id == from.pin_id);
332 if already_connected {
333 return Err(format!(
334 "Pin '{}' only accepts a single connection",
335 from_info.label
336 ));
337 }
338 }
339
340 let duplicate = self.edges.iter().any(|(f, t)| {
342 (f.node_id == from.node_id
343 && f.pin_id == from.pin_id
344 && t.node_id == to.node_id
345 && t.pin_id == to.pin_id)
346 || (f.node_id == to.node_id
347 && f.pin_id == to.pin_id
348 && t.node_id == from.node_id
349 && t.pin_id == from.pin_id)
350 });
351 if duplicate {
352 return Err("Connection already exists".into());
353 }
354
355 Ok(format!(
356 "Connected: {} -> {} ({} -> {})",
357 from_info.label,
358 to_info.label,
359 from_info.pin_type.name(),
360 to_info.pin_type.name()
361 ))
362 }
363
364 fn update(&mut self, message: Message) -> iced::Task<Message> {
365 match message {
366 Message::Screenshot(msg) => return self.screenshot.update(msg),
367 Message::EdgeConnected { from, to } => match self.validate_connection(&from, &to) {
368 Ok(msg) => {
369 self.edges.push((from, to));
370 self.feedback.push(msg);
371 }
372 Err(msg) => {
373 self.feedback.push(format!("Rejected: {}", msg));
374 }
375 },
376 Message::EdgeDisconnected { from, to } => {
377 self.edges.retain(|(f, t)| {
378 !((f.node_id == from.node_id
379 && f.pin_id == from.pin_id
380 && t.node_id == to.node_id
381 && t.pin_id == to.pin_id)
382 || (f.node_id == to.node_id
383 && f.pin_id == to.pin_id
384 && t.node_id == from.node_id
385 && t.pin_id == from.pin_id))
386 });
387 if let (Some(from_info), Some(to_info)) = (
388 self.pin_registry.get(&(from.node_id, from.pin_id)),
389 self.pin_registry.get(&(to.node_id, to.pin_id)),
390 ) {
391 self.feedback.push(format!(
392 "Disconnected: {} -- {}",
393 from_info.label, to_info.label
394 ));
395 }
396 }
397 Message::SelectionChanged(node_ids) => {
398 self.selected_nodes = node_ids.into_iter().collect();
399 }
400 Message::NodesMoved { delta, node_ids } => {
401 for id in node_ids {
402 if let Some(pos) = self.node_positions.get_mut(&id) {
403 pos.x += delta.x;
404 pos.y += delta.y;
405 }
406 }
407 }
408 Message::ClearAll => {
409 self.edges.clear();
410 self.feedback.push("All connections cleared.".into());
411 }
412 Message::Reset => {
413 self.edges.clear();
414 self.node_positions = Self::default_positions();
415 self.selected_nodes.clear();
416 self.feedback = vec!["Reset to initial state.".into()];
417 }
418 Message::ToggleRules => {
419 self.show_rules = !self.show_rules;
420 }
421 }
422
423 if self.feedback.len() > 50 {
425 self.feedback.drain(0..self.feedback.len() - 50);
426 }
427 iced::Task::none()
428 }
429
430 fn subscription(&self) -> Subscription<Message> {
431 self.screenshot.subscription().map(Message::Screenshot)
432 }
433
434 fn theme(&self) -> Theme {
435 self.theme.clone()
436 }
437
438 fn view(&self) -> Element<'_, Message> {
439 let theme = self.theme();
440
441 let registry = self.pin_registry.clone();
442 let mut ng: ::iced_nodegraph::NodeGraph<usize, usize, ::std::any::TypeId, _, _, _> =
443 ::iced_nodegraph::NodeGraph::default()
444 .can_connect(move |from, to| {
445 if from.node_id() == to.node_id() && from.pin_id() == to.pin_id() {
448 return false;
449 }
450 let from_info = registry.get(&(*from.node_id(), *from.pin_id()));
451 let to_info = registry.get(&(*to.node_id(), *to.pin_id()));
452 match (from_info, to_info) {
453 (Some(f), Some(t)) => {
454 let dir_ok = !matches!(
455 (f.direction, t.direction),
456 (PinDir::Input, PinDir::Input) | (PinDir::Output, PinDir::Output)
457 );
458 dir_ok && f.pin_type.is_compatible(t.pin_type)
459 }
460 _ => false,
461 }
462 })
463 .on_connect(|from, to| Message::EdgeConnected { from, to })
464 .on_disconnect(|from, to| Message::EdgeDisconnected { from, to })
465 .on_move(|delta, node_ids| Message::NodesMoved { delta, node_ids })
466 .on_select(Message::SelectionChanged)
467 .keymap(Keymap {
470 select_all: Some(KeyCombo::command('l')),
471 ..Keymap::default()
472 })
473 .selection(&self.selected_nodes);
474
475 let pos = self
477 .node_positions
478 .get(&0)
479 .copied()
480 .unwrap_or(Point::ORIGIN);
481 ng.push_node(node(0usize, pos, self.number_generator_node(&theme)).pin_style(pin_style));
482
483 let pos = self
485 .node_positions
486 .get(&1)
487 .copied()
488 .unwrap_or(Point::ORIGIN);
489 ng.push_node(node(1usize, pos, self.math_operations_node(&theme)).pin_style(pin_style));
490
491 let pos = self
493 .node_positions
494 .get(&2)
495 .copied()
496 .unwrap_or(Point::ORIGIN);
497 ng.push_node(node(2usize, pos, self.type_converter_node(&theme)).pin_style(pin_style));
498
499 let pos = self
501 .node_positions
502 .get(&3)
503 .copied()
504 .unwrap_or(Point::ORIGIN);
505 ng.push_node(node(3usize, pos, self.display_node(&theme)).pin_style(pin_style));
506
507 let pos = self
509 .node_positions
510 .get(&4)
511 .copied()
512 .unwrap_or(Point::ORIGIN);
513 ng.push_node(node(4usize, pos, self.bidirectional_hub_node(&theme)).pin_style(pin_style));
514
515 for (from, to) in &self.edges {
517 ng.push_edge(edge!(*from, *to));
518 }
519
520 let toolbar = container(
522 row![
523 button("Clear All").on_press(Message::ClearAll),
524 button("Reset").on_press(Message::Reset),
525 button(if self.show_rules {
526 "Hide Rules"
527 } else {
528 "Show Rules"
529 })
530 .on_press(Message::ToggleRules),
531 Space::new().width(Length::Fill),
532 text(format!("{} connections", self.edges.len())).size(13),
533 ]
534 .spacing(8)
535 .align_y(iced::Alignment::Center)
536 .padding(4),
537 )
538 .padding(4)
539 .width(Length::Fill);
540
541 let feedback_content: Element<Message> = if self.show_rules {
543 self.rules_panel()
544 } else {
545 let items: Vec<Element<Message>> = self
546 .feedback
547 .iter()
548 .rev()
549 .take(10)
550 .map(|msg| text(msg).size(12).into())
551 .collect();
552 scrollable(column(items).spacing(2).padding(4))
553 .height(Length::Fixed(100.0))
554 .into()
555 };
556
557 let status_bar = container(feedback_content)
558 .width(Length::Fill)
559 .height(Length::Fixed(if self.show_rules { 180.0 } else { 100.0 }));
560
561 column![toolbar, Element::from(ng), status_bar]
562 .height(Length::Fill)
563 .into()
564 }
565
566 fn rules_panel(&self) -> Element<'_, Message> {
567 let rules = column![
568 text("Connection Rules").size(14),
569 text("- Output pins connect to Input pins").size(12),
570 text("- Bidirectional pins connect to any direction").size(12),
571 text("- Same types connect (Integer, Float, String, Boolean)").size(12),
572 text("- Any type is compatible with all types").size(12),
573 text("- Integer implicitly converts to Float").size(12),
574 text("- Single-connection pins reject additional connections").size(12),
575 text("- Duplicate connections are rejected").size(12),
576 text("- Keymap rebind: Select All is Ctrl/Cmd+L in this demo").size(12),
577 ]
578 .spacing(4)
579 .padding(8);
580 scrollable(rules).height(Length::Fixed(180.0)).into()
581 }
582
583 fn number_generator_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
584 let style = NodeContentStyle::custom(theme, Color::from_rgb(0.2, 0.5, 0.9));
585 container(simple_node(
586 "Number Generator",
587 style,
588 column![
589 right_pin(pin!(
590 Right,
591 0usize,
592 text("Int Out").size(12),
593 Output,
594 ::std::any::TypeId::of::<Integer>()
595 )),
596 right_pin(pin!(
597 Right,
598 1usize,
599 text("Float Out").size(12),
600 Output,
601 ::std::any::TypeId::of::<Float>()
602 )),
603 ]
604 .spacing(4),
605 ))
606 .width(160.0)
607 .into()
608 }
609
610 fn math_operations_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
611 let style = NodeContentStyle::custom(theme, Color::from_rgb(0.1, 0.7, 0.3));
612 container(simple_node(
613 "Math Operations",
614 style,
615 column![
616 pin!(
617 Left,
618 0usize,
619 text("A (Float)").size(12),
620 Input,
621 ::std::any::TypeId::of::<Float>()
622 ),
623 pin!(
624 Left,
625 1usize,
626 text("B (Float)").size(12),
627 Input,
628 ::std::any::TypeId::of::<Float>()
629 ),
630 right_pin(pin!(
631 Right,
632 2usize,
633 text("Result").size(12),
634 Output,
635 ::std::any::TypeId::of::<Float>()
636 )),
637 ]
638 .spacing(4),
639 ))
640 .width(160.0)
641 .into()
642 }
643
644 fn type_converter_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
645 let style = NodeContentStyle::custom(theme, Color::from_rgb(0.6, 0.4, 0.8));
646 container(simple_node(
647 "Type Converter",
648 style,
649 column![
650 pin!(
651 Left,
652 0usize,
653 text("In (Any)").size(12),
654 Input,
655 ::std::any::TypeId::of::<AnyType>()
656 ),
657 right_pin(pin!(
658 Right,
659 1usize,
660 text("Int").size(12),
661 Output,
662 ::std::any::TypeId::of::<Integer>()
663 )),
664 right_pin(pin!(
665 Right,
666 2usize,
667 text("Float").size(12),
668 Output,
669 ::std::any::TypeId::of::<Float>()
670 )),
671 right_pin(pin!(
672 Right,
673 3usize,
674 text("String").size(12),
675 Output,
676 ::std::any::TypeId::of::<StringType>()
677 )),
678 ]
679 .spacing(4),
680 ))
681 .width(160.0)
682 .into()
683 }
684
685 fn display_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
686 let style = NodeContentStyle::custom(theme, Color::from_rgb(0.9, 0.6, 0.1));
687 container(simple_node(
688 "Display",
689 style,
690 column![
691 pin!(
692 Left,
693 0usize,
694 text("Value (Any)").size(12),
695 Input,
696 ::std::any::TypeId::of::<AnyType>()
697 ),
698 pin!(
699 Left,
700 1usize,
701 text("Label (String)").size(12),
702 Input,
703 ::std::any::TypeId::of::<StringType>()
704 ),
705 ]
706 .spacing(4),
707 ))
708 .width(160.0)
709 .into()
710 }
711
712 fn bidirectional_hub_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
713 let style = NodeContentStyle::custom(theme, Color::from_rgb(0.5, 0.5, 0.5));
714 container(simple_node(
715 "Bidirectional Hub",
716 style,
717 column![
718 pin!(
719 Top,
720 0usize,
721 text("Float").size(12),
722 Both,
723 ::std::any::TypeId::of::<Float>()
724 ),
725 right_pin(pin!(
726 Right,
727 1usize,
728 text("Int").size(12),
729 Both,
730 ::std::any::TypeId::of::<Integer>()
731 )),
732 pin!(
733 Bottom,
734 2usize,
735 text("Any").size(12),
736 Both,
737 ::std::any::TypeId::of::<AnyType>()
738 ),
739 pin!(
740 Left,
741 3usize,
742 text("Str").size(12),
743 Both,
744 ::std::any::TypeId::of::<StringType>()
745 ),
746 ]
747 .spacing(4),
748 ))
749 .width(160.0)
750 .into()
751 }
752}
753
754fn right_pin<'a, Message: Clone + 'a>(
757 pin: impl Into<Element<'a, Message>>,
758) -> Element<'a, Message> {
759 container(pin)
760 .width(Length::Fill)
761 .align_x(Horizontal::Right)
762 .into()
763}
764
765pub fn main() -> iced::Result {
766 #[cfg(feature = "wasm")]
767 let window_settings = iced::window::Settings {
768 platform_specific: iced::window::settings::PlatformSpecific {
769 target: Some(String::from("demo-canvas-container")),
770 },
771 ..Default::default()
772 };
773
774 #[cfg(not(feature = "wasm"))]
775 let window_settings = iced::window::Settings {
776 size: iced::Size::new(1100.0, 750.0),
777 ..Default::default()
778 };
779
780 iced::application(App::new, App::update, App::view)
781 .subscription(App::subscription)
782 .theme(App::theme)
783 .title("Interaction Demo - Connection Validation")
784 .window(window_settings)
785 .run()
786}
787
788#[cfg(feature = "wasm")]
789#[wasm_bindgen]
790pub fn run_demo() {
791 let _ = main();
792}