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, 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(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
63 children.iter().map(|_| bounds).collect()
65 }
66}
67
68#[derive(Debug, Clone)]
74pub enum PositionTarget {
75 Absolute {
77 x: Computed<f32>,
79 y: Computed<f32>,
81 },
82 Fractional {
84 unit: UnitPoint,
86 offset_x: Computed<f32>,
88 offset_y: Computed<f32>,
90 },
91 Pinned(PinConstraints),
93}
94
95#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
182pub struct PositionedLayout {
188 pub anchor: UnitPoint,
190 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
400pub 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
428pub trait PositionExt: View + Sized {
436 fn position(self, x: impl IntoSignalF32, y: impl IntoSignalF32) -> PositionedChild<Self> {
447 self.position_anchor(UnitPoint::CENTER, x, y)
448 }
449
450 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 fn position_in(self, position: UnitPoint) -> PositionedChild<Self> {
492 self.position_in_anchor(position, position)
493 }
494
495 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 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 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
590pub 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 #[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#[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 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 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}