Skip to main content

tessera_ui/
system_back.rs

1//! Application handling for Android system back navigation.
2
3use std::{
4    collections::BTreeMap,
5    sync::{Arc, Weak},
6};
7
8use parking_lot::Mutex;
9
10use crate::{CallbackWith, remember};
11
12/// The screen edge that started a predictive back gesture.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum BackSwipeEdge {
15    /// Left edge of the screen.
16    Left,
17    /// Right edge of the screen.
18    Right,
19}
20
21/// A phase of system back navigation.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum SystemBackEvent {
24    /// A predictive gesture started.
25    Started(BackSwipeEdge),
26    /// The gesture moved. The progress runs from zero to one.
27    Progress {
28        /// Fraction of the gesture completed.
29        progress: f32,
30        /// Edge where the gesture started.
31        edge: BackSwipeEdge,
32    },
33    /// The gesture was cancelled. Restore the original view.
34    Cancelled,
35    /// Perform the back action. A button press can commit without a preview.
36    Committed,
37}
38
39type BackCallback = CallbackWith<SystemBackEvent>;
40
41/// Marks an off-screen destination preview that must not handle system back.
42#[doc(hidden)]
43#[derive(Clone, Copy)]
44pub struct BackPreviewScope(pub bool);
45
46#[derive(Default)]
47struct Registry {
48    next_id: u64,
49    callbacks: BTreeMap<u64, BackCallback>,
50    #[cfg(any(target_os = "android", test, feature = "testing"))]
51    gesture: Option<Option<u64>>,
52}
53
54impl Registry {
55    #[cfg(any(target_os = "android", test, feature = "testing"))]
56    fn active(&self) -> Option<u64> {
57        self.callbacks.last_key_value().map(|(&id, _)| id)
58    }
59
60    #[cfg(any(target_os = "android", test, feature = "testing"))]
61    fn callback(&mut self, event: SystemBackEvent) -> Option<BackCallback> {
62        let id = match event {
63            SystemBackEvent::Started(_) => {
64                let active = self.active();
65                self.gesture = Some(active);
66                active
67            }
68            SystemBackEvent::Progress { .. } => self.gesture.flatten(),
69            SystemBackEvent::Cancelled => self.gesture.take().flatten(),
70            SystemBackEvent::Committed => self.gesture.take().unwrap_or_else(|| self.active()),
71        }?;
72        self.callbacks.get(&id).copied()
73    }
74}
75
76type Handlers = Mutex<Registry>;
77
78struct Registration {
79    handlers: Weak<Handlers>,
80    id: u64,
81}
82
83impl Drop for Registration {
84    fn drop(&mut self) {
85        if let Some(handlers) = self.handlers.upgrade() {
86            handlers.lock().callbacks.remove(&self.id);
87        }
88    }
89}
90
91/// Registers an application back handler while this component is mounted.
92///
93/// On Android, the most recently mounted enabled handler receives system back.
94/// Enable it only while its destination or overlay can go back. With no enabled
95/// handler, Android performs its normal back action. A predictive gesture keeps
96/// its starting handler until it commits or is cancelled.
97#[crate::tessera(crate)]
98pub fn system_back_handler(enabled: bool, on_back: Option<CallbackWith<SystemBackEvent>>) {
99    if crate::use_context::<BackPreviewScope>().is_some_and(|scope| scope.get().0) {
100        return;
101    }
102    let handlers = crate::runtime_resource::<Handlers>();
103    let registration = remember(|| {
104        let mut registry = handlers.lock();
105        let id = registry.next_id;
106        registry.next_id += 1;
107        Registration {
108            handlers: Arc::downgrade(&handlers),
109            id,
110        }
111    });
112    let id = registration.with(|registration| registration.id);
113    let mut registry = handlers.lock();
114    if let Some(callback) = on_back.filter(|_| enabled) {
115        registry.callbacks.insert(id, callback);
116    } else {
117        registry.callbacks.remove(&id);
118    }
119}
120
121#[cfg(target_os = "android")]
122pub(crate) fn enabled() -> bool {
123    crate::runtime_resource::<Handlers>()
124        .lock()
125        .active()
126        .is_some()
127}
128
129#[cfg(any(target_os = "android", test, feature = "testing"))]
130pub(crate) fn dispatch(event: SystemBackEvent) {
131    let callback = crate::runtime_resource::<Handlers>().lock().callback(event);
132    if let Some(callback) = callback {
133        callback.call(event);
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use crate::{
140        State, remember,
141        testing::{TestProbe, layout_test, pump_layout_test_session, start_layout_test_session},
142    };
143
144    use super::*;
145
146    #[crate::tessera(crate)]
147    fn fixture(
148        overlay: TestProbe<Option<State<bool>>>,
149        events: TestProbe<Vec<(&'static str, SystemBackEvent)>>,
150    ) {
151        let visible = remember(|| false);
152        overlay.set(Some(visible));
153        let app = events.clone();
154        system_back_handler(true).on_back(move |event| {
155            app.with_mut(|events| events.push(("app", event)));
156        });
157        system_back_handler(visible.get()).on_back(move |event| {
158            events.with_mut(|events| events.push(("overlay", event)));
159        });
160    }
161
162    #[test]
163    fn gesture_keeps_its_owner_and_a_later_button_press_dismisses_the_overlay() {
164        let overlay = TestProbe::default();
165        let events = TestProbe::default();
166        let probe = overlay.clone();
167        let log = events.clone();
168        let mut session = start_layout_test_session(layout_test(move || {
169            fixture(probe.clone(), log.clone());
170        }));
171        pump_layout_test_session(&mut session, 0);
172        let overlay = overlay.get().expect("mounted overlay");
173        let started = SystemBackEvent::Started(BackSwipeEdge::Right);
174        session.dispatch_system_back(started);
175        session.run(|| overlay.set(true));
176        pump_layout_test_session(&mut session, 16_000_000);
177        session.dispatch_system_back(SystemBackEvent::Cancelled);
178        session.dispatch_system_back(SystemBackEvent::Committed);
179        assert_eq!(
180            events.take(),
181            vec![
182                ("app", started),
183                ("app", SystemBackEvent::Cancelled),
184                ("overlay", SystemBackEvent::Committed),
185            ]
186        );
187    }
188}