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#[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
34const SNAP_SPACING: f32 = 40.0;
36
37struct Integer;
40struct Float;
41struct StringType;
42struct Boolean;
43struct AnyType;
44
45#[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 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
102fn 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#[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 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 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 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 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 self.register(3, 0, PinType::Any, PinDir::Input, false, "Value");
210 self.register(3, 1, PinType::String, PinDir::Input, false, "Label");
211
212 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 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 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 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 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 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 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 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 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 .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 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 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 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 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 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 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 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
630pub 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
834fn 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}