Skip to main content

ferroui_browser/
browser_input_pane.rs

1use ferroui_base::reactive::IDisposable;
2use ferroui_base::Rect;
3use ferroui_controls::platform::{IInputPane, InputPaneBase, InputPaneState, InputPaneStateEventArgs};
4use std::rc::Rc;
5
6/// The input pane of the browser: the part of a view the on-screen
7/// keyboard covers, as the virtual keyboard of the page reports it.
8#[derive(Default)]
9pub struct BrowserInputPane {
10    base: InputPaneBase,
11}
12
13impl BrowserInputPane {
14    /// Creates a closed input pane.
15    pub fn new() -> Rc<Self> {
16        Rc::new(Self::default())
17    }
18
19    /// The page reported the rectangle of the on-screen keyboard, relative
20    /// to the element of the view.
21    pub fn on_geometry_change(&self, x: f64, y: f64, width: f64, height: f64) -> bool {
22        let old_state = (self.base.occluded_rect(), self.base.state());
23
24        let occluded_rect = Rect::new(x, y, width, height);
25        self.base.set_occluded_rect(occluded_rect);
26        let state = if occluded_rect.width != 0.0 { InputPaneState::Open } else { InputPaneState::Closed };
27        self.base.set_state(state);
28
29        if old_state != (occluded_rect, state) {
30            self.base.on_state_changed(InputPaneStateEventArgs::new(state, None, occluded_rect));
31        }
32
33        true
34    }
35}
36
37impl IInputPane for BrowserInputPane {
38    fn state(&self) -> InputPaneState {
39        self.base.state()
40    }
41
42    fn occluded_rect(&self) -> Rect {
43        self.base.occluded_rect()
44    }
45
46    fn state_changed(&self, handler: Rc<dyn Fn(&InputPaneStateEventArgs)>) -> Rc<dyn IDisposable> {
47        self.base.state_changed(handler)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use ferroui_base::threading::Dispatcher;
55    use std::cell::RefCell;
56
57    type Seen = Rc<RefCell<Vec<(InputPaneState, Option<Rect>, Rect)>>>;
58
59    fn pane() -> (Rc<BrowserInputPane>, Seen, Rc<dyn IDisposable>) {
60        let pane = BrowserInputPane::new();
61        let seen: Seen = Rc::new(RefCell::new(Vec::new()));
62        let log = seen.clone();
63        let subscription = pane.state_changed(Rc::new(move |e: &InputPaneStateEventArgs| {
64            log.borrow_mut().push((e.new_state(), e.start_rect(), e.end_rect()))
65        }));
66        (pane, seen, subscription)
67    }
68
69    #[test]
70    fn the_pane_starts_closed() {
71        let pane = BrowserInputPane::new();
72
73        assert_eq!(InputPaneState::Closed, pane.state());
74        assert_eq!(Rect::default(), pane.occluded_rect());
75    }
76
77    #[test]
78    fn a_keyboard_with_a_width_opens_the_pane() {
79        let _scope = Dispatcher::unit_test_scope();
80        let (pane, seen, _subscription) = pane();
81
82        assert!(pane.on_geometry_change(0.0, 300.0, 400.0, 200.0));
83
84        let rect = Rect::new(0.0, 300.0, 400.0, 200.0);
85        assert_eq!(InputPaneState::Open, pane.state());
86        assert_eq!(rect, pane.occluded_rect());
87        assert_eq!(vec![(InputPaneState::Open, None, rect)], *seen.borrow());
88    }
89
90    #[test]
91    fn a_keyboard_without_a_width_closes_the_pane() {
92        let _scope = Dispatcher::unit_test_scope();
93        let (pane, seen, _subscription) = pane();
94        pane.on_geometry_change(0.0, 300.0, 400.0, 200.0);
95
96        assert!(pane.on_geometry_change(0.0, 0.0, 0.0, 0.0));
97
98        assert_eq!(InputPaneState::Closed, pane.state());
99        assert_eq!(Rect::default(), pane.occluded_rect());
100        assert_eq!(2, seen.borrow().len());
101        assert_eq!((InputPaneState::Closed, None, Rect::default()), seen.borrow()[1]);
102    }
103
104    #[test]
105    fn the_change_is_raised_only_when_the_rectangle_or_the_state_changes() {
106        let _scope = Dispatcher::unit_test_scope();
107        let (pane, seen, _subscription) = pane();
108
109        // Closed and empty already.
110        pane.on_geometry_change(0.0, 0.0, 0.0, 0.0);
111        assert!(seen.borrow().is_empty());
112
113        pane.on_geometry_change(0.0, 300.0, 400.0, 200.0);
114        pane.on_geometry_change(0.0, 300.0, 400.0, 200.0);
115        assert_eq!(1, seen.borrow().len());
116
117        // The same state with another rectangle is a change.
118        pane.on_geometry_change(0.0, 250.0, 400.0, 250.0);
119        assert_eq!(2, seen.borrow().len());
120        assert_eq!(InputPaneState::Open, seen.borrow()[1].0);
121
122        // A rectangle without a width is closed, whatever its height.
123        pane.on_geometry_change(0.0, 250.0, 0.0, 250.0);
124        assert_eq!(3, seen.borrow().len());
125        assert_eq!(InputPaneState::Closed, seen.borrow()[2].0);
126    }
127
128    #[test]
129    fn a_disposed_subscription_is_not_called() {
130        let _scope = Dispatcher::unit_test_scope();
131        let (pane, seen, subscription) = pane();
132
133        subscription.dispose();
134        pane.on_geometry_change(0.0, 300.0, 400.0, 200.0);
135
136        assert!(seen.borrow().is_empty());
137        assert_eq!(InputPaneState::Open, pane.state());
138    }
139}