Skip to main content

ui/
layout.rs

1// layout -- box layout (flex-like, measured over passes)              [src/ui/ui_layout.s]
2//
3//   box_begin(id, flags, gap)          open a container (id 0 = automatic)
4//   box_end()                          close it
5//   next_size(w, h)                    size of the next widget / box in DIP: > 0 fixed,
6//                                      0 natural, < 0 fill weight
7//   next_rect(x, y, w, h)              absolute placement of the next widget in DIP,
8//                                      relative to the current box's content origin
9//   next_rect_px(x, y, w, h)           absolute placement in window px (w / h 0 = natural)
10//   next_flags(ST_*)   next_icon(cp)   style flags / icon for the next widget
11//   spacer(dip)                        empty space along the main axis (< 0 = fill)
12//   last_rect() -> [x, y, w, h]        the last placed widget (px)
13//   box_rect() -> [x, y, w, h]         content rectangle of the current box (px)
14//   box_id() -> id                     id of the current box
15//   box_offset(dx, dy)                 scroll the current box's content
16//   avail_width(iw) -> (px, fixed)     width the next widget will get (TX_WRAP)
17// Internal: place(iw, ih), take_flags(), take_icon(), layout_root(), layout_end_root()
18//
19// A box lays its children out along its main axis (BOX_H: x, BOX_V: y).  Fill children
20// share the space left by the others; because that is only known after all children were
21// placed, the previous pass's totals are used and a pass is repeated when they change (the
22// runtime caps this at 4 passes).
23//
24// BOX_WRAP (horizontal boxes): a child that does not fit after the others of its row starts
25// a new row (a row's first child stays, however wide).  Each row is as tall as its tallest
26// child; the cross-axis alignment (BOX_XCENTER ..., stretched: the row's height) and
27// BOX_CENTER / BOX_END work per row with the previous pass's rows.  Natural size: the rows
28// and the gaps between them high, as wide as one row with every child (the box is placed at
29// most as wide as avail_width allows, so it wraps where the parent has less room).  Fill
30// children share what is left only while everything fits on one row, else they keep their
31// natural size.
32
33use alloc::collections::BTreeMap;
34use alloc::vec::Vec;
35
36use super::consts::*;
37use super::{Sp, Ui};
38
39// boxes inside each other (fixed: nesting; deeper ones warn and are left out)
40pub(crate) const BOX_STACK_MAX: usize = 48;
41
42/// layout frame (box stack)  [BX_*]
43#[derive(Clone, Copy, Default)]
44pub(crate) struct BoxFrame {
45    pub(crate) id: u32,
46    pub(crate) flags: u32,
47    /// content rectangle (px, absolute): x, y, w, h -- index 0 / 1 by axis
48    pub(crate) pos: [i32; 2],
49    pub(crate) len: [i32; 2],
50    /// next position along the main axis (absolute px)
51    pub(crate) cur: i32,
52    pub(crate) gap: i32,
53    /// accumulated non-fill main size + gaps, fill weights; previous pass values
54    pub(crate) fixed: i32,
55    pub(crate) wsum: i32,
56    pub(crate) fixedp: i32,
57    pub(crate) wsump: i32,
58    /// children placed
59    pub(crate) n: i32,
60    /// largest cross-axis child size
61    pub(crate) cross: i32,
62    pub(crate) pad: i32,
63    /// bit 0: natural size along the parent's main axis, bit 1: cross axis (aligned),
64    /// bit 2: cross axis stretched or filling (box_end reports the natural cross size)
65    pub(crate) auto: u32,
66    /// size the parent used along its main axis when placing us
67    pub(crate) pmain: i32,
68    /// automatic-id child boxes begun so far
69    pub(crate) nbox: u32,
70    /// a grid (grid.rs): its run (index + 1, 0 = a plain box); as a grid child: the parent
71    /// grid's run and the cell (col, row, cols, rows)
72    pub(crate) grid: u16,
73    pub(crate) gcell: (u16, [u16; 4]),
74}
75
76/// a row of a wrapping box (BOX_WRAP): its height (the tallest child) and its children's
77/// width with the gaps between them
78#[derive(Clone, Copy, Default, PartialEq)]
79pub(crate) struct Row {
80    pub(crate) h: i32,
81    pub(crate) w: i32,
82}
83
84/// a wrapping box being built: its rows so far (the last one is being filled), that row's
85/// top (px below the content start) and children, the previous pass's rows
86#[derive(Default)]
87pub(crate) struct Wrap {
88    rows: Vec<Row>,
89    top: i32,
90    n: i32,
91    prev: Vec<Row>,
92}
93
94// a box whose children wrap: horizontal with BOX_WRAP
95fn wraps(flags: u32) -> bool {
96    flags & (BOX_WRAP | BOX_V) == BOX_WRAP
97}
98
99// where a row of a wrapping box starts (px after the content start): BOX_CENTER / BOX_END
100// by the row's width in the previous pass
101fn row_start(flags: u32, len: i32, prev: Option<&Row>) -> i32 {
102    let spare = prev.map_or(0, |r| len.wrapping_sub(r.w));
103    if spare <= 0 {
104        0
105    } else if flags & BOX_CENTER != 0 {
106        spare >> 1
107    } else if flags & BOX_END != 0 {
108        spare
109    } else {
110        0
111    }
112}
113
114/// the per-window part of the layout (ui_layout_wb0 .. ui_layout_wb1)
115#[derive(Default)]
116pub(crate) struct W {
117    pub(crate) boxes: Vec<BoxFrame>,
118    pub(crate) next_w: i32,
119    pub(crate) next_h: i32,
120    pub(crate) next_rx: i32,
121    pub(crate) next_ry: i32,
122    pub(crate) next_rw: i32,
123    pub(crate) next_rh: i32,
124    /// 0 none, 1 next_rect (DIP), 2 next_rect_px
125    pub(crate) next_rect_set: u32,
126    pub(crate) next_flags: u32,
127    pub(crate) next_icon: u32,
128    pub(crate) place_auto: u32,
129    pub(crate) place_main: i32,
130    /// box_begin is placing a box: its natural cross size counts at its box_end (with this
131    /// pass's measurement), not with the previous pass's at placement
132    pub(crate) placing_box: bool,
133    /// id of the window's root box (1 = main window)
134    pub(crate) root_id: u32,
135    /// grids (grid.rs): the open ones, their measured sizes, the next child's span / cell
136    pub(crate) grid: super::grid::W,
137    /// wrapping boxes (BOX_WRAP): the open ones, the rows each one measured (previous pass)
138    pub(crate) wraps: Vec<Wrap>,
139    pub(crate) wrap_rows: BTreeMap<u32, Vec<Row>>,
140}
141
142impl Ui {
143    // frame_top: the top box frame (outside a pass, where the asm reads a stale frame, an
144    // empty one)
145    fn top(&mut self) -> &mut BoxFrame {
146        let b = &mut self.wp.lay.boxes;
147        if b.is_empty() {
148            b.push(BoxFrame::default());
149        }
150        let n = b.len();
151        &mut b[n - 1]
152    }
153
154    /// layout_root(x, y, w, h)
155    pub(crate) fn layout_root(&mut self, x: i32, y: i32, w: i32, h: i32) {
156        let id = self.wp.lay.root_id;
157        let s = *self.slot(id);
158        let l = &mut self.wp.lay;
159        l.boxes.clear();
160        l.boxes.push(BoxFrame {
161            id,
162            flags: BOX_V,
163            pos: [x, y],
164            len: [w, h],
165            cur: y,
166            fixedp: s.fixed,
167            wsump: s.wsum,
168            ..BoxFrame::default()
169        });
170        l.next_w = 0;
171        l.next_h = 0;
172        l.next_rect_set = 0;
173        l.next_flags = 0;
174        l.next_icon = 0;
175        l.grid.runs.clear();
176        l.grid.span = (1, 1);
177        l.grid.cell = None;
178        l.grid.align = (0, 0);
179        l.wraps.clear();
180    }
181
182    /// layout_end_root: close unbalanced boxes and store the root measurements
183    pub(crate) fn layout_end_root(&mut self) {
184        while self.wp.lay.boxes.len() > 1 {
185            self.warn("ui_box_begin without ui_box_end");
186            self.box_end();
187        }
188        let b = self.wp.lay.boxes.first().copied().unwrap_or_default();
189        let id = self.wp.lay.root_id;
190        let s = self.slot(id);
191        let changed = b.fixed != s.fixed || b.wsum != s.wsum;
192        s.fixed = b.fixed;
193        s.wsum = b.wsum;
194        if changed {
195            self.repass = true;
196        }
197        self.wp.lay.boxes.clear();
198    }
199
200    /// ui_next_size(w, h): DIP; > 0 fixed, 0 natural, < 0 fill weight
201    pub fn next_size(&mut self, w: i32, h: i32) {
202        self.wp.lay.next_w = w;
203        self.wp.lay.next_h = h;
204    }
205
206    /// ui_next_rect(x, y, w, h): DIP relative to the current box's content origin
207    pub fn next_rect(&mut self, x: i32, y: i32, w: i32, h: i32) {
208        let l = &mut self.wp.lay;
209        l.next_rx = x;
210        l.next_ry = y;
211        l.next_rw = w;
212        l.next_rh = h;
213        l.next_rect_set = 1;
214    }
215
216    /// ui_next_rect_px(x, y, w, h): absolute placement of the next widget / box in window
217    /// px (w or h 0 = natural size); nothing is accounted in the parent box
218    pub fn next_rect_px(&mut self, x: i32, y: i32, w: i32, h: i32) {
219        let l = &mut self.wp.lay;
220        l.next_rx = x;
221        l.next_ry = y;
222        l.next_rw = w;
223        l.next_rh = h;
224        l.next_rect_set = 2;
225    }
226
227    /// ui_box_offset(dx, dy): shift the content of the current box by (-dx, -dy) px
228    pub fn box_offset(&mut self, dx: i32, dy: i32) {
229        let b = self.top();
230        b.pos[0] = b.pos[0].wrapping_sub(dx);
231        b.pos[1] = b.pos[1].wrapping_sub(dy);
232        if b.flags & BOX_V != 0 {
233            b.cur = b.cur.wrapping_sub(dy);
234        } else {
235            b.cur = b.cur.wrapping_sub(dx);
236        }
237    }
238
239    /// ui_next_flags(ST_*): style flags for the next widget (OR-ed)
240    pub fn next_flags(&mut self, f: u32) {
241        self.wp.lay.next_flags |= f;
242    }
243
244    /// ui_take_flags: the pending next_flags value (cleared; for widgets)
245    pub fn take_flags(&mut self) -> u32 {
246        core::mem::take(&mut self.wp.lay.next_flags)
247    }
248
249    /// ui_next_icon(cp): icon code point (IC_*) for the next widget
250    pub fn next_icon(&mut self, cp: u32) {
251        self.wp.lay.next_icon = cp;
252    }
253
254    /// ui_take_icon: the pending icon (cleared; for widgets)
255    pub fn take_icon(&mut self) -> u32 {
256        core::mem::take(&mut self.wp.lay.next_icon)
257    }
258
259    /// ui_last_rect: [x, y, w, h] of the last placed widget (px)
260    pub fn last_rect(&self) -> [i32; 4] {
261        self.wp.ctx.last_rect
262    }
263
264    /// ui_box_id: id of the current box (the root box's id at the top level; 0 outside a pass)
265    pub fn box_id(&self) -> u32 {
266        self.wp.lay.boxes.last().map_or(0, |b| b.id)
267    }
268
269    /// ui_box_rect: [x, y, w, h] content rectangle of the current box (px)
270    pub fn box_rect(&self) -> [i32; 4] {
271        let b = self.wp.lay.boxes.last().copied().unwrap_or_default();
272        [b.pos[0], b.pos[1], b.len[0], b.len[1]]
273    }
274
275    /// flags of the current box (ui_box_rect - 8 + 4 in the asm)
276    pub fn box_flags(&self) -> u32 {
277        self.wp.lay.boxes.last().map_or(0, |b| b.flags)
278    }
279
280    /// ui_place(iw, ih) -> last_rect: place the next widget (natural size in px)
281    pub fn place(&mut self, iw: i32, ih: i32) -> [i32; 4] {
282        let dpi = self.wp.rt.dpi;
283        let dp = |v: i32| dp_at(v, dpi);
284        let mut repass = false;
285        self.top();
286        // a grid places its children in cells (grid.rs)
287        let gk = self.wp.lay.boxes.last().map_or(0, |b| b.grid);
288        if gk != 0 && self.wp.lay.next_rect_set == 0 {
289            let r = self.grid_place(gk as usize, iw, ih);
290            self.wp.ctx.last_rect = r;
291            return r;
292        }
293        let l = &mut self.wp.lay;
294        let r = if l.next_rect_set == 0 {
295            let n = l.boxes.len();
296            let b = &mut l.boxes[n - 1];
297            let a = (b.flags & BOX_V) as usize; // main axis index
298            let c = a ^ 1; // cross axis index
299            // a wrapping box: its rows
300            let mut wr = if wraps(b.flags) { l.wraps.last_mut() } else { None };
301            // requested sizes along main and cross axis
302            let (mut rmain, mut rcross) = (l.next_w, l.next_h);
303            let (nat_main, mut nat_cross) = if a == 1 { (ih, iw) } else { (iw, ih) };
304            if a == 1 {
305                core::mem::swap(&mut rmain, &mut rcross);
306            }
307            // remember which sizes are natural (for box_end's late correction)
308            let aligned = b.flags & (BOX_XCENTER | BOX_XSTART | BOX_XEND) != 0;
309            let mut auto = 0;
310            if rmain == 0 {
311                auto |= 1;
312            }
313            if rcross == 0 && aligned {
314                auto |= 2;
315            } else if rcross <= 0 {
316                // stretched or filling: the parent's natural cross size still counts ours
317                auto |= 4;
318            }
319            l.place_auto = auto;
320            // main size (in a wrapping box that does not fit on one row a fill child keeps its
321            // natural size)
322            let mut weight = 0;
323            let main = if rmain == 0 {
324                nat_main
325            } else if rmain < 0 {
326                weight = rmain.wrapping_neg();
327                let spare = (b.len[a].wrapping_sub(b.fixedp)).max(0);
328                if b.wsump == 0 {
329                    repass = true;
330                    if wr.is_some() { nat_main } else { 0 }
331                } else if wr.is_some() && spare == 0 {
332                    nat_main
333                } else {
334                    spare.wrapping_mul(weight) / b.wsump
335                }
336            } else {
337                dp(rmain)
338            };
339            // a wrapping box: a new row when the child does not fit after the row's others (by
340            // the row's width, so a row moved by BOX_CENTER / BOX_END breaks the same way)
341            if let Some(w) = wr.as_deref_mut() {
342                let used = w.rows.last().map_or(0, |r| r.w);
343                if w.n != 0 && used.wrapping_add(b.gap).wrapping_add(main) > b.len[a] {
344                    let h = w.rows.last().map_or(0, |r| r.h);
345                    w.top = w.top.wrapping_add(h).wrapping_add(b.gap);
346                    w.rows.push(Row::default());
347                    w.n = 0;
348                    b.cur = b.pos[a].wrapping_add(row_start(b.flags, b.len[a], w.prev.get(w.rows.len() - 1)));
349                }
350            }
351            // the room across: the box's (a wrapping box: its row's height in the previous
352            // pass, 0 while not known)
353            let (start, room) = match wr.as_deref() {
354                Some(w) => (b.pos[c].wrapping_add(w.top), w.prev.get(w.rows.len() - 1).map_or(0, |p| p.h)),
355                None => (b.pos[c], b.len[c]),
356            };
357            // cross size
358            let cross = if rcross > 0 {
359                nat_cross = dp(rcross); // a fixed cross size is also natural
360                nat_cross
361            } else if (rcross == 0 && aligned) || (wr.is_some() && room == 0) {
362                nat_cross
363            } else {
364                room
365            };
366            // gap before every child but the first (a wrapping box: but a row's first; its
367            // natural width counts them all)
368            let gap = match wr.as_deref() {
369                Some(w) if w.n == 0 => 0,
370                None if b.n == 0 => 0,
371                _ => b.gap,
372            };
373            if b.n != 0 {
374                b.fixed = b.fixed.wrapping_add(b.gap);
375            }
376            b.cur = b.cur.wrapping_add(gap);
377            b.n += 1;
378            l.place_main = main;
379            if weight == 0 {
380                b.fixed = b.fixed.wrapping_add(main);
381            } else {
382                b.wsum = b.wsum.wrapping_add(weight);
383            }
384            let counts = !(l.placing_box && auto & 6 != 0);
385            if nat_cross > b.cross && counts {
386                b.cross = nat_cross;
387            }
388            if let Some(w) = wr.as_deref_mut() {
389                w.n += 1;
390                if let Some(row) = w.rows.last_mut() {
391                    row.w = row.w.wrapping_add(gap).wrapping_add(main);
392                    if nat_cross > row.h && counts {
393                        row.h = nat_cross;
394                    }
395                }
396            }
397            // position: main = cursor, cross = start of the room (+ alignment)
398            let mut r = [0i32; 4];
399            r[a] = b.cur;
400            r[2 + a] = main;
401            b.cur = b.cur.wrapping_add(main);
402            let mut spare = room.wrapping_sub(cross);
403            if wr.is_some() {
404                spare = spare.max(0); // a row not measured yet: at its top
405            }
406            let mut cp = start;
407            if b.flags & BOX_XCENTER != 0 {
408                cp = cp.wrapping_add(spare >> 1);
409            } else if b.flags & BOX_XEND != 0 {
410                cp = cp.wrapping_add(spare);
411            }
412            r[c] = cp;
413            r[2 + c] = cross;
414            r
415        } else if l.next_rect_set == 2 {
416            // next_rect_px: window px, 0 = natural size
417            l.next_rect_set = 0;
418            l.place_auto = 0;
419            [
420                l.next_rx,
421                l.next_ry,
422                if l.next_rw != 0 { l.next_rw } else { iw },
423                if l.next_rh != 0 { l.next_rh } else { ih },
424            ]
425        } else {
426            // absolute placement relative to the box content origin
427            l.next_rect_set = 0;
428            l.place_auto = 0;
429            let n = l.boxes.len();
430            let b = l.boxes[n - 1];
431            [
432                dp(l.next_rx).wrapping_add(b.pos[0]),
433                dp(l.next_ry).wrapping_add(b.pos[1]),
434                if l.next_rw != 0 { dp(l.next_rw) } else { iw },
435                if l.next_rh != 0 { dp(l.next_rh) } else { ih },
436            ]
437        };
438        l.next_w = 0;
439        l.next_h = 0;
440        self.wp.ctx.last_rect = r;
441        if repass {
442            self.repass = true;
443        }
444        r
445    }
446
447    /// ui_avail_width(iw) -> (width px a wrapping widget of natural (unwrapped) width iw
448    /// gets from the next place, 1 when that width does not depend on iw) -- no side
449    /// effects.  0 = unknown (no wrapping).
450    pub fn avail_width(&self, iw: i32) -> (i32, i32) {
451        let l = &self.wp.lay;
452        if l.next_rect_set != 0 {
453            if l.next_rw == 0 {
454                return (iw, 0);
455            }
456            return (if l.next_rect_set == 2 { l.next_rw } else { self.dp(l.next_rw) }, 1);
457        }
458        let b = l.boxes.last().copied().unwrap_or_default();
459        // a grid: the width of the cell the next child gets (grid.rs)
460        if b.grid != 0 {
461            return self.grid_avail_width(b.grid as usize, iw);
462        }
463        let w = l.next_w;
464        if b.flags & BOX_V != 0 {
465            // vertical box: the width is its cross axis
466            if w > 0 {
467                return (self.dp(w), 1);
468            }
469            if w < 0 || b.flags & (BOX_XCENTER | BOX_XSTART | BOX_XEND) == 0 {
470                return (b.len[0], 1);
471            }
472            // natural width, at most the box's
473            if b.len[0] <= 0 {
474                return (iw, 0);
475            }
476            return (b.len[0].min(iw), 0);
477        }
478        if w > 0 {
479            return (self.dp(w), 1);
480        }
481        let wrap = wraps(b.flags);
482        let spare = b.len[0].wrapping_sub(b.fixedp).max(0);
483        if w < 0 && !(wrap && (b.wsump == 0 || spare == 0)) {
484            // fill weight: its share of what the fixed children left (previous pass)
485            if b.wsump == 0 {
486                return (0, 1);
487            }
488            return (spare.wrapping_mul(w.wrapping_neg()) / b.wsump, 1);
489        }
490        if wrap {
491            // a wrapping box (natural, or fill where it wraps): at most the box's width (a
492            // child that does not fit after the others of its row starts the next one)
493            if b.len[0] <= 0 {
494                return (iw, 0);
495            }
496            return (b.len[0].min(iw), 0);
497        }
498        // natural: at most the space left on the row
499        let mut rest = b.pos[0].wrapping_add(b.len[0]).wrapping_sub(b.cur);
500        if b.n != 0 {
501            rest = rest.wrapping_sub(b.gap);
502        }
503        if rest <= 0 {
504            return (iw, 0);
505        }
506        (rest.min(iw), 0)
507    }
508
509    /// ui_box_begin(id, flags, gap): open a container (id 0 = automatic)
510    #[cfg_attr(jfgui_dev, track_caller)]
511    pub fn box_begin(&mut self, id: u32, flags: u32, gap: i32) {
512        let _src = self.src_scope();
513        let id = if id != 0 {
514            id
515        } else {
516            // automatic id from the parent id and the index among its automatic-id boxes
517            // (the child count would miss absolutely placed boxes)
518            let t = self.top();
519            let k = t.id.wrapping_mul(31).wrapping_add(t.nbox);
520            t.nbox = t.nbox.wrapping_add(1);
521            k.wrapping_add(0x2545_F491).wrapping_mul(0x9E37_79B1) | 0x8000_0000
522        };
523        let s = *self.slot(id);
524        let pad = self.dp_space(((flags >> PAD_SHIFT) & 255) as i32); // (theme) spacing
525        let gap = self.dp_space(gap);
526        // natural size from the previous pass (a wrapping box: one row, but at most as wide as
527        // a wrapping text would be there)
528        let pgrid = self.wp.lay.boxes.last().map_or(0, |b| b.grid);
529        let mw = if wraps(flags) { Some(self.avail_width(s.measw).0).filter(|&w| w > 0).unwrap_or(s.measw) } else { s.measw };
530        self.wp.lay.placing_box = true;
531        let r = self.place(mw, s.meash);
532        self.wp.lay.placing_box = false;
533        let gcell = if pgrid != 0 { self.wp.lay.grid.last } else { (0, [0; 4]) };
534        let auto = self.wp.lay.place_auto;
535        let pmain = self.wp.lay.place_main;
536        // decorations
537        if flags & (BOX_BG | BOX_CARD | BOX_LAYER) != 0 {
538            let st = self.style;
539            let mut sp = Sp { flags, ..Sp::default() };
540            sp.place(r);
541            st.box_deco(self, &sp);
542        }
543        if flags & BOX_BLOCK != 0 {
544            self.hit(id, HB_BLOCK);
545        }
546        if flags & BOX_DISABLED != 0 {
547            self.disabled_push();
548        }
549        if flags & BOX_CLIP != 0 {
550            self.clip_push(r[0], r[1], r[2], r[3]);
551        }
552        // push the frame
553        if self.wp.lay.boxes.len() >= BOX_STACK_MAX {
554            self.warn("boxes nested too deeply");
555            return;
556        }
557        let a = (flags & BOX_V) as usize;
558        let mut b = BoxFrame {
559            id,
560            flags,
561            pos: [r[0].wrapping_add(pad), r[1].wrapping_add(pad)],
562            len: [r[2].wrapping_sub(pad).wrapping_sub(pad).max(0), r[3].wrapping_sub(pad).wrapping_sub(pad).max(0)],
563            gap,
564            pad,
565            auto,
566            pmain,
567            fixedp: s.fixed,
568            wsump: s.wsum,
569            gcell,
570            ..BoxFrame::default()
571        };
572        // cursor at the start of the main axis, shifted for centre / end alignment (a wrapping
573        // box: its first row, with the previous pass's rows)
574        b.cur = b.pos[a];
575        if wraps(flags) {
576            let prev = self.wp.lay.wrap_rows.get(&id).cloned().unwrap_or_default();
577            b.cur = b.cur.wrapping_add(row_start(flags, b.len[a], prev.first()));
578            self.wp.lay.wraps.push(Wrap { rows: alloc::vec![Row::default()], prev, ..Wrap::default() });
579        } else if flags & (BOX_CENTER | BOX_END) != 0 && b.wsump == 0 {
580            let spare = b.len[a].wrapping_sub(b.fixedp);
581            if spare > 0 {
582                b.cur = b.cur.wrapping_add(if flags & BOX_CENTER != 0 { spare >> 1 } else { spare });
583            }
584        }
585        self.wp.lay.boxes.push(b);
586        if self.m.ins.on {
587            self.ins_box(id, r); // the inspector's tree and outlines (inspect.rs)
588        }
589    }
590
591    /// ui_box_end: store the measured natural size, correct the parent's totals
592    #[cfg_attr(jfgui_dev, track_caller)]
593    pub fn box_end(&mut self) {
594        let _src = self.src_scope();
595        if self.wp.lay.boxes.len() <= 1 {
596            return; // never pop the root here
597        }
598        let mut b = *self.top();
599        // a wrapping box: as high as its rows and the gaps between them; changed rows align
600        // their children again in another pass
601        if wraps(b.flags) {
602            if let Some(w) = self.wp.lay.wraps.pop() {
603                b.cross = w.top.wrapping_add(w.rows.last().map_or(0, |r| r.h));
604                if self.wp.lay.wrap_rows.get(&b.id) != Some(&w.rows) {
605                    self.repass = true;
606                    self.wp.lay.wrap_rows.insert(b.id, w.rows);
607                }
608            }
609        }
610        // measured natural size (content + padding)
611        let p2 = b.pad.wrapping_add(b.pad);
612        let (mut w, mut h) = (b.fixed.wrapping_add(p2), b.cross.wrapping_add(p2));
613        if b.flags & BOX_V != 0 {
614            core::mem::swap(&mut w, &mut h);
615        }
616        let s = *self.slot(b.id);
617        let size_changed = b.flags & BOX_FIXED == 0 && (w != s.measw || h != s.meash);
618        if size_changed || b.fixed != s.fixed || b.wsum != s.wsum {
619            self.repass = true;
620            if self.ui_debug {
621                let m = alloc::format!(
622                    "layout f{}: box {:x} {}x{} -> {}x{} fixed {}->{} wsum {}->{}\n",
623                    self.frame as i32, b.id, s.measw, s.meash, w, h, s.fixed, b.fixed, s.wsum, b.wsum
624                );
625                plat::write_stderr(m.as_bytes());
626            }
627        }
628        {
629            let s = self.slot(b.id);
630            s.measw = w;
631            s.meash = h;
632            s.fixed = b.fixed;
633            s.wsum = b.wsum;
634        }
635        // late correction: the parent accounted for our previous natural size (a grid parent
636        // takes this pass's size for the cell)
637        let n = self.wp.lay.boxes.len();
638        if n >= 2 && self.wp.lay.boxes[n - 2].grid != 0 && b.gcell.0 != 0 {
639            let k = self.wp.lay.boxes[n - 2].grid as usize;
640            self.grid_child_measured(k, b.gcell.1, w, h);
641        }
642        if n >= 2 {
643            let l = &mut self.wp.lay;
644            let p = &mut l.boxes[n - 2];
645            let (mut along, mut across) = (w, h);
646            if p.flags & BOX_V != 0 {
647                core::mem::swap(&mut along, &mut across);
648            }
649            // a wrapping parent: the same for the row we are on
650            let mut row = if wraps(p.flags) { l.wraps.last_mut().and_then(|w| w.rows.last_mut()) } else { None };
651            if b.auto & 1 != 0 {
652                p.fixed = p.fixed.wrapping_add(along.wrapping_sub(b.pmain));
653                if let Some(r) = row.as_deref_mut() {
654                    r.w = r.w.wrapping_add(along.wrapping_sub(b.pmain));
655                }
656            }
657            // natural cross size: aligned (2) and stretched / filling (4) child boxes count
658            // here with this pass's measurement (not at placement with the previous one), so
659            // a changed subtree settles in one pass instead of one pass per nesting level
660            if b.auto & 6 != 0 {
661                if across > p.cross {
662                    p.cross = across;
663                }
664                if let Some(r) = row.filter(|r| across > r.h) {
665                    r.h = across;
666                }
667            }
668        }
669        self.wp.lay.boxes.pop();
670        if self.m.ins.on {
671            self.ins_box_end();
672        }
673        if b.flags & BOX_CLIP != 0 {
674            self.clip_pop();
675        }
676        if b.flags & BOX_DISABLED != 0 {
677            self.disabled_pop();
678        }
679    }
680
681    /// ui_spacer(dip): empty space along the main axis (< 0 = fill weight)
682    pub fn spacer(&mut self, dip: i32) {
683        if self.top().flags & BOX_V != 0 {
684            self.wp.lay.next_h = dip;
685            self.wp.lay.next_w = 1;
686        } else {
687            self.wp.lay.next_w = dip;
688            self.wp.lay.next_h = 1;
689        }
690        self.place(0, 0);
691    }
692
693    /// ui_dp(dip): DIP -> physical pixels, round(dip * dpi / 96) (signed)
694    pub fn dp(&self, dip: i32) -> i32 {
695        dp_at(dip, self.wp.rt.dpi)
696    }
697
698    /// ui_px2dip(px): physical pixels -> DIP, (px * 96 + dpi / 2) / dpi
699    pub fn px2dip(&self, px: i32) -> i32 {
700        let dpi = self.wp.rt.dpi;
701        px.wrapping_mul(96).wrapping_add(((dpi as u32) >> 1) as i32) / dpi
702    }
703
704    /// ui_dpi: current dpi of the live window (96 = 100 %)
705    pub fn dpi(&self) -> i32 {
706        self.wp.rt.dpi
707    }
708
709    /// ui_set_dpi(dpi): override the dpi (tests)
710    pub fn set_dpi(&mut self, dpi: i32) {
711        self.wp.rt.dpi = dpi;
712    }
713
714    /// dp_fx(dip_fx) -> px in 24.8 (dip_fx * dpi / 96, truncated)
715    pub fn dp_fx(&self, dip_fx: i32) -> i32 {
716        dip_fx.wrapping_mul(self.wp.rt.dpi) / 96
717    }
718}
719
720/// dip2px_at / dp_at: round(dip * dpi / 96) (signed)
721pub fn dp_at(dip: i32, dpi: i32) -> i32 {
722    let v = dip.wrapping_mul(dpi);
723    if v >= 0 { (v + 48) / 96 } else { (v - 48) / 96 }
724}
725
726/// px2dip_at: round(px * 96 / dpi), dpi 0 = 96 (ui_win.s)
727pub fn px2dip_at(px: i32, dpi: i32) -> i32 {
728    let dpi = if dpi == 0 { 96 } else { dpi };
729    let v = px.wrapping_mul(96);
730    let half = ((dpi as u32) >> 1) as i32;
731    if v >= 0 { v.wrapping_add(half) / dpi } else { v.wrapping_sub(half) / dpi }
732}