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