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    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    /// Overriding a guide changes where the child lines up, never how much space
80    /// it claims, so the answer is the child's.
81    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
82        children.first().copied().unwrap_or_default()
83    }
84}
85
86/// A wrapper view that overrides one horizontal alignment guide.
87#[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    /// Creates a new horizontal alignment-guide wrapper.
96    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    /// Overriding a guide changes where the child lines up, never how much space
196    /// it claims, so the answer is the child's.
197    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
198        children.first().copied().unwrap_or_default()
199    }
200}
201
202/// A wrapper view that overrides one vertical alignment guide.
203#[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    /// Creates a new vertical alignment-guide wrapper.
212    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}