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