Skip to main content

waterui_core/ui/
layout.rs

1//! Layout primitives and geometry types for the `WaterUI` layout system.
2//!
3//! # Logical Pixels (Points)
4//!
5//! All layout values in `WaterUI` use **logical pixels** (also called "points" or "dp").
6//! This is the same unit system used by design tools like Figma, Sketch, and Adobe XD,
7//! allowing seamless translation from design to implementation.
8//!
9//! - **1 logical pixel** = 1 point in design tools
10//! - Native backends handle conversion to physical pixels based on screen density
11//! - iOS: `UIKit` uses points natively (1pt = 1-3 physical pixels depending on device)
12//! - Android: Backend converts dp to physical pixels using `displayMetrics.density`
13//! - macOS: `AppKit` uses points (1pt = 1-2 physical pixels on Retina displays)
14//!
15//! This means `spacing: 8.0` or `width: 100.0` will appear the same physical size
16//! across all platforms and screen densities.
17//!
18//! # Example
19//!
20//! ```text
21//! // Shown as text: the authoring layer lives in crates that depend on this
22//! // one, so it cannot be compiled from here.
23//! // In Figma: Button with 16pt horizontal padding, 8pt vertical padding
24//! // In WaterUI: Same values work directly
25//! vstack((
26//!     text("Hello").padding(16.0),  // 16 logical pixels = 16pt in Figma
27//!     Divider,                       // 1pt thick line
28//! )).spacing(8.0)                    // 8 logical pixels between items
29//! ```
30
31use core::any::Any;
32use core::fmt;
33use fmt::Debug;
34
35use alloc::{rc::Rc, vec::Vec};
36use nami::watcher::BoxWatcherGuard;
37use smallvec::SmallVec;
38
39/// The logical horizontal direction used by layout containers.
40///
41/// This is a semantic direction: leading and trailing follow it, while physical
42/// coordinates exposed to renderers remain left-to-right.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum LayoutDirection {
45    /// Leading is the physical left edge.
46    #[default]
47    LeftToRight,
48    /// Leading is the physical right edge.
49    RightToLeft,
50}
51
52/// Locale-derived application direction, lower priority than scoped overrides.
53#[doc(hidden)]
54#[derive(Clone, Debug)]
55pub struct AutomaticLayoutDirection(pub nami::Computed<LayoutDirection>);
56
57impl LayoutDirection {
58    /// Returns whether leading is the physical right edge.
59    #[must_use]
60    pub const fn is_right_to_left(self) -> bool {
61        matches!(self, Self::RightToLeft)
62    }
63}
64
65/// Resolves the reactive layout direction installed in an environment.
66///
67/// Applications normally receive a locale-derived value automatically. A
68/// static value, binding, or computed value can be inserted to override it.
69#[must_use]
70pub fn layout_direction(environment: &crate::Environment) -> nami::Computed<LayoutDirection> {
71    if let Some(direction) = environment.get::<LayoutDirection>() {
72        return nami::Computed::constant(*direction);
73    }
74    if let Some(direction) = environment.get::<nami::Binding<LayoutDirection>>() {
75        return direction.clone().into();
76    }
77    if let Some(direction) = environment.get::<nami::Computed<LayoutDirection>>() {
78        return direction.clone();
79    }
80    environment.get::<AutomaticLayoutDirection>().map_or_else(
81        || nami::Computed::constant(LayoutDirection::default()),
82        |direction| direction.0.clone(),
83    )
84}
85
86// ============================================================================
87// StretchAxis - Specifies which axis a view stretches on
88// ============================================================================
89
90/// Specifies which axis (or axes) a view wants to stretch to fill available space.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
92pub enum StretchAxis {
93    /// No stretching - view uses its intrinsic size
94    #[default]
95    None,
96    /// Stretch horizontally only (expand width, use intrinsic height)
97    Horizontal,
98    /// Stretch vertically only (expand height, use intrinsic width)
99    Vertical,
100    /// Stretch in both directions (expand width and height)
101    Both,
102    /// Stretch along the parent container's main axis.
103    /// In `VStack`: expands vertically. In `HStack`: expands horizontally.
104    /// Used by Spacer.
105    MainAxis,
106    /// Stretch along the parent container's cross axis.
107    /// In `VStack`: expands horizontally. In `HStack`: expands vertically.
108    CrossAxis,
109}
110
111impl StretchAxis {
112    /// Returns true if this stretches horizontally.
113    #[must_use]
114    pub const fn stretches_horizontal(&self) -> bool {
115        matches!(self, Self::Horizontal | Self::Both)
116    }
117
118    /// Returns true if this stretches vertically.
119    #[must_use]
120    pub const fn stretches_vertical(&self) -> bool {
121        matches!(self, Self::Vertical | Self::Both)
122    }
123
124    /// Returns true if this stretches in any direction.
125    #[must_use]
126    pub const fn stretches_any(&self) -> bool {
127        !matches!(self, Self::None)
128    }
129}
130
131/// How strongly a view holds on to space when its container runs short.
132///
133/// A stack takes space from its lowest-priority children first, and only starts
134/// on the next band up once every child below sits at the minimum it reports.
135/// Ties share the shortfall. Defaults to `0`, so raising one child's priority is
136/// enough to protect it.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
138pub struct LayoutPriority(i32);
139
140impl LayoutPriority {
141    /// Wraps a raw priority.
142    #[must_use]
143    pub const fn new(priority: i32) -> Self {
144        Self(priority)
145    }
146
147    /// The raw priority.
148    #[must_use]
149    pub const fn get(self) -> i32 {
150        self.0
151    }
152}
153
154impl crate::components::metadata::MetadataKey for LayoutPriority {}
155
156// ============================================================================
157// Alignment Guides
158// ============================================================================
159
160/// Stable identifier for a built-in alignment guide in the layout runtime.
161#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
162pub struct AlignmentKeyId {
163    low: u64,
164    high: u64,
165}
166
167impl AlignmentKeyId {
168    /// Creates a stable alignment identifier from its low/high 64-bit halves.
169    #[must_use]
170    pub const fn new(low: u64, high: u64) -> Self {
171        Self { low, high }
172    }
173
174    /// Returns the low 64 bits of the identifier.
175    #[must_use]
176    pub const fn low(self) -> u64 {
177        self.low
178    }
179
180    /// Returns the high 64 bits of the identifier.
181    #[must_use]
182    pub const fn high(self) -> u64 {
183        self.high
184    }
185
186    /// Creates an identifier from a stable string name using FNV-1a 128-bit hashing.
187    #[must_use]
188    pub const fn from_name(name: &str) -> Self {
189        let hash = fnv1a_128(name.as_bytes());
190        let bytes = hash.to_le_bytes();
191        Self {
192            low: u64::from_le_bytes([
193                bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
194            ]),
195            high: u64::from_le_bytes([
196                bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
197                bytes[15],
198            ]),
199        }
200    }
201}
202
203const fn fnv1a_128(bytes: &[u8]) -> u128 {
204    const FNV_OFFSET: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
205    const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013b;
206
207    let mut hash = FNV_OFFSET;
208    let mut i = 0;
209    while i < bytes.len() {
210        hash ^= bytes[i] as u128;
211        hash = hash.wrapping_mul(FNV_PRIME);
212        i += 1;
213    }
214    hash
215}
216
217#[derive(Clone, Copy)]
218/// Horizontal alignment guide handle.
219pub struct HorizontalAlignment {
220    stable_id: AlignmentKeyId,
221    default_value: fn(&ViewDimensions) -> f32,
222}
223
224impl HorizontalAlignment {
225    /// Leading alignment guide.
226    #[allow(non_upper_case_globals)]
227    pub const Leading: Self = Self {
228        stable_id: AlignmentKeyId::from_name("waterui.layout.horizontal.leading"),
229        default_value: leading_alignment_default,
230    };
231
232    /// Center alignment guide.
233    #[allow(non_upper_case_globals)]
234    pub const Center: Self = Self {
235        stable_id: AlignmentKeyId::from_name("waterui.layout.horizontal.center"),
236        default_value: center_horizontal_alignment_default,
237    };
238
239    /// Trailing alignment guide.
240    #[allow(non_upper_case_globals)]
241    pub const Trailing: Self = Self {
242        stable_id: AlignmentKeyId::from_name("waterui.layout.horizontal.trailing"),
243        default_value: trailing_alignment_default,
244    };
245
246    #[must_use]
247    /// Returns the stable identifier for this built-in horizontal alignment.
248    pub const fn stable_id(self) -> AlignmentKeyId {
249        self.stable_id
250    }
251
252    #[must_use]
253    pub(crate) fn default_value(self, dimensions: &ViewDimensions) -> f32 {
254        (self.default_value)(dimensions)
255    }
256}
257
258impl Default for HorizontalAlignment {
259    fn default() -> Self {
260        Self::Center
261    }
262}
263
264impl PartialEq for HorizontalAlignment {
265    fn eq(&self, other: &Self) -> bool {
266        self.stable_id == other.stable_id
267    }
268}
269
270impl Eq for HorizontalAlignment {}
271
272impl Debug for HorizontalAlignment {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.debug_struct("HorizontalAlignment")
275            .field("stable_id", &self.stable_id)
276            .finish_non_exhaustive()
277    }
278}
279
280#[derive(Clone, Copy)]
281/// Vertical alignment guide handle.
282pub struct VerticalAlignment {
283    stable_id: AlignmentKeyId,
284    default_value: fn(&ViewDimensions) -> f32,
285}
286
287impl VerticalAlignment {
288    /// Top alignment guide.
289    #[allow(non_upper_case_globals)]
290    pub const Top: Self = Self {
291        stable_id: AlignmentKeyId::from_name("waterui.layout.vertical.top"),
292        default_value: top_alignment_default,
293    };
294
295    /// Center alignment guide.
296    #[allow(non_upper_case_globals)]
297    pub const Center: Self = Self {
298        stable_id: AlignmentKeyId::from_name("waterui.layout.vertical.center"),
299        default_value: center_vertical_alignment_default,
300    };
301
302    /// Bottom alignment guide.
303    #[allow(non_upper_case_globals)]
304    pub const Bottom: Self = Self {
305        stable_id: AlignmentKeyId::from_name("waterui.layout.vertical.bottom"),
306        default_value: bottom_alignment_default,
307    };
308
309    /// First baseline alignment guide.
310    #[allow(non_upper_case_globals)]
311    pub const FirstBaseline: Self = Self {
312        stable_id: AlignmentKeyId::from_name("waterui.layout.vertical.first_baseline"),
313        default_value: first_baseline_alignment_default,
314    };
315
316    /// Last baseline alignment guide.
317    #[allow(non_upper_case_globals)]
318    pub const LastBaseline: Self = Self {
319        stable_id: AlignmentKeyId::from_name("waterui.layout.vertical.last_baseline"),
320        default_value: last_baseline_alignment_default,
321    };
322
323    #[must_use]
324    /// Returns the stable identifier for this built-in vertical alignment.
325    pub const fn stable_id(self) -> AlignmentKeyId {
326        self.stable_id
327    }
328
329    #[must_use]
330    pub(crate) fn default_value(self, dimensions: &ViewDimensions) -> f32 {
331        (self.default_value)(dimensions)
332    }
333}
334
335impl Default for VerticalAlignment {
336    fn default() -> Self {
337        Self::Center
338    }
339}
340
341impl PartialEq for VerticalAlignment {
342    fn eq(&self, other: &Self) -> bool {
343        self.stable_id == other.stable_id
344    }
345}
346
347impl Eq for VerticalAlignment {}
348
349impl Debug for VerticalAlignment {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        f.debug_struct("VerticalAlignment")
352            .field("stable_id", &self.stable_id)
353            .finish_non_exhaustive()
354    }
355}
356
357/// Combined two-dimensional alignment used by layout containers.
358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub struct Alignment {
360    horizontal: HorizontalAlignment,
361    vertical: VerticalAlignment,
362}
363
364impl Alignment {
365    /// Top-center alignment.
366    #[allow(non_upper_case_globals)]
367    pub const Top: Self = Self::new(HorizontalAlignment::Center, VerticalAlignment::Top);
368
369    /// Top-leading alignment.
370    #[allow(non_upper_case_globals)]
371    pub const TopLeading: Self = Self::new(HorizontalAlignment::Leading, VerticalAlignment::Top);
372
373    /// Top-trailing alignment.
374    #[allow(non_upper_case_globals)]
375    pub const TopTrailing: Self = Self::new(HorizontalAlignment::Trailing, VerticalAlignment::Top);
376
377    /// Center alignment.
378    #[allow(non_upper_case_globals)]
379    pub const Center: Self = Self::new(HorizontalAlignment::Center, VerticalAlignment::Center);
380
381    /// Leading-center alignment.
382    #[allow(non_upper_case_globals)]
383    pub const Leading: Self = Self::new(HorizontalAlignment::Leading, VerticalAlignment::Center);
384
385    /// Trailing-center alignment.
386    #[allow(non_upper_case_globals)]
387    pub const Trailing: Self = Self::new(HorizontalAlignment::Trailing, VerticalAlignment::Center);
388
389    /// Bottom-center alignment.
390    #[allow(non_upper_case_globals)]
391    pub const Bottom: Self = Self::new(HorizontalAlignment::Center, VerticalAlignment::Bottom);
392
393    /// Bottom-leading alignment.
394    #[allow(non_upper_case_globals)]
395    pub const BottomLeading: Self =
396        Self::new(HorizontalAlignment::Leading, VerticalAlignment::Bottom);
397
398    /// Bottom-trailing alignment.
399    #[allow(non_upper_case_globals)]
400    pub const BottomTrailing: Self =
401        Self::new(HorizontalAlignment::Trailing, VerticalAlignment::Bottom);
402
403    /// Creates a combined alignment from horizontal and vertical guides.
404    #[must_use]
405    pub const fn new(horizontal: HorizontalAlignment, vertical: VerticalAlignment) -> Self {
406        Self {
407            horizontal,
408            vertical,
409        }
410    }
411
412    /// Returns the horizontal component.
413    #[must_use]
414    pub const fn horizontal(&self) -> HorizontalAlignment {
415        self.horizontal
416    }
417
418    /// Returns the vertical component.
419    #[must_use]
420    pub const fn vertical(&self) -> VerticalAlignment {
421        self.vertical
422    }
423}
424
425impl Default for Alignment {
426    fn default() -> Self {
427        Self::Center
428    }
429}
430
431/// Measured dimensions together with explicit alignment guides.
432#[derive(Clone, Debug, PartialEq, Default)]
433pub struct ViewDimensions {
434    /// The measured size.
435    pub size: Size,
436    explicit_horizontal_guides: Vec<(HorizontalAlignment, f32)>,
437    explicit_vertical_guides: Vec<(VerticalAlignment, f32)>,
438}
439
440impl ViewDimensions {
441    /// Creates dimensions for the provided size.
442    #[must_use]
443    pub const fn new(size: Size) -> Self {
444        Self {
445            size,
446            explicit_horizontal_guides: Vec::new(),
447            explicit_vertical_guides: Vec::new(),
448        }
449    }
450
451    /// Returns the resolved horizontal guide value.
452    #[must_use]
453    pub fn horizontal(&self, alignment: HorizontalAlignment) -> f32 {
454        self.explicit_horizontal(alignment)
455            .unwrap_or_else(|| alignment.default_value(self))
456    }
457
458    /// Returns the resolved vertical guide value.
459    #[must_use]
460    pub fn vertical(&self, alignment: VerticalAlignment) -> f32 {
461        self.explicit_vertical(alignment)
462            .unwrap_or_else(|| alignment.default_value(self))
463    }
464
465    /// Returns the explicit horizontal guide value, if any.
466    #[must_use]
467    pub fn explicit_horizontal(&self, alignment: HorizontalAlignment) -> Option<f32> {
468        self.explicit_horizontal_guides
469            .iter()
470            .rev()
471            .find_map(|(guide, value)| (*guide == alignment).then_some(*value))
472    }
473
474    /// Returns the explicit vertical guide value, if any.
475    #[must_use]
476    pub fn explicit_vertical(&self, alignment: VerticalAlignment) -> Option<f32> {
477        self.explicit_vertical_guides
478            .iter()
479            .rev()
480            .find_map(|(guide, value)| (*guide == alignment).then_some(*value))
481    }
482
483    /// Returns an iterator over explicit horizontal guides.
484    pub fn explicit_horizontal_guides(
485        &self,
486    ) -> impl Iterator<Item = (HorizontalAlignment, f32)> + '_ {
487        self.explicit_horizontal_guides.iter().copied()
488    }
489
490    /// Returns an iterator over explicit vertical guides.
491    pub fn explicit_vertical_guides(&self) -> impl Iterator<Item = (VerticalAlignment, f32)> + '_ {
492        self.explicit_vertical_guides.iter().copied()
493    }
494
495    /// Stores an explicit horizontal guide value.
496    pub fn set_horizontal(&mut self, alignment: HorizontalAlignment, value: f32) {
497        self.explicit_horizontal_guides.push((alignment, value));
498    }
499
500    /// Stores an explicit vertical guide value.
501    pub fn set_vertical(&mut self, alignment: VerticalAlignment, value: f32) {
502        self.explicit_vertical_guides.push((alignment, value));
503    }
504
505    /// Builder-style horizontal guide setter.
506    #[must_use]
507    pub fn with_horizontal(mut self, alignment: HorizontalAlignment, value: f32) -> Self {
508        self.set_horizontal(alignment, value);
509        self
510    }
511
512    /// Builder-style vertical guide setter.
513    #[must_use]
514    pub fn with_vertical(mut self, alignment: VerticalAlignment, value: f32) -> Self {
515        self.set_vertical(alignment, value);
516        self
517    }
518}
519
520/// A child's frame and the size proposal selected to produce its layout.
521#[derive(Clone, Copy, Debug, PartialEq)]
522pub struct SubviewPlacement {
523    /// The child frame in the parent layout's coordinate space.
524    pub frame: Rect,
525    /// The proposal used to measure and recursively place the child; it is not inferred from the frame.
526    pub proposal: ProposalSize,
527}
528
529impl SubviewPlacement {
530    /// Pairs a placed frame with its selected measurement proposal.
531    #[must_use]
532    pub const fn new(frame: Rect, proposal: ProposalSize) -> Self {
533        Self { frame, proposal }
534    }
535}
536
537/// A child view together with its placed frame inside a container.
538#[derive(Clone, Copy)]
539pub struct PlacedSubview<'a> {
540    /// The child proxy.
541    pub view: &'a dyn SubView,
542    /// The child frame in container-local coordinates.
543    pub frame: Rect,
544    /// The proposal selected for this child's placement.
545    pub proposal: ProposalSize,
546}
547
548impl Debug for PlacedSubview<'_> {
549    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550        f.debug_struct("PlacedSubview")
551            .field("frame", &self.frame)
552            .field("proposal", &self.proposal)
553            .finish_non_exhaustive()
554    }
555}
556
557impl<'a> PlacedSubview<'a> {
558    /// Creates a placed subview.
559    #[must_use]
560    pub const fn new(view: &'a dyn SubView, placement: SubviewPlacement) -> Self {
561        Self {
562            view,
563            frame: placement.frame,
564            proposal: placement.proposal,
565        }
566    }
567
568    /// Returns the child's dimensions for its placed size proposal.
569    #[must_use]
570    pub fn dimensions(&self) -> ViewDimensions {
571        self.view.measure(self.proposal)
572    }
573
574    /// Returns the resolved horizontal guide in container coordinates.
575    #[must_use]
576    pub fn horizontal(&self, alignment: HorizontalAlignment) -> f32 {
577        self.frame.x() + self.dimensions().horizontal(alignment)
578    }
579
580    /// Returns the resolved vertical guide in container coordinates.
581    #[must_use]
582    pub fn vertical(&self, alignment: VerticalAlignment) -> f32 {
583        self.frame.y() + self.dimensions().vertical(alignment)
584    }
585
586    /// Returns the explicit horizontal guide in container coordinates, if any.
587    #[must_use]
588    pub fn explicit_horizontal(&self, alignment: HorizontalAlignment) -> Option<f32> {
589        self.dimensions()
590            .explicit_horizontal(alignment)
591            .map(|value| self.frame.x() + value)
592    }
593
594    /// Returns the explicit vertical guide in container coordinates, if any.
595    #[must_use]
596    pub fn explicit_vertical(&self, alignment: VerticalAlignment) -> Option<f32> {
597        self.dimensions()
598            .explicit_vertical(alignment)
599            .map(|value| self.frame.y() + value)
600    }
601}
602
603const fn leading_alignment_default(dimensions: &ViewDimensions) -> f32 {
604    let _ = dimensions;
605    0.0
606}
607
608const fn center_horizontal_alignment_default(dimensions: &ViewDimensions) -> f32 {
609    dimensions.size.width * 0.5
610}
611
612const fn trailing_alignment_default(dimensions: &ViewDimensions) -> f32 {
613    dimensions.size.width
614}
615
616const fn top_alignment_default(dimensions: &ViewDimensions) -> f32 {
617    let _ = dimensions;
618    0.0
619}
620
621const fn center_vertical_alignment_default(dimensions: &ViewDimensions) -> f32 {
622    dimensions.size.height * 0.5
623}
624
625const fn bottom_alignment_default(dimensions: &ViewDimensions) -> f32 {
626    dimensions.size.height
627}
628
629const fn first_baseline_alignment_default(dimensions: &ViewDimensions) -> f32 {
630    dimensions.size.height
631}
632
633const fn last_baseline_alignment_default(dimensions: &ViewDimensions) -> f32 {
634    dimensions.size.height
635}
636
637// ============================================================================
638// SubView Trait - Child View Proxy
639// ============================================================================
640
641/// A proxy for querying child view sizes during layout.
642///
643/// This trait allows layout containers to negotiate with children by asking
644/// "if I propose this size, how big would you be?" multiple times with
645/// different proposals.
646///
647/// # Pure Functions
648///
649/// All methods are pure (take `&self`) with no side effects.
650///
651/// # Caching is the `SubView`'s responsibility, never the [`Layout`]'s
652///
653/// The [`Layout`] trait deliberately has **no** caching: containers probe their
654/// children freely with many proposals. Any measurement caching must therefore be
655/// owned by the `SubView` implementation itself (the leaf), not the container.
656/// Expensive measures — text shaping above all — **must** cache.
657///
658/// # Measurement is single-threaded by contract
659///
660/// Measuring runs on whichever thread drives layout, so a `SubView` is neither
661/// `Send` nor `Sync` and its cache may be a plain
662/// [`RefCell`](core::cell::RefCell).
663///
664/// Measuring siblings on a worker pool was tried and removed. A container's
665/// children number in the single digits and a cache-hit measure costs tens of
666/// nanoseconds, while a fork-join costs tens of microseconds — and it was paid by
667/// every container, including the majority with nothing to offload. Parallelism
668/// belongs where the work is genuinely large and batched (a renderer pre-shaping
669/// every visible text run for a frame), not inside the per-container measurement
670/// loop.
671pub trait SubView {
672    /// Measure the child for a given proposal.
673    ///
674    /// This method may be called multiple times with different proposals
675    /// to probe the child's flexibility:
676    ///
677    /// - `ProposalSize::new(None, None)` - ideal/intrinsic size
678    /// - `ProposalSize::new(Some(0.0), None)` - minimum width
679    /// - `ProposalSize::new(Some(f32::INFINITY), None)` - maximum width
680    /// - `ProposalSize::new(Some(200.0), None)` - constrained width
681    ///
682    /// # The three-point contract
683    ///
684    /// Containers work out how far a child may shrink, and how far it wants to
685    /// grow, from those first three answers, so on each axis they must satisfy
686    /// `min <= ideal <= max`. Every extent is finite and non-negative, except a
687    /// maximum, where `f32::INFINITY` means unbounded — that is how a view says
688    /// it will take whatever it is offered.
689    ///
690    /// Breaking this is not a local error: a minimum above the ideal makes a
691    /// stack compress a child past a size it cannot take, and an ideal above the
692    /// maximum makes it stretch one past a size it cannot take. Both surface as
693    /// a misplaced layout somewhere else entirely.
694    #[must_use]
695    fn measure(&self, proposal: ProposalSize) -> ViewDimensions;
696
697    /// Which axis (or axes) this view stretches to fill available space.
698    ///
699    /// - `StretchAxis::None`: Content-sized, uses intrinsic size
700    /// - `StretchAxis::Horizontal`: Expands width only (e.g., `TextField`, Slider)
701    /// - `StretchAxis::Vertical`: Expands height only
702    /// - `StretchAxis::Both`: Greedy, fills all space (e.g., Spacer, Color)
703    ///
704    /// Layout containers use this to distribute remaining space appropriately:
705    /// - `VStack` checks `stretches_vertical()` for height distribution
706    /// - `HStack` checks `stretches_horizontal()` for width distribution
707    fn stretch_axis(&self) -> StretchAxis;
708
709    /// Layout priority for space distribution.
710    ///
711    /// Higher priority views are measured first and get space preference.
712    fn priority(&self) -> i32;
713}
714
715/// A [`SubView`] that remembers what each proposal measured.
716///
717/// Containers probe the same child repeatedly — `size_that_fits` measures at the
718/// ideal proposal, `place` measures again at the resolved bounds, alignment-guide
719/// resolution measures once more per guide — and a container that is itself a child
720/// re-runs all of that for its own parent's probes, so the repeats multiply with
721/// tree depth. Wrapping each child for the duration of one pass collapses them to
722/// one measure per distinct proposal.
723///
724/// The cache is a fixed-capacity inline array, scanned linearly: a pass probes any
725/// one child a handful of times, and keeping it inline means wrapping a child costs
726/// no allocation — which matters on the embedded backend, whose budget is heap
727/// traffic per frame rather than CPU. A child probed at more than
728/// [`MEMOIZED_PROPOSALS`] distinct proposals simply measures again for the extras.
729/// Entries are keyed on the proposal's bits, so `-0.0` and a NaN proposal compare
730/// consistently instead of by float equality.
731pub struct MemoizedSubView<'a> {
732    inner: &'a dyn SubView,
733    cache: core::cell::RefCell<[Option<(ProposalSize, ViewDimensions)>; MEMOIZED_PROPOSALS]>,
734}
735
736/// How many distinct proposals one child's memo retains.
737///
738/// A container probes a child at its ideal size, at the resolved bounds, and —
739/// once the distribution algorithm asks — at its minimum and maximum, so four
740/// covers a pass without spilling.
741pub const MEMOIZED_PROPOSALS: usize = 4;
742
743impl<'a> MemoizedSubView<'a> {
744    /// Wraps `inner` with a cache that lives as long as this value.
745    #[must_use]
746    pub fn new(inner: &'a dyn SubView) -> Self {
747        Self {
748            inner,
749            cache: core::cell::RefCell::new([const { None }; MEMOIZED_PROPOSALS]),
750        }
751    }
752}
753
754impl Debug for MemoizedSubView<'_> {
755    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756        f.debug_struct("MemoizedSubView").finish_non_exhaustive()
757    }
758}
759
760const fn proposal_axis_bits(axis: Option<f32>) -> Option<u32> {
761    match axis {
762        Some(value) => Some(value.to_bits()),
763        None => None,
764    }
765}
766
767fn same_proposal(left: ProposalSize, right: ProposalSize) -> bool {
768    proposal_axis_bits(left.width) == proposal_axis_bits(right.width)
769        && proposal_axis_bits(left.height) == proposal_axis_bits(right.height)
770}
771
772impl SubView for MemoizedSubView<'_> {
773    fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
774        if let Some((_, dimensions)) = self
775            .cache
776            .borrow()
777            .iter()
778            .flatten()
779            .find(|(cached, _)| same_proposal(*cached, proposal))
780        {
781            return dimensions.clone();
782        }
783        let dimensions = self.inner.measure(proposal);
784        if let Some(slot) = self
785            .cache
786            .borrow_mut()
787            .iter_mut()
788            .find(|slot| slot.is_none())
789        {
790            *slot = Some((proposal, dimensions.clone()));
791        }
792        dimensions
793    }
794
795    fn stretch_axis(&self) -> StretchAxis {
796        self.inner.stretch_axis()
797    }
798
799    fn priority(&self) -> i32 {
800        self.inner.priority()
801    }
802}
803
804/// Runs `pass` with every child wrapped in a [`MemoizedSubView`], so repeated
805/// probes within that one layout pass measure each child at most once per
806/// distinct proposal.
807///
808/// Call this at the boundary where a layout pass begins — driving a [`Layout`]
809/// directly rather than through [`measure_layout`] means owning this yourself.
810pub fn with_memoized_children<R>(
811    children: &[&dyn SubView],
812    pass: impl FnOnce(&[&dyn SubView]) -> R,
813) -> R {
814    let memoized: SmallVec<[MemoizedSubView<'_>; 4]> =
815        children.iter().copied().map(MemoizedSubView::new).collect();
816    let refs: SmallVec<[&dyn SubView; 4]> =
817        memoized.iter().map(|child| child as &dyn SubView).collect();
818    pass(&refs)
819}
820
821// ============================================================================
822// Layout Trait - Container Layout
823// ============================================================================
824
825/// Callback used by reactive layouts to invalidate their native container.
826#[doc(hidden)]
827pub type LayoutInvalidationCallback = Rc<dyn Fn() + 'static>;
828
829/// A layout algorithm for arranging child views.
830///
831/// Layouts receive a size proposal from their parent, query their children
832/// to determine sizes, and then place children within the final bounds.
833///
834/// # Two-Phase Layout
835///
836/// 1. **Sizing** ([`size_that_fits`](Self::size_that_fits)): Determine how big
837///    this container should be given a proposal
838/// 2. **Placement** ([`place`](Self::place)): Position children within the
839///    final bounds
840///
841/// # Note on Safe Area
842///
843/// Safe area handling is intentionally **not** part of the Layout trait.
844/// Safe area is a platform-specific concept handled by backends. Views can
845/// use the `IgnoresSafeArea` metadata to opt out of safe area insets.
846pub trait Layout: Debug + Any {
847    /// Calculate the size this layout wants given a proposal.
848    ///
849    /// The layout can query children multiple times with different proposals
850    /// to determine optimal sizing.
851    ///
852    /// # Arguments
853    ///
854    /// * `proposal` - The size proposed by the parent
855    /// * `children` - References to child proxies for size queries
856    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size;
857
858    /// Place children within the given bounds.
859    ///
860    /// Called after sizing is complete. `proposal` is the measurement input
861    /// selected for this layout — the proposal it was asked to fit — never the
862    /// last probe it sent a child or a proposal converted back from `bounds`.
863    /// Returns one [`SubviewPlacement`] per child, in exactly the order
864    /// `children` was given; each placement retains the proposal the child was
865    /// measured and recursively placed with.
866    ///
867    /// # Arguments
868    ///
869    /// * `bounds` - The rectangle this layout should fill
870    /// * `proposal` - The size proposal selected to measure this layout
871    /// * `children` - References to child proxies (may query sizes again)
872    fn place(
873        &self,
874        bounds: Rect,
875        proposal: ProposalSize,
876        children: &[&dyn SubView],
877    ) -> Vec<SubviewPlacement>;
878
879    /// Returns an explicit horizontal guide for this container, if any.
880    fn explicit_horizontal(
881        &self,
882        _alignment: HorizontalAlignment,
883        _bounds: Rect,
884        _children: &[PlacedSubview<'_>],
885    ) -> Option<f32> {
886        None
887    }
888
889    /// Returns an explicit vertical guide for this container, if any.
890    fn explicit_vertical(
891        &self,
892        _alignment: VerticalAlignment,
893        _bounds: Rect,
894        _children: &[PlacedSubview<'_>],
895    ) -> Option<f32> {
896        None
897    }
898
899    /// Returns the horizontal alignments this container may expose explicitly.
900    fn explicit_horizontal_alignments(&self) -> Vec<HorizontalAlignment> {
901        Vec::new()
902    }
903
904    /// Returns the vertical alignments this container may expose explicitly.
905    fn explicit_vertical_alignments(&self) -> Vec<VerticalAlignment> {
906        Vec::new()
907    }
908
909    /// Which axis this container stretches to fill available space, given what
910    /// its children said about themselves.
911    ///
912    /// A container claims nothing of its own, but it must relay what its children
913    /// claim: a stack of labels is content-sized, while a stack holding something
914    /// greedy answers with that child's axis. A container that stayed silent would
915    /// be handed its own intrinsic size by its parent, and the greedy child inside
916    /// it would then fill a box of nothing. A layout that is transparent to its
917    /// content — a background, an overlay, an alignment guide — answers with that
918    /// content's axis, and a frame answers from its own constraints.
919    ///
920    /// `children` carries each child's own [`StretchAxis`] in order. Passing it is
921    /// what lets a transparent layout answer from live state: without it such a
922    /// layout has to copy its content's axis when it is built, and that copy goes
923    /// stale the moment the content's own answer changes — the container then
924    /// claims space its content no longer wants, or refuses space it now does.
925    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
926        let _ = children;
927        StretchAxis::None
928    }
929
930    /// Watches layout inputs whose changes require a new native layout pass.
931    ///
932    /// This is backend infrastructure. Layout implementations return guards for
933    /// their precise reactive fields; native containers retain those guards for
934    /// the layout object's lifetime.
935    #[doc(hidden)]
936    fn watch_invalidation(&self, _invalidate: LayoutInvalidationCallback) -> Vec<BoxWatcherGuard> {
937        Vec::new()
938    }
939}
940
941/// Measures a layout and resolves its explicit guides for the given children.
942///
943/// Children are memoized for the duration of the call (see
944/// [`with_memoized_children`]): sizing, placement and guide resolution all probe
945/// the same children, so without it each child is measured several times over.
946///
947/// Only finite measurements resolve explicit guides: a maximum-size probe may
948/// answer with an unbounded extent, and placing children or resolving guides
949/// against infinite geometry is meaningless, so an infinite axis returns the
950/// measured size without calling `place` at all.
951#[must_use]
952pub fn measure_layout(
953    layout: &dyn Layout,
954    proposal: ProposalSize,
955    children: &[&dyn SubView],
956) -> ViewDimensions {
957    with_memoized_children(children, |children| {
958        measure_layout_memoized(layout, proposal, children)
959    })
960}
961
962fn measure_layout_memoized(
963    layout: &dyn Layout,
964    proposal: ProposalSize,
965    children: &[&dyn SubView],
966) -> ViewDimensions {
967    let size = layout.size_that_fits(proposal, children);
968    if size.width.is_infinite() || size.height.is_infinite() {
969        return ViewDimensions::new(size);
970    }
971    let bounds = Rect::from_size(size);
972    let child_placements = layout.place(bounds, proposal, children);
973    let placed_subviews: SmallVec<[PlacedSubview<'_>; 4]> = children
974        .iter()
975        .zip(child_placements.iter().copied())
976        .map(|(view, placement)| PlacedSubview::new(*view, placement))
977        .collect();
978
979    let mut dimensions = ViewDimensions::new(size);
980    let mut horizontal_keys = layout.explicit_horizontal_alignments();
981    let mut vertical_keys = layout.explicit_vertical_alignments();
982
983    for child in &placed_subviews {
984        let child_dimensions = child.dimensions();
985        for (alignment, _) in child_dimensions.explicit_horizontal_guides() {
986            if !horizontal_keys.contains(&alignment) {
987                horizontal_keys.push(alignment);
988            }
989        }
990        for (alignment, _) in child_dimensions.explicit_vertical_guides() {
991            if !vertical_keys.contains(&alignment) {
992                vertical_keys.push(alignment);
993            }
994        }
995    }
996
997    for alignment in horizontal_keys {
998        if let Some(value) = layout.explicit_horizontal(alignment, bounds, &placed_subviews) {
999            dimensions.set_horizontal(alignment, value);
1000        }
1001    }
1002    for alignment in vertical_keys {
1003        if let Some(value) = layout.explicit_vertical(alignment, bounds, &placed_subviews) {
1004            dimensions.set_vertical(alignment, value);
1005        }
1006    }
1007
1008    dimensions
1009}
1010
1011// ============================================================================
1012// Geometry Types
1013// ============================================================================
1014
1015/// Axis-aligned rectangle relative to its parent.
1016#[derive(Clone, Copy, Debug, PartialEq)]
1017pub struct Rect {
1018    origin: Point,
1019    size: Size,
1020}
1021
1022impl Rect {
1023    /// Creates a new [`Rect`] with the provided `origin` and `size`.
1024    #[must_use]
1025    pub const fn new(origin: Point, size: Size) -> Self {
1026        Self { origin, size }
1027    }
1028
1029    /// Creates a rectangle from origin (0, 0) with the given size.
1030    #[must_use]
1031    pub const fn from_size(size: Size) -> Self {
1032        Self {
1033            origin: Point::zero(),
1034            size,
1035        }
1036    }
1037
1038    /// Returns the rectangle's origin (top-left corner).
1039    #[must_use]
1040    pub const fn origin(&self) -> Point {
1041        self.origin
1042    }
1043
1044    /// Returns the rectangle's size.
1045    #[must_use]
1046    pub const fn size(&self) -> &Size {
1047        &self.size
1048    }
1049
1050    /// Returns the rectangle's x-coordinate (left edge).
1051    #[must_use]
1052    pub const fn x(&self) -> f32 {
1053        self.origin.x
1054    }
1055
1056    /// Returns the rectangle's y-coordinate (top edge).
1057    #[must_use]
1058    pub const fn y(&self) -> f32 {
1059        self.origin.y
1060    }
1061
1062    /// Returns the rectangle's width.
1063    #[must_use]
1064    pub const fn width(&self) -> f32 {
1065        self.size.width
1066    }
1067
1068    /// Returns the rectangle's height.
1069    #[must_use]
1070    pub const fn height(&self) -> f32 {
1071        self.size.height
1072    }
1073
1074    /// Returns the minimum x-coordinate (left edge).
1075    #[must_use]
1076    pub const fn min_x(&self) -> f32 {
1077        self.origin.x
1078    }
1079
1080    /// Returns the minimum y-coordinate (top edge).
1081    #[must_use]
1082    pub const fn min_y(&self) -> f32 {
1083        self.origin.y
1084    }
1085
1086    /// Returns the maximum x-coordinate (right edge).
1087    #[must_use]
1088    pub const fn max_x(&self) -> f32 {
1089        self.origin.x + self.size.width
1090    }
1091
1092    /// Returns the maximum y-coordinate (bottom edge).
1093    #[must_use]
1094    pub const fn max_y(&self) -> f32 {
1095        self.origin.y + self.size.height
1096    }
1097
1098    /// Returns the midpoint x-coordinate.
1099    #[must_use]
1100    pub const fn mid_x(&self) -> f32 {
1101        self.origin.x + self.size.width / 2.0
1102    }
1103
1104    /// Returns the midpoint y-coordinate.
1105    #[must_use]
1106    pub const fn mid_y(&self) -> f32 {
1107        self.origin.y + self.size.height / 2.0
1108    }
1109
1110    /// Returns the center point of the rectangle.
1111    #[must_use]
1112    pub const fn center(&self) -> Point {
1113        Point::new(self.mid_x(), self.mid_y())
1114    }
1115
1116    /// Inset the rectangle by the given amounts on each edge.
1117    #[must_use]
1118    pub fn inset(&self, top: f32, bottom: f32, leading: f32, trailing: f32) -> Self {
1119        Self::new(
1120            Point::new(self.origin.x + leading, self.origin.y + top),
1121            Size::new(
1122                (self.size.width - leading - trailing).max(0.0),
1123                (self.size.height - top - bottom).max(0.0),
1124            ),
1125        )
1126    }
1127}
1128
1129// ============================================================================
1130// Size
1131// ============================================================================
1132
1133/// Two-dimensional size expressed in points.
1134#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
1135pub struct Size {
1136    /// The width in points.
1137    pub width: f32,
1138    /// The height in points.
1139    pub height: f32,
1140}
1141
1142impl Size {
1143    /// Constructs a [`Size`] with the given `width` and `height`.
1144    #[must_use]
1145    pub const fn new(width: f32, height: f32) -> Self {
1146        Self { width, height }
1147    }
1148
1149    /// Creates a [`Size`] with zero width and height.
1150    #[must_use]
1151    pub const fn zero() -> Self {
1152        Self {
1153            width: 0.0,
1154            height: 0.0,
1155        }
1156    }
1157
1158    /// Returns true if both dimensions are zero.
1159    #[must_use]
1160    pub const fn is_zero(&self) -> bool {
1161        self.width == 0.0 && self.height == 0.0
1162    }
1163}
1164
1165// ============================================================================
1166// Point
1167// ============================================================================
1168
1169/// Absolute coordinate relative to a parent layout's origin.
1170#[derive(Clone, Copy, Debug, PartialEq, Default)]
1171pub struct Point {
1172    /// The x-coordinate in points.
1173    pub x: f32,
1174    /// The y-coordinate in points.
1175    pub y: f32,
1176}
1177
1178impl Point {
1179    /// Constructs a [`Point`] at the given `x` and `y`.
1180    #[must_use]
1181    pub const fn new(x: f32, y: f32) -> Self {
1182        Self { x, y }
1183    }
1184
1185    /// Creates a [`Point`] at the origin (0, 0).
1186    #[must_use]
1187    pub const fn zero() -> Self {
1188        Self { x: 0.0, y: 0.0 }
1189    }
1190}
1191
1192impl From<(f32, f32)> for Point {
1193    fn from((x, y): (f32, f32)) -> Self {
1194        Self { x, y }
1195    }
1196}
1197
1198impl From<[f32; 2]> for Point {
1199    fn from([x, y]: [f32; 2]) -> Self {
1200        Self { x, y }
1201    }
1202}
1203
1204impl From<(f32, f32)> for Size {
1205    fn from((width, height): (f32, f32)) -> Self {
1206        Self { width, height }
1207    }
1208}
1209
1210impl From<[f32; 2]> for Size {
1211    fn from([width, height]: [f32; 2]) -> Self {
1212        Self { width, height }
1213    }
1214}
1215
1216// ============================================================================
1217// Vec2
1218// ============================================================================
1219
1220/// Two-dimensional displacement vector (e.g. velocity, gravity, wind, offset).
1221///
1222/// Distinct from [`Point`], which represents an absolute position. A `Vec2`
1223/// encodes a delta and is the natural input for physics-style modifiers such
1224/// as `gravity(...)` and `wind(...)` on a particle system.
1225#[derive(Clone, Copy, Debug, PartialEq, Default)]
1226pub struct Vec2 {
1227    /// Displacement along the x axis.
1228    pub dx: f32,
1229    /// Displacement along the y axis.
1230    pub dy: f32,
1231}
1232
1233impl Vec2 {
1234    /// Constructs a [`Vec2`] with the given components.
1235    #[must_use]
1236    pub const fn new(dx: f32, dy: f32) -> Self {
1237        Self { dx, dy }
1238    }
1239
1240    /// The zero vector.
1241    pub const ZERO: Self = Self { dx: 0.0, dy: 0.0 };
1242}
1243
1244impl From<(f32, f32)> for Vec2 {
1245    fn from((dx, dy): (f32, f32)) -> Self {
1246        Self { dx, dy }
1247    }
1248}
1249
1250impl From<[f32; 2]> for Vec2 {
1251    fn from([dx, dy]: [f32; 2]) -> Self {
1252        Self { dx, dy }
1253    }
1254}
1255
1256// ============================================================================
1257// UnitPoint
1258// ============================================================================
1259
1260/// Normalized coordinates (0.0–1.0) for positioning and gradient endpoints.
1261///
1262/// Used to specify both anchor points on views and target positions in parents.
1263/// Values outside `0.0..=1.0` are valid and will position outside bounds.
1264#[derive(Debug, Clone, Copy, PartialEq, Default)]
1265pub struct UnitPoint {
1266    /// X coordinate (0.0 = left edge, 1.0 = right edge).
1267    pub x: f32,
1268    /// Y coordinate (0.0 = top edge, 1.0 = bottom edge).
1269    pub y: f32,
1270}
1271
1272impl UnitPoint {
1273    /// Top-left corner (0.0, 0.0).
1274    pub const TOP_LEADING: Self = Self { x: 0.0, y: 0.0 };
1275    /// Top center (0.5, 0.0).
1276    pub const TOP: Self = Self { x: 0.5, y: 0.0 };
1277    /// Top-right corner (1.0, 0.0).
1278    pub const TOP_TRAILING: Self = Self { x: 1.0, y: 0.0 };
1279    /// Left center (0.0, 0.5).
1280    pub const LEADING: Self = Self { x: 0.0, y: 0.5 };
1281    /// Center (0.5, 0.5).
1282    pub const CENTER: Self = Self { x: 0.5, y: 0.5 };
1283    /// Right center (1.0, 0.5).
1284    pub const TRAILING: Self = Self { x: 1.0, y: 0.5 };
1285    /// Bottom-left corner (0.0, 1.0).
1286    pub const BOTTOM_LEADING: Self = Self { x: 0.0, y: 1.0 };
1287    /// Bottom center (0.5, 1.0).
1288    pub const BOTTOM: Self = Self { x: 0.5, y: 1.0 };
1289    /// Bottom-right corner (1.0, 1.0).
1290    pub const BOTTOM_TRAILING: Self = Self { x: 1.0, y: 1.0 };
1291
1292    /// Creates a custom unit point.
1293    #[must_use]
1294    pub const fn new(x: f32, y: f32) -> Self {
1295        Self { x, y }
1296    }
1297}
1298
1299impl From<(f32, f32)> for UnitPoint {
1300    fn from((x, y): (f32, f32)) -> Self {
1301        Self { x, y }
1302    }
1303}
1304
1305impl From<[f32; 2]> for UnitPoint {
1306    fn from([x, y]: [f32; 2]) -> Self {
1307        Self { x, y }
1308    }
1309}
1310
1311impl From<Alignment> for UnitPoint {
1312    fn from(alignment: Alignment) -> Self {
1313        let horizontal = alignment.horizontal();
1314        let vertical = alignment.vertical();
1315        if horizontal == HorizontalAlignment::Leading && vertical == VerticalAlignment::Top {
1316            Self::TOP_LEADING
1317        } else if horizontal == HorizontalAlignment::Trailing && vertical == VerticalAlignment::Top
1318        {
1319            Self::TOP_TRAILING
1320        } else if horizontal == HorizontalAlignment::Leading
1321            && vertical == VerticalAlignment::Bottom
1322        {
1323            Self::BOTTOM_LEADING
1324        } else if horizontal == HorizontalAlignment::Trailing
1325            && vertical == VerticalAlignment::Bottom
1326        {
1327            Self::BOTTOM_TRAILING
1328        } else if horizontal == HorizontalAlignment::Leading {
1329            Self::LEADING
1330        } else if horizontal == HorizontalAlignment::Trailing {
1331            Self::TRAILING
1332        } else if vertical == VerticalAlignment::Top {
1333            Self::TOP
1334        } else if vertical == VerticalAlignment::Bottom {
1335            Self::BOTTOM
1336        } else {
1337            Self::CENTER
1338        }
1339    }
1340}
1341
1342// ============================================================================
1343// Affine2
1344// ============================================================================
1345
1346/// 2D affine transform stored as a row-major 2x3 matrix.
1347///
1348/// The transform maps a point `(x, y)` to:
1349///
1350/// ```text
1351/// x' = a * x + c * y + e
1352/// y' = b * x + d * y + f
1353/// ```
1354///
1355/// Layout matches the canonical 6-coefficient ordering used by `kurbo` and the
1356/// HTML Canvas 2D context, so a transform built here can be losslessly handed to
1357/// a 2D rendering backend.
1358#[derive(Clone, Copy, Debug, PartialEq, Default)]
1359pub struct Affine2 {
1360    /// Scale on the x axis.
1361    pub a: f32,
1362    /// Shear on the y axis (i.e. y component of the transformed x basis).
1363    pub b: f32,
1364    /// Shear on the x axis (i.e. x component of the transformed y basis).
1365    pub c: f32,
1366    /// Scale on the y axis.
1367    pub d: f32,
1368    /// Translation on the x axis.
1369    pub e: f32,
1370    /// Translation on the y axis.
1371    pub f: f32,
1372}
1373
1374impl Affine2 {
1375    /// Identity transform: leaves any point unchanged.
1376    pub const IDENTITY: Self = Self {
1377        a: 1.0,
1378        b: 0.0,
1379        c: 0.0,
1380        d: 1.0,
1381        e: 0.0,
1382        f: 0.0,
1383    };
1384
1385    /// Constructs an affine transform from raw coefficients.
1386    #[must_use]
1387    pub const fn new(
1388        scale_x: f32,
1389        shear_y: f32,
1390        shear_x: f32,
1391        scale_y: f32,
1392        translate_x: f32,
1393        translate_y: f32,
1394    ) -> Self {
1395        Self {
1396            a: scale_x,
1397            b: shear_y,
1398            c: shear_x,
1399            d: scale_y,
1400            e: translate_x,
1401            f: translate_y,
1402        }
1403    }
1404
1405    /// Pure translation by `(tx, ty)`.
1406    #[must_use]
1407    pub const fn translate(tx: f32, ty: f32) -> Self {
1408        Self {
1409            a: 1.0,
1410            b: 0.0,
1411            c: 0.0,
1412            d: 1.0,
1413            e: tx,
1414            f: ty,
1415        }
1416    }
1417
1418    /// Non-uniform scale around the origin.
1419    #[must_use]
1420    pub const fn scale(sx: f32, sy: f32) -> Self {
1421        Self {
1422            a: sx,
1423            b: 0.0,
1424            c: 0.0,
1425            d: sy,
1426            e: 0.0,
1427            f: 0.0,
1428        }
1429    }
1430
1431    /// Rotation around the origin by `radians`.
1432    #[must_use]
1433    pub fn rotate(radians: f32) -> Self {
1434        let (s, c) = radians.sin_cos();
1435        Self {
1436            a: c,
1437            b: s,
1438            c: -s,
1439            d: c,
1440            e: 0.0,
1441            f: 0.0,
1442        }
1443    }
1444}
1445
1446impl From<[f32; 6]> for Affine2 {
1447    fn from(coefficients: [f32; 6]) -> Self {
1448        Self {
1449            a: coefficients[0],
1450            b: coefficients[1],
1451            c: coefficients[2],
1452            d: coefficients[3],
1453            e: coefficients[4],
1454            f: coefficients[5],
1455        }
1456    }
1457}
1458
1459impl From<Affine2> for [f32; 6] {
1460    fn from(t: Affine2) -> Self {
1461        [t.a, t.b, t.c, t.d, t.e, t.f]
1462    }
1463}
1464
1465macro_rules! impl_layout_signal_constant {
1466    ($($ty:ty),+ $(,)?) => {
1467        $(
1468            impl nami::Signal for $ty {
1469                type Output = Self;
1470                type Guard = ();
1471
1472                fn get(&self) -> Self::Output {
1473                    *self
1474                }
1475
1476                fn watch(
1477                    &self,
1478                    _watcher: impl Fn(nami::watcher::Context<Self::Output>) + 'static,
1479                ) {
1480                }
1481            }
1482        )+
1483    };
1484}
1485
1486impl_layout_signal_constant!(
1487    LayoutDirection,
1488    Point,
1489    Size,
1490    Rect,
1491    Vec2,
1492    UnitPoint,
1493    Affine2,
1494    HorizontalAlignment,
1495    VerticalAlignment,
1496    Alignment
1497);
1498
1499// ============================================================================
1500// ProposalSize
1501// ============================================================================
1502
1503/// A size proposal from parent to child during layout negotiation.
1504///
1505/// Each dimension can be:
1506/// - `None` - "Tell me your ideal size" (unspecified)
1507/// - `Some(0.0)` - "Tell me your minimum size"
1508/// - `Some(f32::INFINITY)` - "Tell me your maximum size"
1509/// - `Some(value)` - "I suggest you use this size"
1510///
1511/// Children are free to return any size; the proposal is just a suggestion.
1512#[derive(Clone, Copy, Debug, PartialEq, Default)]
1513pub struct ProposalSize {
1514    /// Width proposal: `None` = unspecified, `Some(f32)` = suggested width
1515    pub width: Option<f32>,
1516    /// Height proposal: `None` = unspecified, `Some(f32)` = suggested height
1517    pub height: Option<f32>,
1518}
1519
1520impl ProposalSize {
1521    /// Creates a [`ProposalSize`] from optional width and height.
1522    #[must_use]
1523    pub fn new(width: impl Into<Option<f32>>, height: impl Into<Option<f32>>) -> Self {
1524        Self {
1525            width: width.into(),
1526            height: height.into(),
1527        }
1528    }
1529
1530    /// Unspecified proposal - asks for ideal/intrinsic size.
1531    pub const UNSPECIFIED: Self = Self {
1532        width: None,
1533        height: None,
1534    };
1535
1536    /// Zero proposal - asks for minimum size.
1537    pub const ZERO: Self = Self {
1538        width: Some(0.0),
1539        height: Some(0.0),
1540    };
1541
1542    /// Infinite proposal - asks for maximum size.
1543    pub const INFINITY: Self = Self {
1544        width: Some(f32::INFINITY),
1545        height: Some(f32::INFINITY),
1546    };
1547
1548    /// Returns the width or a default value if unspecified.
1549    #[must_use]
1550    pub fn width_or(&self, default: f32) -> f32 {
1551        self.width.unwrap_or(default)
1552    }
1553
1554    /// Returns the height or a default value if unspecified.
1555    #[must_use]
1556    pub fn height_or(&self, default: f32) -> f32 {
1557        self.height.unwrap_or(default)
1558    }
1559
1560    /// Replace only the width, keeping the height.
1561    #[must_use]
1562    pub const fn with_width(self, width: Option<f32>) -> Self {
1563        Self {
1564            width,
1565            height: self.height,
1566        }
1567    }
1568
1569    /// Replace only the height, keeping the width.
1570    #[must_use]
1571    pub const fn with_height(self, height: Option<f32>) -> Self {
1572        Self {
1573            width: self.width,
1574            height,
1575        }
1576    }
1577}
1578
1579// ============================================================================
1580// Tests
1581// ============================================================================
1582
1583#[cfg(test)]
1584#[allow(clippy::float_cmp)]
1585mod tests {
1586    use super::*;
1587
1588    #[test]
1589    fn test_rect_geometry() {
1590        let rect = Rect::new(Point::new(10.0, 20.0), Size::new(100.0, 50.0));
1591
1592        assert_eq!(rect.min_x(), 10.0);
1593        assert_eq!(rect.min_y(), 20.0);
1594        assert_eq!(rect.max_x(), 110.0);
1595        assert_eq!(rect.max_y(), 70.0);
1596        assert_eq!(rect.mid_x(), 60.0);
1597        assert_eq!(rect.mid_y(), 45.0);
1598        assert_eq!(rect.width(), 100.0);
1599        assert_eq!(rect.height(), 50.0);
1600    }
1601
1602    #[test]
1603    fn test_rect_inset() {
1604        let rect = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
1605        let inset = rect.inset(10.0, 10.0, 20.0, 20.0);
1606
1607        assert_eq!(inset.x(), 20.0);
1608        assert_eq!(inset.y(), 10.0);
1609        assert_eq!(inset.width(), 60.0);
1610        assert_eq!(inset.height(), 80.0);
1611    }
1612
1613    #[test]
1614    fn test_proposal_size() {
1615        let proposal = ProposalSize::new(Some(100.0), None);
1616
1617        assert_eq!(proposal.width_or(0.0), 100.0);
1618        assert_eq!(proposal.height_or(50.0), 50.0);
1619
1620        let with_height = proposal.with_height(Some(200.0));
1621        assert_eq!(with_height.width, Some(100.0));
1622        assert_eq!(with_height.height, Some(200.0));
1623    }
1624
1625    /// A child that records how many times it was actually measured.
1626    struct CountingSubView {
1627        measures: core::cell::Cell<usize>,
1628    }
1629
1630    impl SubView for CountingSubView {
1631        fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
1632            self.measures.set(self.measures.get() + 1);
1633            ViewDimensions::new(Size::new(proposal.width_or(10.0), proposal.height_or(20.0)))
1634        }
1635
1636        fn stretch_axis(&self) -> StretchAxis {
1637            StretchAxis::None
1638        }
1639
1640        fn priority(&self) -> i32 {
1641            0
1642        }
1643    }
1644
1645    #[test]
1646    fn memoized_subview_measures_once_per_distinct_proposal() {
1647        let inner = CountingSubView {
1648            measures: core::cell::Cell::new(0),
1649        };
1650        let memo = MemoizedSubView::new(&inner);
1651
1652        let ideal = ProposalSize::UNSPECIFIED;
1653        let constrained = ProposalSize::new(Some(80.0), None);
1654
1655        for _ in 0..5 {
1656            assert_eq!(memo.measure(ideal).size, Size::new(10.0, 20.0));
1657            assert_eq!(memo.measure(constrained).size, Size::new(80.0, 20.0));
1658        }
1659
1660        assert_eq!(
1661            inner.measures.get(),
1662            2,
1663            "ten probes over two distinct proposals must reach the child twice"
1664        );
1665    }
1666
1667    #[test]
1668    fn memoized_subview_forwards_priority_and_stretch() {
1669        let inner = CountingSubView {
1670            measures: core::cell::Cell::new(0),
1671        };
1672        let memo = MemoizedSubView::new(&inner);
1673
1674        assert_eq!(memo.priority(), inner.priority());
1675        assert_eq!(memo.stretch_axis(), inner.stretch_axis());
1676    }
1677
1678    /// A leaf whose size ignores the proposal but whose `Leading` guide marks
1679    /// the proposal it was measured with: `1` for an unspecified width, `2`
1680    /// for a concrete one.
1681    struct GuideProbe;
1682
1683    impl SubView for GuideProbe {
1684        fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
1685            let marker = if proposal.width.is_none() { 1.0 } else { 2.0 };
1686            ViewDimensions::new(Size::new(160.0, 20.0))
1687                .with_horizontal(HorizontalAlignment::Leading, marker)
1688        }
1689
1690        fn stretch_axis(&self) -> StretchAxis {
1691            StretchAxis::None
1692        }
1693
1694        fn priority(&self) -> i32 {
1695            0
1696        }
1697    }
1698
1699    #[test]
1700    fn placed_subview_measures_selected_proposal_not_frame() {
1701        let frame = Rect::new(Point::zero(), Size::new(160.0, 20.0));
1702        let unspecified = ProposalSize::UNSPECIFIED;
1703        let fixed = ProposalSize::new(Some(160.0), Some(20.0));
1704
1705        let placed_unspecified =
1706            PlacedSubview::new(&GuideProbe, SubviewPlacement::new(frame, unspecified));
1707        let placed_fixed = PlacedSubview::new(&GuideProbe, SubviewPlacement::new(frame, fixed));
1708
1709        assert_eq!(placed_unspecified.proposal, unspecified);
1710        assert_eq!(placed_fixed.proposal, fixed);
1711        assert_eq!(
1712            placed_unspecified
1713                .dimensions()
1714                .explicit_horizontal(HorizontalAlignment::Leading),
1715            Some(1.0),
1716            "the same frame must not re-derive a proposal: the unspecified one was selected"
1717        );
1718        assert_eq!(
1719            placed_fixed
1720                .dimensions()
1721                .explicit_horizontal(HorizontalAlignment::Leading),
1722            Some(2.0),
1723            "the same frame must not re-derive a proposal: the fixed one was selected"
1724        );
1725    }
1726
1727    /// A container that records every proposal handed to `place` and places
1728    /// each child at the full bounds under that same proposal.
1729    #[derive(Debug)]
1730    struct RecordingLayout {
1731        proposals: core::cell::RefCell<Vec<ProposalSize>>,
1732    }
1733
1734    impl Layout for RecordingLayout {
1735        fn size_that_fits(&self, _proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
1736            Size::new(160.0, 20.0)
1737        }
1738
1739        fn place(
1740            &self,
1741            bounds: Rect,
1742            proposal: ProposalSize,
1743            children: &[&dyn SubView],
1744        ) -> Vec<SubviewPlacement> {
1745            self.proposals.borrow_mut().push(proposal);
1746            children
1747                .iter()
1748                .map(|_| SubviewPlacement::new(bounds, proposal))
1749                .collect()
1750        }
1751
1752        fn explicit_horizontal(
1753            &self,
1754            alignment: HorizontalAlignment,
1755            _bounds: Rect,
1756            children: &[PlacedSubview<'_>],
1757        ) -> Option<f32> {
1758            children.first()?.explicit_horizontal(alignment)
1759        }
1760    }
1761
1762    #[test]
1763    fn measure_layout_forwards_selected_proposal_to_placement() {
1764        let layout = RecordingLayout {
1765            proposals: core::cell::RefCell::new(Vec::new()),
1766        };
1767        let child = GuideProbe;
1768        let children: [&dyn SubView; 1] = [&child];
1769
1770        let proposals = [
1771            ProposalSize::UNSPECIFIED,
1772            ProposalSize::new(Some(160.0), Some(20.0)),
1773            ProposalSize::UNSPECIFIED,
1774        ];
1775        let guides: Vec<Option<f32>> = proposals
1776            .iter()
1777            .map(|proposal| {
1778                measure_layout(&layout, *proposal, &children)
1779                    .explicit_horizontal(HorizontalAlignment::Leading)
1780            })
1781            .collect();
1782
1783        assert_eq!(guides, [Some(1.0), Some(2.0), Some(1.0)]);
1784        assert_eq!(*layout.proposals.borrow(), proposals.to_vec());
1785    }
1786
1787    /// A container whose measurement is unbounded; `place` must never run for
1788    /// a maximum-size probe because placement needs finite geometry.
1789    #[derive(Debug)]
1790    struct InfiniteLayout;
1791
1792    impl Layout for InfiniteLayout {
1793        fn size_that_fits(&self, _proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
1794            Size::new(f32::INFINITY, 20.0)
1795        }
1796
1797        fn place(
1798            &self,
1799            _bounds: Rect,
1800            _proposal: ProposalSize,
1801            _children: &[&dyn SubView],
1802        ) -> Vec<SubviewPlacement> {
1803            panic!("an infinite measurement must never reach placement")
1804        }
1805    }
1806
1807    #[test]
1808    fn maximum_measurement_does_not_place_unbounded_geometry() {
1809        let dimensions = measure_layout(&InfiniteLayout, ProposalSize::INFINITY, &[]);
1810
1811        assert_eq!(dimensions.size, Size::new(f32::INFINITY, 20.0));
1812        assert!(dimensions.explicit_horizontal_guides().next().is_none());
1813        assert!(dimensions.explicit_vertical_guides().next().is_none());
1814    }
1815}