Skip to main content

waterui_layout/modifiers/
alignment_guide.rs

1//! Alignment-guide wrapper views.
2//!
3//! These wrappers are layout-transparent: they preserve the wrapped view's size,
4//! placement, and stretch behavior, while overriding the guide value exposed to
5//! parent layouts.
6
7use 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    /// Overriding a guide changes where the child lines up, never how much space
75    /// it claims, so the answer is the child's.
76    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
77        children.first().copied().unwrap_or_default()
78    }
79}
80
81/// A wrapper view that overrides one horizontal alignment guide.
82#[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    /// Creates a new horizontal alignment-guide wrapper.
91    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    /// Overriding a guide changes where the child lines up, never how much space
186    /// it claims, so the answer is the child's.
187    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
188        children.first().copied().unwrap_or_default()
189    }
190}
191
192/// A wrapper view that overrides one vertical alignment guide.
193#[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    /// Creates a new vertical alignment-guide wrapper.
202    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}