1use std::cell::RefCell;
5use std::cmp::Ordering;
6use std::fmt::Display;
7use std::future::Future;
8use std::ops::Deref;
9use std::rc::Rc;
10
11use oxframe_2d::{
12 BaseProps, Group, Line, Muted, Node, NodeBuilder, NodeId, Rect, Surface, Text, Txt,
13};
14use oxframe_core::{Tween, all};
15
16#[derive(Clone)]
19pub struct TableRow {
20 group: Group,
21 cells: Vec<Txt>,
22 background: Rect,
23}
24
25impl TableRow {
26 pub fn cells(&self) -> Vec<Txt> {
28 self.cells.clone()
29 }
30
31 pub fn cell(&self, column: usize) -> Txt {
37 self.cells[column]
38 }
39
40 pub fn background(&self) -> Rect {
43 self.background
44 }
45
46 pub fn values(&self) -> Vec<String> {
48 self.cells.iter().map(|c| c.text().get()).collect()
49 }
50
51 pub fn node(&self) -> Node {
53 *self.group
54 }
55}
56
57impl Deref for TableRow {
58 type Target = Node;
59 fn deref(&self) -> &Node {
60 &self.group
61 }
62}
63
64struct Layout {
65 xs: Vec<f64>,
66 width: f64,
67 row_height: f64,
68 top: f64,
70 font_size: Option<f64>,
71}
72
73impl Layout {
74 fn text(&self, text: String) -> oxframe_2d::TxtBuilder {
75 let b = Txt::new().text(text).body();
76 match self.font_size {
77 Some(px) => b.font_size(px),
78 None => b,
79 }
80 }
81}
82
83impl Layout {
84 fn y(&self, i: usize) -> f64 {
85 self.top + i as f64 * self.row_height
86 }
87}
88
89#[derive(Clone)]
100pub struct Table {
101 root: Group,
102 header: Vec<Txt>,
103 rows: Rc<RefCell<Vec<TableRow>>>,
104 layout: Rc<Layout>,
105}
106
107impl Table {
108 #[allow(clippy::new_ret_no_self)]
111 pub fn new<V: Display>(header: impl IntoIterator<Item = V>) -> TableBuilder {
112 TableBuilder {
113 base: BaseProps::default(),
114 header: header.into_iter().map(|v| v.to_string()).collect(),
115 rows: Vec::new(),
116 widths: Vec::new(),
117 row_height: 64.0,
118 font_size: None,
119 }
120 }
121
122 pub fn header(&self) -> Vec<Txt> {
124 self.header.clone()
125 }
126
127 pub fn row(&self, i: usize) -> TableRow {
133 self.rows.borrow()[i].clone()
134 }
135
136 pub fn cell(&self, row: usize, column: usize) -> Txt {
138 self.row(row).cell(column)
139 }
140
141 pub fn len(&self) -> usize {
143 self.rows.borrow().len()
144 }
145
146 pub fn is_empty(&self) -> bool {
148 self.rows.borrow().is_empty()
149 }
150
151 pub fn insert_row<V: Display>(
154 &self,
155 at: usize,
156 cells: impl IntoIterator<Item = V>,
157 ) -> impl Future<Output = ()> + 'static {
158 let at = at.min(self.len());
159 let values: Vec<String> = cells.into_iter().map(|v| v.to_string()).collect();
160 let row = make_row(&self.root, &self.layout, &values, self.layout.y(at), 0.0);
161 let mut moves: Vec<Tween<f64>> = Vec::new();
162 {
163 let mut rows = self.rows.borrow_mut();
164 rows.insert(at, row.clone());
165 for (i, r) in rows.iter().enumerate().skip(at + 1) {
166 moves.push(r.y().to(self.layout.y(i), 0.4));
167 }
168 }
169 async move {
170 all(moves).await;
171 row.opacity().to(1.0, 0.3).await;
172 }
173 }
174
175 pub fn push_row<V: Display>(
177 &self,
178 cells: impl IntoIterator<Item = V>,
179 ) -> impl Future<Output = ()> + 'static {
180 self.insert_row(usize::MAX, cells)
181 }
182
183 pub fn remove_row(&self, i: usize) -> impl Future<Output = ()> + 'static {
189 let row = self.rows.borrow_mut().remove(i);
190 let fade = row.opacity().to(0.0, 0.3);
191 let moves: Vec<Tween<f64>> = self
192 .rows
193 .borrow()
194 .iter()
195 .enumerate()
196 .skip(i)
197 .map(|(k, r)| r.y().to(self.layout.y(k), 0.4))
198 .collect();
199 async move {
200 fade.await;
201 all(moves).await;
202 row.remove();
203 }
204 }
205
206 pub fn sort_by(
210 &self,
211 compare: impl Fn(&[String], &[String]) -> Ordering,
212 duration: f64,
213 ) -> impl Future<Output = ()> + 'static {
214 let mut rows = self.rows.borrow_mut();
215 let mut keyed: Vec<(Vec<String>, TableRow)> =
216 rows.drain(..).map(|r| (r.values(), r)).collect();
217 keyed.sort_by(|a, b| compare(&a.0, &b.0));
218 rows.extend(keyed.into_iter().map(|(_, r)| r));
219 let moves: Vec<Tween<f64>> = rows
220 .iter()
221 .enumerate()
222 .map(|(i, r)| r.y().to(self.layout.y(i), duration))
223 .collect();
224 async move {
225 all(moves).await;
226 }
227 }
228
229 pub fn sort_by_column(
232 &self,
233 column: usize,
234 duration: f64,
235 ) -> impl Future<Output = ()> + 'static {
236 self.sort_by(
237 move |a, b| compare_cells(a.get(column), b.get(column)),
238 duration,
239 )
240 }
241
242 pub fn node(&self) -> Node {
244 *self.root
245 }
246}
247
248impl Deref for Table {
249 type Target = Node;
250 fn deref(&self) -> &Node {
251 &self.root
252 }
253}
254
255fn leading_number(s: &str) -> Option<f64> {
257 let s = s.trim();
258 let end = s
259 .char_indices()
260 .take_while(|&(i, c)| c.is_ascii_digit() || c == '.' || ((c == '-' || c == '+') && i == 0))
261 .map(|(i, c)| i + c.len_utf8())
262 .last()?;
263 s[..end].parse().ok()
264}
265
266fn compare_cells(a: Option<&String>, b: Option<&String>) -> Ordering {
267 let (a, b) = (a.map_or("", String::as_str), b.map_or("", String::as_str));
268 match (leading_number(a), leading_number(b)) {
269 (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
270 _ => a.cmp(b),
271 }
272}
273
274fn make_row(root: &Group, layout: &Layout, values: &[String], y: f64, opacity: f64) -> TableRow {
275 let group = root.add(Group::new().y(y).opacity(opacity).name("row"));
276 let background = group.add(
277 Rect::new()
278 .width(layout.width)
279 .height(layout.row_height - 4.0)
280 .radius(8.0)
281 .fill(Surface)
282 .opacity(0.0)
283 .name("row_background"),
284 );
285 let cells = layout
286 .xs
287 .iter()
288 .enumerate()
289 .map(|(c, &x)| {
290 let text = values.get(c).cloned().unwrap_or_default();
291 group.add(layout.text(text).x(x).name(format!("cell{c}")))
292 })
293 .collect();
294 let h = layout.row_height / 2.0;
295 group.add(
296 Line::new()
297 .points([(-layout.width / 2.0, h), (layout.width / 2.0, h)])
298 .stroke(Muted)
299 .line_width(1.0)
300 .name("rule"),
301 );
302 TableRow {
303 group,
304 cells,
305 background,
306 }
307}
308
309#[must_use = "a builder does nothing until it is passed to `add`"]
311pub struct TableBuilder {
312 base: BaseProps,
313 header: Vec<String>,
314 rows: Vec<Vec<String>>,
315 widths: Vec<f64>,
316 row_height: f64,
317 font_size: Option<f64>,
318}
319
320impl TableBuilder {
321 pub fn row<V: Display>(mut self, cells: impl IntoIterator<Item = V>) -> Self {
323 self.rows
324 .push(cells.into_iter().map(|v| v.to_string()).collect());
325 self
326 }
327
328 pub fn widths(mut self, px: impl IntoIterator<Item = f64>) -> Self {
332 self.widths = px.into_iter().collect();
333 self
334 }
335
336 pub fn row_height(mut self, px: f64) -> Self {
338 self.row_height = px;
339 self
340 }
341
342 pub fn font_size(mut self, px: f64) -> Self {
345 self.font_size = Some(px);
346 self
347 }
348}
349
350impl NodeBuilder for TableBuilder {
351 type Handle = Table;
352 fn base(&mut self) -> &mut BaseProps {
353 &mut self.base
354 }
355 fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> Table {
356 let base = self.base;
357 let mut group = Group::new().x(base.x).y(base.y).opacity(base.opacity);
358 if let Some(name) = base.name {
359 group = group.name(name);
360 }
361 let root = Node::from_id(parent).add(group);
362 let columns = self
363 .rows
364 .iter()
365 .map(Vec::len)
366 .chain([self.header.len()])
367 .max()
368 .unwrap_or(0);
369 let font_size = self.font_size;
370 let text = move |t: String| {
371 let b = Txt::new().text(t).body();
372 match font_size {
373 Some(px) => b.font_size(px),
374 None => b,
375 }
376 };
377 let header: Vec<Txt> = (0..columns)
379 .map(|c| {
380 let t = self.header.get(c).cloned().unwrap_or_default();
381 root.add(text(t).fill(Muted).name(format!("head{c}")))
382 })
383 .collect();
384 let mut widths: Vec<f64> = header.iter().map(|t| t.size().0).collect();
385 for row in &self.rows {
386 for (c, v) in row.iter().enumerate() {
387 let probe = root.add(text(v.clone()));
388 widths[c] = widths[c].max(probe.size().0);
389 probe.remove();
390 }
391 }
392 for (c, w) in self.widths.iter().enumerate().take(columns) {
393 widths[c] = *w;
394 }
395 let pad = 48.0;
396 let width = widths.iter().map(|w| w + pad).sum::<f64>();
397 let mut xs = Vec::with_capacity(columns);
398 let mut left = -width / 2.0;
399 for w in &widths {
400 xs.push(left + (w + pad) / 2.0);
401 left += w + pad;
402 }
403 let rh = self.row_height;
404 let total = rh * (self.rows.len() + 1) as f64;
405 let head_y = -total / 2.0 + rh / 2.0;
406 for (t, x) in header.iter().zip(&xs) {
407 t.x().set(*x);
408 t.y().set(head_y);
409 }
410 root.add(
411 Line::new()
412 .points([
413 (-width / 2.0, head_y + rh / 2.0),
414 (width / 2.0, head_y + rh / 2.0),
415 ])
416 .stroke(Text)
417 .line_width(2.0)
418 .name("header_rule"),
419 );
420 let layout = Rc::new(Layout {
421 xs,
422 width,
423 row_height: rh,
424 top: head_y + rh,
425 font_size,
426 });
427 let rows = self
428 .rows
429 .iter()
430 .enumerate()
431 .map(|(i, values)| make_row(&root, &layout, values, layout.y(i), 1.0))
432 .collect();
433 Table {
434 root,
435 header,
436 rows: Rc::new(RefCell::new(rows)),
437 layout,
438 }
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use oxframe_2d::{SceneGraph, View};
446 use oxframe_core::Executor;
447
448 fn table(graph: &Rc<SceneGraph>) -> Table {
449 graph.enter(|| {
450 View::current().add(
451 Table::new(["Renderer", "Frame"])
452 .row(["vello", "4 ms"])
453 .row(["skia", "12 ms"])
454 .row(["cairo", "9 ms"]),
455 )
456 })
457 }
458
459 #[test]
460 fn numbers_compare_as_numbers() {
461 assert_eq!(leading_number("12 ms"), Some(12.0));
462 assert_eq!(leading_number("-3.5"), Some(-3.5));
463 assert_eq!(leading_number("ms"), None);
464 let (a, b) = ("9 ms".to_string(), "12 ms".to_string());
465 assert_eq!(compare_cells(Some(&a), Some(&b)), Ordering::Less);
466 let (a, b) = ("b".to_string(), "a".to_string());
467 assert_eq!(compare_cells(Some(&a), Some(&b)), Ordering::Greater);
468 }
469
470 #[test]
471 fn rows_sit_under_the_header_in_order() {
472 let graph = SceneGraph::new(1920, 1080);
473 let t = table(&graph);
474 graph.enter(|| {
475 assert_eq!(t.len(), 3);
476 assert!(t.header()[0].y().get() < t.row(0).y().get());
477 assert!(t.row(0).y().get() < t.row(1).y().get());
478 assert_eq!(t.cell(2, 0).text().get(), "cairo");
479 assert_eq!(t.row(1).cell(0).x().get(), t.header()[0].x().get());
480 });
481 }
482
483 #[test]
484 fn sorting_insertion_and_removal_move_rows_into_place() {
485 let graph = SceneGraph::new(1920, 1080);
486 let t = table(&graph);
487 let ys: Vec<f64> = graph.enter(|| (0..3).map(|i| t.row(i).y().get()).collect());
488 let t2 = t.clone();
489 graph.enter(|| {
490 let mut ex = Executor::new(
491 60.0,
492 oxframe_protocol::Meta::default(),
493 0,
494 std::pin::Pin::from(Box::new(async move {
495 t2.sort_by_column(1, 0.5).await;
496 t2.insert_row(0, ["wgpu", "2 ms"]).await;
497 t2.remove_row(3).await;
498 }) as Box<dyn Future<Output = ()>>),
499 );
500 while !ex.step() {}
501 let names: Vec<String> = (0..t.len()).map(|i| t.cell(i, 0).text().get()).collect();
502 assert_eq!(names, ["wgpu", "vello", "cairo"]);
503 for (i, y) in ys.iter().enumerate() {
504 assert!((t.row(i).y().get() - y).abs() < 1e-6, "row {i}");
505 }
506 assert_eq!(t.row(0).opacity().get(), 1.0);
507 });
508 }
509}