Skip to main content

demo_interaction/
lib.rs

1//! # Interaction Demo
2//!
3//! Demonstrates connection validation with typed pins, directional flow,
4//! and user feedback messages.
5//!
6//! ## Interactive Demo
7//!
8//! <link rel="stylesheet" href="pkg/demo.css">
9//! <div id="demo-container">
10//!   <div id="demo-loading">
11//!     <div class="demo-spinner"></div>
12//!     <p>Loading demo...</p>
13//!   </div>
14//!   <div id="demo-canvas-container"></div>
15//!   <div id="demo-error">
16//!     <strong>Failed to load demo.</strong> WebGPU required.
17//!   </div>
18//! </div>
19//! <script type="module" src="pkg/demo-loader.js"></script>
20//!
21//! ## Controls
22//!
23//! - **Drag from pins** - Create connections between nodes
24//! - **Drag nodes** - Move nodes around the canvas
25//! - **Scroll** - Zoom in/out
26//! - **Right-drag** - Pan the canvas
27//! - **Ctrl/Cmd+L** - Select all (rebound from Ctrl/Cmd+A via `keymap`)
28//!
29//! ## Connection Rules
30//!
31//! - Output pins connect to Input pins (directional)
32//! - Bidirectional pins connect to any direction
33//! - Type-compatible pins only (Integer, Float, String, Any)
34//! - Integer implicitly converts to Float
35//! - Single-connection pins reject additional connections
36
37use 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
59// -- Marker types for pin data_type matching --
60
61struct Integer;
62struct Float;
63struct StringType;
64struct AnyType;
65
66// -- Pin metadata for validation --
67
68#[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        // Integer -> Float implicit conversion
107        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
124/// Colors a node's pins by their data-type marker (pins carry no style; the
125/// owning node styles them).
126fn 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// -- Application --
158
159#[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        // Node 0: Number Generator
227        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        // Node 1: Math Operations
231        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        // Node 2: Type Converter
236        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        // Node 3: Display
242        self.register(3, 0, PinType::Any, PinDir::Input, false, "Value");
243        self.register(3, 1, PinType::String, PinDir::Input, false, "Label");
244
245        // Node 4: Bidirectional Hub
246        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        // Reject a pin wired to itself (degenerate self-loop: start == end).
287        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        // Check direction compatibility (Input<->Input is the only invalid combo)
292        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        // Check type compatibility
304        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        // Check single-connection constraint on target
313        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        // Check single-connection constraint on source
327        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        // Check duplicate
341        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        // Keep feedback log bounded
424        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                    // Live snap feedback: reject self-loops, direction conflicts and
446                    // type mismatches before the edge is committed on release.
447                    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                // Rebound keymap: Select All lives on Cmd/Ctrl+L here to
468                // demonstrate host rebinding (see the rules panel).
469                .keymap(Keymap {
470                    select_all: Some(KeyCombo::command('l')),
471                    ..Keymap::default()
472                })
473                .selection(&self.selected_nodes);
474
475        // Node 0: Number Generator
476        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        // Node 1: Math Operations
484        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        // Node 2: Type Converter
492        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        // Node 3: Display
500        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        // Node 4: Bidirectional Hub
508        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        // Add edges
516        for (from, to) in &self.edges {
517            ng.push_edge(edge!(*from, *to));
518        }
519
520        // Toolbar
521        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        // Status bar with scrollable feedback log
542        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
754/// Wraps a Right-side pin in a right-aligned container so the pin dot
755/// appears at the right edge of the node.
756fn 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}