Skip to main content

demo_interaction/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc = r#"<link rel="stylesheet" href="../gallery/pkg/demo.css"><script type="module" src="../gallery/pkg/demo-loader.js"></script>"#]
3
4mod content;
5
6use content::simple_node;
7use demo_common::{Demo, NodeContentStyle};
8use iced::{
9    Color, Element, Length, Point, Theme, Vector,
10    alignment::Horizontal,
11    time::Instant,
12    widget::{Space, button, checkbox, column, container, row, scrollable, text},
13};
14use iced_nodegraph::{
15    Ids, KeyCombo, Keymap, ParticleStyle, PinInfo as NgPinInfo, PinRef, PinStatus, PinStyle,
16    default_particle_style, default_pin_style, edge, node, particle, pin,
17};
18use std::collections::{HashMap, HashSet};
19use std::time::Duration;
20
21/// The id vocabulary of this demo: indexed nodes and pins, unidentified edges,
22/// and a `TypeId` pin payload carrying the data type of each pin.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24struct TypedIds;
25
26impl Ids for TypedIds {
27    type NodeId = usize;
28    type PinId = usize;
29    type EdgeId = ();
30    type AnchorId = usize;
31    type Payload = std::any::TypeId;
32}
33
34/// World-unit grid the "Snap to grid" toggle drags land on.
35const SNAP_SPACING: f32 = 40.0;
36
37// -- Marker types for pin data_type matching --
38
39struct Integer;
40struct Float;
41struct StringType;
42struct Boolean;
43struct AnyType;
44
45// -- Pin metadata for validation --
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum PinType {
49    Integer,
50    Float,
51    String,
52    Boolean,
53    Any,
54}
55
56impl PinType {
57    fn name(self) -> &'static str {
58        match self {
59            PinType::Integer => "Integer",
60            PinType::Float => "Float",
61            PinType::String => "String",
62            PinType::Boolean => "Boolean",
63            PinType::Any => "Any",
64        }
65    }
66
67    fn color(self) -> Color {
68        match self {
69            PinType::Integer => Color::from_rgb(0.2, 0.6, 1.0),
70            PinType::Float => Color::from_rgb(0.2, 0.9, 0.4),
71            PinType::String => Color::from_rgb(1.0, 0.8, 0.2),
72            PinType::Boolean => Color::from_rgb(0.9, 0.3, 0.3),
73            PinType::Any => Color::from_rgb(0.7, 0.7, 0.7),
74        }
75    }
76
77    fn is_compatible(self, other: PinType) -> bool {
78        if self == PinType::Any || other == PinType::Any {
79            return true;
80        }
81        if self == other {
82            return true;
83        }
84        // Integer -> Float implicit conversion
85        if self == PinType::Integer && other == PinType::Float {
86            return true;
87        }
88        if self == PinType::Float && other == PinType::Integer {
89            return true;
90        }
91        false
92    }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96enum PinDir {
97    Input,
98    Output,
99    Bidirectional,
100}
101
102/// Colors a node's pins by their data-type marker (pins carry no style; the
103/// owning node styles them).
104fn pin_style(
105    theme: &Theme,
106    pin: &NgPinInfo<'_, TypedIds>,
107    _other: Option<&NgPinInfo<'_, TypedIds>>,
108    status: PinStatus,
109) -> PinStyle {
110    use std::any::TypeId;
111    let ty = *pin.info();
112    let color = if ty == TypeId::of::<Integer>() {
113        PinType::Integer.color()
114    } else if ty == TypeId::of::<Float>() {
115        PinType::Float.color()
116    } else if ty == TypeId::of::<StringType>() {
117        PinType::String.color()
118    } else if ty == TypeId::of::<Boolean>() {
119        PinType::Boolean.color()
120    } else {
121        PinType::Any.color()
122    };
123    PinStyle {
124        color: color.into(),
125        ..default_pin_style(theme, status)
126    }
127}
128
129#[derive(Debug, Clone)]
130struct PinInfo {
131    pin_type: PinType,
132    direction: PinDir,
133    single_connection: bool,
134    label: &'static str,
135}
136
137// -- Application --
138
139#[derive(Debug, Clone)]
140enum Message {
141    EdgeConnected {
142        from: PinRef<TypedIds>,
143        to: PinRef<TypedIds>,
144    },
145    EdgeDisconnected {
146        from: PinRef<TypedIds>,
147        to: PinRef<TypedIds>,
148    },
149    SelectionChanged(Vec<usize>),
150    NodesMoved {
151        delta: Vector,
152        node_ids: Vec<usize>,
153    },
154    ClearAll,
155    Reset,
156    ToggleRules,
157    ToggleSnap(bool),
158    ToggleTraffic(bool),
159    Frame(Instant),
160}
161
162struct App {
163    edges: Vec<(PinRef<TypedIds>, PinRef<TypedIds>)>,
164    node_positions: HashMap<usize, Point>,
165    pin_registry: HashMap<(usize, usize), PinInfo>,
166    selected_nodes: HashSet<usize>,
167    feedback: Vec<String>,
168    show_rules: bool,
169    snap_to_grid: bool,
170    traffic: bool,
171    last_emit: Option<Instant>,
172    /// In-flight particles as (index into `edges`, birth). The host owns
173    /// their lifetime; the widget only draws them.
174    particles: Vec<(usize, Instant)>,
175    theme: Theme,
176}
177
178impl App {
179    fn default_positions() -> HashMap<usize, Point> {
180        let mut m = HashMap::new();
181        m.insert(0, Point::new(50.0, 120.0));
182        m.insert(1, Point::new(350.0, 50.0));
183        m.insert(2, Point::new(350.0, 250.0));
184        m.insert(3, Point::new(650.0, 120.0));
185        m.insert(4, Point::new(350.0, 440.0));
186        m
187    }
188
189    fn register_pins(&mut self) {
190        self.pin_registry.clear();
191
192        // Node 0: Number Generator
193        self.register(0, 0, PinType::Integer, PinDir::Output, false, "Int Out");
194        self.register(0, 1, PinType::Float, PinDir::Output, false, "Float Out");
195
196        // Node 1: Math Operations
197        self.register(1, 0, PinType::Float, PinDir::Input, true, "A");
198        self.register(1, 1, PinType::Float, PinDir::Input, true, "B");
199        self.register(1, 2, PinType::Float, PinDir::Output, false, "Result");
200
201        // Node 2: Type Converter
202        self.register(2, 0, PinType::Any, PinDir::Input, false, "In");
203        self.register(2, 1, PinType::Integer, PinDir::Output, false, "Int");
204        self.register(2, 2, PinType::Float, PinDir::Output, false, "Float");
205        self.register(2, 3, PinType::String, PinDir::Output, false, "String");
206        self.register(2, 4, PinType::Boolean, PinDir::Output, false, "Bool");
207
208        // Node 3: Display
209        self.register(3, 0, PinType::Any, PinDir::Input, false, "Value");
210        self.register(3, 1, PinType::String, PinDir::Input, false, "Label");
211
212        // Node 4: Bidirectional Hub
213        self.register(4, 0, PinType::Float, PinDir::Bidirectional, false, "Float");
214        self.register(4, 1, PinType::Integer, PinDir::Bidirectional, false, "Int");
215        self.register(4, 2, PinType::Any, PinDir::Bidirectional, false, "Any");
216        self.register(4, 3, PinType::String, PinDir::Bidirectional, false, "Str");
217    }
218
219    fn register(
220        &mut self,
221        node: usize,
222        pin: usize,
223        pin_type: PinType,
224        direction: PinDir,
225        single_connection: bool,
226        label: &'static str,
227    ) {
228        self.pin_registry.insert(
229            (node, pin),
230            PinInfo {
231                pin_type,
232                direction,
233                single_connection,
234                label,
235            },
236        );
237    }
238
239    fn validate_connection(
240        &self,
241        from: &PinRef<TypedIds>,
242        to: &PinRef<TypedIds>,
243    ) -> Result<String, String> {
244        let from_info = self
245            .pin_registry
246            .get(&(from.node_id, from.pin_id))
247            .ok_or("Unknown source pin")?;
248        let to_info = self
249            .pin_registry
250            .get(&(to.node_id, to.pin_id))
251            .ok_or("Unknown target pin")?;
252
253        // Reject a pin wired to itself (degenerate self-loop: start == end).
254        if from.node_id == to.node_id && from.pin_id == to.pin_id {
255            return Err("Cannot connect a pin to itself".into());
256        }
257
258        // Check direction compatibility (Input<->Input is the only invalid combo)
259        let dir_ok = !matches!(
260            (from_info.direction, to_info.direction),
261            (PinDir::Input, PinDir::Input) | (PinDir::Output, PinDir::Output)
262        );
263        if !dir_ok {
264            return Err(format!(
265                "Direction mismatch: cannot connect {:?} to {:?}",
266                from_info.direction, to_info.direction
267            ));
268        }
269
270        // Check type compatibility
271        if !from_info.pin_type.is_compatible(to_info.pin_type) {
272            return Err(format!(
273                "Type mismatch: {} is not compatible with {}",
274                from_info.pin_type.name(),
275                to_info.pin_type.name()
276            ));
277        }
278
279        // Check single-connection constraint on target
280        if to_info.single_connection {
281            let already_connected = self
282                .edges
283                .iter()
284                .any(|(_, t)| t.node_id == to.node_id && t.pin_id == to.pin_id);
285            if already_connected {
286                return Err(format!(
287                    "Pin '{}' only accepts a single connection",
288                    to_info.label
289                ));
290            }
291        }
292
293        // Check single-connection constraint on source
294        if from_info.single_connection {
295            let already_connected = self
296                .edges
297                .iter()
298                .any(|(f, _)| f.node_id == from.node_id && f.pin_id == from.pin_id);
299            if already_connected {
300                return Err(format!(
301                    "Pin '{}' only accepts a single connection",
302                    from_info.label
303                ));
304            }
305        }
306
307        // Check duplicate
308        let duplicate = self.edges.iter().any(|(f, t)| {
309            (f.node_id == from.node_id
310                && f.pin_id == from.pin_id
311                && t.node_id == to.node_id
312                && t.pin_id == to.pin_id)
313                || (f.node_id == to.node_id
314                    && f.pin_id == to.pin_id
315                    && t.node_id == from.node_id
316                    && t.pin_id == from.pin_id)
317        });
318        if duplicate {
319            return Err("Connection already exists".into());
320        }
321
322        Ok(format!(
323            "Connected: {} -> {} ({} -> {})",
324            from_info.label,
325            to_info.label,
326            from_info.pin_type.name(),
327            to_info.pin_type.name()
328        ))
329    }
330}
331
332impl demo_common::Demo for App {
333    type Message = Message;
334
335    fn boot() -> (Self, iced::Task<Message>) {
336        let mut app = Self {
337            edges: Vec::new(),
338            node_positions: Self::default_positions(),
339            pin_registry: HashMap::new(),
340            selected_nodes: HashSet::new(),
341            feedback: vec!["Drag between pins to connect nodes.".into()],
342            show_rules: false,
343            snap_to_grid: false,
344            traffic: false,
345            last_emit: None,
346            particles: Vec::new(),
347            theme: Theme::Dark,
348        };
349        app.register_pins();
350        (app, iced::Task::none())
351    }
352
353    fn subscription(&self) -> iced::Subscription<Message> {
354        if self.traffic {
355            iced::window::frames().map(Message::Frame)
356        } else {
357            iced::Subscription::none()
358        }
359    }
360
361    fn update(&mut self, message: Message) -> iced::Task<Message> {
362        match message {
363            Message::EdgeConnected { from, to } => match self.validate_connection(&from, &to) {
364                Ok(msg) => {
365                    self.edges.push((from, to));
366                    self.feedback.push(msg);
367                }
368                Err(msg) => {
369                    self.feedback.push(format!("Rejected: {}", msg));
370                }
371            },
372            Message::EdgeDisconnected { from, to } => {
373                self.edges.retain(|(f, t)| {
374                    !((f.node_id == from.node_id
375                        && f.pin_id == from.pin_id
376                        && t.node_id == to.node_id
377                        && t.pin_id == to.pin_id)
378                        || (f.node_id == to.node_id
379                            && f.pin_id == to.pin_id
380                            && t.node_id == from.node_id
381                            && t.pin_id == from.pin_id))
382                });
383                self.particles.clear();
384                if let (Some(from_info), Some(to_info)) = (
385                    self.pin_registry.get(&(from.node_id, from.pin_id)),
386                    self.pin_registry.get(&(to.node_id, to.pin_id)),
387                ) {
388                    self.feedback.push(format!(
389                        "Disconnected: {} -- {}",
390                        from_info.label, to_info.label
391                    ));
392                }
393            }
394            Message::SelectionChanged(node_ids) => {
395                self.selected_nodes = node_ids.into_iter().collect();
396            }
397            Message::NodesMoved { delta, node_ids } => {
398                for id in node_ids {
399                    if let Some(pos) = self.node_positions.get_mut(&id) {
400                        pos.x += delta.x;
401                        pos.y += delta.y;
402                    }
403                }
404            }
405            Message::ClearAll => {
406                self.edges.clear();
407                self.particles.clear();
408                self.feedback.push("All connections cleared.".into());
409            }
410            Message::Reset => {
411                self.edges.clear();
412                self.particles.clear();
413                self.node_positions = Self::default_positions();
414                self.selected_nodes.clear();
415                self.feedback = vec!["Reset to initial state.".into()];
416            }
417            Message::ToggleRules => {
418                self.show_rules = !self.show_rules;
419            }
420            Message::ToggleSnap(on) => {
421                self.snap_to_grid = on;
422            }
423            Message::ToggleTraffic(on) => {
424                self.traffic = on;
425                if !on {
426                    self.particles.clear();
427                    self.last_emit = None;
428                }
429            }
430            Message::Frame(now) => {
431                if self
432                    .last_emit
433                    .is_none_or(|t| now.duration_since(t) >= Duration::from_millis(600))
434                {
435                    self.particles
436                        .extend((0..self.edges.len()).map(|i| (i, now)));
437                    self.last_emit = Some(now);
438                }
439                self.particles
440                    .retain(|(_, born)| now.duration_since(*born) < Duration::from_secs(6));
441            }
442        }
443
444        // Keep feedback log bounded
445        if self.feedback.len() > 50 {
446            self.feedback.drain(0..self.feedback.len() - 50);
447        }
448        iced::Task::none()
449    }
450
451    fn theme(&self) -> Theme {
452        self.theme.clone()
453    }
454
455    fn set_theme(&mut self, theme: Theme) {
456        self.theme = theme;
457    }
458
459    fn view(&self) -> Element<'_, Message> {
460        let theme = self.theme();
461
462        let registry = self.pin_registry.clone();
463        let mut ng = ::iced_nodegraph::NodeGraph::<TypedIds, _, _, _>::new()
464            .can_connect(move |from, to| {
465                // Live snap feedback: reject self-loops, direction conflicts and
466                // type mismatches before the edge is committed on release.
467                if from.node_id() == to.node_id() && from.pin_id() == to.pin_id() {
468                    return false;
469                }
470                let from_info = registry.get(&(*from.node_id(), *from.pin_id()));
471                let to_info = registry.get(&(*to.node_id(), *to.pin_id()));
472                match (from_info, to_info) {
473                    (Some(f), Some(t)) => {
474                        let dir_ok = !matches!(
475                            (f.direction, t.direction),
476                            (PinDir::Input, PinDir::Input) | (PinDir::Output, PinDir::Output)
477                        );
478                        dir_ok && f.pin_type.is_compatible(t.pin_type)
479                    }
480                    _ => false,
481                }
482            })
483            .on_connect(|from, to| Message::EdgeConnected { from, to })
484            .on_disconnect(|from, to| Message::EdgeDisconnected { from, to })
485            .on_move(|delta, node_ids| Message::NodesMoved { delta, node_ids })
486            .on_select(Message::SelectionChanged)
487            // Rebound keymap: Select All lives on Cmd/Ctrl+L here to
488            // demonstrate host rebinding (see the rules panel).
489            .keymap(Keymap {
490                select_all: Some(KeyCombo::command('l')),
491                ..Keymap::default()
492            });
493        if self.snap_to_grid {
494            ng = ng.snap_grid(SNAP_SPACING);
495        }
496
497        // Node 0: Number Generator
498        let pos = self
499            .node_positions
500            .get(&0)
501            .copied()
502            .unwrap_or(Point::ORIGIN);
503        ng = ng.push_node(
504            node(0, pos, self.number_generator_node(&theme))
505                .selected(self.selected_nodes.contains(&0))
506                .pin_style(pin_style),
507        );
508
509        // Node 1: Math Operations
510        let pos = self
511            .node_positions
512            .get(&1)
513            .copied()
514            .unwrap_or(Point::ORIGIN);
515        ng = ng.push_node(
516            node(1, pos, self.math_operations_node(&theme))
517                .selected(self.selected_nodes.contains(&1))
518                .pin_style(pin_style),
519        );
520
521        // Node 2: Type Converter
522        let pos = self
523            .node_positions
524            .get(&2)
525            .copied()
526            .unwrap_or(Point::ORIGIN);
527        ng = ng.push_node(
528            node(2, pos, self.type_converter_node(&theme))
529                .selected(self.selected_nodes.contains(&2))
530                .pin_style(pin_style),
531        );
532
533        // Node 3: Display
534        let pos = self
535            .node_positions
536            .get(&3)
537            .copied()
538            .unwrap_or(Point::ORIGIN);
539        ng = ng.push_node(
540            node(3, pos, self.display_node(&theme))
541                .selected(self.selected_nodes.contains(&3))
542                .pin_style(pin_style),
543        );
544
545        // Node 4: Bidirectional Hub
546        let pos = self
547            .node_positions
548            .get(&4)
549            .copied()
550            .unwrap_or(Point::ORIGIN);
551        ng = ng.push_node(
552            node(4, pos, self.bidirectional_hub_node(&theme))
553                .selected(self.selected_nodes.contains(&4))
554                .pin_style(pin_style),
555        );
556
557        let particle_speed = 160.0;
558        ng = ng.edges(self.edges.iter().enumerate().map(|(i, (from, to))| {
559            let color = self
560                .pin_registry
561                .get(&(from.node_id, from.pin_id))
562                .map(|info| info.pin_type.color())
563                .unwrap_or(PinType::Any.color());
564            edge((), *from, *to).particles(
565                self.particles
566                    .iter()
567                    .filter(|(edge_index, _)| *edge_index == i)
568                    .map(move |(_, born)| {
569                        particle(*born, particle_speed).style(move |theme| ParticleStyle {
570                            color,
571                            ..default_particle_style(theme)
572                        })
573                    }),
574            )
575        }));
576
577        // Toolbar
578        let toolbar = container(
579            row![
580                button("Clear All").on_press(Message::ClearAll),
581                button("Reset").on_press(Message::Reset),
582                button(if self.show_rules {
583                    "Hide Rules"
584                } else {
585                    "Show Rules"
586                })
587                .on_press(Message::ToggleRules),
588                checkbox(self.snap_to_grid)
589                    .label("Snap to grid")
590                    .on_toggle(Message::ToggleSnap),
591                checkbox(self.traffic)
592                    .label("Traffic")
593                    .on_toggle(Message::ToggleTraffic),
594                Space::new().width(Length::Fill),
595                text(format!("{} connections", self.edges.len())).size(13),
596            ]
597            .spacing(8)
598            .align_y(iced::Alignment::Center)
599            .padding(4),
600        )
601        .padding(4)
602        .width(Length::Fill);
603
604        // Status bar with scrollable feedback log
605        let feedback_content: Element<Message> = if self.show_rules {
606            self.rules_panel()
607        } else {
608            let items: Vec<Element<Message>> = self
609                .feedback
610                .iter()
611                .rev()
612                .take(10)
613                .map(|msg| text(msg).size(12).into())
614                .collect();
615            scrollable(column(items).spacing(2).padding(4))
616                .height(Length::Fixed(100.0))
617                .into()
618        };
619
620        let status_bar = container(feedback_content)
621            .width(Length::Fill)
622            .height(Length::Fixed(if self.show_rules { 180.0 } else { 100.0 }));
623
624        column![toolbar, Element::from(ng), status_bar]
625            .height(Length::Fill)
626            .into()
627    }
628}
629
630/// Boots this demo for the gallery.
631pub fn scene() -> (
632    Box<dyn demo_common::Scene>,
633    iced::Task<demo_common::SceneMessage>,
634) {
635    demo_common::erase::<App>()
636}
637
638impl App {
639    fn rules_panel(&self) -> Element<'_, Message> {
640        let rules = column![
641            text("Connection Rules").size(14),
642            text("- Output pins connect to Input pins").size(12),
643            text("- Bidirectional pins connect to any direction").size(12),
644            text("- Same types connect (Integer, Float, String, Boolean)").size(12),
645            text("- Any type is compatible with all types").size(12),
646            text("- Integer implicitly converts to Float").size(12),
647            text("- Single-connection pins reject additional connections").size(12),
648            text("- Duplicate connections are rejected").size(12),
649            text("- Keymap rebind: Select All is Ctrl/Cmd+L in this demo").size(12),
650        ]
651        .spacing(4)
652        .padding(8);
653        scrollable(rules).height(Length::Fixed(180.0)).into()
654    }
655
656    fn number_generator_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
657        let style = NodeContentStyle::custom(theme, Color::from_rgb(0.2, 0.5, 0.9));
658        container(simple_node(
659            "Number Generator",
660            style,
661            column![
662                right_pin(pin!(
663                    Right,
664                    0usize,
665                    text("Int Out").size(12),
666                    Output,
667                    ::std::any::TypeId::of::<Integer>()
668                )),
669                right_pin(pin!(
670                    Right,
671                    1usize,
672                    text("Float Out").size(12),
673                    Output,
674                    ::std::any::TypeId::of::<Float>()
675                )),
676            ]
677            .spacing(4),
678        ))
679        .width(160.0)
680        .into()
681    }
682
683    fn math_operations_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
684        let style = NodeContentStyle::custom(theme, Color::from_rgb(0.1, 0.7, 0.3));
685        container(simple_node(
686            "Math Operations",
687            style,
688            column![
689                pin!(
690                    Left,
691                    0usize,
692                    text("A (Float)").size(12),
693                    Input,
694                    ::std::any::TypeId::of::<Float>()
695                ),
696                pin!(
697                    Left,
698                    1usize,
699                    text("B (Float)").size(12),
700                    Input,
701                    ::std::any::TypeId::of::<Float>()
702                ),
703                right_pin(pin!(
704                    Right,
705                    2usize,
706                    text("Result").size(12),
707                    Output,
708                    ::std::any::TypeId::of::<Float>()
709                )),
710            ]
711            .spacing(4),
712        ))
713        .width(160.0)
714        .into()
715    }
716
717    fn type_converter_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
718        let style = NodeContentStyle::custom(theme, Color::from_rgb(0.6, 0.4, 0.8));
719        container(simple_node(
720            "Type Converter",
721            style,
722            column![
723                pin!(
724                    Left,
725                    0usize,
726                    text("In (Any)").size(12),
727                    Input,
728                    ::std::any::TypeId::of::<AnyType>()
729                ),
730                right_pin(pin!(
731                    Right,
732                    1usize,
733                    text("Int").size(12),
734                    Output,
735                    ::std::any::TypeId::of::<Integer>()
736                )),
737                right_pin(pin!(
738                    Right,
739                    2usize,
740                    text("Float").size(12),
741                    Output,
742                    ::std::any::TypeId::of::<Float>()
743                )),
744                right_pin(pin!(
745                    Right,
746                    3usize,
747                    text("String").size(12),
748                    Output,
749                    ::std::any::TypeId::of::<StringType>()
750                )),
751                right_pin(pin!(
752                    Right,
753                    4usize,
754                    text("Bool").size(12),
755                    Output,
756                    ::std::any::TypeId::of::<Boolean>()
757                )),
758            ]
759            .spacing(4),
760        ))
761        .width(160.0)
762        .into()
763    }
764
765    fn display_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
766        let style = NodeContentStyle::custom(theme, Color::from_rgb(0.9, 0.6, 0.1));
767        container(simple_node(
768            "Display",
769            style,
770            column![
771                pin!(
772                    Left,
773                    0usize,
774                    text("Value (Any)").size(12),
775                    Input,
776                    ::std::any::TypeId::of::<AnyType>()
777                ),
778                pin!(
779                    Left,
780                    1usize,
781                    text("Label (String)").size(12),
782                    Input,
783                    ::std::any::TypeId::of::<StringType>()
784                ),
785            ]
786            .spacing(4),
787        ))
788        .width(160.0)
789        .into()
790    }
791
792    fn bidirectional_hub_node<'a>(&self, theme: &Theme) -> Element<'a, Message> {
793        let style = NodeContentStyle::custom(theme, Color::from_rgb(0.5, 0.5, 0.5));
794        container(simple_node(
795            "Bidirectional Hub",
796            style,
797            column![
798                pin!(
799                    Top,
800                    0usize,
801                    text("Float").size(12),
802                    Both,
803                    ::std::any::TypeId::of::<Float>()
804                ),
805                right_pin(pin!(
806                    Right,
807                    1usize,
808                    text("Int").size(12),
809                    Both,
810                    ::std::any::TypeId::of::<Integer>()
811                )),
812                pin!(
813                    Bottom,
814                    2usize,
815                    text("Any").size(12),
816                    Both,
817                    ::std::any::TypeId::of::<AnyType>()
818                ),
819                pin!(
820                    Left,
821                    3usize,
822                    text("Str").size(12),
823                    Both,
824                    ::std::any::TypeId::of::<StringType>()
825                ),
826            ]
827            .spacing(4),
828        ))
829        .width(160.0)
830        .into()
831    }
832}
833
834/// Wraps a Right-side pin in a right-aligned container so the pin dot
835/// appears at the right edge of the node.
836fn right_pin<'a, Message: Clone + 'a>(
837    pin: impl Into<Element<'a, Message>>,
838) -> Element<'a, Message> {
839    container(pin)
840        .width(Length::Fill)
841        .align_x(Horizontal::Right)
842        .into()
843}
844
845pub fn main() -> iced::Result {
846    let window_settings = iced::window::Settings {
847        size: iced::Size::new(1100.0, 750.0),
848        ..Default::default()
849    };
850
851    iced::application(App::boot, App::update, App::view)
852        .theme(App::theme)
853        .subscription(App::subscription)
854        .title("Interaction Demo - Connection Validation")
855        .window(window_settings)
856        .run()
857}