Skip to main content

inset_services/
system_chrome.rs

1//! Flutter `services/system_chrome.dart`: the application switcher description, the system
2//! overlay style, and what a lifecycle change does to it. The rest of `SystemChrome` waits for
3//! the host capabilities it would talk to.
4
5use inset_embedder::{AppLifecycleState, ApplicationSwitcherDescription, SystemUiOverlayStyle};
6use inset_foundation::{App, Handle, Listener};
7
8/// Specifies a set of mutually exclusive system UI overlays.
9///
10/// Used by [`SystemChrome`] to specify the set of overlays that should be visible.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum SystemUiOverlay {
13    /// The status bar provided by the embedder on the top of the application
14    /// surface, if any.
15    Top,
16
17    /// The status bar provided by the embedder on the bottom of the application
18    /// surface, if any.
19    Bottom,
20}
21
22/// Dart's `SystemChrome._pendingStyle` and `SystemChrome._latestStyle`, which are static
23/// fields of a Dart class and one arena object per [`App`] here.
24#[derive(Default)]
25struct SystemChromeStyles {
26    pending_style: Option<SystemUiOverlayStyle>,
27    latest_style: Option<SystemUiOverlayStyle>,
28}
29
30/// Controls specific aspects of the operating system's graphical interface and
31/// how it interacts with the application.
32pub struct SystemChrome;
33
34impl SystemChrome {
35    /// Specifies the description of the current state of the application as it
36    /// pertains to the application switcher (also known as "recent tasks").
37    ///
38    /// Any part of the description that is unsupported on the current platform
39    /// will be ignored.
40    pub fn set_application_switcher_description(
41        app: &App,
42        description: &ApplicationSwitcherDescription,
43    ) {
44        if let Some(chrome) = app.platform().system_chrome() {
45            chrome.set_application_switcher_description(description);
46        }
47    }
48
49    /// Specifies the style to use for the system overlays (e.g. the status bar on
50    /// Android or iOS, the system navigation bar on Android) that are visible (if any).
51    ///
52    /// This method will schedule the embedder update to be run in a microtask.
53    /// Any subsequent calls to this method during the current event loop will
54    /// overwrite the pending value, such that only the last specified value takes
55    /// effect.
56    ///
57    /// Call this API in code whose lifecycle matches that of the desired
58    /// system UI styles. For instance, to change the system UI style on a new
59    /// page, consider calling when pushing/popping a new `PageRoute`.
60    ///
61    /// If a particular style is not supported on the platform, selecting it will
62    /// have no effect.
63    ///
64    /// For more complex control of the system overlay styles, consider using
65    /// an `AnnotatedRegion` widget instead of calling this method directly. That widget
66    /// places a value directly into the layer tree where it can be hit-tested by the
67    /// framework.
68    pub fn set_system_ui_overlay_style(app: &mut App, style: &SystemUiOverlayStyle) {
69        let styles = app.singleton::<SystemChromeStyles>();
70        if app.get(styles).pending_style.is_some() {
71            // The microtask has already been queued; just update the pending value.
72            app.get_mut(styles).pending_style = Some(*style);
73            return;
74        }
75        if Some(*style) == app.get(styles).latest_style {
76            // Trivial success: no microtask has been queued and the given style is
77            // already in effect, so no need to queue a microtask.
78            return;
79        }
80        app.get_mut(styles).pending_style = Some(*style);
81        app.schedule_microtask(Listener::new(move |app| {
82            Self::send_pending_style(app, styles)
83        }));
84    }
85
86    /// The body of the microtask Dart's `setSystemUIOverlayStyle` queues.
87    fn send_pending_style(app: &mut App, styles: Handle<SystemChromeStyles>) {
88        let pending = app.get(styles).pending_style;
89        debug_assert!(pending.is_some());
90        if pending != app.get(styles).latest_style {
91            if let (Some(style), Some(chrome)) = (pending, app.platform().system_chrome()) {
92                chrome.set_overlay_style(&style);
93            }
94            app.get_mut(styles).latest_style = pending;
95        }
96        app.get_mut(styles).pending_style = None;
97    }
98
99    /// Called by the binding during a transition to a new app lifecycle state.
100    pub fn handle_app_lifecycle_state_changed(app: &mut App, state: AppLifecycleState) {
101        // When the app is detached, clear the record of the style sent to the host
102        // so that it will be sent again when the app is reattached.
103        if state == AppLifecycleState::Detached {
104            let styles = app.singleton::<SystemChromeStyles>();
105            app.schedule_microtask(Listener::new(move |app| {
106                app.get_mut(styles).latest_style = None;
107            }));
108        }
109    }
110
111    /// The last style that was set using
112    /// [`set_system_ui_overlay_style`](SystemChrome::set_system_ui_overlay_style).
113    pub fn latest_style(app: &mut App) -> Option<SystemUiOverlayStyle> {
114        let styles = app.singleton::<SystemChromeStyles>();
115        app.get(styles).latest_style
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use inset_foundation::AppCell;
122    use std::cell::RefCell;
123    use std::rc::Rc;
124
125    use inset_embedder::{Brightness, Color, SystemChrome as SystemChromeHost, TargetPlatform};
126    use inset_test::TestPlatform;
127
128    use super::*;
129
130    /// A host that records the overlay styles it is handed.
131    #[derive(Default)]
132    struct RecordingChrome {
133        styles: RefCell<Vec<SystemUiOverlayStyle>>,
134    }
135
136    impl SystemChromeHost for RecordingChrome {
137        fn set_overlay_style(&self, style: &SystemUiOverlayStyle) {
138            self.styles.borrow_mut().push(*style);
139        }
140
141        fn set_application_switcher_description(
142            &self,
143            _description: &ApplicationSwitcherDescription,
144        ) {
145        }
146    }
147
148    fn recording_app() -> (Rc<RecordingChrome>, Rc<AppCell>) {
149        let chrome = Rc::new(RecordingChrome::default());
150        let platform = TestPlatform::new()
151            .on(TargetPlatform::IOS)
152            .with_system_chrome(chrome.clone());
153        (chrome, AppCell::with_platform(Rc::new(platform)))
154    }
155
156    #[test]
157    fn light_and_dark_differ_only_in_the_two_status_bar_brightnesses() {
158        assert_eq!(
159            SystemUiOverlayStyle::LIGHT.status_bar_icon_brightness,
160            Some(Brightness::Light)
161        );
162        assert_eq!(
163            SystemUiOverlayStyle::LIGHT.status_bar_brightness,
164            Some(Brightness::Dark)
165        );
166        assert_eq!(
167            SystemUiOverlayStyle::DARK.status_bar_icon_brightness,
168            Some(Brightness::Dark)
169        );
170        assert_eq!(
171            SystemUiOverlayStyle::DARK.status_bar_brightness,
172            Some(Brightness::Light)
173        );
174        assert_eq!(
175            SystemUiOverlayStyle::LIGHT.system_navigation_bar_color,
176            SystemUiOverlayStyle::DARK.system_navigation_bar_color
177        );
178        assert_ne!(SystemUiOverlayStyle::LIGHT, SystemUiOverlayStyle::DARK);
179    }
180
181    #[test]
182    fn copy_with_replaces_only_what_it_is_given() {
183        let style = SystemUiOverlayStyle::LIGHT
184            .copy_with()
185            .status_bar_color(Color::new(0xFF112233));
186        assert_eq!(style.status_bar_color, Some(Color::new(0xFF112233)));
187        assert_eq!(
188            style.status_bar_icon_brightness,
189            SystemUiOverlayStyle::LIGHT.status_bar_icon_brightness
190        );
191        assert_eq!(SystemUiOverlayStyle::LIGHT.status_bar_color, None);
192    }
193
194    /// `system_chrome_test.dart`: `SystemChrome overlay style test`.
195    #[test]
196    fn only_the_last_style_of_a_turn_reaches_the_host() {
197        let (chrome, cell) = recording_app();
198        let mut app = cell.borrow_mut();
199        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
200        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::DARK);
201        assert!(
202            chrome.styles.borrow().is_empty(),
203            "nothing is sent before the microtask runs"
204        );
205        assert_eq!(SystemChrome::latest_style(&mut app), None);
206
207        app.drain_microtasks();
208        assert_eq!(
209            chrome.styles.borrow().as_slice(),
210            [SystemUiOverlayStyle::DARK]
211        );
212        assert_eq!(
213            SystemChrome::latest_style(&mut app),
214            Some(SystemUiOverlayStyle::DARK)
215        );
216    }
217
218    #[test]
219    fn a_style_already_in_effect_queues_nothing() {
220        let (chrome, cell) = recording_app();
221        let mut app = cell.borrow_mut();
222        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
223        app.drain_microtasks();
224
225        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
226        app.drain_microtasks();
227        assert_eq!(
228            chrome.styles.borrow().as_slice(),
229            [SystemUiOverlayStyle::LIGHT],
230            "an unchanged style does not reach the host again"
231        );
232
233        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::DARK);
234        app.drain_microtasks();
235        assert_eq!(
236            chrome.styles.borrow().as_slice(),
237            [SystemUiOverlayStyle::LIGHT, SystemUiOverlayStyle::DARK]
238        );
239    }
240
241    /// `system_chrome_test.dart`: the end of `SystemChrome overlay style test`.
242    #[test]
243    fn the_style_is_sent_again_after_the_app_was_detached() {
244        let (chrome, cell) = recording_app();
245        let mut app = cell.borrow_mut();
246        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
247        app.drain_microtasks();
248        assert_eq!(chrome.styles.borrow().len(), 1);
249
250        // The state should be sent again if the app was detached.
251        SystemChrome::handle_app_lifecycle_state_changed(&mut app, AppLifecycleState::Detached);
252        app.drain_microtasks();
253        SystemChrome::handle_app_lifecycle_state_changed(&mut app, AppLifecycleState::Resumed);
254        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
255        app.drain_microtasks();
256        assert_eq!(chrome.styles.borrow().len(), 2);
257    }
258
259    #[test]
260    fn a_style_that_returns_to_the_latest_within_the_turn_sends_nothing() {
261        let (chrome, cell) = recording_app();
262        let mut app = cell.borrow_mut();
263        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
264        app.drain_microtasks();
265        chrome.styles.borrow_mut().clear();
266
267        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::DARK);
268        SystemChrome::set_system_ui_overlay_style(&mut app, &SystemUiOverlayStyle::LIGHT);
269        app.drain_microtasks();
270        assert!(chrome.styles.borrow().is_empty());
271        assert_eq!(
272            SystemChrome::latest_style(&mut app),
273            Some(SystemUiOverlayStyle::LIGHT)
274        );
275    }
276}