waterui_layout/modifiers/
background.rs1use 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#[derive(Debug, Clone, Copy, Default)]
35pub struct BackgroundLayout;
36
37impl Layout for BackgroundLayout {
38 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 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 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 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
109pub struct BackgroundView<Content, Bg> {
114 content: Content,
115 background: Bg,
116}
117
118impl<Content: View, Bg: View> BackgroundView<Content, Bg> {
119 #[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 FixedContainer::new(BackgroundLayout, (background, content))
148 }
149
150 fn stretch_axis(&self) -> StretchAxis {
154 BackgroundLayout
155 .stretch_axis(&[self.background.stretch_axis(), self.content.stretch_axis()])
156 }
157}
158
159#[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 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 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 assert_eq!(
240 BackgroundLayout.stretch_axis(&[StretchAxis::Both, StretchAxis::Horizontal]),
241 StretchAxis::Horizontal
242 );
243 }
244}