Skip to main content

ui/
grid.rs

1// grid -- a grid box: rows and columns with spans and alignment (a box with its own placing
2// mode; data.rs's grid_begin is the data table)
3//
4//   gridbox_begin(id, cols, flags, gap)   open a grid: one entry per column in DIP, > 0
5//                                         fixed, 0 its widest child, < 0 a share of the width
6//                                         left (fill weight); rows grow with their tallest
7//                                         child.  flags: the box decorations and padding of
8//                                         box_begin (BOX_CARD, BOX_CLIP, pad(n), ...) and the
9//                                         alignment of the children: GRID_H_START | _CENTER |
10//                                         _END (default: stretched across the cell),
11//                                         GRID_V_START | _END | _STRETCH (default: centred);
12//                                         gap: DIP between cells
13//   gridbox_rows(rows)                    (after gridbox_begin) row sizes like cols; rows
14//                                         without an entry are 0 (their tallest child); < 0
15//                                         share the height left when the grid has a fixed one
16//   gridbox_end()
17//   next_span(cols, rows)                 the next child covers that many cells
18//   next_cell(col, row)                   the next child starts there (later cells follow it)
19//   next_align(h, v)                      the next child's alignment: AL_START, AL_CENTER,
20//                                         AL_END, AL_STRETCH (AL_DEFAULT: the grid's)
21//
22// Children fill the cells row by row, skipping cells a row span still covers.  next_size
23// still applies: a fixed size is aligned in the cell, a natural one too, a fill size (< 0)
24// stretches.  Natural column widths and row heights come from the previous pass, as box
25// sizes do; a change asks for another pass.  A spanned child widens the last of its natural
26// columns (rows) when it does not fit.  A TX_WRAP text (and a BOX_WRAP box) wraps at the
27// width its cell gives it (avail_width), so its row grows with its lines.
28
29use alloc::collections::BTreeMap;
30use alloc::vec::Vec;
31
32use super::consts::*;
33use super::Ui;
34
35/// the natural sizes of a grid's columns and rows (previous pass), per window
36#[derive(Clone, Default)]
37pub(crate) struct Mem {
38    colw: Vec<i32>,
39    rowh: Vec<i32>,
40}
41
42/// a grid being built
43#[derive(Clone, Default)]
44pub(crate) struct Run {
45    id: u32,
46    flags: u32,
47    gap: i32,
48    /// column specs (DIP, as given)
49    cols: Vec<i32>,
50    /// column x (absolute px) and widths of this pass
51    colx: Vec<i32>,
52    colw: Vec<i32>,
53    /// row specs (DIP, as given) and the rows' heights / tops as laid out so far
54    rows: Vec<i32>,
55    rowh: Vec<i32>,
56    rowy: Vec<i32>,
57    /// content origin and size of the grid box
58    pos: [i32; 2],
59    len: [i32; 2],
60    /// the next free cell
61    col: usize,
62    row: usize,
63    /// per column: the first row not covered by a row span
64    busy: Vec<usize>,
65    /// natural sizes measured in this pass, and the spanned children (start, count, size)
66    mcol: Vec<i32>,
67    mrow: Vec<i32>,
68    spans_c: Vec<(usize, usize, i32)>,
69    spans_r: Vec<(usize, usize, i32)>,
70}
71
72impl Run {
73    // the cell a child of `span` (cols, rows) starts in: `at`, else the next free one, past
74    // cells a row span still covers and on the next row where the span does not fit
75    fn next_cell(&self, span: (u16, u16), at: Option<(u16, u16)>) -> (usize, usize) {
76        let ncol = self.colw.len();
77        let cs = (span.0 as usize).min(ncol);
78        let (mut col, mut row) = (self.col, self.row);
79        if let Some((c, r)) = at {
80            col = (c as usize).min(ncol - 1);
81            row = r as usize;
82        }
83        loop {
84            if col + cs > ncol {
85                col = 0;
86                row += 1;
87                continue;
88            }
89            if (col..col + cs).any(|c| self.busy[c] > row) {
90                col += 1;
91                continue;
92            }
93            return (col, row);
94        }
95    }
96}
97
98/// the per-window part of the grids (in layout::W)
99#[derive(Default)]
100pub(crate) struct W {
101    pub(crate) runs: Vec<Run>,
102    pub(crate) mem: BTreeMap<u32, Mem>,
103    /// next_span / next_cell / next_align for the next child
104    pub(crate) span: (u16, u16),
105    pub(crate) cell: Option<(u16, u16)>,
106    pub(crate) align: (u8, u8),
107    /// the cell of the last child (col, row, cols, rows) and its grid (run index + 1)
108    pub(crate) last: (u16, [u16; 4]),
109}
110
111// the alignment fields of the grid flags
112fn h_of(flags: u32) -> u8 {
113    match (flags >> 1) & 3 {
114        1 => AL_START as u8,
115        2 => AL_CENTER as u8,
116        3 => AL_END as u8,
117        _ => AL_STRETCH as u8,
118    }
119}
120fn v_of(flags: u32) -> u8 {
121    match (flags >> 3) & 3 {
122        1 => AL_START as u8,
123        2 => AL_END as u8,
124        3 => AL_STRETCH as u8,
125        _ => AL_CENTER as u8,
126    }
127}
128
129// sizes of `specs` (DIP) in `avail` px: fixed, natural (from `nat`), fill shares of the rest
130fn sizes(specs: &[i32], nat: &[i32], avail: i32, gap: i32, dpi: i32) -> Vec<i32> {
131    let n = specs.len();
132    let mut out: Vec<i32> = alloc::vec![0; n];
133    let (mut used, mut wsum) = (gap * (n as i32 - 1).max(0), 0i32);
134    for (k, &s) in specs.iter().enumerate() {
135        if s > 0 {
136            out[k] = super::layout::dp_at(s, dpi);
137        } else if s == 0 {
138            out[k] = nat.get(k).copied().unwrap_or(0);
139        } else {
140            wsum += -s;
141            continue;
142        }
143        used += out[k];
144    }
145    if wsum > 0 {
146        let spare = (avail - used).max(0);
147        let mut left = spare;
148        let last = specs.iter().rposition(|&s| s < 0).unwrap_or(0);
149        for (k, &s) in specs.iter().enumerate() {
150            if s < 0 {
151                out[k] = if k == last { left } else { spare * -s / wsum };
152                left -= out[k];
153            }
154        }
155    }
156    out
157}
158
159impl Ui {
160    /// gridbox_begin(id, cols, flags, gap): open a grid (see the module comment)
161    #[cfg_attr(jfgui_dev, track_caller)]
162    pub fn gridbox_begin(&mut self, id: u32, cols: &[i32], flags: u32, gap: i32) {
163        let _src = self.src_scope();
164        // the grid's box: vertical, its natural size from gridbox_end; the alignment bits are ours
165        self.box_begin(id, BOX_V | (flags & !0x3F), 0);
166        let id = self.box_id();
167        let b = *self.wp.lay.boxes.last().unwrap_or(&Default::default());
168        let gap = self.dp_space(gap); // (theme) spacing
169        let dpi = self.wp.rt.dpi;
170        let mem = self.wp.lay.grid.mem.get(&id).cloned().unwrap_or_default();
171        let cols = if cols.is_empty() { &[0][..] } else { cols };
172        let colw = sizes(cols, &mem.colw, b.len[0], gap, dpi);
173        let mut colx = Vec::with_capacity(colw.len());
174        let mut x = b.pos[0];
175        for w in &colw {
176            colx.push(x);
177            x += w + gap;
178        }
179        let n = cols.len();
180        let run = Run {
181            id,
182            flags,
183            gap,
184            cols: cols.to_vec(),
185            colx,
186            colw,
187            rows: Vec::new(),
188            rowh: mem.rowh.clone(),
189            rowy: Vec::new(),
190            pos: b.pos,
191            len: b.len,
192            busy: alloc::vec![0; n],
193            mcol: alloc::vec![0; n],
194            mrow: Vec::new(),
195            ..Run::default()
196        };
197        self.wp.lay.grid.runs.push(run);
198        let k = self.wp.lay.grid.runs.len() as u16;
199        if let Some(top) = self.wp.lay.boxes.last_mut() {
200            top.grid = k;
201        }
202        self.grid_layout_rows();
203    }
204
205    /// gridbox_rows(rows): row sizes of the open grid (DIP; > 0 fixed, 0 natural, < 0 fill)
206    pub fn gridbox_rows(&mut self, rows: &[i32]) {
207        if let Some(r) = self.wp.lay.grid.runs.last_mut() {
208            r.rows = rows.to_vec();
209        }
210        self.grid_layout_rows();
211    }
212
213    // the rows' heights and tops from their specs and the previous pass
214    fn grid_layout_rows(&mut self) {
215        let dpi = self.wp.rt.dpi;
216        let mem = {
217            let Some(r) = self.wp.lay.grid.runs.last() else { return };
218            self.wp.lay.grid.mem.get(&r.id).cloned().unwrap_or_default()
219        };
220        let Some(r) = self.wp.lay.grid.runs.last_mut() else { return };
221        let n = r.rows.len().max(mem.rowh.len());
222        let specs: Vec<i32> = (0..n).map(|k| r.rows.get(k).copied().unwrap_or(0)).collect();
223        r.rowh = sizes(&specs, &mem.rowh, r.len[1], r.gap, dpi);
224        r.rowy.clear();
225        let mut y = r.pos[1];
226        for h in &r.rowh {
227            r.rowy.push(y);
228            y += h + r.gap;
229        }
230    }
231
232    /// next_span(cols, rows): the next grid child covers that many cells
233    pub fn next_span(&mut self, cols: u32, rows: u32) {
234        self.wp.lay.grid.span = (cols.clamp(1, 1000) as u16, rows.clamp(1, 1000) as u16);
235    }
236
237    /// next_cell(col, row): the next grid child starts in that cell
238    pub fn next_cell(&mut self, col: u32, row: u32) {
239        self.wp.lay.grid.cell = Some((col.min(1000) as u16, row.min(100_000) as u16));
240    }
241
242    /// next_align(h, v): the next grid child's alignment (AL_*; AL_DEFAULT: the grid's)
243    pub fn next_align(&mut self, h: u32, v: u32) {
244        self.wp.lay.grid.align = (h.min(4) as u8, v.min(4) as u8);
245    }
246
247    /// (avail_width) the width grid k (run index + 1) gives its next child of natural width
248    /// iw, as grid_place will: the cell's width (stretched or fill), a fixed width (at most the
249    /// cell's), else (aligned, natural) at most the cell's; 1 when it does not depend on iw
250    pub(crate) fn grid_avail_width(&self, k: usize, iw: i32) -> (i32, i32) {
251        let l = &self.wp.lay;
252        let Some(r) = l.grid.runs.get(k - 1) else { return (iw, 0) };
253        let span = (l.grid.span.0.max(1), l.grid.span.1.max(1));
254        let (c, _) = r.next_cell(span, l.grid.cell);
255        let e = c + (span.0 as usize).min(r.colw.len()) - 1;
256        let cw = r.colx[e] + r.colw[e] - r.colx[c];
257        let ha = if l.grid.align.0 != 0 { l.grid.align.0 } else { h_of(r.flags) };
258        if l.next_w > 0 {
259            return (self.dp(l.next_w).min(cw), 1);
260        }
261        if l.next_w < 0 || ha == AL_STRETCH as u8 {
262            return (cw, 1);
263        }
264        if cw <= 0 {
265            return (iw, 0);
266        }
267        (cw.min(iw), 0)
268    }
269
270    /// (place) the next child of grid k (run index + 1): its rectangle
271    pub(crate) fn grid_place(&mut self, k: usize, iw: i32, ih: i32) -> [i32; 4] {
272        let dpi = self.wp.rt.dpi;
273        let l = &mut self.wp.lay;
274        let (rw, rh) = (l.next_w, l.next_h);
275        l.next_w = 0;
276        l.next_h = 0;
277        l.place_auto = 0;
278        l.place_main = 0;
279        let span = core::mem::replace(&mut l.grid.span, (1, 1));
280        let span = (span.0.max(1), span.1.max(1));
281        let at = l.grid.cell.take();
282        let al = core::mem::take(&mut l.grid.align);
283        let r = &mut l.grid.runs[k - 1];
284        let cs = (span.0 as usize).min(r.colw.len());
285        let rs = span.1 as usize;
286        // the cell: an explicit one, else the next one free of row spans where it fits
287        let (c, row) = r.next_cell(span, at);
288        r.row = row;
289        // natural size and what this child asks for
290        let nat_w = if rw > 0 { super::layout::dp_at(rw, dpi) } else { iw };
291        let nat_h = if rh > 0 { super::layout::dp_at(rh, dpi) } else { ih };
292        // rows not known yet (new this pass): as tall as their first child
293        while r.rowh.len() < row + rs {
294            let y = match (r.rowy.last(), r.rowh.last()) {
295                (Some(y), Some(h)) => y + h + r.gap,
296                _ => r.pos[1],
297            };
298            r.rowy.push(y);
299            r.rowh.push(if rs == 1 { nat_h } else { 0 });
300        }
301        if r.mrow.len() < row + rs {
302            r.mrow.resize(row + rs, 0);
303        }
304        // record the natural sizes
305        if cs == 1 {
306            r.mcol[c] = r.mcol[c].max(nat_w);
307        } else {
308            r.spans_c.push((c, cs, nat_w));
309        }
310        if rs == 1 {
311            r.mrow[row] = r.mrow[row].max(nat_h);
312        } else {
313            r.spans_r.push((row, rs, nat_h));
314        }
315        // the cell rectangle
316        let cx = r.colx[c];
317        let cw = r.colx[c + cs - 1] + r.colw[c + cs - 1] - cx;
318        let cy = r.rowy[row];
319        let ch = r.rowy[row + rs - 1] + r.rowh[row + rs - 1] - cy;
320        // alignment
321        let ha = if al.0 != 0 { al.0 } else { h_of(r.flags) };
322        let va = if al.1 != 0 { al.1 } else { v_of(r.flags) };
323        let place = |a: u8, start: i32, size: i32, want: i32, fill: bool| -> (i32, i32) {
324            if fill || (a == AL_STRETCH as u8 && want <= 0) {
325                return (start, size);
326            }
327            let w = if a == AL_STRETCH as u8 { want.min(size) } else { want.min(size).max(0) };
328            match a {
329                x if x == AL_CENTER as u8 => (start + (size - w) / 2, w),
330                x if x == AL_END as u8 => (start + size - w, w),
331                _ => (start, w),
332            }
333        };
334        let (x, w) = place(ha, cx, cw, if rw > 0 { nat_w } else if ha == AL_STRETCH as u8 { 0 } else { nat_w }, rw < 0);
335        let (y, h) = place(va, cy, ch, if rh > 0 { nat_h } else if va == AL_STRETCH as u8 { 0 } else { nat_h }, rh < 0);
336        // the next cell; a row span keeps its columns busy
337        if rs > 1 {
338            for b in &mut r.busy[c..c + cs] {
339                *b = row + rs;
340            }
341        }
342        r.col = c + cs;
343        l.grid.last = (k as u16, [c as u16, row as u16, cs as u16, rs as u16]);
344        [x, y, w, h]
345    }
346
347    /// (box_end) a child box of grid k measured (w, h) this pass in its cell
348    pub(crate) fn grid_child_measured(&mut self, k: usize, cell: [u16; 4], w: i32, h: i32) {
349        let Some(r) = self.wp.lay.grid.runs.get_mut(k - 1) else { return };
350        let [c, row, cs, rs] = cell.map(|v| v as usize);
351        if cs == 1 && c < r.mcol.len() {
352            r.mcol[c] = r.mcol[c].max(w);
353        } else if cs > 1 {
354            r.spans_c.push((c, cs, w));
355        }
356        if rs == 1 && row < r.mrow.len() {
357            r.mrow[row] = r.mrow[row].max(h);
358        } else if rs > 1 {
359            r.spans_r.push((row, rs, h));
360        }
361    }
362
363    /// gridbox_end: store the measured sizes (another pass when they changed), close the box
364    #[cfg_attr(jfgui_dev, track_caller)]
365    pub fn gridbox_end(&mut self) {
366        let _src = self.src_scope();
367        let Some(mut r) = self.wp.lay.grid.runs.pop() else {
368            self.warn("gridbox_end without gridbox_begin");
369            return;
370        };
371        // spanned children: widen the last natural column / row of their span
372        let fit = |m: &mut Vec<i32>, specs: &dyn Fn(usize) -> i32, spans: &[(usize, usize, i32)], gap: i32| {
373            for &(s, n, want) in spans {
374                if s + n > m.len() {
375                    m.resize(s + n, 0);
376                }
377                let have: i32 = m[s..s + n].iter().sum::<i32>() + gap * (n as i32 - 1);
378                if want > have {
379                    if let Some(k) = (s..s + n).rev().find(|&k| specs(k) == 0) {
380                        m[k] += want - have;
381                    }
382                }
383            }
384        };
385        let mut mcol = core::mem::take(&mut r.mcol);
386        let mut mrow = core::mem::take(&mut r.mrow);
387        let (cols_spec, rows_spec) = (r.cols.clone(), r.rows.clone());
388        fit(&mut mcol, &|k| cols_spec.get(k).copied().unwrap_or(0), &r.spans_c, r.gap);
389        fit(&mut mrow, &|k| rows_spec.get(k).copied().unwrap_or(0), &r.spans_r, r.gap);
390        let changed = self.wp.lay.grid.mem.get(&r.id).is_none_or(|m| m.colw != mcol || m.rowh != mrow);
391        if changed {
392            self.repass = true;
393        }
394        // the grid's natural size: the columns and rows as laid out (natural ones measured)
395        let dpi = self.wp.rt.dpi;
396        let gap = r.gap;
397        // fixed: its size; natural and fill: the widest child
398        let colw: i32 = (0..cols_spec.len())
399            .map(|k| match cols_spec[k] {
400                s if s > 0 => super::layout::dp_at(s, dpi),
401                _ => mcol.get(k).copied().unwrap_or(0),
402            })
403            .sum::<i32>()
404            + gap * (cols_spec.len() as i32 - 1).max(0);
405        let nrow = mrow.len().max(rows_spec.len());
406        let rowh: i32 = (0..nrow)
407            .map(|k| match rows_spec.get(k).copied().unwrap_or(0) {
408                s if s > 0 => super::layout::dp_at(s, dpi),
409                _ => mrow.get(k).copied().unwrap_or(0),
410            })
411            .sum::<i32>()
412            + gap * (nrow as i32 - 1).max(0);
413        self.wp.lay.grid.mem.insert(r.id, Mem { colw: mcol, rowh: mrow });
414        if let Some(top) = self.wp.lay.boxes.last_mut() {
415            top.grid = 0;
416            top.fixed = rowh; // BOX_V: main axis = height
417            top.cross = colw;
418            top.wsum = 0;
419        }
420        self.box_end();
421    }
422}