1use 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#[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 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 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#[derive(Debug, Clone)]
84pub enum PositionTarget {
85 Absolute {
87 x: Computed<f32>,
89 y: Computed<f32>,
91 },
92 Fractional {
94 unit: UnitPoint,
96 offset_x: Computed<f32>,
98 offset_y: Computed<f32>,
100 },
101 Pinned(PinConstraints),
103}
104
105#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
192pub struct PositionedLayout {
198 pub anchor: UnitPoint,
200 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 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
425pub 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
453pub trait PositionExt: View + Sized {
461 fn position(self, x: impl IntoSignalF32, y: impl IntoSignalF32) -> PositionedChild<Self> {
472 self.position_anchor(UnitPoint::CENTER, x, y)
473 }
474
475 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 fn position_in(self, position: UnitPoint) -> PositionedChild<Self> {
517 self.position_in_anchor(position, position)
518 }
519
520 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 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 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
615pub 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 #[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#[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 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 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}