Skip to main content

ferroui_browser/
browser_insets_manager.rs

1use crate::interop::dom_helper;
2use crate::windowing_platform::BrowserWindowingPlatform;
3use ferroui_base::media::Color;
4use ferroui_base::reactive::IDisposable;
5use ferroui_base::Thickness;
6use ferroui_controls::platform::{IInsetsManager, InsetsManagerBase, SafeAreaChangedArgs};
7use std::rc::Rc;
8
9/// The page reads; replaced by a fake in the tests.
10pub(crate) trait IInsetsPage {
11    fn is_fullscreen(&self) -> bool;
12    fn set_fullscreen(&self, is_fullscreen: bool);
13    fn get_safe_area_padding(&self) -> Vec<f64>;
14}
15
16struct InsetsPage;
17
18impl IInsetsPage for InsetsPage {
19    fn is_fullscreen(&self) -> bool {
20        dom_helper::is_fullscreen(&BrowserWindowingPlatform::global_this())
21    }
22
23    fn set_fullscreen(&self, is_fullscreen: bool) {
24        dom_helper::set_fullscreen(&BrowserWindowingPlatform::global_this(), is_fullscreen);
25    }
26
27    fn get_safe_area_padding(&self) -> Vec<f64> {
28        dom_helper::get_safe_area_padding(&BrowserWindowingPlatform::global_this())
29    }
30}
31
32/// The insets of the page: the safe area of the device (the CSS
33/// `safe-area-inset-*` environment variables) and the full screen, which
34/// hides the system bars.
35pub struct BrowserInsetsManager {
36    base: InsetsManagerBase,
37    page: Box<dyn IInsetsPage>,
38}
39
40impl Default for BrowserInsetsManager {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl BrowserInsetsManager {
47    /// Creates the insets manager of a view.
48    pub fn new() -> Self {
49        Self::with_page(Box::new(InsetsPage))
50    }
51
52    pub(crate) fn with_page(page: Box<dyn IInsetsPage>) -> Self {
53        Self { base: InsetsManagerBase::new(), page }
54    }
55
56    /// Raises the safe area changed event with the current safe area.
57    pub fn notify_safe_area_padding_changed(&self) {
58        self.base.on_safe_area_changed(SafeAreaChangedArgs::new(self.safe_area_padding()));
59    }
60}
61
62impl IInsetsManager for BrowserInsetsManager {
63    // As the original, which answers whether the page is in full screen: the inverse of what the
64    // setter takes, which hides the bars by requesting full screen.
65    fn is_system_bar_visible(&self) -> Option<bool> {
66        Some(self.page.is_fullscreen())
67    }
68
69    fn set_is_system_bar_visible(&self, value: Option<bool>) {
70        self.page.set_fullscreen(!value.unwrap_or(true));
71    }
72
73    fn display_edge_to_edge_preference(&self) -> bool {
74        self.base.display_edge_to_edge_preference()
75    }
76
77    fn set_display_edge_to_edge_preference(&self, value: bool) {
78        self.base.set_display_edge_to_edge_preference(value);
79    }
80
81    fn displays_edge_to_edge(&self) -> bool {
82        self.base.displays_edge_to_edge()
83    }
84
85    fn safe_area_padding(&self) -> Thickness {
86        match self.page.get_safe_area_padding()[..] {
87            [left, top, right, bottom, ..] => Thickness::new(left, top, right, bottom),
88            _ => Thickness::default(),
89        }
90    }
91
92    fn system_bar_color(&self) -> Option<Color> {
93        self.base.system_bar_color()
94    }
95
96    fn set_system_bar_color(&self, value: Option<Color>) {
97        self.base.set_system_bar_color(value);
98    }
99
100    fn safe_area_changed(&self, handler: Rc<dyn Fn(&SafeAreaChangedArgs)>) -> Rc<dyn IDisposable> {
101        self.base.safe_area_changed(handler)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use ferroui_base::threading::Dispatcher;
109    use std::cell::{Cell, RefCell};
110
111    #[derive(Default)]
112    struct PageState {
113        fullscreen: Cell<bool>,
114        requests: RefCell<Vec<bool>>,
115        padding: RefCell<Vec<f64>>,
116    }
117
118    struct FakePage(Rc<PageState>);
119
120    impl IInsetsPage for FakePage {
121        fn is_fullscreen(&self) -> bool {
122            self.0.fullscreen.get()
123        }
124
125        fn set_fullscreen(&self, is_fullscreen: bool) {
126            self.0.requests.borrow_mut().push(is_fullscreen);
127        }
128
129        fn get_safe_area_padding(&self) -> Vec<f64> {
130            self.0.padding.borrow().clone()
131        }
132    }
133
134    fn manager() -> (BrowserInsetsManager, Rc<PageState>) {
135        let state = Rc::new(PageState::default());
136        (BrowserInsetsManager::with_page(Box::new(FakePage(state.clone()))), state)
137    }
138
139    #[test]
140    fn the_system_bar_visibility_is_the_full_screen_of_the_page() {
141        let (manager, state) = manager();
142        assert_eq!(Some(false), manager.is_system_bar_visible());
143        state.fullscreen.set(true);
144        assert_eq!(Some(true), manager.is_system_bar_visible());
145    }
146
147    #[test]
148    fn hiding_the_system_bars_requests_the_full_screen() {
149        let (manager, state) = manager();
150        manager.set_is_system_bar_visible(Some(false));
151        manager.set_is_system_bar_visible(Some(true));
152        manager.set_is_system_bar_visible(None);
153        assert_eq!(vec![true, false, false], *state.requests.borrow());
154    }
155
156    #[test]
157    fn the_safe_area_is_read_as_left_top_right_bottom() {
158        let (manager, state) = manager();
159        *state.padding.borrow_mut() = vec![1.0, 2.0, 3.0, 4.0];
160        assert_eq!(Thickness::new(1.0, 2.0, 3.0, 4.0), manager.safe_area_padding());
161
162        state.padding.borrow_mut().clear();
163        assert_eq!(Thickness::default(), manager.safe_area_padding());
164    }
165
166    #[test]
167    fn the_edge_to_edge_preference_and_the_bar_color_are_kept() {
168        let (manager, _) = manager();
169        assert!(!manager.display_edge_to_edge_preference());
170        manager.set_display_edge_to_edge_preference(true);
171        assert!(manager.display_edge_to_edge_preference());
172        assert!(manager.displays_edge_to_edge());
173
174        assert_eq!(None, manager.system_bar_color());
175        manager.set_system_bar_color(Some(Color::from_rgb(1, 2, 3)));
176        assert_eq!(Some(Color::from_rgb(1, 2, 3)), manager.system_bar_color());
177    }
178
179    #[test]
180    fn a_change_of_the_safe_area_is_reported_with_the_current_padding() {
181        let _scope = Dispatcher::unit_test_scope();
182        let (manager, state) = manager();
183        *state.padding.borrow_mut() = vec![0.0, 10.0, 0.0, 20.0];
184        let reported = Rc::new(RefCell::new(Vec::new()));
185        let _subscription = manager.safe_area_changed({
186            let reported = reported.clone();
187            Rc::new(move |args: &SafeAreaChangedArgs| reported.borrow_mut().push(args.safe_area_padding()))
188        });
189
190        manager.notify_safe_area_padding_changed();
191
192        assert_eq!(vec![Thickness::new(0.0, 10.0, 0.0, 20.0)], *reported.borrow());
193    }
194}