Skip to main content

oxframe_components/
flow.rs

1//! A graph of named nodes, laid out in layers from the edges.
2
3use std::collections::BTreeMap;
4
5use oxframe_2d::{
6    Accent, BaseProps, Group, Len, Lg, Line, Md, Muted, NodeBuilder, NodeId, Rect, Surface, Txt, Xl,
7};
8use oxframe_core::Tween;
9
10use crate::host::{self, Host};
11
12/// Nodes and arrows. The diagram places itself: left to right, or top to
13/// bottom after [`down`](FlowDiagramBuilder::down). `next` highlights the
14/// next node.
15pub struct FlowDiagram {
16    host: Host,
17}
18
19impl FlowDiagram {
20    /// An empty diagram. Add links with [`edge`](FlowDiagramBuilder::edge).
21    #[allow(clippy::new_ret_no_self)]
22    pub fn new() -> FlowDiagramBuilder {
23        FlowDiagramBuilder {
24            base: BaseProps::default(),
25            edges: Vec::new(),
26            down: false,
27        }
28    }
29
30    /// Fades the diagram in. Highlights are still hidden.
31    pub fn enter(&self) -> Tween<f64> {
32        self.host.enter()
33    }
34    /// Shows the accent bar on the next node.
35    pub fn next(&self) -> Tween<f64> {
36        self.host.next()
37    }
38    /// Fades the diagram out.
39    pub fn exit(&self) -> Tween<f64> {
40        self.host.exit()
41    }
42    /// How many nodes `next` can highlight.
43    pub fn len(&self) -> usize {
44        self.host.len()
45    }
46    /// No nodes were declared.
47    pub fn is_empty(&self) -> bool {
48        self.host.is_empty()
49    }
50}
51
52/// Builder for [`FlowDiagram`].
53#[must_use = "a builder does nothing until it is passed to `add`"]
54pub struct FlowDiagramBuilder {
55    base: BaseProps,
56    edges: Vec<(String, String)>,
57    down: bool,
58}
59
60impl FlowDiagramBuilder {
61    /// A directed link. Nodes are created the first time their name appears.
62    pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
63        self.edges.push((from.into(), to.into()));
64        self
65    }
66    /// Stack layers top to bottom instead of left to right.
67    pub fn down(mut self) -> Self {
68        self.down = true;
69        self
70    }
71}
72
73impl NodeBuilder for FlowDiagramBuilder {
74    type Handle = FlowDiagram;
75    fn base(&mut self) -> &mut BaseProps {
76        &mut self.base
77    }
78    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> FlowDiagram {
79        let root: Group = host::root(parent, self.base);
80        let mut index = BTreeMap::<String, usize>::new();
81        let mut names = Vec::new();
82        let mut links = Vec::new();
83        for (from, to) in self.edges {
84            let a = *index.entry(from.clone()).or_insert_with(|| {
85                names.push(from);
86                names.len() - 1
87            });
88            let b = *index.entry(to.clone()).or_insert_with(|| {
89                names.push(to);
90                names.len() - 1
91            });
92            links.push((a, b));
93        }
94        let layers = layers_of(names.len(), &links);
95        let (card_w, card_h) = (200.0, 84.0);
96        let gap = Md.px();
97        let layer_gap = Xl.px();
98        let max_layer = layers.iter().copied().max().unwrap_or(0);
99        let mut counts = vec![0usize; max_layer + 1];
100        for layer in &layers {
101            counts[*layer] += 1;
102        }
103        let along = (max_layer as f64) * (card_w + layer_gap);
104        let mut slot = vec![0usize; max_layer + 1];
105        let mut pos = vec![(0.0, 0.0); names.len()];
106        for (i, layer) in layers.iter().copied().enumerate() {
107            let n = counts[layer].max(1) as f64;
108            let across = (n - 1.0) * (card_h + gap);
109            let a = -(along / 2.0) + layer as f64 * (card_w + layer_gap);
110            let b = -(across / 2.0) + slot[layer] as f64 * (card_h + gap);
111            slot[layer] += 1;
112            pos[i] = if self.down { (b, a) } else { (a, b) };
113        }
114        let mut reveals = Vec::new();
115        for (i, name) in names.iter().enumerate() {
116            let (x, y) = pos[i];
117            root.add(
118                Rect::new()
119                    .width(card_w)
120                    .height(card_h)
121                    .radius(Lg)
122                    .fill(Surface)
123                    .x(x)
124                    .y(y)
125                    .name(name.clone()),
126            );
127            root.add(
128                Txt::new()
129                    .text(name.clone())
130                    .body()
131                    .x(x)
132                    .y(y)
133                    .name(format!("{name}-label")),
134            );
135            let bar = root.add(
136                Rect::new()
137                    .width(8.0)
138                    .height(card_h - 28.0)
139                    .fill(Accent)
140                    .x(x - card_w / 2.0 + 16.0)
141                    .y(y)
142                    .opacity(0.0)
143                    .name(format!("{name}-bar")),
144            );
145            reveals.push(bar.node());
146        }
147        for (a, b) in links {
148            let (x1, y1) = pos[a];
149            let (x2, y2) = pos[b];
150            let (start, end) = if self.down {
151                ((x1, y1 + card_h / 2.0), (x2, y2 - card_h / 2.0))
152            } else {
153                ((x1 + card_w / 2.0, y1), (x2 - card_w / 2.0, y2))
154            };
155            root.add(
156                Line::new()
157                    .points([start, end])
158                    .end_arrow(true)
159                    .stroke(Muted)
160                    .line_width(4.0)
161                    .name("link"),
162            );
163        }
164        FlowDiagram {
165            host: Host::new(root.node(), reveals),
166        }
167    }
168}
169
170fn layers_of(n: usize, links: &[(usize, usize)]) -> Vec<usize> {
171    let mut layer = vec![0; n];
172    for _ in 0..n.max(1) {
173        let mut changed = false;
174        for &(from, to) in links {
175            let next = layer[from] + 1;
176            if next > layer[to] && next < n {
177                layer[to] = next;
178                changed = true;
179            }
180        }
181        if !changed {
182            break;
183        }
184    }
185    layer
186}