1use 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#[derive(Clone)]
22pub struct Matrix {
23 root: Group,
24 cells: Vec<Vec<Txt>>,
25 brackets: [Path; 2],
26 xs: Vec<f64>,
28 ys: Vec<f64>,
30 widths: Vec<f64>,
31 row_height: f64,
32}
33
34impl Matrix {
35 #[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 pub fn cell(&self, row: usize, column: usize) -> Txt {
62 self.cells[row][column]
63 }
64
65 pub fn row(&self, row: usize) -> Vec<Txt> {
67 self.cells[row].clone()
68 }
69
70 pub fn column(&self, column: usize) -> Vec<Txt> {
72 self.cells.iter().map(|r| r[column]).collect()
73 }
74
75 pub fn rows(&self) -> usize {
77 self.cells.len()
78 }
79
80 pub fn columns(&self) -> usize {
82 self.xs.len()
83 }
84
85 pub fn brackets(&self) -> [Path; 2] {
87 self.brackets
88 }
89
90 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 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 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 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#[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 pub fn cell_width(mut self, px: f64) -> Self {
185 self.cell_width = px;
186 self
187 }
188
189 pub fn column_gap(mut self, px: f64) -> Self {
191 self.column_gap = px;
192 self
193 }
194
195 pub fn row_gap(mut self, px: f64) -> Self {
197 self.row_gap = px;
198 self
199 }
200
201 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 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 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}