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 nodes;
5
6use demo_common::{Demo, NodeContentStyle, node_title_bar};
7use iced::{
8 Element, Length, Point, Rectangle, Size, Task, Theme, Vector,
9 widget::{Space, button, column, container, opaque, pick_list, row, slider, stack, text},
10};
11use iced_nodegraph::{
12 GraphStyle, Ids, NodeStatus, NodeStyle, Pattern, PinDirection, PinInfo, PinRef, PinStatus,
13 PinStyle, TilingBackground, anchor, default_graph_style, default_pin_style, edge, node,
14};
15use nodes::styled_node;
16use std::collections::HashSet;
17
18const FRAME_NODE: usize = usize::MAX;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25struct StylingIds;
26
27impl Ids for StylingIds {
28 type NodeId = usize;
29 type PinId = usize;
30 type EdgeId = usize;
31 type AnchorId = usize;
32 type Payload = ::std::any::TypeId;
33}
34
35fn styling_pin_style(
37 theme: &Theme,
38 pin: &PinInfo<'_, StylingIds>,
39 _other: Option<&PinInfo<'_, StylingIds>>,
40 status: PinStatus,
41) -> PinStyle {
42 let color = match pin.direction() {
43 PinDirection::Output => iced::Color::from_rgb(0.9, 0.7, 0.5),
44 _ => iced::Color::from_rgb(0.5, 0.7, 0.9),
45 };
46 PinStyle {
47 color: color.into(),
48 ..default_pin_style(theme, status)
49 }
50}
51
52pub fn main() -> iced::Result {
53 iced::application(Application::boot, Application::update, Application::view)
54 .subscription(Application::subscription)
55 .title("Styling Demo - iced_nodegraph")
56 .theme(Application::theme)
57 .window(iced::window::Settings::default())
58 .run()
59}
60
61#[derive(Debug, Clone)]
62enum Message {
63 EdgeConnected {
65 from: PinRef<StylingIds>,
66 to: PinRef<StylingIds>,
67 },
68 EdgeDisconnected {
69 from: PinRef<StylingIds>,
70 to: PinRef<StylingIds>,
71 },
72 SelectionChanged(Vec<usize>),
73 NodesMoved {
74 delta: Vector,
75 indices: Vec<usize>,
76 },
77 FrameResized(Size),
78
79 AnchorCreated {
81 edge: usize,
82 position: Point,
83 },
84 AnchorMoved {
85 anchor: usize,
86 position: Point,
87 },
88 AnchorDeleted(usize),
89 RouteAttached {
90 edge: usize,
91 anchor: usize,
92 },
93 RouteDetached {
94 edge: usize,
95 anchor: usize,
96 },
97 DragEnded,
101
102 CornerRadiusChanged(f32),
104 OpacityChanged(f32),
105 BorderWidthChanged(f32),
106 SelectNode(usize),
107 ApplyPreset(NodePreset),
108 ChangeTheme(Theme),
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112enum NodePreset {
113 Input,
114 Process,
115 Output,
116 Comment,
117}
118
119impl std::fmt::Display for NodePreset {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 NodePreset::Input => write!(f, "Input"),
123 NodePreset::Process => write!(f, "Process"),
124 NodePreset::Output => write!(f, "Output"),
125 NodePreset::Comment => write!(f, "Comment"),
126 }
127 }
128}
129
130impl NodePreset {
131 const ALL: [NodePreset; 4] = [
132 NodePreset::Input,
133 NodePreset::Process,
134 NodePreset::Output,
135 NodePreset::Comment,
136 ];
137
138 fn style(self, theme: &Theme, status: NodeStatus) -> NodeStyle {
140 match self {
141 NodePreset::Input => NodeStyle::input(theme, status),
142 NodePreset::Process => NodeStyle::process(theme, status),
143 NodePreset::Output => NodeStyle::output(theme, status),
144 NodePreset::Comment => NodeStyle::comment(theme, status),
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
161struct EdgeModel {
162 id: usize,
163 from: PinRef<StylingIds>,
164 to: PinRef<StylingIds>,
165 route: Vec<usize>,
166}
167
168#[derive(Debug, Clone, Copy)]
174struct NodeOverrides {
175 corner_radius: f32,
176 opacity: f32,
177 border_width: f32,
178}
179
180impl NodeOverrides {
181 const START: Self = Self {
183 corner_radius: 5.0,
184 opacity: 0.75,
185 border_width: 1.5,
186 };
187
188 fn apply(self, base: NodeStyle) -> NodeStyle {
191 NodeStyle {
192 corner_radius: self.corner_radius,
193 opacity: self.opacity,
194 border_pattern: Pattern {
195 thickness: self.border_width,
196 ..base.border_pattern
197 },
198 ..base
199 }
200 }
201}
202
203struct StyledNode {
206 position: Point,
207 name: String,
208 preset: NodePreset,
209 overrides: NodeOverrides,
210}
211
212impl StyledNode {
213 fn new(position: Point, name: &str, preset: NodePreset) -> Self {
214 Self {
215 position,
216 name: name.to_string(),
217 preset,
218 overrides: NodeOverrides::START,
219 }
220 }
221}
222
223struct Application {
224 edges: Vec<EdgeModel>,
225 next_edge: usize,
228 anchors: Vec<(usize, Point)>,
230 next_anchor: usize,
233 nodes: Vec<StyledNode>,
234 current_theme: Theme,
235 selected_node: Option<usize>,
236 graph_selection: HashSet<usize>,
237 frame: Rectangle,
240}
241
242impl Default for Application {
243 fn default() -> Self {
244 Self {
245 edges: vec![
246 EdgeModel {
248 id: 0,
249 from: PinRef::new(0, 1),
250 to: PinRef::new(1, 0),
251 route: vec![0],
252 },
253 EdgeModel {
255 id: 1,
256 from: PinRef::new(1, 1),
257 to: PinRef::new(2, 0),
258 route: vec![1],
259 },
260 EdgeModel {
262 id: 2,
263 from: PinRef::new(0, 1),
264 to: PinRef::new(3, 0),
265 route: vec![0, 1],
266 },
267 EdgeModel {
281 id: 3,
282 from: PinRef::new(5, 1),
283 to: PinRef::new(4, 0),
284 route: vec![0, 1],
285 },
286 ],
287 next_edge: 4,
288 anchors: vec![(0, Point::new(300.0, 300.0)), (1, Point::new(550.0, 300.0))],
294 next_anchor: 2,
295 nodes: vec![
296 StyledNode::new(Point::new(100.0, 150.0), "Input Data", NodePreset::Input),
297 StyledNode::new(Point::new(350.0, 200.0), "Transform", NodePreset::Process),
298 StyledNode::new(
299 Point::new(600.0, 150.0),
300 "Output Result",
301 NodePreset::Output,
302 ),
303 StyledNode::new(
304 Point::new(600.0, 420.0),
305 "Note: This is a comment",
306 NodePreset::Comment,
307 ),
308 StyledNode::new(Point::new(600.0, 280.0), "Output Log", NodePreset::Output),
309 StyledNode::new(Point::new(120.0, 60.0), "Aux Input", NodePreset::Input),
310 ],
311 current_theme: Theme::CatppuccinFrappe,
312 selected_node: Some(0),
313 graph_selection: HashSet::new(),
314 frame: Rectangle::new(Point::new(60.0, 20.0), Size::new(270.0, 270.0)),
315 }
316 }
317}
318
319impl Demo for Application {
320 type Message = Message;
321
322 fn boot() -> (Self, Task<Message>) {
323 (Self::default(), Task::none())
324 }
325
326 fn theme(&self) -> Theme {
327 self.current_theme.clone()
328 }
329
330 fn set_theme(&mut self, theme: Theme) {
331 self.current_theme = theme;
332 }
333
334 fn update(&mut self, message: Message) -> Task<Message> {
335 match message {
336 Message::EdgeConnected { from, to } => {
337 let id = self.next_edge;
338 self.next_edge += 1;
339 self.edges.push(EdgeModel {
340 id,
341 from,
342 to,
343 route: Vec::new(),
344 });
345 }
346 Message::EdgeDisconnected { from, to } => {
347 self.edges.retain(|e| !(e.from == from && e.to == to));
348 self.drop_unused_anchors();
349 }
350 Message::AnchorCreated { edge, position } => {
351 let id = self.next_anchor;
352 self.next_anchor += 1;
353 if let Some(model) = self.edges.iter_mut().find(|e| e.id == edge) {
357 model.route.push(id);
358 self.anchors.push((id, position));
359 }
360 }
361 Message::AnchorMoved { anchor, position } => {
362 if let Some((_, p)) = self.anchors.iter_mut().find(|(id, _)| *id == anchor) {
363 *p = position;
364 }
365 }
366 Message::AnchorDeleted(anchor) => {
367 self.anchors.retain(|(id, _)| *id != anchor);
368 for model in &mut self.edges {
369 model.route.retain(|id| *id != anchor);
370 }
371 }
372 Message::RouteAttached { edge, anchor } => {
373 if let Some(model) = self.edges.iter_mut().find(|e| e.id == edge)
374 && !model.route.contains(&anchor)
375 {
376 model.route.push(anchor);
377 }
378 }
379 Message::RouteDetached { edge, anchor } => {
380 if let Some(model) = self.edges.iter_mut().find(|e| e.id == edge) {
381 model.route.retain(|id| *id != anchor);
382 }
383 }
388 Message::DragEnded => self.drop_unused_anchors(),
389 Message::SelectionChanged(indices) => {
390 self.graph_selection = indices.into_iter().collect();
391 }
392 Message::NodesMoved { delta, indices } => {
393 for idx in indices {
394 if idx == FRAME_NODE {
395 self.frame.x += delta.x;
396 self.frame.y += delta.y;
397 } else if let Some(styled) = self.nodes.get_mut(idx) {
398 styled.position.x += delta.x;
399 styled.position.y += delta.y;
400 }
401 }
402 }
403 Message::FrameResized(size) => {
406 self.frame.width = size.width;
407 self.frame.height = size.height;
408 }
409 Message::CornerRadiusChanged(value) => {
410 if let Some(overrides) = self.selected_overrides_mut() {
411 overrides.corner_radius = value;
412 }
413 }
414 Message::OpacityChanged(value) => {
415 if let Some(overrides) = self.selected_overrides_mut() {
416 overrides.opacity = value;
417 }
418 }
419 Message::BorderWidthChanged(value) => {
420 if let Some(overrides) = self.selected_overrides_mut() {
421 overrides.border_width = value;
422 }
423 }
424 Message::SelectNode(index) => {
425 self.selected_node = Some(index);
426 }
427 Message::ApplyPreset(preset) => {
428 if let Some(index) = self.selected_node
429 && let Some(styled) = self.nodes.get_mut(index)
430 {
431 styled.preset = preset;
432 }
433 }
434 Message::ChangeTheme(theme) => {
435 self.current_theme = theme;
436 }
437 }
438 Task::none()
439 }
440
441 fn view(&self) -> Element<'_, Message> {
442 let control_panel = self.build_control_panel();
443 let graph = self.build_graph();
444
445 stack![
451 graph,
452 container(opaque(
453 container(control_panel)
454 .width(Length::Fixed(280.0))
455 .height(Length::Fill)
456 .padding(15)
457 .style(|theme: &Theme| {
458 let palette = theme.extended_palette();
459 container::Style {
460 background: Some(palette.background.weak.color.into()),
461 ..Default::default()
462 }
463 })
464 ))
465 .width(Length::Fill)
466 .height(Length::Fill)
467 .align_x(iced::alignment::Horizontal::Right),
468 ]
469 .into()
470 }
471}
472
473pub fn scene() -> (
475 Box<dyn demo_common::Scene>,
476 iced::Task<demo_common::SceneMessage>,
477) {
478 demo_common::erase::<Application>()
479}
480
481impl Application {
482 fn drop_unused_anchors(&mut self) {
488 let routed: HashSet<usize> = self
489 .edges
490 .iter()
491 .flat_map(|model| model.route.iter().copied())
492 .collect();
493 self.anchors.retain(|(id, _)| routed.contains(id));
494 }
495
496 fn selected_overrides(&self) -> NodeOverrides {
499 self.selected_node
500 .and_then(|index| self.nodes.get(index))
501 .map(|styled| styled.overrides)
502 .unwrap_or(NodeOverrides::START)
503 }
504
505 fn selected_overrides_mut(&mut self) -> Option<&mut NodeOverrides> {
506 let index = self.selected_node?;
507 self.nodes
508 .get_mut(index)
509 .map(|styled| &mut styled.overrides)
510 }
511
512 fn build_control_panel(&self) -> Element<'_, Message> {
513 let theme = &self.current_theme;
514 let palette = theme.extended_palette();
515 let text_color = palette.background.base.text;
516
517 let title = text("Style Controls").size(18).color(text_color);
518
519 let selected = self.selected_node.and_then(|index| self.nodes.get(index));
520 let selected_label = if let Some(styled) = selected {
521 text(format!("Selected: {} ({})", styled.name, styled.preset))
522 .size(14)
523 .color(text_color)
524 } else {
525 text("No node selected").size(14).color(text_color)
526 };
527
528 let node_buttons: Element<'_, Message> = column(
530 self.nodes
531 .iter()
532 .enumerate()
533 .map(|(i, styled)| {
534 let is_selected = self.selected_node == Some(i);
535 button(text(styled.name.clone()).size(12))
536 .on_press(Message::SelectNode(i))
537 .style(move |theme: &Theme, status| {
538 if is_selected {
539 button::primary(theme, status)
540 } else {
541 button::secondary(theme, status)
542 }
543 })
544 .width(Length::Fill)
545 .into()
546 })
547 .collect::<Vec<_>>(),
548 )
549 .spacing(5)
550 .into();
551
552 let overrides = self.selected_overrides();
554 let corner_slider = column![
555 text("Corner Radius").size(12).color(text_color),
556 row![
557 slider(
558 0.0..=20.0,
559 overrides.corner_radius,
560 Message::CornerRadiusChanged
561 ),
562 text(format!("{:.1}", overrides.corner_radius))
563 .size(12)
564 .color(text_color),
565 ]
566 .spacing(10),
567 ]
568 .spacing(4);
569
570 let opacity_slider = column![
571 text("Opacity").size(12).color(text_color),
572 row![
573 slider(0.1..=1.0, overrides.opacity, Message::OpacityChanged).step(0.05_f32),
574 text(format!("{:.2}", overrides.opacity))
575 .size(12)
576 .color(text_color),
577 ]
578 .spacing(10),
579 ]
580 .spacing(4);
581
582 let border_slider = column![
583 text("Border Width").size(12).color(text_color),
584 row![
585 slider(
586 0.5..=5.0,
587 overrides.border_width,
588 Message::BorderWidthChanged
589 )
590 .step(0.5_f32),
591 text(format!("{:.1}", overrides.border_width))
592 .size(12)
593 .color(text_color),
594 ]
595 .spacing(10),
596 ]
597 .spacing(4);
598
599 let preset_label = text("Apply Preset").size(12).color(text_color);
601 let preset_buttons: Element<'_, Message> = row(NodePreset::ALL
602 .iter()
603 .map(|preset| {
604 button(text(preset.to_string()).size(11))
605 .on_press(Message::ApplyPreset(*preset))
606 .padding([4, 8])
607 .into()
608 })
609 .collect::<Vec<_>>())
610 .spacing(5)
611 .wrap()
612 .into();
613
614 let theme_label = text("Theme").size(12).color(text_color);
616 let themes = vec![
617 Theme::Dark,
618 Theme::Light,
619 Theme::CatppuccinFrappe,
620 Theme::CatppuccinMocha,
621 Theme::Dracula,
622 Theme::Nord,
623 Theme::SolarizedDark,
624 Theme::SolarizedLight,
625 Theme::GruvboxDark,
626 Theme::GruvboxLight,
627 ];
628 let theme_picker = pick_list(
629 themes,
630 Some(self.current_theme.clone()),
631 Message::ChangeTheme,
632 )
633 .width(Length::Fill);
634
635 column![
636 title,
637 text("").height(Length::Fixed(10.0)), selected_label,
639 text("").height(Length::Fixed(10.0)),
640 text("Select Node").size(12).color(text_color),
641 node_buttons,
642 text("").height(Length::Fixed(20.0)),
643 corner_slider,
644 text("").height(Length::Fixed(10.0)),
645 opacity_slider,
646 text("").height(Length::Fixed(10.0)),
647 border_slider,
648 text("").height(Length::Fixed(20.0)),
649 preset_label,
650 preset_buttons,
651 text("").height(Length::Fixed(20.0)),
652 theme_label,
653 theme_picker,
654 ]
655 .spacing(5)
656 .into()
657 }
658
659 fn build_graph(&self) -> Element<'_, Message> {
660 let theme = &self.current_theme;
661
662 ::iced_nodegraph::NodeGraph::<StylingIds, _, _, _>::new()
665 .on_connect(|from, to| Message::EdgeConnected { from, to })
666 .on_disconnect(|from, to| Message::EdgeDisconnected { from, to })
667 .on_move(|delta, indices| Message::NodesMoved { delta, indices })
668 .on_select(Message::SelectionChanged)
669 .on_anchor_move(|anchor, position| Message::AnchorMoved { anchor, position })
670 .on_anchor_create(|edge, position| Message::AnchorCreated { edge, position })
671 .on_anchor_delete(Message::AnchorDeleted)
672 .on_route_attach(|edge, anchor| Message::RouteAttached { edge, anchor })
673 .on_route_detach(|edge, anchor| Message::RouteDetached { edge, anchor })
674 .on_drag_end(|| Message::DragEnded)
675 .on_resize(|_, size| Message::FrameResized(size))
676 .graph_style(|theme: &Theme| {
677 let line = theme.extended_palette().background.strong.color;
678 GraphStyle {
679 tiling: Some(TilingBackground::grid(
680 40.0,
681 1.0,
682 iced::Color { a: 0.5, ..line },
683 )),
684 ..default_graph_style(theme)
685 }
686 })
687 .nodes(self.nodes.iter().enumerate().map(|(index, styled)| {
688 let preset = styled.preset;
689 let overrides = styled.overrides;
690 let idle = overrides.apply(preset.style(theme, NodeStatus::Idle));
693 node(
694 index,
695 styled.position,
696 styled_node(&styled.name, &idle, theme),
697 )
698 .selected(self.graph_selection.contains(&index))
699 .style(move |theme, status| overrides.apply(preset.style(theme, status)))
700 .pin_style(styling_pin_style)
701 }))
702 .push_node(
703 node(FRAME_NODE, self.frame.position(), self.frame_content())
704 .frame()
705 .resizable(true),
706 )
707 .anchors(
710 self.anchors
711 .iter()
712 .map(|&(id, position)| anchor(id, position)),
713 )
714 .edges(self.edges.iter().map(|model| {
715 edge(model.id, model.from, model.to).route(model.route.iter().copied())
716 }))
717 .into()
718 }
719
720 fn frame_content(&self) -> Element<'_, Message> {
723 container(column![
724 node_title_bar("Inputs", NodeContentStyle::comment(&self.current_theme)),
725 Space::new().width(Length::Fill).height(Length::Fill),
726 ])
727 .width(self.frame.width)
728 .height(self.frame.height)
729 .into()
730 }
731}