1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::math::Vec2;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub struct Rect {
10 pub x: f32,
11 pub y: f32,
12 pub width: f32,
13 pub height: f32,
14}
15
16impl Rect {
17 pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
18
19 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
20 Self {
21 x,
22 y,
23 width,
24 height,
25 }
26 }
27
28 pub fn right(self) -> f32 {
29 self.x + self.width
30 }
31
32 pub fn bottom(self) -> f32 {
33 self.y + self.height
34 }
35
36 pub fn center(self) -> Vec2 {
38 Vec2::new(self.x + self.width * 0.5, self.y + self.height * 0.5)
39 }
40
41 pub fn translate(self, delta: Vec2) -> Self {
43 Self::new(self.x + delta.x, self.y + delta.y, self.width, self.height)
44 }
45
46 pub fn is_empty(self) -> bool {
47 self.width <= 0.0 || self.height <= 0.0
48 }
49
50 pub fn is_finite(self) -> bool {
52 self.x.is_finite()
53 && self.y.is_finite()
54 && self.width.is_finite()
55 && self.height.is_finite()
56 && self.right().is_finite()
57 && self.bottom().is_finite()
58 }
59
60 pub fn inset(self, inset: Insets) -> Self {
61 if !self.is_finite() || !inset.is_finite() {
62 return Self::ZERO;
63 }
64 let x = self.x + inset.left;
65 let y = self.y + inset.top;
66 let width = (self.width - inset.left - inset.right).max(0.0);
67 let height = (self.height - inset.top - inset.bottom).max(0.0);
68 let rect = Self::new(x, y, width, height);
69 if rect.is_finite() {
70 rect
71 } else {
72 Self::ZERO
73 }
74 }
75
76 pub fn contains(self, x: f32, y: f32) -> bool {
77 self.is_finite()
78 && !self.is_empty()
79 && x.is_finite()
80 && y.is_finite()
81 && x >= self.x
82 && x <= self.right()
83 && y >= self.y
84 && y <= self.bottom()
85 }
86
87 pub fn intersection(self, other: Self) -> Option<Self> {
89 if !self.is_finite() || !other.is_finite() {
90 return None;
91 }
92 let x = self.x.max(other.x);
93 let y = self.y.max(other.y);
94 let right = self.right().min(other.right());
95 let bottom = self.bottom().min(other.bottom());
96 let width = right - x;
97 let height = bottom - y;
98 (width > 0.0 && height > 0.0).then(|| Self::new(x, y, width, height))
99 }
100
101 pub fn union(self, other: Self) -> Self {
103 if !self.is_finite() {
104 return if other.is_finite() { other } else { Self::ZERO };
105 }
106 if !other.is_finite() {
107 return self;
108 }
109 if self.is_empty() {
110 return other;
111 }
112 if other.is_empty() {
113 return self;
114 }
115 let x = self.x.min(other.x);
116 let y = self.y.min(other.y);
117 let right = self.right().max(other.right());
118 let bottom = self.bottom().max(other.bottom());
119 let rect = Self::new(x, y, right - x, bottom - y);
120 if rect.is_finite() {
121 rect
122 } else {
123 Self::ZERO
124 }
125 }
126
127 pub fn clamp_within(self, bounds: Self) -> Self {
129 if !self.is_finite() || !bounds.is_finite() || self.is_empty() || bounds.is_empty() {
130 return Self::ZERO;
131 }
132 let width = self.width.min(bounds.width).max(0.0);
133 let height = self.height.min(bounds.height).max(0.0);
134 let max_x = bounds.x + (bounds.width - width).max(0.0);
135 let max_y = bounds.y + (bounds.height - height).max(0.0);
136 let rect = Self::new(
137 self.x.clamp(bounds.x, max_x),
138 self.y.clamp(bounds.y, max_y),
139 width,
140 height,
141 );
142 if rect.is_finite() {
143 rect
144 } else {
145 Self::ZERO
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq)]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
152pub struct Insets {
153 pub top: f32,
154 pub right: f32,
155 pub bottom: f32,
156 pub left: f32,
157}
158
159impl Insets {
160 pub const fn all(value: f32) -> Self {
161 Self {
162 top: value,
163 right: value,
164 bottom: value,
165 left: value,
166 }
167 }
168
169 pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
170 Self {
171 top: vertical,
172 right: horizontal,
173 bottom: vertical,
174 left: horizontal,
175 }
176 }
177
178 pub fn is_finite(self) -> bool {
179 self.top.is_finite()
180 && self.right.is_finite()
181 && self.bottom.is_finite()
182 && self.left.is_finite()
183 }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq)]
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189pub struct Size {
190 pub width: f32,
191 pub height: f32,
192}
193
194impl Size {
195 pub const ZERO: Self = Self::new(0.0, 0.0);
196 pub const INFINITE: Self = Self::new(f32::INFINITY, f32::INFINITY);
197
198 pub const fn new(width: f32, height: f32) -> Self {
199 Self { width, height }
200 }
201
202 pub fn is_finite(self) -> bool {
203 self.width.is_finite() && self.height.is_finite()
204 }
205
206 pub fn sanitized(self) -> Self {
207 Self::new(finite_px(self.width, 0.0), finite_px(self.height, 0.0))
208 }
209
210 pub fn clamp(self, min: Self, max: Self) -> Self {
211 let min = min.sanitized();
212 let max = max.sanitized_or_infinite();
213 Self::new(
214 finite_px(self.width, 0.0).clamp(min.width.min(max.width), max.width.max(min.width)),
215 finite_px(self.height, 0.0)
216 .clamp(min.height.min(max.height), max.height.max(min.height)),
217 )
218 }
219
220 fn sanitized_or_infinite(self) -> Self {
221 Self::new(
222 if self.width.is_finite() {
223 self.width.max(0.0)
224 } else {
225 f32::INFINITY
226 },
227 if self.height.is_finite() {
228 self.height.max(0.0)
229 } else {
230 f32::INFINITY
231 },
232 )
233 }
234}
235
236impl Default for Size {
237 fn default() -> Self {
238 Self::ZERO
239 }
240}
241
242#[derive(Clone, Copy, Debug, PartialEq)]
243#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
244#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
245pub enum FluidConstraint {
246 Min(f32),
247 Max,
248}
249
250impl FluidConstraint {
251 fn stack(self, other: Self) -> Self {
252 match (self, other) {
253 (Self::Min(left), Self::Min(right)) => {
254 Self::Min(finite_px(left, 0.0) + finite_px(right, 0.0))
255 }
256 (Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
257 Self::Min(finite_px(min, 0.0))
258 }
259 (Self::Max, Self::Max) => Self::Max,
260 }
261 }
262
263 fn cross(self, other: Self) -> Self {
264 match (self, other) {
265 (Self::Min(left), Self::Min(right)) => {
266 Self::Min(finite_px(left, 0.0).max(finite_px(right, 0.0)))
267 }
268 (Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
269 Self::Min(finite_px(min, 0.0))
270 }
271 (Self::Max, Self::Max) => Self::Max,
272 }
273 }
274}
275
276#[derive(Clone, Copy, Debug, PartialEq)]
277#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
278#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
279pub enum LengthBounds {
280 Min(f32),
281 Max(f32),
282 Both { min: f32, max: f32 },
283}
284
285impl LengthBounds {
286 pub fn min(self, min: f32) -> Self {
287 let min = finite_px(min, 0.0);
288 match self {
289 Self::Min(_) => Self::Min(min),
290 Self::Max(max) | Self::Both { max, .. } => Self::Both {
291 min,
292 max: finite_px(max, min).max(min),
293 },
294 }
295 }
296
297 pub fn max(self, max: f32) -> Self {
298 let max = finite_px(max, 0.0);
299 match self {
300 Self::Max(_) => Self::Max(max),
301 Self::Min(min) | Self::Both { min, .. } => Self::Both {
302 min: finite_px(min, 0.0).min(max),
303 max,
304 },
305 }
306 }
307
308 fn constraint(self) -> FluidConstraint {
309 match self {
310 Self::Min(min) | Self::Both { min, .. } => FluidConstraint::Min(finite_px(min, 0.0)),
311 Self::Max(_) => FluidConstraint::Max,
312 }
313 }
314
315 fn apply(self, min_value: &mut f32, max_value: &mut f32) {
316 match self {
317 Self::Min(min) => *min_value = finite_px(min, 0.0).min(*max_value).max(*min_value),
318 Self::Max(max) => *max_value = finite_px(max, 0.0).min(*max_value).max(*min_value),
319 Self::Both { min, max } => {
320 *min_value = finite_px(min, 0.0).min(*max_value).max(*min_value);
321 *max_value = finite_px(max, *min_value).min(*max_value).max(*min_value);
322 }
323 }
324 }
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
329#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
330pub enum LengthSizing {
331 Fit,
332 Fill(u16),
333 Shrink,
334}
335
336#[derive(Clone, Copy, Debug, PartialEq)]
338#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
339#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
340pub enum Length {
341 Fit,
342 Fill,
343 FillPortion(u16),
344 Shrink,
345 Fixed(f32),
346 Bounded {
347 bounds: LengthBounds,
348 sizing: LengthSizing,
349 },
350 Fluid(FluidConstraint),
351}
352
353impl Length {
354 pub const fn fixed(px: f32) -> Self {
355 Self::Fixed(px)
356 }
357
358 pub const fn fill_portion(portion: u16) -> Self {
359 Self::FillPortion(portion)
360 }
361
362 pub fn min(self, min: f32) -> Self {
363 let min = finite_px(min, 0.0);
364 match self {
365 Self::Fixed(_) => self,
366 Self::Bounded { bounds, sizing } => Self::Bounded {
367 bounds: bounds.min(min),
368 sizing,
369 },
370 length => Self::Bounded {
371 bounds: LengthBounds::Min(min),
372 sizing: length.sizing(),
373 },
374 }
375 }
376
377 pub fn max(self, max: f32) -> Self {
378 let max = finite_px(max, 0.0);
379 match self {
380 Self::Fixed(_) => self,
381 Self::Bounded { bounds, sizing } => Self::Bounded {
382 bounds: bounds.max(max),
383 sizing,
384 },
385 length => Self::Bounded {
386 bounds: LengthBounds::Max(max),
387 sizing: length.sizing(),
388 },
389 }
390 }
391
392 pub fn fill_factor(self) -> u16 {
393 match self {
394 Self::Fill | Self::Fluid(_) => 1,
395 Self::FillPortion(portion) => portion.max(1),
396 Self::Bounded {
397 sizing: LengthSizing::Fill(portion),
398 ..
399 } => portion.max(1),
400 Self::Fit | Self::Shrink | Self::Fixed(_) | Self::Bounded { .. } => 0,
401 }
402 }
403
404 pub fn is_fill(self) -> bool {
405 self.fill_factor() != 0
406 }
407
408 pub fn is_fit(self) -> bool {
409 matches!(self, Self::Fit)
410 }
411
412 pub fn stack(self, other: Self) -> Self {
413 self.merge_with(other, FluidConstraint::stack)
414 }
415
416 pub fn cross(self, other: Self) -> Self {
417 self.merge_with(other, FluidConstraint::cross)
418 }
419
420 fn sizing(self) -> LengthSizing {
421 match self {
422 Self::Fill => LengthSizing::Fill(1),
423 Self::FillPortion(portion) => LengthSizing::Fill(portion.max(1)),
424 Self::Shrink => LengthSizing::Shrink,
425 _ => LengthSizing::Fit,
426 }
427 }
428
429 fn merge_with(
430 self,
431 other: Self,
432 merge: impl Fn(FluidConstraint, FluidConstraint) -> FluidConstraint,
433 ) -> Self {
434 match (self, other) {
435 (Self::Shrink | Self::Fixed(_) | Self::Fill | Self::FillPortion(_), _) => self,
436 (Self::Fluid(left), Self::Fluid(right)) => Self::Fluid(merge(left, right)),
437 (
438 Self::Fluid(left),
439 Self::Bounded {
440 bounds,
441 sizing: LengthSizing::Fill(_),
442 },
443 ) => Self::Fluid(merge(left, bounds.constraint())),
444 (Self::Fluid(constraint), _) => Self::Fluid(constraint),
445 (Self::Bounded { bounds, sizing }, _) => Self::Bounded { bounds, sizing },
446 (
447 _,
448 Self::Bounded {
449 bounds,
450 sizing: LengthSizing::Fill(_),
451 },
452 ) => Self::Fluid(bounds.constraint()),
453 (_, Self::Fluid(constraint)) => Self::Fluid(constraint),
454 (_, Self::Fill | Self::FillPortion(_)) => Self::Fill,
455 _ => Self::Fit,
456 }
457 }
458
459 fn resolve_axis(self, min: f32, max: f32, intrinsic: f32, compressed: bool) -> f32 {
460 let min = finite_px(min, 0.0);
461 let max = if max.is_finite() {
462 max.max(min)
463 } else {
464 f32::INFINITY
465 };
466 let intrinsic = finite_px(intrinsic, min);
467 match self {
468 Self::Fill
469 | Self::FillPortion(_)
470 | Self::Fluid(_)
471 | Self::Bounded {
472 sizing: LengthSizing::Fill(_),
473 ..
474 } if !compressed => {
475 if max.is_finite() {
476 max
477 } else {
478 intrinsic.max(min)
479 }
480 }
481 Self::Fixed(px) => finite_px(px, min).clamp(min, max),
482 Self::Bounded { bounds, sizing } => {
483 let mut bounded_min = min;
484 let mut bounded_max = max;
485 bounds.apply(&mut bounded_min, &mut bounded_max);
486 match sizing {
487 LengthSizing::Fill(_) if !compressed => bounded_max,
488 LengthSizing::Shrink => {
489 bounded_min.min(intrinsic).clamp(bounded_min, bounded_max)
490 }
491 _ => intrinsic.clamp(bounded_min, bounded_max),
492 }
493 }
494 Self::Shrink => min.min(intrinsic).clamp(min, max),
495 _ => intrinsic.clamp(min, max),
496 }
497 }
498}
499
500impl From<f32> for Length {
501 fn from(value: f32) -> Self {
502 Self::Fixed(value)
503 }
504}
505
506#[derive(Clone, Copy, Debug, PartialEq)]
508#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
509pub struct LayoutLimits {
510 pub min: Size,
511 pub max: Size,
512 pub compress_width: bool,
513 pub compress_height: bool,
514}
515
516impl LayoutLimits {
517 pub const NONE: Self = Self {
518 min: Size::ZERO,
519 max: Size::INFINITE,
520 compress_width: false,
521 compress_height: false,
522 };
523
524 pub const fn new(min: Size, max: Size) -> Self {
525 Self {
526 min,
527 max,
528 compress_width: false,
529 compress_height: false,
530 }
531 }
532
533 pub fn width(mut self, width: impl Into<Length>) -> Self {
534 match width.into() {
535 Length::Shrink => self.compress_width = true,
536 Length::Fit | Length::Fluid(_) => self.compress_width = false,
537 Length::Fixed(px) => {
538 let px = finite_px(px, self.min.width).clamp(self.min.width, self.max.width);
539 self.min.width = px;
540 self.max.width = px;
541 self.compress_width = false;
542 }
543 Length::Bounded { bounds, sizing } => {
544 bounds.apply(&mut self.min.width, &mut self.max.width);
545 if matches!(sizing, LengthSizing::Shrink) {
546 self.compress_width = true;
547 } else if matches!(sizing, LengthSizing::Fit) {
548 self.compress_width = false;
549 }
550 }
551 Length::Fill | Length::FillPortion(_) => {}
552 }
553 self
554 }
555
556 pub fn height(mut self, height: impl Into<Length>) -> Self {
557 match height.into() {
558 Length::Shrink => self.compress_height = true,
559 Length::Fit | Length::Fluid(_) => self.compress_height = false,
560 Length::Fixed(px) => {
561 let px = finite_px(px, self.min.height).clamp(self.min.height, self.max.height);
562 self.min.height = px;
563 self.max.height = px;
564 self.compress_height = false;
565 }
566 Length::Bounded { bounds, sizing } => {
567 bounds.apply(&mut self.min.height, &mut self.max.height);
568 if matches!(sizing, LengthSizing::Shrink) {
569 self.compress_height = true;
570 } else if matches!(sizing, LengthSizing::Fit) {
571 self.compress_height = false;
572 }
573 }
574 Length::Fill | Length::FillPortion(_) => {}
575 }
576 self
577 }
578
579 pub fn shrink(self, size: Size) -> Self {
580 let size = size.sanitized();
581 Self {
582 min: Size::new(
583 (self.min.width - size.width).max(0.0),
584 (self.min.height - size.height).max(0.0),
585 ),
586 max: Size::new(
587 (self.max.width - size.width).max(0.0),
588 (self.max.height - size.height).max(0.0),
589 ),
590 compress_width: self.compress_width,
591 compress_height: self.compress_height,
592 }
593 }
594
595 pub fn loose(self) -> Self {
596 Self {
597 min: Size::ZERO,
598 ..self
599 }
600 }
601
602 pub fn resolve(
603 self,
604 width: impl Into<Length>,
605 height: impl Into<Length>,
606 intrinsic: Size,
607 ) -> Size {
608 Size::new(
609 width.into().resolve_axis(
610 self.min.width,
611 self.max.width,
612 intrinsic.width,
613 self.compress_width,
614 ),
615 height.into().resolve_axis(
616 self.min.height,
617 self.max.height,
618 intrinsic.height,
619 self.compress_height,
620 ),
621 )
622 }
623}
624
625#[derive(Clone, Copy, Debug, PartialEq, Eq)]
626#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
627#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
628pub enum LayoutAxis {
629 Horizontal,
630 Vertical,
631}
632
633#[derive(Clone, Copy, Debug, PartialEq, Eq)]
634#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
635#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
636pub enum LayoutAlign {
637 Start,
638 Center,
639 End,
640 Stretch,
641}
642
643#[derive(Clone, Debug, PartialEq)]
644#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
645pub struct LayoutItem {
646 pub width: Length,
647 pub height: Length,
648 pub intrinsic_size: Size,
649 pub min_size: Size,
650}
651
652impl LayoutItem {
653 pub fn new(width: impl Into<Length>, height: impl Into<Length>, intrinsic_size: Size) -> Self {
654 Self {
655 width: width.into(),
656 height: height.into(),
657 intrinsic_size: intrinsic_size.sanitized(),
658 min_size: Size::ZERO,
659 }
660 }
661
662 pub fn with_min_size(mut self, min_size: Size) -> Self {
663 self.min_size = min_size.sanitized();
664 self
665 }
666}
667
668#[derive(Clone, Debug, Default, PartialEq)]
669#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
670pub struct LayoutNode {
671 pub bounds: Rect,
672 pub children: Vec<LayoutNode>,
673}
674
675impl LayoutNode {
676 pub fn new(bounds: Rect) -> Self {
677 Self {
678 bounds: sanitize_rect(bounds),
679 children: Vec::new(),
680 }
681 }
682
683 pub fn with_children(bounds: Rect, children: Vec<LayoutNode>) -> Self {
684 Self {
685 bounds: sanitize_rect(bounds),
686 children,
687 }
688 }
689
690 pub fn size(&self) -> Size {
691 Size::new(self.bounds.width, self.bounds.height)
692 }
693
694 pub fn translate(mut self, delta: Vec2) -> Self {
695 self.bounds = self.bounds.translate(delta);
696 self.children = self
697 .children
698 .into_iter()
699 .map(|child| child.translate(delta))
700 .collect();
701 self
702 }
703}
704
705#[derive(Clone, Copy, Debug, PartialEq)]
706#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
707pub struct FlexLayout {
708 pub axis: LayoutAxis,
709 pub bounds: Rect,
710 pub gap_px: f32,
711 pub padding: Insets,
712 pub align: LayoutAlign,
713}
714
715impl FlexLayout {
716 pub fn row(bounds: Rect) -> Self {
717 Self::new(LayoutAxis::Horizontal, bounds)
718 }
719
720 pub fn column(bounds: Rect) -> Self {
721 Self::new(LayoutAxis::Vertical, bounds)
722 }
723
724 pub fn new(axis: LayoutAxis, bounds: Rect) -> Self {
725 Self {
726 axis,
727 bounds: sanitize_rect(bounds),
728 gap_px: 0.0,
729 padding: Insets::default(),
730 align: LayoutAlign::Stretch,
731 }
732 }
733
734 pub fn with_gap(mut self, gap_px: f32) -> Self {
735 self.gap_px = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
736 self
737 }
738
739 pub fn with_padding(mut self, padding: Insets) -> Self {
740 self.padding = if padding.is_finite() {
741 padding
742 } else {
743 Insets::default()
744 };
745 self
746 }
747
748 pub fn with_align(mut self, align: LayoutAlign) -> Self {
749 self.align = align;
750 self
751 }
752
753 pub fn resolve(self, items: &[LayoutItem]) -> LayoutNode {
754 let content = sanitize_rect(self.bounds).inset(self.padding);
755 if content.is_empty() || items.is_empty() {
756 return LayoutNode::new(content);
757 }
758
759 let gap = finite_px(self.gap_px, 0.0).clamp(0.0, 512.0);
760 let total_gap = gap * items.len().saturating_sub(1) as f32;
761 let available_main = (main_len(content, self.axis) - total_gap).max(0.0);
762 let mut fixed_main = 0.0;
763 let mut fill_factor = 0_u32;
764
765 for item in items {
766 let length = item.main_length(self.axis);
767 let factor = length.fill_factor();
768 if factor == 0 {
769 fixed_main += item.resolve_main(self.axis, available_main);
770 } else {
771 fill_factor = fill_factor.saturating_add(factor as u32);
772 }
773 }
774
775 let remaining = (available_main - fixed_main).max(0.0);
776 let mut cursor = main_start(content, self.axis);
777 let mut children = Vec::with_capacity(items.len());
778 for item in items {
779 let main = if item.main_length(self.axis).fill_factor() == 0 {
780 item.resolve_main(self.axis, available_main)
781 } else if fill_factor == 0 {
782 0.0
783 } else {
784 remaining * item.main_length(self.axis).fill_factor() as f32 / fill_factor as f32
785 };
786 let cross = item.resolve_cross(self.axis, cross_len(content, self.axis), self.align);
787 let cross_origin = align_cross(content, self.axis, cross, self.align);
788 let rect = rect_from_axes(content, self.axis, cursor, main, cross_origin, cross);
789 children.push(LayoutNode::new(rect));
790 cursor += main + gap;
791 }
792
793 LayoutNode::with_children(content, children)
794 }
795}
796
797impl LayoutItem {
798 fn main_length(&self, axis: LayoutAxis) -> Length {
799 match axis {
800 LayoutAxis::Horizontal => self.width,
801 LayoutAxis::Vertical => self.height,
802 }
803 }
804
805 fn cross_length(&self, axis: LayoutAxis) -> Length {
806 match axis {
807 LayoutAxis::Horizontal => self.height,
808 LayoutAxis::Vertical => self.width,
809 }
810 }
811
812 fn intrinsic_main(&self, axis: LayoutAxis) -> f32 {
813 match axis {
814 LayoutAxis::Horizontal => self.intrinsic_size.width,
815 LayoutAxis::Vertical => self.intrinsic_size.height,
816 }
817 }
818
819 fn intrinsic_cross(&self, axis: LayoutAxis) -> f32 {
820 match axis {
821 LayoutAxis::Horizontal => self.intrinsic_size.height,
822 LayoutAxis::Vertical => self.intrinsic_size.width,
823 }
824 }
825
826 fn min_main(&self, axis: LayoutAxis) -> f32 {
827 match axis {
828 LayoutAxis::Horizontal => self.min_size.width,
829 LayoutAxis::Vertical => self.min_size.height,
830 }
831 }
832
833 fn min_cross(&self, axis: LayoutAxis) -> f32 {
834 match axis {
835 LayoutAxis::Horizontal => self.min_size.height,
836 LayoutAxis::Vertical => self.min_size.width,
837 }
838 }
839
840 fn resolve_main(&self, axis: LayoutAxis, available: f32) -> f32 {
841 self.main_length(axis)
842 .resolve_axis(
843 self.min_main(axis),
844 available.max(self.min_main(axis)),
845 self.intrinsic_main(axis),
846 false,
847 )
848 .max(0.0)
849 }
850
851 fn resolve_cross(&self, axis: LayoutAxis, available: f32, align: LayoutAlign) -> f32 {
852 if matches!(align, LayoutAlign::Stretch) && self.cross_length(axis).fill_factor() != 0 {
853 return available.max(0.0);
854 }
855 self.cross_length(axis)
856 .resolve_axis(
857 self.min_cross(axis),
858 available.max(self.min_cross(axis)),
859 self.intrinsic_cross(axis),
860 false,
861 )
862 .max(0.0)
863 }
864}
865
866pub fn layout_row(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
867 FlexLayout::row(bounds).with_gap(gap_px).resolve(items)
868}
869
870pub fn layout_column(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
871 FlexLayout::column(bounds).with_gap(gap_px).resolve(items)
872}
873
874pub fn layout_stack(bounds: Rect, items: &[LayoutItem]) -> LayoutNode {
875 let bounds = sanitize_rect(bounds);
876 let children = items.iter().map(|_| LayoutNode::new(bounds)).collect();
877 LayoutNode::with_children(bounds, children)
878}
879
880pub fn layout_grid(bounds: Rect, columns: usize, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
881 let bounds = sanitize_rect(bounds);
882 if bounds.is_empty() || columns == 0 || items.is_empty() {
883 return LayoutNode::new(bounds);
884 }
885 let columns = columns.min(items.len()).max(1);
886 let rows = items.len().div_ceil(columns).max(1);
887 let gap = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
888 let cell_width =
889 ((bounds.width - gap * columns.saturating_sub(1) as f32) / columns as f32).max(0.0);
890 let cell_height =
891 ((bounds.height - gap * rows.saturating_sub(1) as f32) / rows as f32).max(0.0);
892 let children = items
893 .iter()
894 .enumerate()
895 .map(|(index, _)| {
896 let column = index % columns;
897 let row = index / columns;
898 LayoutNode::new(Rect::new(
899 bounds.x + (cell_width + gap) * column as f32,
900 bounds.y + (cell_height + gap) * row as f32,
901 cell_width,
902 cell_height,
903 ))
904 })
905 .collect();
906 LayoutNode::with_children(bounds, children)
907}
908
909pub fn layout_space(
910 bounds: Rect,
911 width: impl Into<Length>,
912 height: impl Into<Length>,
913 intrinsic: Size,
914) -> LayoutNode {
915 let bounds = sanitize_rect(bounds);
916 let limits = LayoutLimits::new(Size::ZERO, Size::new(bounds.width, bounds.height));
917 let size = limits.resolve(width, height, intrinsic);
918 LayoutNode::new(Rect::new(bounds.x, bounds.y, size.width, size.height))
919}
920
921fn main_len(rect: Rect, axis: LayoutAxis) -> f32 {
922 match axis {
923 LayoutAxis::Horizontal => rect.width,
924 LayoutAxis::Vertical => rect.height,
925 }
926}
927
928fn cross_len(rect: Rect, axis: LayoutAxis) -> f32 {
929 match axis {
930 LayoutAxis::Horizontal => rect.height,
931 LayoutAxis::Vertical => rect.width,
932 }
933}
934
935fn main_start(rect: Rect, axis: LayoutAxis) -> f32 {
936 match axis {
937 LayoutAxis::Horizontal => rect.x,
938 LayoutAxis::Vertical => rect.y,
939 }
940}
941
942fn align_cross(rect: Rect, axis: LayoutAxis, cross: f32, align: LayoutAlign) -> f32 {
943 let start = match axis {
944 LayoutAxis::Horizontal => rect.y,
945 LayoutAxis::Vertical => rect.x,
946 };
947 let available = cross_len(rect, axis);
948 match align {
949 LayoutAlign::Start | LayoutAlign::Stretch => start,
950 LayoutAlign::Center => start + (available - cross).max(0.0) * 0.5,
951 LayoutAlign::End => start + (available - cross).max(0.0),
952 }
953}
954
955fn rect_from_axes(
956 _content: Rect,
957 axis: LayoutAxis,
958 main: f32,
959 main_len: f32,
960 cross: f32,
961 cross_len: f32,
962) -> Rect {
963 match axis {
964 LayoutAxis::Horizontal => Rect::new(main, cross, main_len, cross_len),
965 LayoutAxis::Vertical => Rect::new(cross, main, cross_len, main_len),
966 }
967}
968
969fn sanitize_rect(rect: Rect) -> Rect {
970 if rect.is_finite() && rect.width >= 0.0 && rect.height >= 0.0 {
971 rect
972 } else {
973 Rect::ZERO
974 }
975}
976
977fn finite_px(value: f32, fallback: f32) -> f32 {
978 if value.is_finite() {
979 value.max(0.0)
980 } else if fallback.is_finite() {
981 fallback.max(0.0)
982 } else {
983 0.0
984 }
985}
986
987#[derive(Clone, Copy, Debug, PartialEq)]
988#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
989pub struct AppLayout {
990 pub root: Rect,
991 pub header: Rect,
992 pub main: Rect,
993 pub sidebar: Rect,
994 pub prompt: Rect,
995 pub status: Rect,
996 pub sidebar_collapsed: bool,
997}
998
999pub fn compute_shell_layout(viewport: Rect) -> AppLayout {
1000 let viewport = if viewport.is_finite() && !viewport.is_empty() {
1001 viewport
1002 } else {
1003 Rect::ZERO
1004 };
1005 let outer_gap = 16.0;
1006 let inner_gap = 12.0;
1007 let status_height = 32.0;
1008 let prompt_height = if viewport.height < 680.0 { 92.0 } else { 126.0 };
1009 let header_height = if viewport.height < 620.0 { 0.0 } else { 58.0 };
1010
1011 let content = viewport.inset(Insets::all(outer_gap));
1012 if content.height < status_height + inner_gap + prompt_height || content.width <= 0.0 {
1013 return AppLayout {
1014 root: viewport,
1015 header: Rect::ZERO,
1016 main: Rect::ZERO,
1017 sidebar: Rect::ZERO,
1018 prompt: Rect::ZERO,
1019 status: Rect::ZERO,
1020 sidebar_collapsed: true,
1021 };
1022 }
1023
1024 let sidebar_collapsed = viewport.width < 980.0;
1025 let sidebar_width = if sidebar_collapsed {
1026 0.0
1027 } else {
1028 (viewport.width * 0.27).clamp(300.0, 440.0)
1029 };
1030
1031 let header = Rect::new(content.x, content.y, content.width, header_height);
1032 let status = Rect::new(
1033 content.x,
1034 content.bottom() - status_height,
1035 content.width,
1036 status_height,
1037 );
1038 let prompt = Rect::new(
1039 content.x,
1040 status.y - inner_gap - prompt_height,
1041 content.width,
1042 prompt_height,
1043 );
1044 let body_y = header.y + header.height + if header_height > 0.0 { inner_gap } else { 0.0 };
1045 let body_h = (prompt.y - inner_gap - body_y).max(0.0);
1046 let sidebar = if sidebar_collapsed {
1047 Rect::ZERO
1048 } else {
1049 Rect::new(
1050 content.right() - sidebar_width,
1051 body_y,
1052 sidebar_width,
1053 body_h,
1054 )
1055 };
1056 let main_width = if sidebar_collapsed {
1057 content.width
1058 } else {
1059 (content.width - sidebar_width - inner_gap).max(0.0)
1060 };
1061 let main = Rect::new(content.x, body_y, main_width, body_h);
1062
1063 AppLayout {
1064 root: viewport,
1065 header,
1066 main,
1067 sidebar,
1068 prompt,
1069 status,
1070 sidebar_collapsed,
1071 }
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077
1078 #[test]
1079 fn layout_collapses_sidebar_on_small_width() {
1080 let layout = compute_shell_layout(Rect::new(0.0, 0.0, 800.0, 700.0));
1081
1082 assert!(layout.sidebar_collapsed);
1083 assert!(layout.sidebar.is_empty());
1084 assert!(layout.main.width > 700.0);
1085 }
1086
1087 #[test]
1088 fn layout_keeps_prompt_and_status_visible() {
1089 let layout = compute_shell_layout(Rect::new(0.0, 0.0, 1440.0, 900.0));
1090
1091 assert!(layout.main.height > 500.0);
1092 assert!(layout.prompt.y > layout.main.y);
1093 assert!(layout.status.y > layout.prompt.y);
1094 }
1095
1096 #[test]
1097 fn layout_returns_empty_children_for_malformed_or_tiny_viewports() {
1098 let malformed = compute_shell_layout(Rect::new(f32::NAN, 0.0, 100.0, 100.0));
1099 let tiny = compute_shell_layout(Rect::new(0.0, 0.0, 64.0, 80.0));
1100
1101 assert_eq!(malformed.root, Rect::ZERO);
1102 assert_eq!(malformed.prompt, Rect::ZERO);
1103 assert_eq!(tiny.root, Rect::new(0.0, 0.0, 64.0, 80.0));
1104 assert_eq!(tiny.status, Rect::ZERO);
1105 assert!(tiny.sidebar_collapsed);
1106 }
1107
1108 #[test]
1109 fn length_limits_resolve_fixed_fill_and_bounded_lengths() {
1110 let limits = LayoutLimits::new(Size::new(20.0, 10.0), Size::new(300.0, 120.0));
1111
1112 assert_eq!(Length::Fixed(500.0).fill_factor(), 0);
1113 assert_eq!(Length::FillPortion(3).fill_factor(), 3);
1114 assert_eq!(
1115 limits
1116 .resolve(Length::Fixed(500.0), Length::Fit, Size::new(50.0, 30.0))
1117 .width,
1118 300.0
1119 );
1120 assert_eq!(
1121 limits
1122 .resolve(Length::Fill, Length::Fit, Size::new(50.0, 30.0))
1123 .width,
1124 300.0
1125 );
1126 assert_eq!(
1127 limits
1128 .resolve(Length::Fit.max(80.0), Length::Fit, Size::new(140.0, 30.0))
1129 .width,
1130 80.0
1131 );
1132 assert!(limits.width(Length::Shrink).compress_width);
1133 }
1134
1135 #[test]
1136 fn flex_row_distributes_fill_portions_after_fixed_space() {
1137 let items = [
1138 LayoutItem::new(100.0, Length::Fill, Size::new(100.0, 20.0)),
1139 LayoutItem::new(Length::FillPortion(1), Length::Fill, Size::new(10.0, 20.0)),
1140 LayoutItem::new(Length::FillPortion(2), Length::Fill, Size::new(10.0, 20.0)),
1141 ];
1142 let node = layout_row(Rect::new(0.0, 0.0, 320.0, 40.0), 10.0, &items);
1143
1144 assert_eq!(node.children.len(), 3);
1145 assert_eq!(node.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 40.0));
1146 assert!((node.children[1].bounds.width - 66.666_67).abs() < 0.01);
1147 assert!((node.children[2].bounds.width - 133.333_34).abs() < 0.01);
1148 assert!((node.children[2].bounds.x - 186.666_67).abs() < 0.01);
1149 }
1150
1151 #[test]
1152 fn grid_stack_and_space_layouts_are_finite() {
1153 let items = [
1154 LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1155 LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1156 LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1157 ];
1158 let grid = layout_grid(Rect::new(0.0, 0.0, 210.0, 100.0), 2, 10.0, &items);
1159 let stack = layout_stack(Rect::new(4.0, 5.0, 30.0, 20.0), &items[..2]);
1160 let space = layout_space(
1161 Rect::new(0.0, 0.0, 80.0, 60.0),
1162 Length::Fill,
1163 12.0,
1164 Size::ZERO,
1165 );
1166
1167 assert_eq!(grid.children.len(), 3);
1168 assert_eq!(grid.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 45.0));
1169 assert_eq!(grid.children[2].bounds, Rect::new(0.0, 55.0, 100.0, 45.0));
1170 assert_eq!(stack.children[0].bounds, Rect::new(4.0, 5.0, 30.0, 20.0));
1171 assert_eq!(space.bounds, Rect::new(0.0, 0.0, 80.0, 12.0));
1172 }
1173
1174 #[test]
1175 fn rect_intersection_union_and_translation_are_stable() {
1176 let a = Rect::new(10.0, 20.0, 100.0, 80.0);
1177 let b = Rect::new(80.0, 60.0, 80.0, 80.0);
1178
1179 assert_eq!(a.center(), Vec2::new(60.0, 60.0));
1180 assert_eq!(
1181 a.translate(Vec2::new(5.0, -10.0)),
1182 Rect::new(15.0, 10.0, 100.0, 80.0)
1183 );
1184 assert_eq!(a.intersection(b), Some(Rect::new(80.0, 60.0, 30.0, 40.0)));
1185 assert_eq!(a.union(b), Rect::new(10.0, 20.0, 150.0, 120.0));
1186 assert_eq!(a.intersection(Rect::new(120.0, 20.0, 10.0, 10.0)), None);
1187 }
1188
1189 #[test]
1190 fn rect_clamp_keeps_popovers_inside_bounds() {
1191 let bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1192
1193 assert_eq!(
1194 Rect::new(260.0, 180.0, 90.0, 60.0).clamp_within(bounds),
1195 Rect::new(210.0, 140.0, 90.0, 60.0)
1196 );
1197 assert_eq!(
1198 Rect::new(-20.0, -30.0, 360.0, 260.0).clamp_within(bounds),
1199 bounds
1200 );
1201 assert_eq!(Rect::ZERO.clamp_within(bounds), Rect::ZERO);
1202 }
1203
1204 #[test]
1205 fn rect_clamp_handles_tiny_rounding_ranges_without_panic() {
1206 let x = f32::from_bits(0x2042_a815);
1207 let width = f32::from_bits(0x2390_0b54);
1208 let bounds = Rect::new(x, 0.0, width, 1.0);
1209 let clamped = bounds.clamp_within(bounds);
1210
1211 assert!(clamped.is_finite());
1212 assert_eq!(clamped.x, bounds.x);
1213 }
1214
1215 #[test]
1216 fn rect_helpers_tolerate_non_finite_public_values() {
1217 let valid = Rect::new(0.0, 0.0, 10.0, 10.0);
1218 let malformed = Rect::new(f32::NAN, 0.0, 10.0, 10.0);
1219 let overflowing = Rect::new(f32::MAX, 0.0, f32::MAX, 10.0);
1220
1221 assert!(!malformed.is_finite());
1222 assert!(!overflowing.is_finite());
1223 assert_eq!(malformed.intersection(valid), None);
1224 assert_eq!(overflowing.intersection(valid), None);
1225 assert_eq!(malformed.union(valid), valid);
1226 assert_eq!(overflowing.union(valid), valid);
1227 assert_eq!(valid.clamp_within(malformed), Rect::ZERO);
1228 assert_eq!(valid.clamp_within(overflowing), Rect::ZERO);
1229 assert_eq!(valid.inset(Insets::all(f32::INFINITY)), Rect::ZERO);
1230 assert_eq!(overflowing.inset(Insets::all(1.0)), Rect::ZERO);
1231 assert!(!valid.contains(f32::NAN, 1.0));
1232 assert!(!malformed.contains(1.0, 1.0));
1233 assert!(!overflowing.contains(f32::MAX, 1.0));
1234 }
1235}