Skip to main content

proof_engine/ui/
layout.rs

1//! Layout system for UI elements.
2//!
3//! Provides both the legacy anchor-based world-space layout and a new
4//! flex/grid/absolute/stack/flow layout system for pixel-space UI.
5//!
6//! ## Legacy types (world-space, used by UiRoot)
7//! - [`UiRect`], [`Anchor`], [`UiLayout`], [`AutoLayout`]
8//!
9//! ## New layout types (pixel-space)
10//! - [`Constraint`], [`FlexLayout`], [`GridLayout`], [`AbsoluteLayout`]
11//! - [`StackLayout`], [`FlowLayout`], [`LayoutNode`]
12//! - [`ResponsiveBreakpoints`], [`SafeAreaInsets`]
13
14use glam::{Vec2, Vec3};
15use super::framework::Rect;
16
17// ═══════════════════════════════════════════════════════════════════════════════
18// LEGACY WORLD-SPACE LAYOUT (preserved from original layout.rs)
19// ═══════════════════════════════════════════════════════════════════════════════
20
21// ── UiRect ────────────────────────────────────────────────────────────────────
22
23/// A 2D rectangle in world space, used for layout calculations.
24#[derive(Clone, Copy, Debug)]
25pub struct UiRect {
26    pub min: Vec2,
27    pub max: Vec2,
28}
29
30impl UiRect {
31    pub fn new(min: Vec2, max: Vec2) -> Self { Self { min, max } }
32
33    pub fn from_center_size(center: Vec2, size: Vec2) -> Self {
34        Self { min: center - size * 0.5, max: center + size * 0.5 }
35    }
36
37    pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self {
38        Self { min: pos, max: pos + size }
39    }
40
41    pub fn width(&self)  -> f32 { self.max.x - self.min.x }
42    pub fn height(&self) -> f32 { self.max.y - self.min.y }
43    pub fn size(&self)   -> Vec2 { Vec2::new(self.width(), self.height()) }
44    pub fn center(&self) -> Vec2 { (self.min + self.max) * 0.5 }
45
46    pub fn contains(&self, p: Vec2) -> bool {
47        p.x >= self.min.x && p.x <= self.max.x &&
48        p.y >= self.min.y && p.y <= self.max.y
49    }
50
51    pub fn expand(&self, margin: f32) -> Self {
52        Self { min: self.min - Vec2::splat(margin), max: self.max + Vec2::splat(margin) }
53    }
54
55    pub fn shrink(&self, padding: f32) -> Self {
56        Self {
57            min: self.min + Vec2::splat(padding),
58            max: (self.max - Vec2::splat(padding)).max(self.min),
59        }
60    }
61
62    pub fn split_vertical(&self, ratio: f32) -> (Self, Self) {
63        let mid = self.min.y + self.height() * ratio.clamp(0.0, 1.0);
64        (
65            Self::new(self.min, Vec2::new(self.max.x, mid)),
66            Self::new(Vec2::new(self.min.x, mid), self.max),
67        )
68    }
69
70    pub fn split_horizontal(&self, ratio: f32) -> (Self, Self) {
71        let mid = self.min.x + self.width() * ratio.clamp(0.0, 1.0);
72        (
73            Self::new(self.min, Vec2::new(mid, self.max.y)),
74            Self::new(Vec2::new(mid, self.min.y), self.max),
75        )
76    }
77
78    pub fn grid(&self, cols: usize, rows: usize) -> Vec<Self> {
79        let cols   = cols.max(1);
80        let rows   = rows.max(1);
81        let cell_w = self.width()  / cols as f32;
82        let cell_h = self.height() / rows as f32;
83        let mut cells = Vec::with_capacity(cols * rows);
84        for row in 0..rows {
85            for col in 0..cols {
86                let min = Vec2::new(self.min.x + col as f32 * cell_w, self.min.y + row as f32 * cell_h);
87                cells.push(UiRect::new(min, min + Vec2::new(cell_w, cell_h)));
88            }
89        }
90        cells
91    }
92}
93
94// ── Anchor ────────────────────────────────────────────────────────────────────
95
96/// Screen anchor for positioning UI elements.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum Anchor {
99    TopLeft, TopCenter, TopRight,
100    MiddleLeft, Center, MiddleRight,
101    BottomLeft, BottomCenter, BottomRight,
102}
103
104impl Anchor {
105    pub fn point(&self, screen: &UiRect) -> Vec2 {
106        match self {
107            Anchor::TopLeft      => Vec2::new(screen.min.x, screen.max.y),
108            Anchor::TopCenter    => Vec2::new(screen.center().x, screen.max.y),
109            Anchor::TopRight     => Vec2::new(screen.max.x, screen.max.y),
110            Anchor::MiddleLeft   => Vec2::new(screen.min.x, screen.center().y),
111            Anchor::Center       => screen.center(),
112            Anchor::MiddleRight  => Vec2::new(screen.max.x, screen.center().y),
113            Anchor::BottomLeft   => Vec2::new(screen.min.x, screen.min.y),
114            Anchor::BottomCenter => Vec2::new(screen.center().x, screen.min.y),
115            Anchor::BottomRight  => Vec2::new(screen.max.x, screen.min.y),
116        }
117    }
118
119    pub fn stack_dir(&self) -> Vec2 {
120        match self {
121            Anchor::TopLeft | Anchor::TopCenter | Anchor::TopRight     => Vec2::new(0.0, -1.0),
122            Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight => Vec2::new(0.0, 1.0),
123            Anchor::MiddleLeft  => Vec2::new(1.0, 0.0),
124            Anchor::MiddleRight => Vec2::new(-1.0, 0.0),
125            Anchor::Center      => Vec2::new(0.0, -1.0),
126        }
127    }
128}
129
130// ── UiLayout ──────────────────────────────────────────────────────────────────
131
132pub struct UiLayout {
133    pub screen_rect: UiRect,
134    pub anchor:      Anchor,
135    pub line_height: f32,
136    pub margin:      Vec2,
137    cursor:          Vec2,
138}
139
140impl UiLayout {
141    pub fn new(screen_rect: UiRect, anchor: Anchor, line_height: f32, margin: Vec2) -> Self {
142        let anchor_pt = anchor.point(&screen_rect);
143        Self {
144            screen_rect, anchor, line_height, margin,
145            cursor: anchor_pt + margin * Vec2::new(
146                if matches!(anchor, Anchor::TopRight | Anchor::MiddleRight | Anchor::BottomRight) { -1.0 } else { 1.0 },
147                if matches!(anchor, Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight) { 1.0 } else { -1.0 },
148            ),
149        }
150    }
151
152    pub fn next_line(&mut self) -> Vec3 {
153        let pos = Vec3::new(self.cursor.x, self.cursor.y, 1.0);
154        let dir = self.anchor.stack_dir();
155        self.cursor += dir * self.line_height;
156        pos
157    }
158
159    pub fn skip_lines(&mut self, n: usize) {
160        let dir = self.anchor.stack_dir();
161        self.cursor += dir * self.line_height * n as f32;
162    }
163
164    pub fn col_offset(&self, col: f32) -> Vec3 {
165        Vec3::new(self.cursor.x + col, self.cursor.y, 1.0)
166    }
167
168    pub fn reset(&mut self) {
169        let ap = self.anchor.point(&self.screen_rect);
170        self.cursor = ap + self.margin * Vec2::new(
171            if matches!(self.anchor, Anchor::TopRight | Anchor::MiddleRight | Anchor::BottomRight) { -1.0 } else { 1.0 },
172            if matches!(self.anchor, Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight) { 1.0 } else { -1.0 },
173        );
174    }
175
176    pub fn from_camera(cam_target: Vec2, cam_z: f32, fov_deg: f32, aspect: f32, anchor: Anchor, line_height: f32, margin: Vec2) -> Self {
177        let half_h = cam_z * (fov_deg.to_radians() * 0.5).tan();
178        let half_w = half_h * aspect;
179        let screen = UiRect::new(Vec2::new(cam_target.x - half_w, cam_target.y - half_h), Vec2::new(cam_target.x + half_w, cam_target.y + half_h));
180        Self::new(screen, anchor, line_height, margin)
181    }
182}
183
184// ── AutoLayout ────────────────────────────────────────────────────────────────
185
186pub struct AutoLayout {
187    pub origin:   Vec2,
188    pub cell_w:   f32,
189    pub cell_h:   f32,
190    pub cols:     usize,
191    cursor_col:   usize,
192    cursor_row:   usize,
193}
194
195impl AutoLayout {
196    pub fn new(origin: Vec2, cell_w: f32, cell_h: f32, cols: usize) -> Self {
197        Self { origin, cell_w, cell_h, cols: cols.max(1), cursor_col: 0, cursor_row: 0 }
198    }
199
200    pub fn next(&mut self) -> Vec3 {
201        let x = self.origin.x + self.cursor_col as f32 * self.cell_w;
202        let y = self.origin.y - self.cursor_row as f32 * self.cell_h;
203        self.cursor_col += 1;
204        if self.cursor_col >= self.cols { self.cursor_col = 0; self.cursor_row += 1; }
205        Vec3::new(x, y, 1.0)
206    }
207
208    pub fn reset(&mut self) { self.cursor_col = 0; self.cursor_row = 0; }
209}
210
211// ═══════════════════════════════════════════════════════════════════════════════
212// NEW PIXEL-SPACE LAYOUT SYSTEM
213// ═══════════════════════════════════════════════════════════════════════════════
214
215// ── Constraint ────────────────────────────────────────────────────────────────
216
217/// Size constraint for layout nodes.
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub enum Constraint {
220    /// Exactly this many pixels.
221    Exact(f32),
222    /// At least `min` pixels.
223    Min(f32),
224    /// At most `max` pixels.
225    Max(f32),
226    /// Between `min` and `max`.
227    MinMax { min: f32, max: f32 },
228    /// Fill remaining space (with optional weight).
229    Fill(f32),
230}
231
232impl Constraint {
233    /// Resolve to a pixel value given available space.
234    pub fn resolve(&self, available: f32, fill_unit: f32) -> f32 {
235        match self {
236            Constraint::Exact(v)          => *v,
237            Constraint::Min(v)            => v.max(0.0),
238            Constraint::Max(v)            => available.min(*v),
239            Constraint::MinMax { min, max } => available.clamp(*min, *max),
240            Constraint::Fill(w)           => (fill_unit * w).max(0.0),
241        }
242    }
243}
244
245// ── Axis ──────────────────────────────────────────────────────────────────────
246
247/// Layout axis.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum Axis { Horizontal, Vertical }
250
251// ── JustifyContent ────────────────────────────────────────────────────────────
252
253/// Main-axis content distribution for flex/grid.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum JustifyContent {
256    Start,
257    End,
258    Center,
259    SpaceBetween,
260    SpaceAround,
261    SpaceEvenly,
262}
263
264// ── CrossAlign ────────────────────────────────────────────────────────────────
265
266/// Cross-axis alignment.
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum CrossAlign {
269    Start,
270    Center,
271    End,
272    Stretch,
273    Baseline,
274}
275
276// ── FlexWrap ─────────────────────────────────────────────────────────────────
277
278/// Whether flex items wrap to a new line.
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum FlexWrap {
281    NoWrap,
282    Wrap,
283    WrapReverse,
284}
285
286// ── FlexLayout ────────────────────────────────────────────────────────────────
287
288/// An item in a flex layout.
289#[derive(Debug, Clone)]
290pub struct FlexItem {
291    pub constraint: Constraint,
292    pub cross:      Constraint,
293    pub flex_grow:  f32,
294    pub flex_shrink: f32,
295    pub align_self: Option<CrossAlign>,
296}
297
298impl FlexItem {
299    pub fn new(constraint: Constraint) -> Self {
300        Self { constraint, cross: Constraint::Fill(1.0), flex_grow: 0.0, flex_shrink: 1.0, align_self: None }
301    }
302    pub fn with_grow(mut self, g: f32) -> Self { self.flex_grow = g; self }
303    pub fn with_shrink(mut self, s: f32) -> Self { self.flex_shrink = s; self }
304    pub fn with_align(mut self, a: CrossAlign) -> Self { self.align_self = Some(a); self }
305}
306
307/// Flex-style layout (row or column).
308#[derive(Debug, Clone)]
309pub struct FlexLayout {
310    pub axis:     Axis,
311    pub justify:  JustifyContent,
312    pub align:    CrossAlign,
313    pub wrap:     FlexWrap,
314    pub gap:      f32,
315    pub padding:  f32,
316}
317
318impl FlexLayout {
319    pub fn row() -> Self {
320        Self { axis: Axis::Horizontal, justify: JustifyContent::Start, align: CrossAlign::Start, wrap: FlexWrap::NoWrap, gap: 0.0, padding: 0.0 }
321    }
322
323    pub fn column() -> Self {
324        Self { axis: Axis::Vertical, justify: JustifyContent::Start, align: CrossAlign::Start, wrap: FlexWrap::NoWrap, gap: 0.0, padding: 0.0 }
325    }
326
327    pub fn with_justify(mut self, j: JustifyContent) -> Self { self.justify = j; self }
328    pub fn with_align(mut self, a: CrossAlign) -> Self { self.align = a; self }
329    pub fn with_wrap(mut self, w: FlexWrap) -> Self { self.wrap = w; self }
330    pub fn with_gap(mut self, g: f32) -> Self { self.gap = g; self }
331    pub fn with_padding(mut self, p: f32) -> Self { self.padding = p; self }
332
333    /// Compute child rects within `parent`.
334    pub fn compute(&self, parent: Rect, items: &[FlexItem]) -> Vec<Rect> {
335        let inner  = parent.shrink(self.padding);
336        let n      = items.len();
337        if n == 0 { return Vec::new(); }
338
339        let is_row = self.axis == Axis::Horizontal;
340        let main   = if is_row { inner.w } else { inner.h };
341        let cross  = if is_row { inner.h } else { inner.w };
342        let gaps   = self.gap * (n.saturating_sub(1)) as f32;
343
344        // First pass: base sizes
345        let total_fill_w: f32 = items.iter().map(|i| {
346            if let Constraint::Fill(w) = i.constraint { w } else { 0.0 }
347        }).sum();
348
349        let fixed_total: f32 = items.iter().map(|i| {
350            match i.constraint {
351                Constraint::Exact(v) | Constraint::Min(v) => v,
352                Constraint::Fill(_)   => 0.0,
353                Constraint::Max(v)    => v,
354                Constraint::MinMax { min, .. } => min,
355            }
356        }).sum::<f32>() + gaps;
357
358        let fill_avail = (main - fixed_total).max(0.0);
359        let fill_unit  = if total_fill_w > 0.0 { fill_avail / total_fill_w } else { 0.0 };
360
361        let sizes: Vec<f32> = items.iter().map(|i| i.constraint.resolve(main, fill_unit)).collect();
362        let total_used: f32 = sizes.iter().sum::<f32>() + gaps;
363
364        let mut cursor = match self.justify {
365            JustifyContent::Start        => if is_row { inner.x } else { inner.y },
366            JustifyContent::End          => if is_row { inner.x + main - total_used } else { inner.y + main - total_used },
367            JustifyContent::Center       => if is_row { inner.x + (main - total_used) * 0.5 } else { inner.y + (main - total_used) * 0.5 },
368            JustifyContent::SpaceBetween => if is_row { inner.x } else { inner.y },
369            JustifyContent::SpaceAround  => {
370                let slack = (main - total_used) / n as f32;
371                if is_row { inner.x + slack * 0.5 } else { inner.y + slack * 0.5 }
372            }
373            JustifyContent::SpaceEvenly  => {
374                let slack = (main - total_used) / (n + 1) as f32;
375                if is_row { inner.x + slack } else { inner.y + slack }
376            }
377        };
378
379        let between_gap = match self.justify {
380            JustifyContent::SpaceBetween => if n > 1 { (main - total_used + gaps) / (n - 1) as f32 } else { 0.0 },
381            JustifyContent::SpaceAround  => (main - total_used + gaps) / n as f32,
382            JustifyContent::SpaceEvenly  => (main - total_used + gaps) / (n + 1) as f32,
383            _                            => self.gap,
384        };
385
386        let mut result = Vec::with_capacity(n);
387        for (i, item) in items.iter().enumerate() {
388            let item_main  = sizes[i];
389            let item_cross = match item.cross {
390                Constraint::Exact(v) | Constraint::Min(v) => v,
391                Constraint::Fill(w)  => cross * w,
392                Constraint::Max(v)   => v.min(cross),
393                Constraint::MinMax { min, max } => cross.clamp(min, max),
394            };
395            let align = item.align_self.unwrap_or(self.align);
396            let cross_off = match align {
397                CrossAlign::Start    | CrossAlign::Baseline => if is_row { inner.y } else { inner.x },
398                CrossAlign::End      => if is_row { inner.y + cross - item_cross } else { inner.x + cross - item_cross },
399                CrossAlign::Center   => if is_row { inner.y + (cross - item_cross) * 0.5 } else { inner.x + (cross - item_cross) * 0.5 },
400                CrossAlign::Stretch  => if is_row { inner.y } else { inner.x },
401            };
402            let (x, y, w, h) = if is_row {
403                let ic = if matches!(align, CrossAlign::Stretch) { cross } else { item_cross };
404                (cursor, cross_off, item_main, ic)
405            } else {
406                let ic = if matches!(align, CrossAlign::Stretch) { cross } else { item_cross };
407                (cross_off, cursor, ic, item_main)
408            };
409            result.push(Rect::new(x, y, w, h));
410            cursor += item_main;
411            if i + 1 < n { cursor += between_gap; }
412        }
413        result
414    }
415}
416
417// ── GridLayout ────────────────────────────────────────────────────────────────
418
419/// A column or row track definition.
420#[derive(Debug, Clone, Copy)]
421pub enum Track {
422    Fixed(f32),
423    Fr(f32),      // fractional unit
424    Auto,
425    MinMax { min: f32, max: f32 },
426}
427
428/// A grid cell placement.
429#[derive(Debug, Clone, Copy)]
430pub struct GridPlacement {
431    pub col:      usize,
432    pub row:      usize,
433    pub col_span: usize,
434    pub row_span: usize,
435}
436
437impl GridPlacement {
438    pub fn at(col: usize, row: usize) -> Self { Self { col, row, col_span: 1, row_span: 1 } }
439    pub fn span(mut self, col_span: usize, row_span: usize) -> Self { self.col_span = col_span; self.row_span = row_span; self }
440}
441
442/// CSS-like grid layout.
443#[derive(Debug, Clone)]
444pub struct GridLayout {
445    pub columns:     Vec<Track>,
446    pub rows:        Vec<Track>,
447    pub col_gap:     f32,
448    pub row_gap:     f32,
449    pub padding:     f32,
450    pub auto_fill:   bool,
451    pub auto_fit:    bool,
452    pub auto_col_w:  f32,
453}
454
455impl GridLayout {
456    pub fn new(columns: Vec<Track>, rows: Vec<Track>) -> Self {
457        Self { columns, rows, col_gap: 4.0, row_gap: 4.0, padding: 0.0, auto_fill: false, auto_fit: false, auto_col_w: 100.0 }
458    }
459
460    pub fn with_gap(mut self, col: f32, row: f32) -> Self { self.col_gap = col; self.row_gap = row; self }
461    pub fn with_padding(mut self, p: f32) -> Self { self.padding = p; self }
462    pub fn with_auto_fill(mut self, col_w: f32) -> Self { self.auto_fill = true; self.auto_col_w = col_w; self }
463
464    fn resolve_tracks(tracks: &[Track], available: f32, gap: f32) -> Vec<f32> {
465        let n          = tracks.len().max(1);
466        let total_gaps = gap * (n.saturating_sub(1)) as f32;
467        let avail      = (available - total_gaps).max(0.0);
468        let total_fr: f32 = tracks.iter().map(|t| if let Track::Fr(f) = t { *f } else { 0.0 }).sum();
469        let fixed_total: f32 = tracks.iter().map(|t| match t {
470            Track::Fixed(v) => *v,
471            Track::Auto     => 50.0,
472            Track::MinMax { min, .. } => *min,
473            Track::Fr(_)    => 0.0,
474        }).sum();
475        let fr_unit = if total_fr > 0.0 { (avail - fixed_total).max(0.0) / total_fr } else { 0.0 };
476
477        tracks.iter().map(|t| match t {
478            Track::Fixed(v)           => *v,
479            Track::Fr(f)              => fr_unit * f,
480            Track::Auto               => 50.0,
481            Track::MinMax { min, max } => fr_unit.clamp(*min, *max),
482        }).collect()
483    }
484
485    /// Compute cell rects for `placements`.
486    pub fn compute(&self, parent: Rect, placements: &[GridPlacement]) -> Vec<Rect> {
487        let inner  = parent.shrink(self.padding);
488        let col_ws = Self::resolve_tracks(&self.columns, inner.w, self.col_gap);
489        let row_hs = Self::resolve_tracks(&self.rows,    inner.h, self.row_gap);
490
491        let mut col_x = Vec::with_capacity(col_ws.len());
492        let mut x = inner.x;
493        for (i, &cw) in col_ws.iter().enumerate() {
494            col_x.push(x);
495            x += cw + if i + 1 < col_ws.len() { self.col_gap } else { 0.0 };
496        }
497
498        let mut row_y = Vec::with_capacity(row_hs.len());
499        let mut y = inner.y;
500        for (i, &rh) in row_hs.iter().enumerate() {
501            row_y.push(y);
502            y += rh + if i + 1 < row_hs.len() { self.row_gap } else { 0.0 };
503        }
504
505        placements.iter().map(|p| {
506            let px  = col_x.get(p.col).copied().unwrap_or(inner.x);
507            let py  = row_y.get(p.row).copied().unwrap_or(inner.y);
508            let pw: f32 = (0..p.col_span).filter_map(|i| {
509                let ci = p.col + i;
510                col_ws.get(ci).copied()
511            }).sum::<f32>() + if p.col_span > 1 { self.col_gap * (p.col_span - 1) as f32 } else { 0.0 };
512            let ph: f32 = (0..p.row_span).filter_map(|i| {
513                let ri = p.row + i;
514                row_hs.get(ri).copied()
515            }).sum::<f32>() + if p.row_span > 1 { self.row_gap * (p.row_span - 1) as f32 } else { 0.0 };
516            Rect::new(px, py, pw, ph)
517        }).collect()
518    }
519}
520
521// ── AbsoluteLayout ────────────────────────────────────────────────────────────
522
523/// Anchor point for absolute positioning.
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum AnchorPoint {
526    TopLeft, TopCenter, TopRight,
527    CenterLeft, Center, CenterRight,
528    BottomLeft, BottomCenter, BottomRight,
529}
530
531/// Absolutely positioned element.
532#[derive(Debug, Clone)]
533pub struct AbsoluteItem {
534    pub anchor:     AnchorPoint,
535    pub offset_x:   f32,
536    pub offset_y:   f32,
537    pub w:          f32,
538    pub h:          f32,
539}
540
541impl AbsoluteItem {
542    pub fn new(anchor: AnchorPoint, w: f32, h: f32) -> Self {
543        Self { anchor, offset_x: 0.0, offset_y: 0.0, w, h }
544    }
545    pub fn with_offset(mut self, dx: f32, dy: f32) -> Self { self.offset_x = dx; self.offset_y = dy; self }
546}
547
548/// Absolute positioning layout (items placed relative to parent edges/center).
549pub struct AbsoluteLayout;
550
551impl AbsoluteLayout {
552    /// Compute rect for an absolutely positioned item within `parent`.
553    pub fn compute(parent: Rect, item: &AbsoluteItem) -> Rect {
554        let (ax, ay) = match item.anchor {
555            AnchorPoint::TopLeft      => (parent.x, parent.y),
556            AnchorPoint::TopCenter    => (parent.center_x() - item.w * 0.5, parent.y),
557            AnchorPoint::TopRight     => (parent.max_x() - item.w, parent.y),
558            AnchorPoint::CenterLeft   => (parent.x, parent.center_y() - item.h * 0.5),
559            AnchorPoint::Center       => (parent.center_x() - item.w * 0.5, parent.center_y() - item.h * 0.5),
560            AnchorPoint::CenterRight  => (parent.max_x() - item.w, parent.center_y() - item.h * 0.5),
561            AnchorPoint::BottomLeft   => (parent.x, parent.max_y() - item.h),
562            AnchorPoint::BottomCenter => (parent.center_x() - item.w * 0.5, parent.max_y() - item.h),
563            AnchorPoint::BottomRight  => (parent.max_x() - item.w, parent.max_y() - item.h),
564        };
565        Rect::new(ax + item.offset_x, ay + item.offset_y, item.w, item.h)
566    }
567
568    /// Compute multiple items.
569    pub fn compute_all(parent: Rect, items: &[AbsoluteItem]) -> Vec<Rect> {
570        items.iter().map(|i| Self::compute(parent, i)).collect()
571    }
572}
573
574// ── StackLayout ───────────────────────────────────────────────────────────────
575
576/// Cross-axis alignment for stack layout.
577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578pub enum StackAlign {
579    Start,
580    Center,
581    End,
582    Stretch,
583}
584
585/// A stack of overlapping items, ordered by z-index.
586pub struct StackLayout {
587    pub align:   StackAlign,
588    pub z_items: Vec<(i32, Rect)>,
589}
590
591impl StackLayout {
592    pub fn new(align: StackAlign) -> Self { Self { align, z_items: Vec::new() } }
593
594    /// Add an item at a given z-index.
595    pub fn push(&mut self, z: i32, rect: Rect) { self.z_items.push((z, rect)); }
596
597    /// Compute aligned rects for all items within `parent`, sorted by z.
598    pub fn compute(&self, parent: Rect, sizes: &[(f32, f32)]) -> Vec<Rect> {
599        sizes.iter().map(|&(w, h)| {
600            let (x, y) = match self.align {
601                StackAlign::Start   => (parent.x, parent.y),
602                StackAlign::Center  => (parent.center_x() - w * 0.5, parent.center_y() - h * 0.5),
603                StackAlign::End     => (parent.max_x() - w, parent.max_y() - h),
604                StackAlign::Stretch => (parent.x, parent.y),
605            };
606            let (fw, fh) = if self.align == StackAlign::Stretch { (parent.w, parent.h) } else { (w, h) };
607            Rect::new(x, y, fw, fh)
608        }).collect()
609    }
610
611    /// Sort items by z-index (ascending = bottom first).
612    pub fn sorted_z(&self) -> Vec<(i32, Rect)> {
613        let mut v = self.z_items.clone();
614        v.sort_by_key(|&(z, _)| z);
615        v
616    }
617}
618
619// ── FlowLayout ────────────────────────────────────────────────────────────────
620
621/// Wrapping inline flow layout (like CSS inline-block).
622pub struct FlowLayout {
623    pub gap_x:    f32,
624    pub gap_y:    f32,
625    pub align:    CrossAlign,
626}
627
628impl FlowLayout {
629    pub fn new() -> Self { Self { gap_x: 4.0, gap_y: 4.0, align: CrossAlign::Start } }
630    pub fn with_gap(mut self, x: f32, y: f32) -> Self { self.gap_x = x; self.gap_y = y; self }
631
632    /// Compute rects for `items` (each is (width, height)) within `parent`.
633    pub fn compute(&self, parent: Rect, items: &[(f32, f32)]) -> Vec<Rect> {
634        let mut result = Vec::with_capacity(items.len());
635        let mut x = parent.x;
636        let mut y = parent.y;
637        let mut row_h = 0.0_f32;
638
639        for &(w, h) in items {
640            if x + w > parent.max_x() && x > parent.x {
641                // Wrap to next row
642                y    += row_h + self.gap_y;
643                x     = parent.x;
644                row_h = 0.0;
645            }
646            result.push(Rect::new(x, y, w, h));
647            x      += w + self.gap_x;
648            row_h   = row_h.max(h);
649        }
650        result
651    }
652}
653
654impl Default for FlowLayout {
655    fn default() -> Self { Self::new() }
656}
657
658// ── LayoutNode ────────────────────────────────────────────────────────────────
659
660/// A node in a layout tree.
661#[derive(Debug, Clone)]
662pub struct LayoutNode {
663    pub constraint_w: Constraint,
664    pub constraint_h: Constraint,
665    pub children:     Vec<LayoutNode>,
666    /// Computed rect (set after arrange).
667    pub rect:         Rect,
668    pub flex_grow:    f32,
669    pub padding:      f32,
670}
671
672impl LayoutNode {
673    pub fn new(cw: Constraint, ch: Constraint) -> Self {
674        Self { constraint_w: cw, constraint_h: ch, children: Vec::new(), rect: Rect::zero(), flex_grow: 0.0, padding: 0.0 }
675    }
676
677    pub fn with_flex(mut self, g: f32) -> Self { self.flex_grow = g; self }
678    pub fn with_padding(mut self, p: f32) -> Self { self.padding = p; self }
679    pub fn push_child(&mut self, child: LayoutNode) { self.children.push(child); }
680
681    /// Measure pass: compute intrinsic (minimum) sizes.
682    pub fn measure(&self) -> (f32, f32) {
683        let child_w: f32 = self.children.iter().map(|c| { let (w, _) = c.measure(); w }).sum();
684        let child_h: f32 = self.children.iter().map(|c| { let (_, h) = c.measure(); h }).fold(0.0_f32, f32::max);
685
686        let base_w = match self.constraint_w {
687            Constraint::Exact(v) | Constraint::Min(v) => v,
688            Constraint::Max(v)    => v,
689            Constraint::MinMax { min, .. } => min,
690            Constraint::Fill(_)   => child_w + self.padding * 2.0,
691        };
692        let base_h = match self.constraint_h {
693            Constraint::Exact(v) | Constraint::Min(v) => v,
694            Constraint::Max(v)    => v,
695            Constraint::MinMax { min, .. } => min,
696            Constraint::Fill(_)   => child_h + self.padding * 2.0,
697        };
698        (base_w.max(child_w + self.padding * 2.0), base_h.max(child_h + self.padding * 2.0))
699    }
700
701    /// Arrange pass: assign rects to self and all children within `available`.
702    pub fn arrange(&mut self, available: Rect) {
703        let fill_unit_w = available.w;
704        let fill_unit_h = available.h;
705
706        let w = match self.constraint_w {
707            Constraint::Exact(v)           => v,
708            Constraint::Min(v)             => v.max(available.w),
709            Constraint::Max(v)             => available.w.min(v),
710            Constraint::MinMax { min, max } => available.w.clamp(min, max),
711            Constraint::Fill(f)            => fill_unit_w * f,
712        };
713        let h = match self.constraint_h {
714            Constraint::Exact(v)           => v,
715            Constraint::Min(v)             => v.max(available.h),
716            Constraint::Max(v)             => available.h.min(v),
717            Constraint::MinMax { min, max } => available.h.clamp(min, max),
718            Constraint::Fill(f)            => fill_unit_h * f,
719        };
720
721        self.rect = Rect::new(available.x, available.y, w, h);
722
723        if self.children.is_empty() { return; }
724
725        // Distribute children row-wise (simple left-to-right flex)
726        let inner      = self.rect.shrink(self.padding);
727        let total_grow: f32 = self.children.iter().map(|c| c.flex_grow.max(0.0)).sum();
728        let fixed_w: f32    = self.children.iter().map(|c| {
729            if c.flex_grow > 0.0 { 0.0 } else { let (mw, _) = c.measure(); mw }
730        }).sum();
731        let flex_avail  = (inner.w - fixed_w).max(0.0);
732        let flex_unit   = if total_grow > 0.0 { flex_avail / total_grow } else { 0.0 };
733
734        let mut cx = inner.x;
735        for child in &mut self.children {
736            let (child_w, _) = child.measure();
737            let actual_w = if child.flex_grow > 0.0 { flex_unit * child.flex_grow } else { child_w };
738            child.arrange(Rect::new(cx, inner.y, actual_w, inner.h));
739            cx += actual_w;
740        }
741    }
742}
743
744// ── ResponsiveBreakpoints ─────────────────────────────────────────────────────
745
746/// Named responsive breakpoints.
747#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
748pub enum Breakpoint { Xs, Sm, Md, Lg, Xl }
749
750/// Responsive breakpoint system.
751pub struct ResponsiveBreakpoints {
752    pub xs: f32,   // < sm
753    pub sm: f32,   // 480
754    pub md: f32,   // 768
755    pub lg: f32,   // 1024
756    pub xl: f32,   // 1280
757}
758
759impl Default for ResponsiveBreakpoints {
760    fn default() -> Self {
761        Self { xs: 0.0, sm: 480.0, md: 768.0, lg: 1024.0, xl: 1280.0 }
762    }
763}
764
765impl ResponsiveBreakpoints {
766    pub fn new() -> Self { Self::default() }
767
768    pub fn with_sm(mut self, v: f32) -> Self { self.sm = v; self }
769    pub fn with_md(mut self, v: f32) -> Self { self.md = v; self }
770    pub fn with_lg(mut self, v: f32) -> Self { self.lg = v; self }
771    pub fn with_xl(mut self, v: f32) -> Self { self.xl = v; self }
772
773    /// Return the current breakpoint for the given viewport width.
774    pub fn current_breakpoint(&self, viewport_w: f32) -> Breakpoint {
775        if viewport_w >= self.xl      { Breakpoint::Xl }
776        else if viewport_w >= self.lg { Breakpoint::Lg }
777        else if viewport_w >= self.md { Breakpoint::Md }
778        else if viewport_w >= self.sm { Breakpoint::Sm }
779        else                          { Breakpoint::Xs }
780    }
781
782    /// Choose a value based on the current breakpoint.
783    /// Pass `None` for breakpoints that should fall through to a smaller one.
784    pub fn choose<T: Clone>(
785        &self,
786        viewport_w: f32,
787        xs: T,
788        sm: Option<T>,
789        md: Option<T>,
790        lg: Option<T>,
791        xl: Option<T>,
792    ) -> T {
793        let bp = self.current_breakpoint(viewport_w);
794        match bp {
795            Breakpoint::Xl if xl.is_some() => xl.unwrap(),
796            Breakpoint::Xl | Breakpoint::Lg if lg.is_some() => lg.unwrap(),
797            Breakpoint::Xl | Breakpoint::Lg | Breakpoint::Md if md.is_some() => md.unwrap(),
798            Breakpoint::Xl | Breakpoint::Lg | Breakpoint::Md | Breakpoint::Sm if sm.is_some() => sm.unwrap(),
799            _ => xs,
800        }
801    }
802}
803
804// ── SafeAreaInsets ────────────────────────────────────────────────────────────
805
806/// Safe area insets (for notch-aware layout).
807#[derive(Debug, Clone, Copy, Default)]
808pub struct SafeAreaInsets {
809    pub top:    f32,
810    pub bottom: f32,
811    pub left:   f32,
812    pub right:  f32,
813}
814
815impl SafeAreaInsets {
816    pub fn new(top: f32, bottom: f32, left: f32, right: f32) -> Self {
817        Self { top, bottom, left, right }
818    }
819
820    pub fn uniform(v: f32) -> Self { Self { top: v, bottom: v, left: v, right: v } }
821    pub fn none()           -> Self { Self::default() }
822
823    /// Apply insets to a rect (shrink the available area).
824    pub fn apply(&self, rect: Rect) -> Rect {
825        Rect::new(
826            rect.x + self.left,
827            rect.y + self.top,
828            (rect.w - self.left - self.right).max(0.0),
829            (rect.h - self.top  - self.bottom).max(0.0),
830        )
831    }
832}
833
834// ── Tests ─────────────────────────────────────────────────────────────────────
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839    use glam::Vec2;
840
841    // ── Legacy tests ────────────────────────────────────────────────────────
842
843    #[test]
844    fn rect_contains() {
845        let r = UiRect::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
846        assert!(r.contains(Vec2::new(5.0, 5.0)));
847        assert!(!r.contains(Vec2::new(11.0, 5.0)));
848    }
849
850    #[test]
851    fn rect_grid_count() {
852        let r     = UiRect::new(Vec2::ZERO, Vec2::new(9.0, 6.0));
853        let cells = r.grid(3, 2);
854        assert_eq!(cells.len(), 6);
855    }
856
857    #[test]
858    fn rect_split_vertical() {
859        let r        = UiRect::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
860        let (top, bot) = r.split_vertical(0.5);
861        assert!((top.height() - 5.0).abs() < 1e-4);
862        assert!((bot.height() - 5.0).abs() < 1e-4);
863    }
864
865    #[test]
866    fn anchor_topleft_point() {
867        let screen = UiRect::new(Vec2::new(-5.0, -4.0), Vec2::new(5.0, 4.0));
868        let pt     = Anchor::TopLeft.point(&screen);
869        assert_eq!(pt.x, -5.0);
870        assert_eq!(pt.y,  4.0);
871    }
872
873    #[test]
874    fn auto_layout_wraps_at_cols() {
875        let mut layout = AutoLayout::new(Vec2::ZERO, 2.0, 1.5, 3);
876        for _ in 0..3 { layout.next(); }
877        let fourth = layout.next();
878        assert!((fourth.y - (-1.5)).abs() < 1e-4);
879        assert!((fourth.x - 0.0).abs() < 1e-4);
880    }
881
882    // ── New layout tests ────────────────────────────────────────────────────
883
884    #[test]
885    fn flex_layout_row_basic() {
886        let fl   = FlexLayout::row().with_gap(4.0);
887        let par  = Rect::new(0.0, 0.0, 200.0, 50.0);
888        let items = vec![
889            FlexItem::new(Constraint::Exact(80.0)),
890            FlexItem::new(Constraint::Exact(80.0)),
891        ];
892        let rects = fl.compute(par, &items);
893        assert_eq!(rects.len(), 2);
894        assert!((rects[1].x - 84.0).abs() < 1.0);
895    }
896
897    #[test]
898    fn flex_layout_fill() {
899        let fl    = FlexLayout::row();
900        let par   = Rect::new(0.0, 0.0, 300.0, 50.0);
901        let items = vec![
902            FlexItem::new(Constraint::Fill(1.0)),
903            FlexItem::new(Constraint::Fill(2.0)),
904        ];
905        let rects = fl.compute(par, &items);
906        assert_eq!(rects.len(), 2);
907        assert!((rects[0].w + rects[1].w - 300.0).abs() < 1.0);
908    }
909
910    #[test]
911    fn grid_layout_basic() {
912        let gl  = GridLayout::new(vec![Track::Fr(1.0), Track::Fr(1.0)], vec![Track::Fixed(50.0)]);
913        let par = Rect::new(0.0, 0.0, 200.0, 50.0);
914        let pl  = vec![GridPlacement::at(0, 0), GridPlacement::at(1, 0)];
915        let r   = gl.compute(par, &pl);
916        assert_eq!(r.len(), 2);
917    }
918
919    #[test]
920    fn grid_span() {
921        let gl  = GridLayout::new(vec![Track::Fr(1.0), Track::Fr(1.0), Track::Fr(1.0)], vec![Track::Fixed(100.0)]);
922        let par = Rect::new(0.0, 0.0, 300.0, 100.0);
923        let pl  = vec![GridPlacement::at(0, 0).span(2, 1)];
924        let r   = gl.compute(par, &pl);
925        // Spanning 2 columns should be wider than 1 column
926        assert!(r[0].w > 90.0);
927    }
928
929    #[test]
930    fn absolute_layout_center() {
931        let par  = Rect::new(0.0, 0.0, 400.0, 300.0);
932        let item = AbsoluteItem::new(AnchorPoint::Center, 100.0, 60.0);
933        let r    = AbsoluteLayout::compute(par, &item);
934        assert!((r.x - 150.0).abs() < 1.0);
935        assert!((r.y - 120.0).abs() < 1.0);
936    }
937
938    #[test]
939    fn flow_layout_wraps() {
940        let fl    = FlowLayout::new();
941        let par   = Rect::new(0.0, 0.0, 100.0, 200.0);
942        let items = vec![(60.0, 30.0), (60.0, 30.0), (60.0, 30.0)];
943        let r     = fl.compute(par, &items);
944        assert_eq!(r.len(), 3);
945        assert!(r[1].y > r[0].y || r[2].y > r[0].y);
946    }
947
948    #[test]
949    fn responsive_breakpoints() {
950        let bp = ResponsiveBreakpoints::default();
951        assert_eq!(bp.current_breakpoint(400.0),  Breakpoint::Xs);
952        assert_eq!(bp.current_breakpoint(800.0),  Breakpoint::Md);
953        assert_eq!(bp.current_breakpoint(1300.0), Breakpoint::Xl);
954    }
955
956    #[test]
957    fn safe_area_insets() {
958        let insets = SafeAreaInsets::new(44.0, 34.0, 0.0, 0.0);
959        let rect   = Rect::new(0.0, 0.0, 390.0, 844.0);
960        let safe   = insets.apply(rect);
961        assert!((safe.y    - 44.0).abs() < 1e-3);
962        assert!((safe.h - (844.0 - 78.0)).abs() < 1e-3);
963    }
964
965    #[test]
966    fn layout_node_arrange() {
967        let mut root = LayoutNode::new(Constraint::Exact(200.0), Constraint::Exact(100.0));
968        root.push_child(LayoutNode::new(Constraint::Fill(1.0), Constraint::Fill(1.0)).with_flex(1.0));
969        root.push_child(LayoutNode::new(Constraint::Fill(1.0), Constraint::Fill(1.0)).with_flex(1.0));
970        root.arrange(Rect::new(0.0, 0.0, 200.0, 100.0));
971        assert!((root.rect.w - 200.0).abs() < 1.0);
972    }
973
974    #[test]
975    fn stack_layout_center() {
976        let stack = StackLayout::new(StackAlign::Center);
977        let par   = Rect::new(0.0, 0.0, 400.0, 300.0);
978        let r     = stack.compute(par, &[(100.0, 60.0)]);
979        assert!((r[0].x - 150.0).abs() < 1.0);
980    }
981}