Skip to main content

waterui_layout/containers/
container.rs

1//! View wrapper that lets arbitrary [`Layout`] implementations
2//! participate in the `WaterUI` view tree.
3
4use core::fmt;
5use fmt::Debug;
6
7use alloc::{boxed::Box, vec::Vec};
8use nami::{Computed, Signal};
9use waterui_core::{
10    AnyView, Native, NativeView, View,
11    layout::{
12        HorizontalAlignment, LayoutDirection, LayoutInvalidationCallback, PlacedSubview,
13        ProposalSize, Rect, Size, SubView, SubviewPlacement, VerticalAlignment,
14    },
15    view::TupleViews,
16    views::{AnyViews, Views, ViewsExt},
17};
18
19use crate::{Layout, StretchAxis};
20
21struct DirectionalLayout {
22    inner: Box<dyn Layout>,
23    direction: Computed<LayoutDirection>,
24}
25
26impl DirectionalLayout {
27    fn new(inner: Box<dyn Layout>, direction: Computed<LayoutDirection>) -> Self {
28        Self { inner, direction }
29    }
30
31    fn mirror(&self, bounds: Rect, frame: Rect) -> Rect {
32        if self.direction.get().is_right_to_left() {
33            Rect::new(
34                waterui_core::layout::Point::new(
35                    bounds.min_x() + bounds.max_x() - frame.max_x(),
36                    frame.y(),
37                ),
38                *frame.size(),
39            )
40        } else {
41            frame
42        }
43    }
44}
45
46impl Debug for DirectionalLayout {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.debug_struct("DirectionalLayout")
49            .field("inner", &self.inner)
50            .field("direction", &self.direction.get())
51            .finish()
52    }
53}
54
55impl Layout for DirectionalLayout {
56    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
57        self.inner.size_that_fits(proposal, children)
58    }
59
60    fn place(
61        &self,
62        bounds: Rect,
63        proposal: ProposalSize,
64        children: &[&dyn SubView],
65    ) -> Vec<SubviewPlacement> {
66        self.inner
67            .place(bounds, proposal, children)
68            .into_iter()
69            .map(|placement| {
70                SubviewPlacement::new(self.mirror(bounds, placement.frame), placement.proposal)
71            })
72            .collect()
73    }
74
75    fn explicit_horizontal(
76        &self,
77        alignment: HorizontalAlignment,
78        bounds: Rect,
79        children: &[PlacedSubview<'_>],
80    ) -> Option<f32> {
81        if !self.direction.get().is_right_to_left() {
82            return self.inner.explicit_horizontal(alignment, bounds, children);
83        }
84        let unmirrored = children
85            .iter()
86            .map(|child| {
87                PlacedSubview::new(
88                    child.view,
89                    SubviewPlacement::new(self.mirror(bounds, child.frame), child.proposal),
90                )
91            })
92            .collect::<Vec<_>>();
93        self.inner
94            .explicit_horizontal(alignment, bounds, &unmirrored)
95            .map(|guide| bounds.min_x() + bounds.max_x() - guide)
96    }
97
98    fn explicit_vertical(
99        &self,
100        alignment: VerticalAlignment,
101        bounds: Rect,
102        children: &[PlacedSubview<'_>],
103    ) -> Option<f32> {
104        if !self.direction.get().is_right_to_left() {
105            return self.inner.explicit_vertical(alignment, bounds, children);
106        }
107        let unmirrored = children
108            .iter()
109            .map(|child| {
110                PlacedSubview::new(
111                    child.view,
112                    SubviewPlacement::new(self.mirror(bounds, child.frame), child.proposal),
113                )
114            })
115            .collect::<Vec<_>>();
116        self.inner.explicit_vertical(alignment, bounds, &unmirrored)
117    }
118
119    fn explicit_horizontal_alignments(&self) -> Vec<HorizontalAlignment> {
120        self.inner.explicit_horizontal_alignments()
121    }
122
123    fn explicit_vertical_alignments(&self) -> Vec<VerticalAlignment> {
124        self.inner.explicit_vertical_alignments()
125    }
126
127    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
128        self.inner.stretch_axis(children)
129    }
130
131    fn watch_invalidation(
132        &self,
133        invalidate: LayoutInvalidationCallback,
134    ) -> Vec<nami::watcher::BoxWatcherGuard> {
135        let mut guards = self.inner.watch_invalidation(invalidate.clone());
136        guards.push(self.direction.watch(move |_| invalidate()));
137        guards
138    }
139}
140
141/// A view wrapper that executes an arbitrary [`Layout`]
142/// implementation.
143pub struct FixedContainer {
144    layout: Box<dyn Layout>,
145    contents: Vec<AnyView>,
146}
147
148impl Debug for FixedContainer {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("Container")
151            .field("layout", &"Box<dyn Layout>")
152            .field("contents", &self.contents)
153            .finish()
154    }
155}
156
157impl FixedContainer {
158    /// Wraps the supplied layout object and tuple of child views into a
159    /// container view.
160    pub fn new(layout: impl Layout + 'static, contents: impl TupleViews) -> Self {
161        Self {
162            layout: Box::new(layout),
163            contents: contents.into_views(),
164        }
165    }
166
167    /// Returns the boxed layout object together with the collected child views.
168    #[must_use]
169    pub fn into_inner(self) -> (Box<dyn Layout>, Vec<AnyView>) {
170        (self.layout, self.contents)
171    }
172
173    /// Creates a fixed container from pre-built layout and children parts.
174    #[must_use]
175    pub fn from_parts(layout: Box<dyn Layout>, contents: Vec<AnyView>) -> Self {
176        Self { layout, contents }
177    }
178
179    /// Returns borrowed access to the boxed layout and collected children.
180    #[must_use = "this borrows the container's parts without consuming it"]
181    pub fn as_parts(&self) -> (&dyn Layout, &[AnyView]) {
182        (self.layout.as_ref(), &self.contents)
183    }
184}
185
186impl FixedContainer {
187    /// What each child says about itself, in order, for the layout to answer from.
188    fn child_stretch_axes(&self) -> Vec<StretchAxis> {
189        self.contents.iter().map(View::stretch_axis).collect()
190    }
191}
192
193impl NativeView for FixedContainer {
194    fn stretch_axis(&self) -> StretchAxis {
195        self.layout.stretch_axis(&self.child_stretch_axes())
196    }
197}
198
199impl View for FixedContainer {
200    fn body(mut self, env: &waterui_core::Environment) -> impl View {
201        self.layout = Box::new(DirectionalLayout::new(
202            self.layout,
203            waterui_core::layout::layout_direction(env),
204        ));
205        Native::new(self)
206    }
207
208    fn stretch_axis(&self) -> StretchAxis {
209        self.layout.stretch_axis(&self.child_stretch_axes())
210    }
211}
212
213/// A view wrapper that executes an arbitrary [`Layout`] implementation
214/// with reconstructable views, which can support lazy layouting.
215///
216/// Unlike [`FixedContainer`], this container stores views as an [`AnyViews`]
217/// collection, allowing backends to reconstruct views on-demand for efficient
218/// rendering of large lists.
219#[derive(Debug)]
220pub struct LazyContainer {
221    layout: Box<dyn Layout>,
222    contents: AnyViews<AnyView>,
223    direction: Computed<LayoutDirection>,
224}
225
226impl LazyContainer {
227    /// Wraps the supplied layout object and views into a lazy container view.
228    pub fn new<V: View>(
229        layout: impl Layout + 'static,
230        contents: impl Views<View = V> + 'static,
231    ) -> Self {
232        Self {
233            layout: Box::new(layout),
234            contents: AnyViews::new(contents.map(|v| AnyView::new(v))),
235            direction: Computed::constant(LayoutDirection::default()),
236        }
237    }
238    /// Returns the boxed layout object together with the collected child views.
239    #[must_use]
240    pub fn into_inner(self) -> (Box<dyn Layout>, AnyViews<AnyView>) {
241        (self.layout, self.contents)
242    }
243
244    /// Returns borrowed access to the boxed layout and lazy child collection.
245    #[must_use]
246    pub fn as_parts(&self) -> (&dyn Layout, &AnyViews<AnyView>) {
247        (self.layout.as_ref(), &self.contents)
248    }
249
250    /// Returns the resolved logical layout direction for lazy placement.
251    #[must_use]
252    pub fn direction(&self) -> Computed<LayoutDirection> {
253        self.direction.clone()
254    }
255}
256
257impl LazyContainer {
258    /// A lazy container cannot enumerate its children without materializing the
259    /// collection, which is the one thing it exists to avoid, so its layout
260    /// answers with no children. Every layout that virtualizes — the two stacks —
261    /// is content-sized and ignores them anyway.
262    const fn child_stretch_axes() -> &'static [StretchAxis] {
263        &[]
264    }
265}
266
267impl NativeView for LazyContainer {
268    fn stretch_axis(&self) -> StretchAxis {
269        self.layout.stretch_axis(Self::child_stretch_axes())
270    }
271}
272
273impl View for LazyContainer {
274    fn body(mut self, env: &waterui_core::Environment) -> impl View {
275        // Keep the concrete stack layout visible to backends: lazy-stack
276        // virtualization identifies its axis from that type. Direction is a
277        // separate signal so placement can mirror without materializing items.
278        self.direction = waterui_core::layout::layout_direction(env);
279        Native::new(self)
280    }
281
282    fn stretch_axis(&self) -> StretchAxis {
283        self.layout.stretch_axis(Self::child_stretch_axes())
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use alloc::vec;
290
291    use super::*;
292    use waterui_core::layout::Point;
293
294    #[derive(Debug)]
295    struct OffsetLayout;
296
297    impl Layout for OffsetLayout {
298        fn size_that_fits(&self, _proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
299            Size::new(100.0, 40.0)
300        }
301
302        fn place(
303            &self,
304            _bounds: Rect,
305            proposal: ProposalSize,
306            _children: &[&dyn SubView],
307        ) -> Vec<SubviewPlacement> {
308            vec![SubviewPlacement::new(
309                Rect::new(Point::new(10.0, 4.0), Size::new(20.0, 12.0)),
310                proposal,
311            )]
312        }
313    }
314
315    #[test]
316    fn right_to_left_direction_mirrors_horizontal_placement_bounded_proposal() {
317        let layout = DirectionalLayout::new(
318            Box::new(OffsetLayout),
319            Computed::constant(LayoutDirection::RightToLeft),
320        );
321        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 40.0));
322        let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
323        let placements = layout.place(bounds, proposal, &[]);
324
325        assert_eq!(
326            placements,
327            vec![SubviewPlacement::new(
328                Rect::new(Point::new(70.0, 4.0), Size::new(20.0, 12.0)),
329                proposal,
330            )]
331        );
332    }
333}