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    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(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
57        assert!(
58            children.len() >= 2,
59            "BackgroundLayout requires at least 2 children: background and content"
60        );
61
62        let mut placements = Vec::with_capacity(children.len());
63
64        // Background (index 0) fills the entire bounds
65        placements.push(Rect::new(
66            bounds.origin(),
67            Size::new(bounds.width(), bounds.height()),
68        ));
69
70        // Content (index 1) also fills the bounds
71        placements.push(Rect::new(
72            bounds.origin(),
73            Size::new(bounds.width(), bounds.height()),
74        ));
75
76        placements
77    }
78
79    fn explicit_horizontal(
80        &self,
81        alignment: HorizontalAlignment,
82        _bounds: Rect,
83        children: &[PlacedSubview<'_>],
84    ) -> Option<f32> {
85        children
86            .get(1)
87            .and_then(|child| child.explicit_horizontal(alignment))
88    }
89
90    fn explicit_vertical(
91        &self,
92        alignment: VerticalAlignment,
93        _bounds: Rect,
94        children: &[PlacedSubview<'_>],
95    ) -> Option<f32> {
96        children
97            .get(1)
98            .and_then(|child| child.explicit_vertical(alignment))
99    }
100}
101
102/// A view that renders a `background` behind the `content`.
103///
104/// The content determines the layout size, and the background fills the bounds.
105/// The background renders first (behind), then content renders on top.
106pub struct BackgroundView<Content, Bg> {
107    content: Content,
108    background: Bg,
109}
110
111impl<Content, Bg> BackgroundView<Content, Bg> {
112    /// Creates a new background view with the provided content and background.
113    #[must_use]
114    pub const fn new(content: Content, background: Bg) -> Self {
115        Self {
116            content,
117            background,
118        }
119    }
120}
121
122impl<Content, Bg> fmt::Debug for BackgroundView<Content, Bg> {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("BackgroundView").finish_non_exhaustive()
125    }
126}
127
128impl<Content, Bg> View for BackgroundView<Content, Bg>
129where
130    Content: View + 'static,
131    Bg: View + 'static,
132{
133    fn body(self, _env: &waterui_core::Environment) -> impl View {
134        let Self {
135            content,
136            background,
137        } = self;
138
139        // Background is first (renders behind), content is second (renders on top)
140        FixedContainer::new(BackgroundLayout, (background, content))
141    }
142}
143
144/// Convenience constructor for creating a [`BackgroundView`].
145#[must_use]
146pub const fn background<Content, Bg>(
147    content: Content,
148    background: Bg,
149) -> BackgroundView<Content, Bg> {
150    BackgroundView::new(content, background)
151}
152
153#[cfg(test)]
154#[allow(clippy::float_cmp)]
155mod tests {
156    use waterui_core::layout::Point;
157    use waterui_core::layout::ViewDimensions;
158
159    use super::*;
160    use alloc::vec;
161
162    struct MockSubView {
163        size: Size,
164    }
165
166    impl SubView for MockSubView {
167        fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
168            ViewDimensions::new(self.size)
169        }
170        fn stretch_axis(&self) -> StretchAxis {
171            StretchAxis::None
172        }
173        fn priority(&self) -> i32 {
174            0
175        }
176    }
177
178    #[test]
179    fn test_background_size_from_content() {
180        let layout = BackgroundLayout;
181
182        let mut bg = MockSubView {
183            size: Size::new(200.0, 200.0),
184        };
185        let mut content = MockSubView {
186            size: Size::new(100.0, 50.0),
187        };
188
189        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
190        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
191
192        // Size comes from content child (index 1)
193        assert_eq!(size.width, 100.0);
194        assert_eq!(size.height, 50.0);
195    }
196
197    #[test]
198    fn test_background_fills_bounds() {
199        let layout = BackgroundLayout;
200
201        let mut bg = MockSubView {
202            size: Size::new(50.0, 50.0),
203        };
204        let mut content = MockSubView {
205            size: Size::new(100.0, 100.0),
206        };
207
208        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
209        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
210        let rects = layout.place(bounds, &children);
211
212        // Both fill the bounds
213        assert_eq!(rects[0].width(), 100.0);
214        assert_eq!(rects[0].height(), 100.0);
215        assert_eq!(rects[1].width(), 100.0);
216        assert_eq!(rects[1].height(), 100.0);
217    }
218
219    #[test]
220    fn test_background_layout_reports_its_content_stretch_axis() {
221        // The background is children[0] and the content children[1]; the pair
222        // claims whatever the content claims.
223        assert_eq!(
224            BackgroundLayout.stretch_axis(&[StretchAxis::Both, StretchAxis::Horizontal]),
225            StretchAxis::Horizontal
226        );
227    }
228}