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