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, ViewDimensions,
15 container::FixedContainer,
16 stack::{Alignment, HorizontalAlignment, VerticalAlignment},
17};
18
19struct ChildMeasurement {
21 dimensions: ViewDimensions,
22}
23
24#[derive(Debug, Clone, Default)]
27pub struct OverlayLayout {
28 alignment: Alignment,
29}
30
31impl OverlayLayout {
32 #[must_use]
34 pub const fn alignment(mut self, alignment: Alignment) -> Self {
35 self.alignment = alignment;
36 self
37 }
38
39 #[must_use]
41 pub const fn alignment_ref(&self) -> Alignment {
42 self.alignment
43 }
44}
45
46impl Layout for OverlayLayout {
47 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
50 children.first().copied().unwrap_or_default()
51 }
52
53 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
54 let base_size = children
55 .first()
56 .map_or(Size::zero(), |c| c.measure(proposal).size);
57
58 Size::new(
59 overlay_axis_size(base_size.width, proposal.width),
60 overlay_axis_size(base_size.height, proposal.height),
61 )
62 }
63
64 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
65 if children.is_empty() {
66 return vec![];
67 }
68
69 let child_proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
71
72 let measurements: Vec<ChildMeasurement> = children
73 .iter()
74 .map(|child| ChildMeasurement {
75 dimensions: child.measure(child_proposal),
76 })
77 .collect();
78
79 let mut placements = Vec::with_capacity(children.len());
80
81 if let Some(base) = measurements.first() {
82 let base_width = overlay_placement_axis(base.dimensions.size.width, bounds.width());
83 let base_height = overlay_placement_axis(base.dimensions.size.height, bounds.height());
84 placements.push(Rect::new(
85 bounds.origin(),
86 Size::new(base_width, base_height),
87 ));
88 }
89
90 for measurement in measurements.iter().skip(1) {
92 let child_dimensions = &measurement.dimensions;
93 let width = if child_dimensions.size.width.is_infinite() {
94 bounds.width()
95 } else {
96 child_dimensions.size.width.min(bounds.width()).max(0.0)
97 };
98 let height = if child_dimensions.size.height.is_infinite() {
99 bounds.height()
100 } else {
101 child_dimensions.size.height.min(bounds.height()).max(0.0)
102 };
103 let size = Size::new(width, height);
104 let mut adjusted_dimensions = child_dimensions.clone();
105 adjusted_dimensions.size = size;
106 let horizontal = self.alignment.horizontal();
107 let vertical = self.alignment.vertical();
108 let target_x = if horizontal == HorizontalAlignment::Leading {
109 0.0
110 } else if horizontal == HorizontalAlignment::Trailing {
111 bounds.width()
112 } else if horizontal == HorizontalAlignment::Center {
113 bounds.width() * 0.5
114 } else {
115 adjusted_dimensions
116 .horizontal(horizontal)
117 .clamp(0.0, size.width)
118 };
119 let target_y = if vertical == VerticalAlignment::Top {
120 0.0
121 } else if vertical == VerticalAlignment::Bottom {
122 bounds.height()
123 } else if vertical == VerticalAlignment::Center {
124 bounds.height() * 0.5
125 } else {
126 adjusted_dimensions
127 .vertical(vertical)
128 .clamp(0.0, size.height)
129 };
130 let origin = Point::new(
131 bounds.x() + target_x
132 - adjusted_dimensions
133 .horizontal(horizontal)
134 .clamp(0.0, size.width),
135 bounds.y() + target_y
136 - adjusted_dimensions
137 .vertical(vertical)
138 .clamp(0.0, size.height),
139 );
140 placements.push(Rect::new(origin, size));
141 }
142
143 placements
144 }
145
146 fn explicit_horizontal(
147 &self,
148 alignment: HorizontalAlignment,
149 _bounds: Rect,
150 children: &[PlacedSubview<'_>],
151 ) -> Option<f32> {
152 children
153 .first()
154 .and_then(|child| child.explicit_horizontal(alignment))
155 }
156
157 fn explicit_vertical(
158 &self,
159 alignment: VerticalAlignment,
160 _bounds: Rect,
161 children: &[PlacedSubview<'_>],
162 ) -> Option<f32> {
163 children
164 .first()
165 .and_then(|child| child.explicit_vertical(alignment))
166 }
167}
168
169fn overlay_axis_size(measured: f32, proposal: Option<f32>) -> f32 {
170 if measured.is_finite() {
171 measured.max(0.0)
172 } else {
173 proposal.unwrap_or(0.0).max(0.0)
174 }
175}
176
177const fn overlay_placement_axis(measured: f32, bounds_axis: f32) -> f32 {
178 if measured.is_infinite() {
179 bounds_axis.max(0.0)
180 } else {
181 measured.min(bounds_axis).max(0.0)
182 }
183}
184
185pub struct Overlay<Base, Layer> {
188 layout: OverlayLayout,
189 base: Base,
190 layer: Layer,
191}
192
193impl<Base, Layer> Overlay<Base, Layer> {
194 #[must_use]
196 pub const fn new(base: Base, layer: Layer) -> Self {
197 Self {
198 layout: OverlayLayout {
199 alignment: Alignment::Center,
200 },
201 base,
202 layer,
203 }
204 }
205
206 #[must_use]
208 pub const fn alignment(mut self, alignment: Alignment) -> Self {
209 self.layout.alignment = alignment;
210 self
211 }
212}
213
214impl<Base, Layer> fmt::Debug for Overlay<Base, Layer> {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.debug_struct("Overlay")
217 .field("layout", &self.layout)
218 .finish_non_exhaustive()
219 }
220}
221
222impl<Base, Layer> View for Overlay<Base, Layer>
223where
224 Base: View + 'static,
225 Layer: View + 'static,
226{
227 fn body(self, _env: &waterui_core::Environment) -> impl View {
228 let Self {
229 layout,
230 base,
231 layer,
232 } = self;
233 FixedContainer::new(layout, (base, layer))
234 }
235}
236
237#[must_use]
239pub const fn overlay<Base, Layer>(base: Base, layer: Layer) -> Overlay<Base, Layer> {
240 Overlay::new(base, layer)
241}
242
243#[cfg(test)]
244#[allow(clippy::float_cmp)]
245mod tests {
246 use super::*;
247 use crate::StretchAxis;
248 use crate::container::FixedContainer;
249 use waterui_core::{AnyView, Environment, View};
250
251 struct MockSubView {
252 size: Size,
253 }
254
255 impl SubView for MockSubView {
256 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
257 ViewDimensions::new(self.size)
258 }
259 fn stretch_axis(&self) -> StretchAxis {
260 StretchAxis::None
261 }
262 fn priority(&self) -> i32 {
263 0
264 }
265 }
266
267 struct StretchingBase;
268
269 impl View for StretchingBase {
270 fn body(self, _env: &Environment) -> impl View {}
271
272 fn stretch_axis(&self) -> StretchAxis {
273 StretchAxis::Horizontal
274 }
275 }
276
277 #[test]
278 fn test_overlay_size_from_base() {
279 let layout = OverlayLayout::default();
280
281 let mut base = MockSubView {
282 size: Size::new(100.0, 50.0),
283 };
284 let mut overlay_child = MockSubView {
285 size: Size::new(20.0, 20.0),
286 };
287
288 let children: Vec<&dyn SubView> = vec![&mut base, &mut overlay_child];
289
290 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
291
292 assert_eq!(size.width, 100.0);
294 assert_eq!(size.height, 50.0);
295 }
296
297 #[test]
298 fn test_overlay_placement_center() {
299 let layout = OverlayLayout {
300 alignment: Alignment::Center,
301 };
302
303 let mut base = MockSubView {
304 size: Size::new(100.0, 100.0),
305 };
306 let mut overlay_child = MockSubView {
307 size: Size::new(20.0, 20.0),
308 };
309
310 let children: Vec<&dyn SubView> = vec![&mut base, &mut overlay_child];
311
312 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
313 let rects = layout.place(bounds, &children);
314
315 assert_eq!(rects[0].width(), 100.0);
317 assert_eq!(rects[0].height(), 100.0);
318
319 assert_eq!(rects[1].x(), 40.0); assert_eq!(rects[1].y(), 40.0); }
323
324 #[test]
325 fn test_overlay_preserves_intrinsic_base_size_under_parent_proposal() {
326 let layout = OverlayLayout::default();
327
328 let base = MockSubView {
329 size: Size::new(40.0, 30.0),
330 };
331 let overlay_child = MockSubView {
332 size: Size::new(10.0, 10.0),
333 };
334
335 let children: Vec<&dyn SubView> = vec![&base, &overlay_child];
336 let size = layout.size_that_fits(ProposalSize::new(Some(200.0), Some(100.0)), &children);
337
338 assert_eq!(size.width, 40.0);
339 assert_eq!(size.height, 30.0);
340 }
341
342 #[test]
343 fn test_overlay_body_preserves_base_stretch_axis() {
344 let env = Environment::default();
345 let body = overlay(StretchingBase, ()).body(&env);
346 let container = AnyView::new(body)
347 .downcast::<FixedContainer>()
348 .expect("overlay body should be a FixedContainer");
349 let (layout, children) = container.as_parts();
350
351 assert_eq!(children.len(), 2);
352 let child_axes: Vec<StretchAxis> = children.iter().map(View::stretch_axis).collect();
353 assert_eq!(layout.stretch_axis(&child_axes), StretchAxis::Horizontal);
354 }
355}