1use frus_core::{Color, Point, Rect, Scene};
22use frus_layout::Style;
23
24use crate::interaction::Status;
25use crate::media::Brightness;
26use crate::theme::Theme;
27use crate::widget::Widget;
28
29#[derive(Clone, Copy, Debug, Default, PartialEq)]
39pub struct SystemUiOverlayStyle {
40 pub status_bar_color: Option<Color>,
42 pub status_bar_icons: Option<Brightness>,
44 pub navigation_bar_color: Option<Color>,
46 pub navigation_bar_icons: Option<Brightness>,
48}
49
50impl SystemUiOverlayStyle {
51 pub const NONE: Self = Self {
53 status_bar_color: None,
54 status_bar_icons: None,
55 navigation_bar_color: None,
56 navigation_bar_icons: None,
57 };
58
59 pub fn for_background(color: Color) -> Self {
61 let icons = icons_on(color);
62 Self {
63 status_bar_color: Some(color),
64 status_bar_icons: Some(icons),
65 navigation_bar_color: Some(color),
66 navigation_bar_icons: Some(icons),
67 }
68 }
69
70 pub fn status_bar_color(mut self, color: Color) -> Self {
73 self.status_bar_color = Some(color);
74 self
75 }
76
77 pub fn status_bar_icons(mut self, icons: Brightness) -> Self {
79 self.status_bar_icons = Some(icons);
80 self
81 }
82
83 pub fn navigation_bar_color(mut self, color: Color) -> Self {
85 self.navigation_bar_color = Some(color);
86 self
87 }
88
89 pub fn navigation_bar_icons(mut self, icons: Brightness) -> Self {
91 self.navigation_bar_icons = Some(icons);
92 self
93 }
94
95 pub fn over(self, under: Self) -> Self {
97 Self {
98 status_bar_color: self.status_bar_color.or(under.status_bar_color),
99 status_bar_icons: self.status_bar_icons.or(under.status_bar_icons),
100 navigation_bar_color: self.navigation_bar_color.or(under.navigation_bar_color),
101 navigation_bar_icons: self.navigation_bar_icons.or(under.navigation_bar_icons),
102 }
103 }
104
105 pub fn resolve(self, theme: &Theme) -> SystemBars {
110 let status_bar_color = self.status_bar_color.unwrap_or(theme.background);
111 let navigation_bar_color = self.navigation_bar_color.unwrap_or(theme.background);
112 SystemBars {
113 status_bar_color,
114 status_bar_icons: self
115 .status_bar_icons
116 .unwrap_or_else(|| icons_on(status_bar_color)),
117 navigation_bar_color,
118 navigation_bar_icons: self
119 .navigation_bar_icons
120 .unwrap_or_else(|| icons_on(navigation_bar_color)),
121 }
122 }
123}
124
125#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct SystemBars {
128 pub status_bar_color: Color,
130 pub status_bar_icons: Brightness,
132 pub navigation_bar_color: Color,
134 pub navigation_bar_icons: Brightness,
136}
137
138pub(crate) fn icons_on(color: Color) -> Brightness {
144 let lin = color.to_linear();
145 let luminance = 0.2126 * lin.r + 0.7152 * lin.g + 0.0722 * lin.b;
146 if (luminance + 0.05) * (luminance + 0.05) > 0.15 {
147 Brightness::Dark
148 } else {
149 Brightness::Light
150 }
151}
152
153pub struct AnnotatedRegion<Msg = crate::callback::Callback> {
173 style: SystemUiOverlayStyle,
174 children: Vec<Box<dyn Widget<Msg>>>,
175}
176
177impl<Msg> AnnotatedRegion<Msg> {
178 pub fn new(style: SystemUiOverlayStyle, child: impl Widget<Msg> + 'static) -> Self {
180 Self {
181 style,
182 children: vec![Box::new(child)],
183 }
184 }
185}
186
187impl<Msg: Clone> Widget<Msg> for AnnotatedRegion<Msg> {
188 fn style(&self) -> Style {
189 Style::default()
190 }
191
192 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
193 &self.children
194 }
195
196 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
197
198 fn on_click(&self) -> Option<Msg> {
199 None
200 }
201
202 fn system_ui_style(&self) -> Option<SystemUiOverlayStyle> {
203 Some(self.style)
204 }
205
206 fn debug_name(&self) -> &'static str {
207 "AnnotatedRegion"
208 }
209}
210
211pub(crate) fn style_under(
214 regions: &[(Rect, SystemUiOverlayStyle)],
215 status_bar: Point,
216 navigation_bar: Point,
217) -> SystemUiOverlayStyle {
218 let under = |point: Point| {
219 regions
220 .iter()
221 .rev()
222 .filter(|(rect, _)| rect.contains(point))
223 .fold(SystemUiOverlayStyle::NONE, |above, (_, style)| {
224 above.over(*style)
225 })
226 };
227 let top = under(status_bar);
228 let bottom = under(navigation_bar);
229 SystemUiOverlayStyle {
230 status_bar_color: top.status_bar_color,
231 status_bar_icons: top.status_bar_icons,
232 navigation_bar_color: bottom.navigation_bar_color,
233 navigation_bar_icons: bottom.navigation_bar_icons,
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::ui::build_ui;
241 use crate::{Container, Flex, Keyed, Runtime};
242 use frus_core::Size;
243
244 const SCREEN: Size = Size::new(200.0, 400.0);
245
246 fn top() -> Point {
248 Point::new(100.0, 0.5)
249 }
250 fn bottom() -> Point {
251 Point::new(100.0, 399.5)
252 }
253
254 fn photo() -> Color {
256 Color::rgb(0.05, 0.05, 0.08)
257 }
258
259 fn header() -> Container<()> {
262 Container::new().width(200.0).height(100.0).color(photo())
263 }
264
265 fn body() -> Container<()> {
266 Container::new().width(200.0).height(300.0)
267 }
268
269 fn style_of(root: &dyn Widget<()>) -> SystemUiOverlayStyle {
270 let ui = build_ui(root, SCREEN, &Runtime::default(), &Theme::light());
271 ui.system_ui_style(top(), bottom())
272 }
273
274 #[test]
278 fn a_region_under_the_status_bar_answers_for_it_and_no_further() {
279 let screen = Flex::<()>::column()
280 .child(AnnotatedRegion::new(
281 SystemUiOverlayStyle::for_background(photo()),
282 header(),
283 ))
284 .child(body());
285 let style = style_of(&screen);
286 assert_eq!(style.status_bar_color, Some(photo()));
287 assert_eq!(style.status_bar_icons, Some(Brightness::Light));
288 assert_eq!(style.navigation_bar_color, None, "the body said nothing");
289 assert_eq!(style.navigation_bar_icons, None);
290 }
291
292 #[test]
296 fn the_theme_answers_what_no_region_states() {
297 let light = SystemUiOverlayStyle::NONE.resolve(&Theme::light());
298 assert_eq!(light.status_bar_color, Theme::light().background);
299 assert_eq!(
300 light.status_bar_icons,
301 Brightness::Dark,
302 "dark icons on a light bar"
303 );
304 assert_eq!(light.navigation_bar_icons, Brightness::Dark);
305
306 let dark = SystemUiOverlayStyle::NONE.resolve(&Theme::dark());
307 assert_eq!(dark.status_bar_color, Theme::dark().background);
308 assert_eq!(dark.status_bar_icons, Brightness::Light);
309 assert_eq!(dark.navigation_bar_icons, Brightness::Light);
310 }
311
312 #[test]
316 fn a_colour_stated_alone_gets_icons_read_on_it() {
317 let bars = SystemUiOverlayStyle::NONE
318 .status_bar_color(photo())
319 .resolve(&Theme::light());
320 assert_eq!(bars.status_bar_icons, Brightness::Light);
321 assert_eq!(
322 bars.navigation_bar_icons,
323 Brightness::Dark,
324 "the other bar is still the theme's"
325 );
326 }
327
328 #[test]
333 fn nested_regions_answer_field_by_field_the_inner_first() {
334 let screen = AnnotatedRegion::new(
335 SystemUiOverlayStyle::for_background(Color::WHITE),
336 Flex::<()>::column()
337 .child(AnnotatedRegion::new(
338 SystemUiOverlayStyle::NONE.status_bar_icons(Brightness::Light),
339 header(),
340 ))
341 .child(body()),
342 );
343 let style = style_of(&screen);
344 assert_eq!(
345 style.status_bar_icons,
346 Some(Brightness::Light),
347 "the inner one"
348 );
349 assert_eq!(style.status_bar_color, Some(Color::WHITE), "the outer one");
350 assert_eq!(style.navigation_bar_color, Some(Color::WHITE));
351 assert_eq!(style.navigation_bar_icons, Some(Brightness::Dark));
352 }
353
354 #[test]
357 fn a_keyed_region_still_answers() {
358 let screen = Flex::<()>::column()
359 .child(Keyed::new(
360 1u8,
361 AnnotatedRegion::new(SystemUiOverlayStyle::for_background(photo()), header()),
362 ))
363 .child(body());
364 assert_eq!(style_of(&screen).status_bar_color, Some(photo()));
365 }
366
367 #[test]
372 fn a_region_replayed_from_the_paint_cache_still_answers() {
373 let screen = || {
374 Flex::<()>::column()
375 .child(
376 Container::new()
377 .repaint_boundary()
378 .child(AnnotatedRegion::new(
379 SystemUiOverlayStyle::for_background(photo()),
380 header(),
381 )),
382 )
383 .child(body())
384 };
385 let runtime = Runtime::default();
386 let theme = Theme::light();
387 let first = build_ui(&screen(), SCREEN, &runtime, &theme);
388 assert_eq!(
389 first.system_ui_style(top(), bottom()).status_bar_color,
390 Some(photo())
391 );
392 let second = build_ui(&screen(), SCREEN, &runtime, &theme);
393 let (hits, _) = runtime.paint_cache.borrow().last_frame_stats();
394 assert!(
395 hits > 0,
396 "the fixture: the second frame came from the cache"
397 );
398 assert_eq!(
399 second.system_ui_style(top(), bottom()).status_bar_color,
400 Some(photo()),
401 "and the region came back with it"
402 );
403 }
404
405 #[test]
407 fn icons_are_read_on_their_bar() {
408 assert_eq!(icons_on(Color::WHITE), Brightness::Dark);
409 assert_eq!(icons_on(Color::BLACK), Brightness::Light);
410 }
411}