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