1use crate::theme::Theme;
31use crate::widget::Widget;
32
33enum Source {
35 Data(Box<Theme>),
38 Tweak(Box<dyn Fn(&mut Theme)>),
40}
41
42pub struct Themed<Msg = crate::callback::Callback> {
44 source: Source,
45 inner: Box<dyn Widget<Msg>>,
46}
47
48impl<Msg> Themed<Msg> {
49 pub fn new(theme: Theme, child: impl Widget<Msg> + 'static) -> Self {
52 Self {
53 source: Source::Data(Box::new(theme)),
54 inner: Box::new(child),
55 }
56 }
57
58 pub fn tweak(change: impl Fn(&mut Theme) + 'static, child: impl Widget<Msg> + 'static) -> Self {
65 Self {
66 source: Source::Tweak(Box::new(change)),
67 inner: Box::new(child),
68 }
69 }
70}
71
72impl<Msg> Themed<Msg> {
73 fn restyle(&self, base: frus_layout::Style) -> frus_layout::Style {
76 base
77 }
78}
79
80crate::transparent::forward_transparent!(Themed {
81 fn key(&self) -> Option<u64> {
83 self.inner.key()
84 }
85
86 fn positioned(&self) -> Option<crate::positioned::Positioning> {
88 self.inner.positioned()
89 }
90
91 fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
98 let mine = match &self.source {
99 Source::Data(theme) => (**theme).clone(),
100 Source::Tweak(change) => {
101 let mut theme = inherited.clone();
102 change(&mut theme);
103 theme
104 }
105 };
106 Some(self.inner.theme_override(&mine).unwrap_or_else(|| Box::new(mine)))
107 }
108
109 fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
111 self.inner.media_override(inherited)
112 }
113 fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
114 self.inner.scaffold_override()
115 }
116 fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
117 self.inner.scroll_behavior_override()
118 }
119 fn autofill_group(&self) -> bool {
121 self.inner.autofill_group()
122 }
123 fn area_toolbar(
125 &self,
126 context: crate::ToolbarContext,
127 ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
128 self.inner.area_toolbar(context)
129 }
130});
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::card::CARD_MARGIN;
136 use crate::divider::Divider;
137 use crate::flex::Flex;
138 use crate::runtime::Runtime;
139 use crate::ui::build_ui;
140 use frus_core::{Color, Primitive, Rect, Size};
141
142 #[derive(Clone, Debug, PartialEq)]
143 enum Msg {}
144
145 fn boxes(tree: &dyn Widget<Msg>) -> Vec<(Rect, Color)> {
147 build_ui(
148 tree,
149 Size::new(200.0, 200.0),
150 &Runtime::default(),
151 &Theme::default(),
152 )
153 .scene()
154 .primitives()
155 .iter()
156 .filter_map(|p| match p {
157 Primitive::Rect {
158 rect, color, blur, ..
159 } if *blur == 0.0 => Some((*rect, *color)),
160 _ => None,
161 })
162 .collect()
163 }
164
165 #[test]
166 fn a_subtree_theme_reaches_paint() {
167 let ink = Color::rgb8(255, 0, 128);
168 let tree = Flex::<Msg>::column()
169 .width(200.0)
170 .height(200.0)
171 .child(Divider::new())
172 .child(Themed::tweak(
173 move |t| t.widgets.divider.color = Some(ink),
174 Divider::new(),
175 ));
176 let painted = boxes(&tree);
177 assert_eq!(painted.len(), 2, "two dividers");
178 assert_ne!(painted[0].1, ink, "the first keeps the theme it inherited");
179 assert_eq!(painted[1].1, ink, "the second takes the subtree's");
180 }
181
182 #[test]
183 fn a_subtree_theme_reaches_layout() {
184 let tree = |themed: bool| {
187 let second: Box<dyn Widget<Msg>> = if themed {
188 Box::new(Themed::tweak(
189 |t| t.widgets.divider.height = Some(40.0),
190 Divider::new(),
191 ))
192 } else {
193 Box::new(Divider::new())
194 };
195 Flex::<Msg>::column()
196 .width(200.0)
197 .height(200.0)
198 .child(second)
199 .child(Divider::new())
200 };
201 let plain = boxes(&tree(false));
202 let themed = boxes(&tree(true));
203 assert!(
204 themed[1].0.y > plain[1].0.y,
205 "the themed divider's box is taller, so the next one is pushed down: \
206 {:?} against {:?}",
207 themed[1].0,
208 plain[1].0
209 );
210 }
211
212 #[test]
213 fn the_theme_is_restored_on_the_way_out() {
214 let ink = Color::rgb8(255, 0, 128);
218 let tree = Flex::<Msg>::column()
219 .width(200.0)
220 .height(200.0)
221 .child(Themed::tweak(
222 move |t| t.widgets.divider.color = Some(ink),
223 Divider::new(),
224 ))
225 .child(Divider::new());
226 let painted = boxes(&tree);
227 assert_eq!(painted[0].1, ink);
228 assert_ne!(painted[1].1, ink, "the sibling after it is not themed");
229 }
230
231 #[test]
232 fn nesting_starts_from_what_it_inherits() {
233 let ink = Color::rgb8(255, 0, 128);
237 let tree = Flex::<Msg>::column()
238 .width(200.0)
239 .height(200.0)
240 .child(Themed::tweak(
241 move |t| t.widgets.divider.color = Some(ink),
242 Themed::tweak(|t| t.widgets.divider.height = Some(40.0), Divider::new()),
243 ));
244 let painted = boxes(&tree);
245 assert_eq!(painted[0].1, ink, "the outer theme's colour survives");
246 assert_eq!(painted[0].0.y, 19.5, "and the inner theme's height applies");
247 }
248
249 #[test]
250 fn a_whole_theme_replaces_rather_than_merges() {
251 let ink = Color::rgb8(255, 0, 128);
254 let tree = Flex::<Msg>::column()
255 .width(200.0)
256 .height(200.0)
257 .child(Themed::tweak(
258 move |t| t.widgets.divider.color = Some(ink),
259 Themed::new(Theme::default(), Divider::new()),
260 ));
261 assert_ne!(boxes(&tree)[0].1, ink);
262 }
263
264 #[test]
265 fn an_overlay_keeps_the_theme_it_was_declared_under() {
266 let ink = Color::rgb8(255, 0, 128);
271 let panel = Flex::<Msg>::column()
272 .width(100.0)
273 .height(20.0)
274 .child(Divider::new());
275 let tree = Flex::<Msg>::column()
276 .width(200.0)
277 .height(200.0)
278 .child(Themed::tweak(
279 move |t| t.widgets.divider.color = Some(ink),
280 crate::portal::OverlayPortal::new(crate::Container::new().width(20.0).height(20.0))
281 .overlay(panel, crate::portal::Placement::Center),
282 ));
283 assert!(
284 boxes(&tree).iter().any(|(_, color)| *color == ink),
285 "the overlay's divider takes the theme of the subtree that declared it"
286 );
287 }
288
289 #[test]
290 fn it_is_transparent_to_layout() {
291 let plain = boxes(
294 &Flex::<Msg>::column()
295 .width(200.0)
296 .height(200.0)
297 .child(crate::card::Card::new()),
298 );
299 let wrapped = boxes(
300 &Flex::<Msg>::column()
301 .width(200.0)
302 .height(200.0)
303 .child(Themed::tweak(|_| {}, crate::card::Card::new())),
304 );
305 assert_eq!(plain[0].0.x, CARD_MARGIN);
306 assert_eq!(plain[0].0, wrapped[0].0, "same box, wrapped or not");
307 }
308
309 #[test]
312 fn a_component_inside_a_themed_subtree_is_built_with_its_theme() {
313 let ink = Color::rgb8(255, 0, 128);
314 let seen = std::rc::Rc::new(std::cell::Cell::new(None));
315 let probe = {
316 let seen = seen.clone();
317 crate::Component::stateless(move |cx: &crate::BuildContext| -> Box<dyn Widget> {
318 seen.set(Some(cx.theme().widgets.divider.color));
319 Box::new(Divider::new())
320 })
321 };
322 let root = Themed::tweak(move |t| t.widgets.divider.color = Some(ink), probe);
323 crate::build_deferred(&root, &Theme::default(), &Runtime::default());
324 assert_eq!(seen.get(), Some(Some(ink)), "the subtree's theme");
325 }
326}