Skip to main content

waterui_layout/containers/
absolute.rs

1//! Absolute positioning layout for `WaterUI`.
2//!
3//! This module provides an `Absolute` container that fills available space
4//! and allows children to be positioned at specific coordinates within.
5//!
6//! # Example
7//!
8//! ```rust
9//! use waterui::prelude::*;
10//!
11//! # fn overlay_stage(badge: impl View, panel: impl View) -> impl View {
12//! absolute((
13//!     Color::grey(), // Fills container
14//!     text("Center").position_in(UnitPoint::CENTER),
15//!     badge.position_in_offset(
16//!         UnitPoint::TOP_TRAILING,
17//!         UnitPoint::TOP_TRAILING,
18//!         -8.0, 8.0
19//!     ),
20//!     panel.pin(PinConstraints::all(16.0)),
21//! ))
22//! # }
23//! ```
24
25use alloc::collections::BTreeSet;
26use alloc::vec;
27use alloc::vec::Vec;
28use core::fmt;
29
30use nami::{Computed, Signal, SignalExt, watcher::BoxWatcherGuard};
31use waterui_core::{IntoSignalF32, View, layout::LayoutInvalidationCallback, view::TupleViews};
32
33use crate::{
34    Layout, Point, ProposalSize, Rect, Size, StretchAxis, SubView, UnitPoint,
35    container::FixedContainer,
36};
37
38// ============================================================================
39// AbsoluteLayout - Fills parent, gives all children full bounds
40// ============================================================================
41
42/// Layout that fills parent and gives each child full bounds.
43///
44/// Children are responsible for their own positioning (via `PositionedLayout`).
45#[derive(Debug, Clone, Default)]
46pub struct AbsoluteLayout;
47
48impl Layout for AbsoluteLayout {
49    fn stretch_axis(&self, _children: &[StretchAxis]) -> StretchAxis {
50        StretchAxis::Both
51    }
52
53    fn size_that_fits(&self, proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
54        // Absolute fills offered space. In unconstrained contexts we use 0
55        // to avoid propagating infinite dimensions to native backends.
56        Size::new(
57            proposal.width.unwrap_or(0.0),
58            proposal.height.unwrap_or(0.0),
59        )
60    }
61
62    fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
63        // Give every child the full bounds - they position themselves
64        children.iter().map(|_| bounds).collect()
65    }
66}
67
68// ============================================================================
69// PositionTarget - Where to position the child
70// ============================================================================
71
72/// Target position for absolute positioning.
73#[derive(Debug, Clone)]
74pub enum PositionTarget {
75    /// Absolute position in points (relative to parent origin)
76    Absolute {
77        /// X coordinate in points
78        x: Computed<f32>,
79        /// Y coordinate in points
80        y: Computed<f32>,
81    },
82    /// Fractional position (0.0-1.0 of parent) plus offset
83    Fractional {
84        /// Fractional position in parent
85        unit: UnitPoint,
86        /// Additional X offset in points
87        offset_x: Computed<f32>,
88        /// Additional Y offset in points
89        offset_y: Computed<f32>,
90    },
91    /// Edge-based pinning with optional explicit size.
92    Pinned(PinConstraints),
93}
94
95/// Edge-based constraints for absolute positioning.
96///
97/// This supports common pinning patterns:
98/// - `leading + trailing` computes width from parent bounds
99/// - `top + bottom` computes height from parent bounds
100/// - explicit `width` / `height` override computed dimensions
101#[derive(Debug, Clone, Default)]
102pub struct PinConstraints {
103    leading: Option<Computed<f32>>,
104    trailing: Option<Computed<f32>>,
105    top: Option<Computed<f32>>,
106    bottom: Option<Computed<f32>>,
107    width: Option<Computed<f32>>,
108    height: Option<Computed<f32>>,
109}
110
111impl PinConstraints {
112    /// Creates empty pin constraints.
113    #[must_use]
114    pub const fn new() -> Self {
115        Self {
116            leading: None,
117            trailing: None,
118            top: None,
119            bottom: None,
120            width: None,
121            height: None,
122        }
123    }
124
125    /// Pins all four edges by the same inset.
126    #[must_use]
127    pub fn all(inset: impl IntoSignalF32) -> Self {
128        let inset = inset.into_signal_f32().computed();
129        Self {
130            leading: Some(inset.clone()),
131            trailing: Some(inset.clone()),
132            top: Some(inset.clone()),
133            bottom: Some(inset),
134            width: None,
135            height: None,
136        }
137    }
138
139    /// Pins leading edge.
140    #[must_use]
141    pub fn leading(mut self, value: impl IntoSignalF32) -> Self {
142        self.leading = Some(value.into_signal_f32().computed());
143        self
144    }
145
146    /// Pins trailing edge.
147    #[must_use]
148    pub fn trailing(mut self, value: impl IntoSignalF32) -> Self {
149        self.trailing = Some(value.into_signal_f32().computed());
150        self
151    }
152
153    /// Pins top edge.
154    #[must_use]
155    pub fn top(mut self, value: impl IntoSignalF32) -> Self {
156        self.top = Some(value.into_signal_f32().computed());
157        self
158    }
159
160    /// Pins bottom edge.
161    #[must_use]
162    pub fn bottom(mut self, value: impl IntoSignalF32) -> Self {
163        self.bottom = Some(value.into_signal_f32().computed());
164        self
165    }
166
167    /// Sets explicit width.
168    #[must_use]
169    pub fn width(mut self, value: impl IntoSignalF32) -> Self {
170        self.width = Some(value.into_signal_f32().computed());
171        self
172    }
173
174    /// Sets explicit height.
175    #[must_use]
176    pub fn height(mut self, value: impl IntoSignalF32) -> Self {
177        self.height = Some(value.into_signal_f32().computed());
178        self
179    }
180}
181
182// ============================================================================
183// PositionedLayout - Positions a single child within received bounds
184// ============================================================================
185
186/// Layout that positions a single child within received bounds.
187pub struct PositionedLayout {
188    /// Anchor point on the child (0.0-1.0)
189    pub anchor: UnitPoint,
190    /// Target position in parent
191    pub target: PositionTarget,
192}
193
194impl fmt::Debug for PositionedLayout {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.debug_struct("PositionedLayout")
197            .field("anchor", &self.anchor)
198            .finish_non_exhaustive()
199    }
200}
201
202impl Layout for PositionedLayout {
203    fn stretch_axis(&self, _children: &[StretchAxis]) -> StretchAxis {
204        StretchAxis::Both
205    }
206
207    fn size_that_fits(&self, proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
208        Size::new(
209            proposal.width.unwrap_or(0.0),
210            proposal.height.unwrap_or(0.0),
211        )
212    }
213
214    #[allow(clippy::too_many_lines)]
215    fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
216        let child_proposal = ProposalSize::new(
217            bounds
218                .width()
219                .is_finite()
220                .then_some(bounds.width().max(0.0)),
221            bounds
222                .height()
223                .is_finite()
224                .then_some(bounds.height().max(0.0)),
225        );
226
227        children
228            .iter()
229            .map(|child| {
230                let intrinsic = child.measure(child_proposal).size;
231
232                match &self.target {
233                    PositionTarget::Absolute { x, y } => {
234                        let child_size = sanitize_size(intrinsic, bounds);
235                        let target_x = bounds.x() + x.get();
236                        let target_y = bounds.y() + y.get();
237
238                        let x = target_x - child_size.width * self.anchor.x;
239                        let y = target_y - child_size.height * self.anchor.y;
240
241                        Rect::new(
242                            Point::new(
243                                if x.is_finite() { x } else { bounds.x() },
244                                if y.is_finite() { y } else { bounds.y() },
245                            ),
246                            child_size,
247                        )
248                    }
249                    PositionTarget::Fractional {
250                        unit,
251                        offset_x,
252                        offset_y,
253                    } => {
254                        let child_size = sanitize_size(intrinsic, bounds);
255                        let target_x = bounds.x() + bounds.width() * unit.x + offset_x.get();
256                        let target_y = bounds.y() + bounds.height() * unit.y + offset_y.get();
257
258                        let x = target_x - child_size.width * self.anchor.x;
259                        let y = target_y - child_size.height * self.anchor.y;
260
261                        Rect::new(
262                            Point::new(
263                                if x.is_finite() { x } else { bounds.x() },
264                                if y.is_finite() { y } else { bounds.y() },
265                            ),
266                            child_size,
267                        )
268                    }
269                    PositionTarget::Pinned(pinned) => {
270                        let leading = pinned.leading.as_ref().map(waterui_core::Signal::get);
271                        let trailing = pinned.trailing.as_ref().map(waterui_core::Signal::get);
272                        let top = pinned.top.as_ref().map(waterui_core::Signal::get);
273                        let bottom = pinned.bottom.as_ref().map(waterui_core::Signal::get);
274                        let explicit_width = pinned.width.as_ref().map(waterui_core::Signal::get);
275                        let explicit_height = pinned.height.as_ref().map(waterui_core::Signal::get);
276
277                        let mut width = explicit_width.unwrap_or_else(|| {
278                            if let (Some(leading), Some(trailing)) = (leading, trailing) {
279                                bounds.width() - leading - trailing
280                            } else {
281                                intrinsic.width
282                            }
283                        });
284                        let mut height = explicit_height.unwrap_or_else(|| {
285                            if let (Some(top), Some(bottom)) = (top, bottom) {
286                                bounds.height() - top - bottom
287                            } else {
288                                intrinsic.height
289                            }
290                        });
291
292                        if !width.is_finite() {
293                            width = bounds.width();
294                        }
295                        if !height.is_finite() {
296                            height = bounds.height();
297                        }
298                        width = width.max(0.0);
299                        height = height.max(0.0);
300
301                        let measured = child
302                            .measure(ProposalSize::new(Some(width), Some(height)))
303                            .size;
304                        if explicit_width.is_none() && !(leading.is_some() && trailing.is_some()) {
305                            width = sanitize_axis(measured.width, width);
306                        }
307                        if explicit_height.is_none() && !(top.is_some() && bottom.is_some()) {
308                            height = sanitize_axis(measured.height, height);
309                        }
310
311                        let x = leading.map_or_else(
312                            || {
313                                trailing.map_or_else(
314                                    || bounds.x(),
315                                    |trailing| bounds.max_x() - trailing - width,
316                                )
317                            },
318                            |leading| bounds.x() + leading,
319                        );
320
321                        let y = top.map_or_else(
322                            || {
323                                bottom.map_or_else(
324                                    || bounds.y(),
325                                    |bottom| bounds.max_y() - bottom - height,
326                                )
327                            },
328                            |top| bounds.y() + top,
329                        );
330
331                        Rect::new(
332                            Point::new(
333                                if x.is_finite() { x } else { bounds.x() },
334                                if y.is_finite() { y } else { bounds.y() },
335                            ),
336                            Size::new(width, height),
337                        )
338                    }
339                }
340            })
341            .collect()
342    }
343
344    fn watch_invalidation(&self, invalidate: LayoutInvalidationCallback) -> Vec<BoxWatcherGuard> {
345        let signals: Vec<&Computed<f32>> = match &self.target {
346            PositionTarget::Absolute { x, y } => vec![x, y],
347            PositionTarget::Fractional {
348                offset_x, offset_y, ..
349            } => vec![offset_x, offset_y],
350            PositionTarget::Pinned(pinned) => [
351                pinned.leading.as_ref(),
352                pinned.trailing.as_ref(),
353                pinned.top.as_ref(),
354                pinned.bottom.as_ref(),
355                pinned.width.as_ref(),
356                pinned.height.as_ref(),
357            ]
358            .into_iter()
359            .flatten()
360            .collect(),
361        };
362        let mut identities = BTreeSet::new();
363        let mut guards = Vec::new();
364        for signal in signals {
365            if signal
366                .identity()
367                .is_some_and(|identity| !identities.insert(identity))
368            {
369                continue;
370            }
371            let invalidate = invalidate.clone();
372            guards.push(signal.watch(move |_| invalidate()));
373        }
374        guards
375    }
376}
377
378const fn sanitize_size(intrinsic: Size, bounds: Rect) -> Size {
379    let width = if intrinsic.width.is_finite() {
380        intrinsic.width.max(0.0)
381    } else {
382        bounds.width().max(0.0)
383    };
384    let height = if intrinsic.height.is_finite() {
385        intrinsic.height.max(0.0)
386    } else {
387        bounds.height().max(0.0)
388    };
389    Size::new(width, height)
390}
391
392const fn sanitize_axis(measured: f32, fallback: f32) -> f32 {
393    if measured.is_finite() {
394        measured.max(0.0)
395    } else {
396        fallback.max(0.0)
397    }
398}
399
400// ============================================================================
401// PositionedChild<V> - View wrapper with position info
402// ============================================================================
403
404/// A view with positioning information, for use in `Absolute` container.
405pub struct PositionedChild<V> {
406    layout: PositionedLayout,
407    content: V,
408}
409
410impl<V> fmt::Debug for PositionedChild<V> {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        f.debug_struct("PositionedChild")
413            .field("layout", &self.layout)
414            .finish_non_exhaustive()
415    }
416}
417
418impl<V: View> View for PositionedChild<V> {
419    fn body(self, _env: &waterui_core::Environment) -> impl View {
420        FixedContainer::new(self.layout, (self.content,))
421    }
422
423    fn stretch_axis(&self) -> StretchAxis {
424        StretchAxis::Both
425    }
426}
427
428// ============================================================================
429// PositionExt - Extension trait for positioning views
430// ============================================================================
431
432/// Extension trait for absolute positioning of views.
433///
434/// Use these methods inside an `Absolute` container.
435pub trait PositionExt: View + Sized {
436    /// Position view's center at absolute (x, y) coordinates.
437    ///
438    /// # Example
439    ///
440    /// ```rust
441    /// # use waterui::prelude::*;
442    /// # fn placed() -> impl View {
443    /// text("Hello").position(100.0, 50.0) // center at (100, 50)
444    /// # }
445    /// ```
446    fn position(self, x: impl IntoSignalF32, y: impl IntoSignalF32) -> PositionedChild<Self> {
447        self.position_anchor(UnitPoint::CENTER, x, y)
448    }
449
450    /// Position view's anchor point at absolute coordinates.
451    ///
452    /// # Example
453    ///
454    /// ```rust
455    /// # use waterui::prelude::*;
456    /// # fn placed() -> impl View {
457    /// text("Hello").position_anchor(UnitPoint::TOP_LEADING, 10.0, 10.0)
458    /// # }
459    /// ```
460    fn position_anchor(
461        self,
462        anchor: UnitPoint,
463        x: impl IntoSignalF32,
464        y: impl IntoSignalF32,
465    ) -> PositionedChild<Self> {
466        PositionedChild {
467            layout: PositionedLayout {
468                anchor,
469                target: PositionTarget::Absolute {
470                    x: x.into_signal_f32().computed(),
471                    y: y.into_signal_f32().computed(),
472                },
473            },
474            content: self,
475        }
476    }
477
478    /// Position view's center at fractional location in parent.
479    ///
480    /// The anchor point on the view matches the target position.
481    /// e.g., `UnitPoint::CENTER` means view's center at parent's center.
482    ///
483    /// # Example
484    ///
485    /// ```rust
486    /// # use waterui::prelude::*;
487    /// # fn placed() -> impl View {
488    /// text("Centered").position_in(UnitPoint::CENTER)
489    /// # }
490    /// ```
491    fn position_in(self, position: UnitPoint) -> PositionedChild<Self> {
492        self.position_in_anchor(position, position)
493    }
494
495    /// Position view's anchor at fractional location in parent.
496    ///
497    /// # Example
498    ///
499    /// ```rust
500    /// # use waterui::prelude::*;
501    /// # fn placed(view: impl View) -> impl View {
502    /// // Place view's top-left at parent's center
503    /// view.position_in_anchor(UnitPoint::TOP_LEADING, UnitPoint::CENTER)
504    /// # }
505    /// ```
506    fn position_in_anchor(self, anchor: UnitPoint, position: UnitPoint) -> PositionedChild<Self> {
507        PositionedChild {
508            layout: PositionedLayout {
509                anchor,
510                target: PositionTarget::Fractional {
511                    unit: position,
512                    offset_x: 0.0_f32.into_signal_f32().computed(),
513                    offset_y: 0.0_f32.into_signal_f32().computed(),
514                },
515            },
516            content: self,
517        }
518    }
519
520    /// Position with fractional location plus offset.
521    ///
522    /// Great for "16pt from bottom-right corner" patterns.
523    ///
524    /// # Example
525    ///
526    /// ```rust
527    /// # use waterui::prelude::*;
528    /// # fn placed(fab: impl View) -> impl View {
529    /// // FAB at bottom-right with 16pt inset
530    /// fab.position_in_offset(
531    ///     UnitPoint::BOTTOM_TRAILING,
532    ///     UnitPoint::BOTTOM_TRAILING,
533    ///     -16.0, -16.0
534    /// )
535    /// # }
536    /// ```
537    fn position_in_offset(
538        self,
539        anchor: UnitPoint,
540        position: UnitPoint,
541        offset_x: impl IntoSignalF32,
542        offset_y: impl IntoSignalF32,
543    ) -> PositionedChild<Self> {
544        PositionedChild {
545            layout: PositionedLayout {
546                anchor,
547                target: PositionTarget::Fractional {
548                    unit: position,
549                    offset_x: offset_x.into_signal_f32().computed(),
550                    offset_y: offset_y.into_signal_f32().computed(),
551                },
552            },
553            content: self,
554        }
555    }
556
557    /// Position using edge pins and optional explicit size.
558    ///
559    /// # Example
560    ///
561    /// ```rust
562    /// # use waterui::prelude::*;
563    /// # fn pinned(background: impl View, badge: impl View) {
564    /// // Fill parent with 12pt inset
565    /// background.pin(PinConstraints::all(12.0));
566    ///
567    /// // Bottom trailing fixed-size badge
568    /// badge.pin(
569    ///     PinConstraints::new()
570    ///         .trailing(12.0)
571    ///         .bottom(12.0)
572    ///         .width(28.0)
573    ///         .height(28.0)
574    /// );
575    /// # }
576    /// ```
577    fn pin(self, constraints: PinConstraints) -> PositionedChild<Self> {
578        PositionedChild {
579            layout: PositionedLayout {
580                anchor: UnitPoint::TOP_LEADING,
581                target: PositionTarget::Pinned(constraints),
582            },
583            content: self,
584        }
585    }
586}
587
588impl<V: View + Sized> PositionExt for V {}
589
590// ============================================================================
591// Absolute<C> - Container for positioned children
592// ============================================================================
593
594/// Container for absolutely positioned children.
595///
596/// Fills available space and positions each child within.
597/// Children can use `PositionExt` methods like `.position()`, `.position_in()`,
598/// and `.pin()`.
599///
600/// # Example
601///
602/// ```rust
603/// # use waterui::prelude::*;
604/// # fn stage(fab: impl View) -> impl View {
605/// absolute((
606///     Color::grey(), // Fills automatically (StretchAxis::Both)
607///     text("Centered").position_in(UnitPoint::CENTER),
608///     fab.position_in_offset(UnitPoint::BOTTOM_TRAILING, UnitPoint::BOTTOM_TRAILING, -16.0, -16.0),
609/// ))
610/// # }
611/// ```
612pub struct Absolute<C> {
613    contents: C,
614}
615
616impl<C> fmt::Debug for Absolute<C> {
617    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618        f.debug_struct("Absolute").finish_non_exhaustive()
619    }
620}
621
622impl<C: TupleViews> Absolute<C> {
623    /// Creates a new `Absolute` container with the given contents.
624    #[must_use]
625    pub const fn new(contents: C) -> Self {
626        Self { contents }
627    }
628}
629
630impl<C: TupleViews + 'static> View for Absolute<C> {
631    fn body(self, _env: &waterui_core::Environment) -> impl View {
632        FixedContainer::new(AbsoluteLayout, self.contents)
633    }
634
635    fn stretch_axis(&self) -> StretchAxis {
636        StretchAxis::Both
637    }
638}
639
640/// Creates an `Absolute` container for positioning children.
641///
642/// The container fills available space and allows children to be
643/// positioned at specific coordinates using `PositionExt` methods.
644///
645/// # Example
646///
647/// ```rust
648/// # use waterui::prelude::*;
649/// # fn stage(background: impl View, icon: impl View) -> impl View {
650/// absolute((
651///     background,
652///     text("Hello").position(50.0, 100.0),
653///     icon.position_in(UnitPoint::CENTER),
654/// ))
655/// # }
656/// ```
657#[must_use]
658pub const fn absolute<C: TupleViews>(contents: C) -> Absolute<C> {
659    Absolute::new(contents)
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::StretchAxis;
666    use crate::ViewDimensions;
667    use alloc::vec;
668    use core::cell::Cell;
669    use waterui_core::MainThreadBound;
670
671    struct MockSubView {
672        size: Size,
673        stretch_axis: StretchAxis,
674    }
675
676    impl SubView for MockSubView {
677        fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
678            ViewDimensions::new(self.size)
679        }
680        fn stretch_axis(&self) -> StretchAxis {
681            self.stretch_axis
682        }
683        fn priority(&self) -> i32 {
684            0
685        }
686    }
687
688    struct ProposalAwareView {
689        intrinsic: Size,
690        constrained: Size,
691        last_width: MainThreadBound<Cell<Option<f32>>>,
692        last_height: MainThreadBound<Cell<Option<f32>>>,
693    }
694
695    impl ProposalAwareView {
696        fn new(intrinsic: Size, constrained: Size) -> Self {
697            Self {
698                intrinsic,
699                constrained,
700                last_width: MainThreadBound::new(Cell::new(None)),
701                last_height: MainThreadBound::new(Cell::new(None)),
702            }
703        }
704    }
705
706    impl SubView for ProposalAwareView {
707        fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
708            self.last_width.set(proposal.width);
709            self.last_height.set(proposal.height);
710            ViewDimensions::new(if proposal.width.is_some() || proposal.height.is_some() {
711                self.constrained
712            } else {
713                self.intrinsic
714            })
715        }
716        fn stretch_axis(&self) -> StretchAxis {
717            StretchAxis::Both
718        }
719        fn priority(&self) -> i32 {
720            0
721        }
722    }
723
724    #[test]
725    fn test_absolute_layout_uses_finite_size_for_unspecified_proposal() {
726        let layout = AbsoluteLayout;
727        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[]);
728
729        assert!(size.width.is_finite());
730        assert!(size.height.is_finite());
731        assert_eq!(size.width, 0.0);
732        assert_eq!(size.height, 0.0);
733    }
734
735    #[test]
736    fn test_absolute_layout_respects_parent_constraints() {
737        let layout = AbsoluteLayout;
738        let size = layout.size_that_fits(ProposalSize::new(Some(200.0), Some(300.0)), &[]);
739        assert_eq!(size.width, 200.0);
740        assert_eq!(size.height, 300.0);
741    }
742
743    #[test]
744    fn test_absolute_layout_gives_full_bounds_to_each_child() {
745        let layout = AbsoluteLayout;
746        let mut child1 = MockSubView {
747            size: Size::new(50.0, 50.0),
748            stretch_axis: StretchAxis::None,
749        };
750        let mut child2 = MockSubView {
751            size: Size::new(30.0, 30.0),
752            stretch_axis: StretchAxis::None,
753        };
754        let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
755        let bounds = Rect::new(Point::new(10.0, 20.0), Size::new(200.0, 300.0));
756        let rects = layout.place(bounds, &children);
757
758        assert_eq!(rects.len(), 2);
759        assert_eq!(rects[0], bounds);
760        assert_eq!(rects[1], bounds);
761    }
762
763    #[test]
764    fn test_positioned_layout_uses_finite_size_for_unspecified_proposal() {
765        let layout = PositionedLayout {
766            anchor: UnitPoint::CENTER,
767            target: PositionTarget::Absolute {
768                x: 10.0_f32.into_signal_f32().computed(),
769                y: 20.0_f32.into_signal_f32().computed(),
770            },
771        };
772        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &[]);
773
774        assert!(size.width.is_finite());
775        assert!(size.height.is_finite());
776        assert_eq!(size.width, 0.0);
777        assert_eq!(size.height, 0.0);
778    }
779
780    #[test]
781    fn test_positioned_layout_measures_child_with_parent_bounds_proposal() {
782        let layout = PositionedLayout {
783            anchor: UnitPoint::CENTER,
784            target: PositionTarget::Fractional {
785                unit: UnitPoint::CENTER,
786                offset_x: 0.0_f32.into_signal_f32().computed(),
787                offset_y: 0.0_f32.into_signal_f32().computed(),
788            },
789        };
790
791        let child = ProposalAwareView::new(Size::new(10.0, 10.0), Size::new(200.0, 100.0));
792        let children: Vec<&dyn SubView> = vec![&child];
793        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(200.0, 100.0));
794
795        let rects = layout.place(bounds, &children);
796
797        assert_eq!(child.last_width.get(), Some(200.0));
798        assert_eq!(child.last_height.get(), Some(100.0));
799        assert_eq!(rects[0].x(), 0.0);
800        assert_eq!(rects[0].y(), 0.0);
801        assert_eq!(rects[0].width(), 200.0);
802        assert_eq!(rects[0].height(), 100.0);
803    }
804
805    #[test]
806    fn test_positioned_layout_absolute_anchor_math() {
807        let layout = PositionedLayout {
808            anchor: UnitPoint::BOTTOM_TRAILING,
809            target: PositionTarget::Absolute {
810                x: 50.0_f32.into_signal_f32().computed(),
811                y: 40.0_f32.into_signal_f32().computed(),
812            },
813        };
814
815        let mut child = MockSubView {
816            size: Size::new(20.0, 10.0),
817            stretch_axis: StretchAxis::None,
818        };
819        let children: Vec<&dyn SubView> = vec![&mut child];
820        let bounds = Rect::new(Point::new(10.0, 20.0), Size::new(200.0, 100.0));
821
822        let rects = layout.place(bounds, &children);
823
824        // Target at (bounds.x + 50, bounds.y + 40) = (60, 60),
825        // bottom-trailing anchor offsets by (20, 10).
826        assert_eq!(rects[0].x(), 40.0);
827        assert_eq!(rects[0].y(), 50.0);
828        assert_eq!(rects[0].width(), 20.0);
829        assert_eq!(rects[0].height(), 10.0);
830    }
831
832    #[test]
833    fn test_positioned_layout_fractional_with_offset() {
834        let layout = PositionedLayout {
835            anchor: UnitPoint::BOTTOM_TRAILING,
836            target: PositionTarget::Fractional {
837                unit: UnitPoint::BOTTOM_TRAILING,
838                offset_x: (-16.0).into_signal_f32().computed(),
839                offset_y: (-16.0).into_signal_f32().computed(),
840            },
841        };
842
843        let mut child = MockSubView {
844            size: Size::new(50.0, 50.0),
845            stretch_axis: StretchAxis::None,
846        };
847        let children: Vec<&dyn SubView> = vec![&mut child];
848        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(200.0, 200.0));
849
850        let rects = layout.place(bounds, &children);
851
852        // Target = bottom-right (200, 200) + offset (-16, -16) = (184, 184)
853        // Child's bottom-right at target, so origin = (184 - 50, 184 - 50) = (134, 134)
854        assert!((rects[0].x() - 134.0).abs() < f32::EPSILON);
855        assert!((rects[0].y() - 134.0).abs() < f32::EPSILON);
856    }
857
858    #[test]
859    fn test_pin_constraints_fill_with_insets() {
860        let layout = PositionedLayout {
861            anchor: UnitPoint::TOP_LEADING,
862            target: PositionTarget::Pinned(PinConstraints::all(12.0)),
863        };
864
865        let mut child = MockSubView {
866            size: Size::new(20.0, 20.0),
867            stretch_axis: StretchAxis::None,
868        };
869        let children: Vec<&dyn SubView> = vec![&mut child];
870        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(200.0, 100.0));
871
872        let rects = layout.place(bounds, &children);
873
874        assert_eq!(rects[0].x(), 12.0);
875        assert_eq!(rects[0].y(), 12.0);
876        assert_eq!(rects[0].width(), 176.0);
877        assert_eq!(rects[0].height(), 76.0);
878    }
879
880    #[test]
881    fn test_pin_constraints_bottom_trailing_fixed_size() {
882        let layout = PositionedLayout {
883            anchor: UnitPoint::TOP_LEADING,
884            target: PositionTarget::Pinned(
885                PinConstraints::new()
886                    .trailing(16.0)
887                    .bottom(10.0)
888                    .width(40.0)
889                    .height(20.0),
890            ),
891        };
892
893        let mut child = MockSubView {
894            size: Size::new(8.0, 8.0),
895            stretch_axis: StretchAxis::None,
896        };
897        let children: Vec<&dyn SubView> = vec![&mut child];
898        let bounds = Rect::new(Point::new(5.0, 10.0), Size::new(200.0, 100.0));
899
900        let rects = layout.place(bounds, &children);
901
902        assert_eq!(rects[0].x(), 149.0);
903        assert_eq!(rects[0].y(), 80.0);
904        assert_eq!(rects[0].width(), 40.0);
905        assert_eq!(rects[0].height(), 20.0);
906    }
907
908    #[test]
909    fn test_pin_constraints_clamp_negative_size() {
910        let layout = PositionedLayout {
911            anchor: UnitPoint::TOP_LEADING,
912            target: PositionTarget::Pinned(
913                PinConstraints::new()
914                    .leading(80.0)
915                    .trailing(80.0)
916                    .top(40.0)
917                    .bottom(40.0),
918            ),
919        };
920
921        let mut child = MockSubView {
922            size: Size::new(30.0, 30.0),
923            stretch_axis: StretchAxis::None,
924        };
925        let children: Vec<&dyn SubView> = vec![&mut child];
926        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 60.0));
927
928        let rects = layout.place(bounds, &children);
929
930        assert_eq!(rects[0].x(), 80.0);
931        assert_eq!(rects[0].y(), 40.0);
932        assert_eq!(rects[0].width(), 0.0);
933        assert_eq!(rects[0].height(), 0.0);
934    }
935
936    #[test]
937    fn test_pin_constraints_remeasure_after_width_resolution() {
938        let layout = PositionedLayout {
939            anchor: UnitPoint::TOP_LEADING,
940            target: PositionTarget::Pinned(PinConstraints::new().leading(10.0).trailing(10.0)),
941        };
942
943        let child = ProposalAwareView::new(Size::new(120.0, 20.0), Size::new(100.0, 40.0));
944        let children: Vec<&dyn SubView> = vec![&child];
945        let bounds = Rect::new(Point::zero(), Size::new(120.0, 200.0));
946
947        let rects = layout.place(bounds, &children);
948
949        assert_eq!(rects[0].x(), 10.0);
950        assert_eq!(rects[0].width(), 100.0);
951        assert_eq!(rects[0].height(), 40.0);
952        assert_eq!(child.last_width.get(), Some(100.0));
953    }
954}