Skip to main content

waterui_layout/modifiers/
background.rs

1//! Background layout for rendering content behind a view.
2//!
3//! `background` is the inverse of `overlay` - the background view renders
4//! behind the content, and the content determines the layout size.
5//!
6//! # Example
7//!
8//! ```rust
9//! use waterui::prelude::*;
10//!
11//! let gradient_view = hstack((Color::blue(), Color::teal()));
12//!
13//! // Gradient behind text
14//! text!("Hello").background(gradient_view);
15//!
16//! // Using the layout directly
17//! background(text!("Hello"), Color::blue());
18//! ```
19
20use core::fmt;
21
22use alloc::vec::Vec;
23use waterui_core::View;
24
25use crate::{
26    HorizontalAlignment, Layout, PlacedSubview, ProposalSize, Rect, Size, StretchAxis, SubView,
27    SubviewPlacement, VerticalAlignment, container::FixedContainer,
28};
29
30/// Layout used by [`BackgroundView`] to render a background behind content.
31///
32/// The content child determines the size, the background fills the bounds.
33/// Children order: `[background, content]`
34#[derive(Debug, Clone, Copy, Default)]
35pub struct BackgroundLayout;
36
37impl Layout for BackgroundLayout {
38    /// A background is transparent to its content: `children[1]`. Answering from
39    /// the child rather than from a copy taken at build time keeps the two from
40    /// disagreeing.
41    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
42        children.get(1).copied().unwrap_or_default()
43    }
44
45    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
46        // Size is driven entirely by the content child (index 1).
47        // Background just fills whatever size content needs.
48        // children[0] = background, children[1] = content
49        children
50            .get(1)
51            .expect("BackgroundLayout requires a content child at index 1")
52            .measure(proposal)
53            .size
54    }
55
56    fn place(
57        &self,
58        bounds: Rect,
59        _proposal: ProposalSize,
60        children: &[&dyn SubView],
61    ) -> Vec<SubviewPlacement> {
62        assert!(
63            children.len() >= 2,
64            "BackgroundLayout requires at least 2 children: background and content"
65        );
66
67        let mut placements = Vec::with_capacity(children.len());
68
69        // Background (index 0) fills the entire bounds; it is offered the
70        // resolved finite bounds, not the incoming proposal.
71        placements.push(SubviewPlacement::new(
72            Rect::new(bounds.origin(), Size::new(bounds.width(), bounds.height())),
73            ProposalSize::new(bounds.width(), bounds.height()),
74        ));
75
76        // Content (index 1) also fills the bounds and, like every child at
77        // placement, is proposed the resolved bounds it is placed in.
78        placements.push(SubviewPlacement::new(
79            Rect::new(bounds.origin(), Size::new(bounds.width(), bounds.height())),
80            ProposalSize::new(bounds.width(), bounds.height()),
81        ));
82
83        placements
84    }
85
86    fn explicit_horizontal(
87        &self,
88        alignment: HorizontalAlignment,
89        _bounds: Rect,
90        children: &[PlacedSubview<'_>],
91    ) -> Option<f32> {
92        children
93            .get(1)
94            .and_then(|child| child.explicit_horizontal(alignment))
95    }
96
97    fn explicit_vertical(
98        &self,
99        alignment: VerticalAlignment,
100        _bounds: Rect,
101        children: &[PlacedSubview<'_>],
102    ) -> Option<f32> {
103        children
104            .get(1)
105            .and_then(|child| child.explicit_vertical(alignment))
106    }
107}
108
109/// A view that renders a `background` behind the `content`.
110///
111/// The content determines the layout size, and the background fills the bounds.
112/// The background renders first (behind), then content renders on top.
113pub struct BackgroundView<Content, Bg> {
114    content: Content,
115    background: Bg,
116}
117
118impl<Content: View, Bg: View> BackgroundView<Content, Bg> {
119    /// Creates a new background view with the provided content and background.
120    #[must_use]
121    pub const fn new(content: Content, background: Bg) -> Self {
122        Self {
123            content,
124            background,
125        }
126    }
127}
128
129impl<Content, Bg> fmt::Debug for BackgroundView<Content, Bg> {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.debug_struct("BackgroundView").finish_non_exhaustive()
132    }
133}
134
135impl<Content, Bg> View for BackgroundView<Content, Bg>
136where
137    Content: View + 'static,
138    Bg: View + 'static,
139{
140    fn body(self, _env: &waterui_core::Environment) -> impl View {
141        let Self {
142            content,
143            background,
144        } = self;
145
146        // Background is first (renders behind), content is second (renders on top)
147        FixedContainer::new(BackgroundLayout, (background, content))
148    }
149
150    /// Resolves to `FixedContainer` over `BackgroundLayout` and the two
151    /// children in `[background, content]` order; reports what that container
152    /// would — `BackgroundLayout` is transparent to the content child's axis.
153    fn stretch_axis(&self) -> StretchAxis {
154        BackgroundLayout
155            .stretch_axis(&[self.background.stretch_axis(), self.content.stretch_axis()])
156    }
157}
158
159/// Convenience constructor for creating a [`BackgroundView`].
160#[must_use]
161pub const fn background<Content: View, Bg: View>(
162    content: Content,
163    background: Bg,
164) -> BackgroundView<Content, Bg> {
165    BackgroundView::new(content, background)
166}
167
168#[cfg(test)]
169#[allow(clippy::float_cmp)]
170mod tests {
171    use waterui_core::layout::Point;
172    use waterui_core::layout::ViewDimensions;
173
174    use super::*;
175    use alloc::vec;
176
177    struct MockSubView {
178        size: Size,
179    }
180
181    impl SubView for MockSubView {
182        fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
183            ViewDimensions::new(self.size)
184        }
185        fn stretch_axis(&self) -> StretchAxis {
186            StretchAxis::None
187        }
188        fn priority(&self) -> i32 {
189            0
190        }
191    }
192
193    #[test]
194    fn test_background_size_from_content() {
195        let layout = BackgroundLayout;
196
197        let mut bg = MockSubView {
198            size: Size::new(200.0, 200.0),
199        };
200        let mut content = MockSubView {
201            size: Size::new(100.0, 50.0),
202        };
203
204        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
205        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
206
207        // Size comes from content child (index 1)
208        assert_eq!(size.width, 100.0);
209        assert_eq!(size.height, 50.0);
210    }
211
212    #[test]
213    fn test_background_fills_bounds_bounded_proposal() {
214        let layout = BackgroundLayout;
215
216        let mut bg = MockSubView {
217            size: Size::new(50.0, 50.0),
218        };
219        let mut content = MockSubView {
220            size: Size::new(100.0, 100.0),
221        };
222
223        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
224        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
225        let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
226        let placements = layout.place(bounds, proposal, &children);
227
228        // Both fill the bounds
229        assert_eq!(placements[0].frame.width(), 100.0);
230        assert_eq!(placements[0].frame.height(), 100.0);
231        assert_eq!(placements[1].frame.width(), 100.0);
232        assert_eq!(placements[1].frame.height(), 100.0);
233    }
234
235    #[test]
236    fn test_background_layout_reports_its_content_stretch_axis() {
237        // The background is children[0] and the content children[1]; the pair
238        // claims whatever the content claims.
239        assert_eq!(
240            BackgroundLayout.stretch_axis(&[StretchAxis::Both, StretchAxis::Horizontal]),
241            StretchAxis::Horizontal
242        );
243    }
244}