1use glam::{Vec2, Vec3};
15use super::framework::Rect;
16
17#[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#[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
130pub 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
184pub 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#[derive(Debug, Clone, Copy, PartialEq)]
219pub enum Constraint {
220 Exact(f32),
222 Min(f32),
224 Max(f32),
226 MinMax { min: f32, max: f32 },
228 Fill(f32),
230}
231
232impl Constraint {
233 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum Axis { Horizontal, Vertical }
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum JustifyContent {
256 Start,
257 End,
258 Center,
259 SpaceBetween,
260 SpaceAround,
261 SpaceEvenly,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum CrossAlign {
269 Start,
270 Center,
271 End,
272 Stretch,
273 Baseline,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum FlexWrap {
281 NoWrap,
282 Wrap,
283 WrapReverse,
284}
285
286#[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#[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 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 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#[derive(Debug, Clone, Copy)]
421pub enum Track {
422 Fixed(f32),
423 Fr(f32), Auto,
425 MinMax { min: f32, max: f32 },
426}
427
428#[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum AnchorPoint {
526 TopLeft, TopCenter, TopRight,
527 CenterLeft, Center, CenterRight,
528 BottomLeft, BottomCenter, BottomRight,
529}
530
531#[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
548pub struct AbsoluteLayout;
550
551impl AbsoluteLayout {
552 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 pub fn compute_all(parent: Rect, items: &[AbsoluteItem]) -> Vec<Rect> {
570 items.iter().map(|i| Self::compute(parent, i)).collect()
571 }
572}
573
574#[derive(Debug, Clone, Copy, PartialEq, Eq)]
578pub enum StackAlign {
579 Start,
580 Center,
581 End,
582 Stretch,
583}
584
585pub 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 pub fn push(&mut self, z: i32, rect: Rect) { self.z_items.push((z, rect)); }
596
597 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 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
619pub 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 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 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#[derive(Debug, Clone)]
662pub struct LayoutNode {
663 pub constraint_w: Constraint,
664 pub constraint_h: Constraint,
665 pub children: Vec<LayoutNode>,
666 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
748pub enum Breakpoint { Xs, Sm, Md, Lg, Xl }
749
750pub struct ResponsiveBreakpoints {
752 pub xs: f32, pub sm: f32, pub md: f32, pub lg: f32, pub xl: f32, }
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 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 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#[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 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#[cfg(test)]
837mod tests {
838 use super::*;
839 use glam::Vec2;
840
841 #[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 #[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 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}