waterui_layout/modifiers/
overlay.rs1use core::fmt;
9
10use alloc::{vec, vec::Vec};
11use waterui_core::View;
12
13use crate::{
14 Layout, PlacedSubview, Point, ProposalSize, Rect, Size, StretchAxis, SubView, SubviewPlacement,
15 container::FixedContainer,
16 stack::{
17 Alignment, HorizontalAlignment, VerticalAlignment,
18 distribute::{LineAnchor, container_line},
19 },
20};
21
22#[derive(Debug, Clone, Default)]
25pub struct OverlayLayout {
26 alignment: Alignment,
27}
28
29impl OverlayLayout {
30 #[must_use]
32 pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
33 self.alignment = alignment.into();
34 self
35 }
36
37 #[must_use]
39 pub const fn alignment_ref(&self) -> Alignment {
40 self.alignment
41 }
42}
43
44impl Layout for OverlayLayout {
45 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
48 children.first().copied().unwrap_or_default()
49 }
50
51 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
52 let base_size = children
53 .first()
54 .map_or(Size::zero(), |c| c.measure(proposal).size);
55
56 Size::new(
57 overlay_axis_size(base_size.width, proposal.width),
58 overlay_axis_size(base_size.height, proposal.height),
59 )
60 }
61
62 fn place(
63 &self,
64 bounds: Rect,
65 _proposal: ProposalSize,
66 children: &[&dyn SubView],
67 ) -> Vec<SubviewPlacement> {
68 if children.is_empty() {
69 return vec![];
70 }
71
72 let placement = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
77
78 let mut placements = Vec::with_capacity(children.len());
79 placements.push(SubviewPlacement::new(bounds, placement));
80
81 let horizontal = self.alignment.horizontal();
82 let vertical = self.alignment.vertical();
83 for child in children.iter().skip(1) {
84 let mut dimensions = child.measure(placement);
85 let width = if dimensions.size.width.is_infinite() {
86 bounds.width()
87 } else {
88 dimensions.size.width.max(0.0)
89 };
90 let height = if dimensions.size.height.is_infinite() {
91 bounds.height()
92 } else {
93 dimensions.size.height.max(0.0)
94 };
95 dimensions.size = Size::new(width, height);
96 let guide_x = dimensions.horizontal(horizontal);
97 let guide_y = dimensions.vertical(vertical);
98 let line_x = container_line(
99 bounds.width(),
100 LineAnchor::horizontal(horizontal),
101 guide_x,
102 width - guide_x,
103 );
104 let line_y = container_line(
105 bounds.height(),
106 LineAnchor::vertical(vertical),
107 guide_y,
108 height - guide_y,
109 );
110 let origin = Point::new(bounds.x() + line_x - guide_x, bounds.y() + line_y - guide_y);
111 placements.push(SubviewPlacement::new(
112 Rect::new(origin, dimensions.size),
113 placement,
114 ));
115 }
116
117 placements
118 }
119
120 fn explicit_horizontal(
121 &self,
122 alignment: HorizontalAlignment,
123 _bounds: Rect,
124 children: &[PlacedSubview<'_>],
125 ) -> Option<f32> {
126 children
127 .first()
128 .and_then(|child| child.explicit_horizontal(alignment))
129 }
130
131 fn explicit_vertical(
132 &self,
133 alignment: VerticalAlignment,
134 _bounds: Rect,
135 children: &[PlacedSubview<'_>],
136 ) -> Option<f32> {
137 children
138 .first()
139 .and_then(|child| child.explicit_vertical(alignment))
140 }
141}
142
143fn overlay_axis_size(measured: f32, proposal: Option<f32>) -> f32 {
144 if measured.is_finite() {
145 measured.max(0.0)
146 } else {
147 proposal.unwrap_or(0.0).max(0.0)
148 }
149}
150
151pub struct Overlay<Base, Layer> {
154 layout: OverlayLayout,
155 base: Base,
156 layer: Layer,
157}
158
159impl<Base: View, Layer: View> Overlay<Base, Layer> {
160 #[must_use]
162 pub const fn new(base: Base, layer: Layer) -> Self {
163 Self {
164 layout: OverlayLayout {
165 alignment: Alignment::Center,
166 },
167 base,
168 layer,
169 }
170 }
171
172 #[must_use]
174 pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
175 self.layout.alignment = alignment.into();
176 self
177 }
178
179 crate::alignment::two_dimensional_alignment_methods!();
180}
181
182impl<Base, Layer> fmt::Debug for Overlay<Base, Layer> {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.debug_struct("Overlay")
185 .field("layout", &self.layout)
186 .finish_non_exhaustive()
187 }
188}
189
190impl<Base, Layer> View for Overlay<Base, Layer>
191where
192 Base: View + 'static,
193 Layer: View + 'static,
194{
195 fn body(self, _env: &waterui_core::Environment) -> impl View {
196 let Self {
197 layout,
198 base,
199 layer,
200 } = self;
201 FixedContainer::new(layout, (base, layer))
202 }
203
204 fn stretch_axis(&self) -> StretchAxis {
207 self.layout
208 .stretch_axis(&[self.base.stretch_axis(), self.layer.stretch_axis()])
209 }
210}
211
212#[must_use]
214pub const fn overlay<Base: View, Layer: View>(base: Base, layer: Layer) -> Overlay<Base, Layer> {
215 Overlay::new(base, layer)
216}
217
218#[cfg(test)]
219#[allow(clippy::float_cmp)]
220mod tests {
221 use super::*;
222 use crate::container::FixedContainer;
223 use crate::{StretchAxis, ViewDimensions};
224 use waterui_core::{AnyView, Environment, View};
225
226 struct MockSubView {
227 size: Size,
228 }
229
230 impl SubView for MockSubView {
231 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
232 ViewDimensions::new(self.size)
233 }
234 fn stretch_axis(&self) -> StretchAxis {
235 StretchAxis::None
236 }
237 fn priority(&self) -> i32 {
238 0
239 }
240 }
241
242 struct StretchingBase;
243
244 impl View for StretchingBase {
245 fn body(self, _env: &Environment) -> impl View {}
246
247 fn stretch_axis(&self) -> StretchAxis {
248 StretchAxis::Horizontal
249 }
250 }
251
252 #[test]
253 fn test_overlay_size_from_base() {
254 let layout = OverlayLayout::default();
255
256 let mut base = MockSubView {
257 size: Size::new(100.0, 50.0),
258 };
259 let mut overlay_child = MockSubView {
260 size: Size::new(20.0, 20.0),
261 };
262
263 let children: Vec<&dyn SubView> = vec![&mut base, &mut overlay_child];
264
265 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
266
267 assert_eq!(size.width, 100.0);
269 assert_eq!(size.height, 50.0);
270 }
271
272 #[test]
273 fn test_overlay_placement_center_bounded_proposal() {
274 let layout = OverlayLayout {
275 alignment: Alignment::Center,
276 };
277
278 let mut base = MockSubView {
279 size: Size::new(100.0, 100.0),
280 };
281 let mut overlay_child = MockSubView {
282 size: Size::new(20.0, 20.0),
283 };
284
285 let children: Vec<&dyn SubView> = vec![&mut base, &mut overlay_child];
286
287 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
288 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
289 let placements = layout.place(bounds, proposal, &children);
290
291 assert_eq!(placements[0].frame.width(), 100.0);
293 assert_eq!(placements[0].frame.height(), 100.0);
294
295 assert_eq!(placements[1].frame.x(), 40.0); assert_eq!(placements[1].frame.y(), 40.0); }
299
300 #[test]
301 fn test_overlay_preserves_intrinsic_base_size_under_parent_proposal() {
302 let layout = OverlayLayout::default();
303
304 let base = MockSubView {
305 size: Size::new(40.0, 30.0),
306 };
307 let overlay_child = MockSubView {
308 size: Size::new(10.0, 10.0),
309 };
310
311 let children: Vec<&dyn SubView> = vec![&base, &overlay_child];
312 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), Some(100.0)), &children);
313
314 assert_eq!(size.width, 40.0);
315 assert_eq!(size.height, 30.0);
316 }
317
318 #[test]
319 fn test_overlay_body_preserves_base_stretch_axis() {
320 let env = Environment::default();
321 let body = overlay(StretchingBase, ()).body(&env);
322 let container = AnyView::new(body)
323 .downcast::<FixedContainer>()
324 .expect("overlay body should be a FixedContainer");
325 let (layout, children) = container.as_parts();
326
327 assert_eq!(children.len(), 2);
328 let child_axes: Vec<StretchAxis> = children.iter().map(View::stretch_axis).collect();
329 assert_eq!(layout.stretch_axis(&child_axes), StretchAxis::Horizontal);
330 }
331}