waterui_layout/modifiers/
alignment_guide.rs1use core::fmt;
8
9use alloc::{vec, vec::Vec};
10use waterui_core::View;
11
12use crate::{
13 HorizontalAlignment, Layout, PlacedSubview, ProposalSize, Rect, Size, StretchAxis, SubView,
14 VerticalAlignment, ViewDimensions, container::FixedContainer,
15};
16
17#[derive(Clone)]
18struct HorizontalAlignmentGuideLayout<F> {
19 alignment: HorizontalAlignment,
20 compute: F,
21}
22
23impl<F> fmt::Debug for HorizontalAlignmentGuideLayout<F> {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.debug_struct("HorizontalAlignmentGuideLayout")
26 .field("alignment", &self.alignment)
27 .finish_non_exhaustive()
28 }
29}
30
31impl<F> Layout for HorizontalAlignmentGuideLayout<F>
32where
33 F: Fn(&ViewDimensions) -> f32 + 'static,
34{
35 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
36 assert!(
37 children.len() == 1,
38 "HorizontalAlignmentGuideLayout expects exactly one child"
39 );
40 children[0].measure(proposal).size
41 }
42
43 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
44 assert!(
45 children.len() == 1,
46 "HorizontalAlignmentGuideLayout expects exactly one child"
47 );
48 vec![bounds]
49 }
50
51 fn explicit_horizontal(
52 &self,
53 alignment: HorizontalAlignment,
54 _bounds: Rect,
55 children: &[PlacedSubview<'_>],
56 ) -> Option<f32> {
57 if alignment != self.alignment {
58 return None;
59 }
60
61 assert!(
62 children.len() == 1,
63 "HorizontalAlignmentGuideLayout expects exactly one placed child"
64 );
65
66 let child = children[0];
67 Some(child.frame.x() + (self.compute)(&child.dimensions()))
68 }
69
70 fn explicit_horizontal_alignments(&self) -> Vec<HorizontalAlignment> {
71 vec![self.alignment]
72 }
73
74 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
77 children.first().copied().unwrap_or_default()
78 }
79}
80
81#[must_use]
83pub struct HorizontalAlignmentGuide<Content, F> {
84 content: Content,
85 alignment: HorizontalAlignment,
86 compute: F,
87}
88
89impl<Content, F> HorizontalAlignmentGuide<Content, F> {
90 pub const fn new(content: Content, alignment: HorizontalAlignment, compute: F) -> Self {
92 Self {
93 content,
94 alignment,
95 compute,
96 }
97 }
98}
99
100impl<Content, F> fmt::Debug for HorizontalAlignmentGuide<Content, F> {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("HorizontalAlignmentGuide")
103 .field("alignment", &self.alignment)
104 .finish_non_exhaustive()
105 }
106}
107
108impl<Content, F> View for HorizontalAlignmentGuide<Content, F>
109where
110 Content: View + 'static,
111 F: Fn(&ViewDimensions) -> f32 + 'static,
112{
113 fn body(self, _env: &waterui_core::Environment) -> impl View {
114 FixedContainer::new(
115 HorizontalAlignmentGuideLayout {
116 alignment: self.alignment,
117 compute: self.compute,
118 },
119 (self.content,),
120 )
121 }
122
123 fn stretch_axis(&self) -> StretchAxis {
124 self.content.stretch_axis()
125 }
126}
127
128#[derive(Clone)]
129struct VerticalAlignmentGuideLayout<F> {
130 alignment: VerticalAlignment,
131 compute: F,
132}
133
134impl<F> fmt::Debug for VerticalAlignmentGuideLayout<F> {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.debug_struct("VerticalAlignmentGuideLayout")
137 .field("alignment", &self.alignment)
138 .finish_non_exhaustive()
139 }
140}
141
142impl<F> Layout for VerticalAlignmentGuideLayout<F>
143where
144 F: Fn(&ViewDimensions) -> f32 + 'static,
145{
146 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
147 assert!(
148 children.len() == 1,
149 "VerticalAlignmentGuideLayout expects exactly one child"
150 );
151 children[0].measure(proposal).size
152 }
153
154 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
155 assert!(
156 children.len() == 1,
157 "VerticalAlignmentGuideLayout expects exactly one child"
158 );
159 vec![bounds]
160 }
161
162 fn explicit_vertical(
163 &self,
164 alignment: VerticalAlignment,
165 _bounds: Rect,
166 children: &[PlacedSubview<'_>],
167 ) -> Option<f32> {
168 if alignment != self.alignment {
169 return None;
170 }
171
172 assert!(
173 children.len() == 1,
174 "VerticalAlignmentGuideLayout expects exactly one placed child"
175 );
176
177 let child = children[0];
178 Some(child.frame.y() + (self.compute)(&child.dimensions()))
179 }
180
181 fn explicit_vertical_alignments(&self) -> Vec<VerticalAlignment> {
182 vec![self.alignment]
183 }
184
185 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
188 children.first().copied().unwrap_or_default()
189 }
190}
191
192#[must_use]
194pub struct VerticalAlignmentGuide<Content, F> {
195 content: Content,
196 alignment: VerticalAlignment,
197 compute: F,
198}
199
200impl<Content, F> VerticalAlignmentGuide<Content, F> {
201 pub const fn new(content: Content, alignment: VerticalAlignment, compute: F) -> Self {
203 Self {
204 content,
205 alignment,
206 compute,
207 }
208 }
209}
210
211impl<Content, F> fmt::Debug for VerticalAlignmentGuide<Content, F> {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 f.debug_struct("VerticalAlignmentGuide")
214 .field("alignment", &self.alignment)
215 .finish_non_exhaustive()
216 }
217}
218
219impl<Content, F> View for VerticalAlignmentGuide<Content, F>
220where
221 Content: View + 'static,
222 F: Fn(&ViewDimensions) -> f32 + 'static,
223{
224 fn body(self, _env: &waterui_core::Environment) -> impl View {
225 FixedContainer::new(
226 VerticalAlignmentGuideLayout {
227 alignment: self.alignment,
228 compute: self.compute,
229 },
230 (self.content,),
231 )
232 }
233
234 fn stretch_axis(&self) -> StretchAxis {
235 self.content.stretch_axis()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use waterui_core::{AnyView, Environment, View, layout::Point};
242
243 use super::*;
244
245 struct StretchingView;
246
247 impl View for StretchingView {
248 fn body(self, _env: &Environment) -> impl View {}
249
250 fn stretch_axis(&self) -> StretchAxis {
251 StretchAxis::Horizontal
252 }
253 }
254
255 struct GuidedSubview {
256 size: Size,
257 }
258
259 impl SubView for GuidedSubview {
260 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
261 ViewDimensions::new(self.size)
262 }
263
264 fn stretch_axis(&self) -> StretchAxis {
265 StretchAxis::None
266 }
267
268 fn priority(&self) -> i32 {
269 0
270 }
271 }
272
273 #[test]
274 fn horizontal_alignment_guide_preserves_content_stretch_axis() {
275 let env = Environment::default();
276 let body = HorizontalAlignmentGuide::new(
277 StretchingView,
278 HorizontalAlignment::Leading,
279 |_dimensions: &ViewDimensions| 12.0,
280 )
281 .body(&env);
282 let container = AnyView::new(body)
283 .downcast::<FixedContainer>()
284 .expect("alignment guide body should be a FixedContainer");
285 let (layout, children) = container.as_parts();
286
287 assert_eq!(children.len(), 1);
288 let child_axes: Vec<StretchAxis> = children.iter().map(View::stretch_axis).collect();
289 assert_eq!(layout.stretch_axis(&child_axes), StretchAxis::Horizontal);
290 }
291
292 #[test]
293 fn horizontal_alignment_guide_body_exposes_override_key() {
294 let env = Environment::default();
295 let body = HorizontalAlignmentGuide::new(
296 (),
297 HorizontalAlignment::Leading,
298 |dimensions: &ViewDimensions| dimensions.size.width * 0.5,
299 )
300 .body(&env);
301 let container = AnyView::new(body)
302 .downcast::<FixedContainer>()
303 .expect("alignment guide body should be a FixedContainer");
304 let (layout, children) = container.as_parts();
305
306 assert_eq!(children.len(), 1);
307 let child_axes: Vec<StretchAxis> = children.iter().map(View::stretch_axis).collect();
308 assert_eq!(layout.stretch_axis(&child_axes), StretchAxis::None);
309 assert_eq!(
310 layout.explicit_horizontal_alignments(),
311 vec![HorizontalAlignment::Leading]
312 );
313
314 let child = GuidedSubview {
315 size: Size::new(40.0, 10.0),
316 };
317 let bounds = Rect::new(Point::zero(), Size::new(40.0, 10.0));
318 let placed = [PlacedSubview::new(&child, bounds)];
319 let guide = layout
320 .explicit_horizontal(HorizontalAlignment::Leading, bounds, &placed)
321 .expect("horizontal guide override should be present");
322
323 assert_eq!(guide, 20.0);
324 }
325
326 #[test]
327 fn vertical_alignment_guide_body_exposes_override_key() {
328 let env = Environment::default();
329 let body = VerticalAlignmentGuide::new(
330 (),
331 VerticalAlignment::Top,
332 |dimensions: &ViewDimensions| dimensions.size.height * 0.25,
333 )
334 .body(&env);
335 let container = AnyView::new(body)
336 .downcast::<FixedContainer>()
337 .expect("alignment guide body should be a FixedContainer");
338 let (layout, children) = container.as_parts();
339
340 assert_eq!(children.len(), 1);
341 let child_axes: Vec<StretchAxis> = children.iter().map(View::stretch_axis).collect();
342 assert_eq!(layout.stretch_axis(&child_axes), StretchAxis::None);
343 assert_eq!(
344 layout.explicit_vertical_alignments(),
345 vec![VerticalAlignment::Top]
346 );
347
348 let child = GuidedSubview {
349 size: Size::new(40.0, 20.0),
350 };
351 let bounds = Rect::new(Point::zero(), Size::new(40.0, 20.0));
352 let placed = [PlacedSubview::new(&child, bounds)];
353 let guide = layout
354 .explicit_vertical(VerticalAlignment::Top, bounds, &placed)
355 .expect("vertical guide override should be present");
356
357 assert_eq!(guide, 5.0);
358 }
359}