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