tessera_ui/
system_back.rs1use std::{
4 collections::BTreeMap,
5 sync::{Arc, Weak},
6};
7
8use parking_lot::Mutex;
9
10use crate::{CallbackWith, remember};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum BackSwipeEdge {
15 Left,
17 Right,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum SystemBackEvent {
24 Started(BackSwipeEdge),
26 Progress {
28 progress: f32,
30 edge: BackSwipeEdge,
32 },
33 Cancelled,
35 Committed,
37}
38
39type BackCallback = CallbackWith<SystemBackEvent>;
40
41#[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#[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}