Skip to main content

demo_styling/
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 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
18/// Node id of the frame. Every other node is named by its index in `nodes`, so
19/// a sentinel outside that space keeps the two apart in `on_move`.
20const FRAME_NODE: usize = usize::MAX;
21
22/// Id vocabulary of this demo: usize node, pin, edge and anchor ids, and a
23/// `TypeId` pin payload naming the data kind a pin carries.
24#[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
35/// Pin style for the styling demo: blue inputs, orange outputs.
36fn 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    // Graph events
64    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    // Routing anchors
80    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    /// Every gesture that can orphan an anchor ends here, which is when one no
98    /// cable names any more is safe to drop: during a drag it is still a target
99    /// the same drag may put the cable back onto.
100    DragEnded,
101
102    // Style controls
103    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    /// The library preset this variant names, resolved for a theme and status.
139    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/// One edge the host owns: a minted id, its two pins, and the anchors it wraps.
150///
151/// The route is a set of anchor ids in no particular order - the widget derives
152/// which way round the cable meets them - so `update` only ever adds to it or
153/// removes from it.
154///
155/// The id is minted rather than taken from the edge's position in `edges`,
156/// because a position is not a name: one `view` produces a BATCH of messages
157/// and the runtime applies them in order, so an earlier `EdgeDisconnected`
158/// shifts every later index and a `RouteAttached` behind it would land on the
159/// wrong cable.
160#[derive(Debug, Clone)]
161struct EdgeModel {
162    id: usize,
163    from: PinRef<StylingIds>,
164    to: PinRef<StylingIds>,
165    route: Vec<usize>,
166}
167
168/// The style fields the sliders drive, held per node.
169///
170/// A preset is a function of theme and status, so a node keeps the preset it
171/// was given plus these overrides and both resolve in the node's style
172/// closure.
173#[derive(Debug, Clone, Copy)]
174struct NodeOverrides {
175    corner_radius: f32,
176    opacity: f32,
177    border_width: f32,
178}
179
180impl NodeOverrides {
181    /// The values every node starts on.
182    const START: Self = Self {
183        corner_radius: 5.0,
184        opacity: 0.75,
185        border_width: 1.5,
186    };
187
188    /// `base` with the slider-driven fields replaced. Only the border's
189    /// thickness is a slider, so a preset's dash pattern survives.
190    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
203/// One node the host owns: where it sits, what it is called, and the preset
204/// plus overrides its style resolves from.
205struct 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    /// The next edge id to mint, on the same never-reused footing as
226    /// `next_anchor`.
227    next_edge: usize,
228    /// Anchor id paired with its world position.
229    anchors: Vec<(usize, Point)>,
230    /// The next anchor id to mint. Only ever grows, so a deleted anchor's id is
231    /// never handed to a different anchor.
232    next_anchor: usize,
233    nodes: Vec<StyledNode>,
234    current_theme: Theme,
235    selected_node: Option<usize>,
236    graph_selection: HashSet<usize>,
237    /// The frame behind the two input nodes, in world units. Host state like
238    /// every node's position: the widget only reports the drag and the grip.
239    frame: Rectangle,
240}
241
242impl Default for Application {
243    fn default() -> Self {
244        Self {
245            edges: vec![
246                // Input -> Transform, dipping under the run to wrap anchor A.
247                EdgeModel {
248                    id: 0,
249                    from: PinRef::new(0, 1),
250                    to: PinRef::new(1, 0),
251                    route: vec![0],
252                },
253                // Transform -> Output Result, wrapping anchor B.
254                EdgeModel {
255                    id: 1,
256                    from: PinRef::new(1, 1),
257                    to: PinRef::new(2, 0),
258                    route: vec![1],
259                },
260                // Input -> the comment node, through both anchors.
261                EdgeModel {
262                    id: 2,
263                    from: PinRef::new(0, 1),
264                    to: PinRef::new(3, 0),
265                    route: vec![0, 1],
266                },
267                // Aux Input -> Output Log, through both anchors as well, so the
268                // stretch from A to B carries two cables. This pair is the point
269                // of the scene: at A edge 3's angular interval is contained in
270                // edge 2's and at B it is the other way round, so each anchor
271                // taken alone would seat them in opposite orders and the two
272                // would cross between A and B. They do not, and clearing that
273                // moves edge 0 at A. Its interval is the smallest of A's three,
274                // so containment seats it innermost, and the exchange that
275                // clears the corridor puts it on the widest of the three rings
276                // instead. Edge 0 wraps one anchor, so it is never one of the
277                // cables a candidate builds and counts: its ring is fallout from
278                // the pair's fix, not a cost weighed against it. Edge 1 is the
279                // third cable at B.
280                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            // Each anchor sits ON the run of the cables that wrap it: every
289            // cable here meets its anchors in increasing x, between its two
290            // pins. An anchor placed off the far end of a run still works - the
291            // visiting order is a projection onto the run, so it resolves - but
292            // the cable has to double back to reach it.
293            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                // An anchor nothing wraps is not something this host keeps, so
354                // it is only pushed once an edge has taken it. The id stays
355                // spent either way and can never name a second anchor.
356                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                // No collection here: a detach fires DURING the drag, and the
384                // anchor just left is exactly the one the drag may put the
385                // cable straight back onto. Dropping it now would delete a live
386                // drop target mid-gesture.
387            }
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            // The frame is the only node carrying a grip, so the report can
404            // only be about its size.
405            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        // Use stack to overlay control panel on right side of graph.
446        // `opaque` claims mouse interaction for the menu's 280px strip so the
447        // NodeGraph below doesn't receive wheel/click events that fall on it.
448        // The outer Fill x Fill container is just for alignment and must stay
449        // transparent.
450        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
473/// Boots this demo for the gallery.
474pub 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    /// Drops every anchor no route names any more.
483    ///
484    /// The library keeps an anchor as long as the host pushes it, cables or no
485    /// cables - so "the last cable left, the anchor goes" is a policy, and this
486    /// is where this host states it.
487    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    /// The slider values of the selected node, or the starting values when no
497    /// node is selected.
498    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        // Node selection buttons
529        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        // Style sliders, driving the selected node's overrides
553        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        // Preset buttons
600        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        // Theme selector
615        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)), // Spacer
638            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        // `StylingIds::EdgeId` is `usize`: each edge carries a minted id, so
663        // the anchor callbacks name one that survives a removal elsewhere.
664        ::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                // The hosted content mirrors the idle style, so the title bar
691                // takes the body's hue and the node's geometry.
692                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 before edges only for readability; the widget resolves
708            // ids through its own lookups, so the order between the two is free.
709            .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    /// The frame's own content: a title bar over an empty body, laid out at the
721    /// size the host holds so the grip reports against what is drawn.
722    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}