oxframe_components/
flow.rs1use std::collections::BTreeMap;
4
5use oxframe_2d::{
6 Accent, BaseProps, Group, Icon, IconBuilder, Len, Lg, Line, Md, Muted, NodeBuilder, NodeId,
7 Rect, Surface, Txt, Xl,
8};
9use oxframe_core::Tween;
10
11use crate::host::{self, Host};
12
13pub struct FlowDiagram {
17 host: Host,
18 icons: BTreeMap<String, Icon>,
19}
20
21impl FlowDiagram {
22 #[allow(clippy::new_ret_no_self)]
24 pub fn new() -> FlowDiagramBuilder {
25 FlowDiagramBuilder {
26 base: BaseProps::default(),
27 edges: Vec::new(),
28 icons: BTreeMap::new(),
29 down: false,
30 }
31 }
32
33 pub fn enter(&self) -> Tween<f64> {
35 self.host.enter()
36 }
37 pub fn next(&self) -> Tween<f64> {
39 self.host.next()
40 }
41 pub fn exit(&self) -> Tween<f64> {
43 self.host.exit()
44 }
45 pub fn len(&self) -> usize {
47 self.host.len()
48 }
49 pub fn is_empty(&self) -> bool {
51 self.host.is_empty()
52 }
53 pub fn icon(&self, name: &str) -> Option<Icon> {
55 self.icons.get(name).cloned()
56 }
57}
58
59#[must_use = "a builder does nothing until it is passed to `add`"]
61pub struct FlowDiagramBuilder {
62 base: BaseProps,
63 edges: Vec<(String, String)>,
64 icons: BTreeMap<String, IconBuilder>,
65 down: bool,
66}
67
68impl FlowDiagramBuilder {
69 pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
71 self.edges.push((from.into(), to.into()));
72 self
73 }
74 pub fn icon(mut self, name: impl Into<String>, icon: IconBuilder) -> Self {
77 self.icons.insert(name.into(), icon);
78 self
79 }
80 pub fn down(mut self) -> Self {
82 self.down = true;
83 self
84 }
85}
86
87impl NodeBuilder for FlowDiagramBuilder {
88 type Handle = FlowDiagram;
89 fn base(&mut self) -> &mut BaseProps {
90 &mut self.base
91 }
92 fn build(mut self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> FlowDiagram {
93 let root: Group = host::root(parent, self.base);
94 let mut index = BTreeMap::<String, usize>::new();
95 let mut names = Vec::new();
96 let mut links = Vec::new();
97 for (from, to) in self.edges {
98 let a = *index.entry(from.clone()).or_insert_with(|| {
99 names.push(from);
100 names.len() - 1
101 });
102 let b = *index.entry(to.clone()).or_insert_with(|| {
103 names.push(to);
104 names.len() - 1
105 });
106 links.push((a, b));
107 }
108 let layers = layers_of(names.len(), &links);
109 let card_w = if self.icons.is_empty() { 200.0 } else { 260.0 };
111 let card_h = 84.0;
112 let gap = Md.px();
113 let layer_gap = Xl.px();
114 let max_layer = layers.iter().copied().max().unwrap_or(0);
115 let mut counts = vec![0usize; max_layer + 1];
116 for layer in &layers {
117 counts[*layer] += 1;
118 }
119 let along = (max_layer as f64) * (card_w + layer_gap);
120 let mut slot = vec![0usize; max_layer + 1];
121 let mut pos = vec![(0.0, 0.0); names.len()];
122 for (i, layer) in layers.iter().copied().enumerate() {
123 let n = counts[layer].max(1) as f64;
124 let across = (n - 1.0) * (card_h + gap);
125 let a = -(along / 2.0) + layer as f64 * (card_w + layer_gap);
126 let b = -(across / 2.0) + slot[layer] as f64 * (card_h + gap);
127 slot[layer] += 1;
128 pos[i] = if self.down { (b, a) } else { (a, b) };
129 }
130 let mut reveals = Vec::new();
131 let mut icons = BTreeMap::new();
132 for (i, name) in names.iter().enumerate() {
133 let (x, y) = pos[i];
134 root.add(
135 Rect::new()
136 .width(card_w)
137 .height(card_h)
138 .radius(Lg)
139 .fill(Surface)
140 .x(x)
141 .y(y)
142 .name(name.clone()),
143 );
144 let label = root.add(
145 Txt::new()
146 .text(name.clone())
147 .body()
148 .x(x)
149 .y(y)
150 .name(format!("{name}-label")),
151 );
152 if let Some(icon) = self.icons.remove(name) {
154 let size = 40.0;
155 let at = x - card_w / 2.0 + 36.0 + size / 2.0;
156 icons.insert(
157 name.clone(),
158 root.add(icon.size(size).x(at).y(y).name(format!("{name}-icon"))),
159 );
160 label.x().set(at + size / 2.0 + 16.0 + label.size().0 / 2.0);
161 }
162 let bar = root.add(
163 Rect::new()
164 .width(8.0)
165 .height(card_h - 28.0)
166 .fill(Accent)
167 .x(x - card_w / 2.0 + 16.0)
168 .y(y)
169 .opacity(0.0)
170 .name(format!("{name}-bar")),
171 );
172 reveals.push(bar.node());
173 }
174 for (a, b) in links {
175 let (x1, y1) = pos[a];
176 let (x2, y2) = pos[b];
177 let (start, end) = if self.down {
178 ((x1, y1 + card_h / 2.0), (x2, y2 - card_h / 2.0))
179 } else {
180 ((x1 + card_w / 2.0, y1), (x2 - card_w / 2.0, y2))
181 };
182 root.add(
183 Line::new()
184 .points([start, end])
185 .end_arrow(true)
186 .stroke(Muted)
187 .line_width(4.0)
188 .name("link"),
189 );
190 }
191 FlowDiagram {
192 host: Host::new(root.node(), reveals),
193 icons,
194 }
195 }
196}
197
198fn layers_of(n: usize, links: &[(usize, usize)]) -> Vec<usize> {
199 let mut layer = vec![0; n];
200 for _ in 0..n.max(1) {
201 let mut changed = false;
202 for &(from, to) in links {
203 let next = layer[from] + 1;
204 if next > layer[to] && next < n {
205 layer[to] = next;
206 changed = true;
207 }
208 }
209 if !changed {
210 break;
211 }
212 }
213 layer
214}