Skip to main content

oxframe_components/
matrix.rs

1//! A matrix of values in square brackets.
2
3use std::fmt::Display;
4use std::ops::Deref;
5
6use oxframe_2d::{Accent, BaseProps, Group, Node, NodeBuilder, NodeId, Path, Rect, Text, Txt};
7use oxframe_core::Color;
8use oxframe_core::kurbo::BezPath;
9
10/// Values in a grid between square brackets. Each cell is a [`Txt`], so
11/// `cell(1, 0).text().set("7")` changes it and `.fill()` colors it.
12/// [`row_frame`](Self::row_frame) and [`column_frame`](Self::column_frame)
13/// give outlines that fade in to point at a row or a column.
14///
15/// ```ignore
16/// let a = view.add(Matrix::new([[1, 2], [3, 4]]));
17/// let r = a.row_frame(0);
18/// r.fade_in().await;
19/// a.cell(0, 1).fill().to(Accent.into(), 0.3).await;
20/// ```
21#[derive(Clone)]
22pub struct Matrix {
23    root: Group,
24    cells: Vec<Vec<Txt>>,
25    brackets: [Path; 2],
26    /// Center x of each column.
27    xs: Vec<f64>,
28    /// Center y of each row.
29    ys: Vec<f64>,
30    widths: Vec<f64>,
31    row_height: f64,
32}
33
34impl Matrix {
35    /// A matrix from rows of values: numbers, `&str` or anything that
36    /// implements `Display`. Rows shorter than the longest get empty cells.
37    #[allow(clippy::new_ret_no_self)]
38    pub fn new<R, V>(rows: impl IntoIterator<Item = R>) -> MatrixBuilder
39    where
40        R: IntoIterator<Item = V>,
41        V: Display,
42    {
43        MatrixBuilder {
44            base: BaseProps::default(),
45            rows: rows
46                .into_iter()
47                .map(|r| r.into_iter().map(|v| v.to_string()).collect())
48                .collect(),
49            cell_width: 0.0,
50            column_gap: 40.0,
51            row_gap: 16.0,
52            font_size: None,
53        }
54    }
55
56    /// The cell in row `row`, column `column` (both from 0).
57    ///
58    /// # Panics
59    ///
60    /// If the cell is outside the matrix.
61    pub fn cell(&self, row: usize, column: usize) -> Txt {
62        self.cells[row][column]
63    }
64
65    /// The cells of a row, left to right.
66    pub fn row(&self, row: usize) -> Vec<Txt> {
67        self.cells[row].clone()
68    }
69
70    /// The cells of a column, top to bottom.
71    pub fn column(&self, column: usize) -> Vec<Txt> {
72        self.cells.iter().map(|r| r[column]).collect()
73    }
74
75    /// Number of rows.
76    pub fn rows(&self) -> usize {
77        self.cells.len()
78    }
79
80    /// Number of columns.
81    pub fn columns(&self) -> usize {
82        self.xs.len()
83    }
84
85    /// The left and right bracket.
86    pub fn brackets(&self) -> [Path; 2] {
87        self.brackets
88    }
89
90    /// A rounded outline around row `row`, hidden: `.fade_in()` shows it.
91    pub fn row_frame(&self, row: usize) -> Rect {
92        let width = self.inner_width() + 24.0;
93        self.frame(
94            0.0,
95            self.ys[row],
96            width,
97            self.row_height + 12.0,
98            "row_frame",
99        )
100    }
101
102    /// A rounded outline around column `column`, hidden.
103    pub fn column_frame(&self, column: usize) -> Rect {
104        let height = self.inner_height() + 12.0;
105        self.frame(
106            self.xs[column],
107            0.0,
108            self.widths[column] + 24.0,
109            height,
110            "column_frame",
111        )
112    }
113
114    /// A rounded outline around one cell, hidden.
115    pub fn cell_frame(&self, row: usize, column: usize) -> Rect {
116        self.frame(
117            self.xs[column],
118            self.ys[row],
119            self.widths[column] + 24.0,
120            self.row_height + 12.0,
121            "cell_frame",
122        )
123    }
124
125    fn frame(&self, x: f64, y: f64, w: f64, h: f64, name: &str) -> Rect {
126        self.root.add(
127            Rect::new()
128                .width(w)
129                .height(h)
130                .radius(12.0)
131                .fill(Color::TRANSPARENT)
132                .stroke(Accent)
133                .line_width(4.0)
134                .x(x)
135                .y(y)
136                .opacity(0.0)
137                .name(name),
138        )
139    }
140
141    fn inner_width(&self) -> f64 {
142        match (self.xs.first(), self.xs.last()) {
143            (Some(a), Some(b)) => {
144                b - a + (self.widths[0] + self.widths[self.widths.len() - 1]) / 2.0
145            }
146            _ => 0.0,
147        }
148    }
149
150    fn inner_height(&self) -> f64 {
151        match (self.ys.first(), self.ys.last()) {
152            (Some(a), Some(b)) => b - a + self.row_height,
153            _ => 0.0,
154        }
155    }
156
157    /// The untyped node handle.
158    pub fn node(&self) -> Node {
159        *self.root
160    }
161}
162
163impl Deref for Matrix {
164    type Target = Node;
165    fn deref(&self) -> &Node {
166        &self.root
167    }
168}
169
170/// Builder for [`Matrix`].
171#[must_use = "a builder does nothing until it is passed to `add`"]
172pub struct MatrixBuilder {
173    base: BaseProps,
174    rows: Vec<Vec<String>>,
175    cell_width: f64,
176    column_gap: f64,
177    row_gap: f64,
178    font_size: Option<f64>,
179}
180
181impl MatrixBuilder {
182    /// The narrowest a column is, in pixels. Set it when values will be
183    /// replaced by longer ones, such as a result filled in step by step.
184    pub fn cell_width(mut self, px: f64) -> Self {
185        self.cell_width = px;
186        self
187    }
188
189    /// Space between columns. Default 40.
190    pub fn column_gap(mut self, px: f64) -> Self {
191        self.column_gap = px;
192        self
193    }
194
195    /// Space between rows. Default 16.
196    pub fn row_gap(mut self, px: f64) -> Self {
197        self.row_gap = px;
198        self
199    }
200
201    /// Size of the values in pixels. Default: the theme's body size.
202    pub fn font_size(mut self, px: f64) -> Self {
203        self.font_size = Some(px);
204        self
205    }
206}
207
208impl NodeBuilder for MatrixBuilder {
209    type Handle = Matrix;
210    fn base(&mut self) -> &mut BaseProps {
211        &mut self.base
212    }
213    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> Matrix {
214        let base = self.base;
215        let mut group = Group::new().x(base.x).y(base.y).opacity(base.opacity);
216        if let Some(name) = base.name {
217            group = group.name(name);
218        }
219        let root = Node::from_id(parent).add(group);
220        let columns = self.rows.iter().map(Vec::len).max().unwrap_or(0);
221        let mut cells: Vec<Vec<Txt>> = Vec::new();
222        let mut widths = vec![self.cell_width.max(24.0); columns];
223        let mut row_height: f64 = 0.0;
224        for (r, row) in self.rows.iter().enumerate() {
225            let mut out = Vec::new();
226            for (c, width) in widths.iter_mut().enumerate() {
227                let text = row.get(c).cloned().unwrap_or_default();
228                let mut b = Txt::new().text(text).body().name(format!("m{r}{c}"));
229                if let Some(px) = self.font_size {
230                    b = b.font_size(px);
231                }
232                let txt = root.add(b);
233                let (w, h) = txt.size();
234                *width = width.max(w);
235                row_height = row_height.max(h);
236                out.push(txt);
237            }
238            cells.push(out);
239        }
240        let row_height = row_height.max(32.0);
241        let total_w =
242            widths.iter().sum::<f64>() + self.column_gap * columns.saturating_sub(1) as f64;
243        let mut xs = Vec::with_capacity(columns);
244        let mut left = -total_w / 2.0;
245        for w in &widths {
246            xs.push(left + w / 2.0);
247            left += w + self.column_gap;
248        }
249        let n = cells.len();
250        let total_h = row_height * n as f64 + self.row_gap * n.saturating_sub(1) as f64;
251        let ys: Vec<f64> = (0..n)
252            .map(|r| -total_h / 2.0 + row_height / 2.0 + r as f64 * (row_height + self.row_gap))
253            .collect();
254        for (r, row) in cells.iter().enumerate() {
255            for (c, txt) in row.iter().enumerate() {
256                txt.x().set(xs[c]);
257                txt.y().set(ys[r]);
258            }
259        }
260        // Square brackets a little taller than the rows, with short serifs.
261        let (bx, by, serif) = (total_w / 2.0 + 24.0, total_h / 2.0 + 10.0, 14.0);
262        let bracket = |side: f64, name: &str| {
263            let mut d = BezPath::new();
264            d.move_to((side * (bx - serif), -by));
265            d.line_to((side * bx, -by));
266            d.line_to((side * bx, by));
267            d.line_to((side * (bx - serif), by));
268            root.add(
269                Path::new()
270                    .data(d)
271                    .fill(Color::TRANSPARENT)
272                    .stroke(Text)
273                    .line_width(4.0)
274                    .name(name),
275            )
276        };
277        let brackets = [bracket(-1.0, "left_bracket"), bracket(1.0, "right_bracket")];
278        Matrix {
279            root,
280            cells,
281            brackets,
282            xs,
283            ys,
284            widths,
285            row_height,
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use oxframe_2d::{SceneGraph, View};
294    use oxframe_core::kurbo::Shape;
295
296    #[test]
297    fn cells_rows_and_columns_line_up() {
298        let graph = SceneGraph::new(1920, 1080);
299        graph.enter(|| {
300            let m = View::current().add(Matrix::new([[1, 20, 3], [4, 5, 600]]));
301            assert_eq!((m.rows(), m.columns()), (2, 3));
302            assert_eq!(m.cell(1, 2).text().get(), "600");
303            let col = m.column(1);
304            assert_eq!(col[0].x().get(), col[1].x().get());
305            let row = m.row(0);
306            assert_eq!(row[0].y().get(), row[2].y().get());
307            assert!(row[0].x().get() < row[1].x().get());
308            assert!(m.cell(0, 0).y().get() < m.cell(1, 0).y().get());
309            let frame = m.row_frame(1);
310            assert_eq!(frame.opacity().get(), 0.0);
311            assert_eq!(frame.y().get(), m.cell(1, 0).y().get());
312            let [l, r] = m.brackets();
313            assert!(l.data().get().bounding_box().x1 < m.cell(0, 0).x().get());
314            assert!(r.data().get().bounding_box().x0 > m.cell(0, 2).x().get());
315            // Centered on the node: the brackets mirror each other.
316            let (lb, rb) = (l.data().get().bounding_box(), r.data().get().bounding_box());
317            assert!((lb.x0 + rb.x1).abs() < 1e-9);
318        });
319    }
320
321    #[test]
322    fn short_rows_get_empty_cells() {
323        let graph = SceneGraph::new(1920, 1080);
324        graph.enter(|| {
325            let m = View::current().add(Matrix::new(vec![vec!["a", "b"], vec!["c"]]));
326            assert_eq!(m.cell(1, 1).text().get(), "");
327        });
328    }
329}