1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! System "back" navigation requests.
//!
//! A platform back affordance — Android's back key / gesture, iOS's left-edge
//! swipe — feeds [`push_back_request`]; the app drains it with
//! [`take_back_requests`] and pops its own navigation. This gives one API
//! across platforms for what is otherwise a per-OS gesture.
//!
//! Whether the platform *routes* its back control here is governed by
//! [`set_back_interception`], the analogue of Compose's `BackHandler(enabled)`:
//!
//! - **Android**: while interception is enabled the back key/gesture is
//! consumed and lands in [`push_back_request`]; while disabled it stays with
//! the system, so the default behavior (leaving the activity) keeps working.
//! Apps enable it exactly while they have somewhere to navigate back to.
//! - **iOS**: the left-edge swipe is a framework-drawn gesture with no system
//! fallback, so it always pushes a request regardless of interception.
//! - **Desktop/web**: no OS back control; apps may map keys themselves and
//! call [`push_back_request`] directly.
//!
//! [`request_exit`] is the other direction: the app, rather than the platform,
//! deciding that it is time to leave.
use AtomicBool;
use AtomicUsize;
use Ordering;
use OnceLock;
type BackListener = ;
static BACK_REQUESTS: AtomicUsize = new;
static BACK_INTERCEPTION: AtomicBool = new;
static BACK_LISTENER: = new;
/// A flag rather than a count: closing twice is closing once.
static EXIT_REQUESTED: AtomicBool = new;
/// Record a system back request (called by the platform backend's gesture /
/// button handler).
/// Registers a callback run whenever a back request arrives, so an app can be
/// told rather than having to ask.
///
/// [`take_back_requests`] alone is a polling API, which quietly assumes the app
/// is already running a frame loop to poll from. An app that has gone idle —
/// the correct thing to do on a screen where nothing moves — has no such loop,
/// and a back gesture would sit in the counter until something unrelated woke
/// it. The listener closes that gap: it is the nudge, the counter is still the
/// source of truth, and the app drains it as before.
///
/// Called from whatever thread the platform reports back on, which is not
/// necessarily the UI thread, so the callback must be `Send + Sync`. It should
/// do as little as possible — waking a parked task is the intended use.
///
/// Only the first registration takes effect; a second is ignored, since two
/// owners of the app's back handling would each see a request the other also
/// consumed.
/// Take (and clear) the number of pending back requests. Polled by the app; a
/// burst collapses into a count the app can coalesce.
/// Declare whether the app currently wants the platform's back control routed
/// to [`push_back_request`] instead of the platform default. Set it `true`
/// while there is in-app navigation to pop and `false` when leaving the app is
/// the right response (mirrors Compose's `BackHandler(enabled)`).
/// Whether the app asked to intercept the platform back control. Read by the
/// platform input path.
/// Ask the platform to close the app.
///
/// The counterpart to [`set_back_interception`]`(false)`: interception says
/// "let the platform's own back control take me out of here", and this says
/// the same thing when the app is the one that decided. An app needs it
/// whenever it owns the affordance that means "leave":
///
/// - a screen-level dismiss gesture the app draws itself. On Android a
/// `NativeActivity` consumes every pointer event on the display, so the
/// platform's window-level swipe never fires and the app's own gesture is
/// the only one there is — completing it has to close the app, and there is
/// no back key on a watch to fall back on.
/// - a Quit item in the app's own menu.
///
/// It is a request, not a teardown: the platform decides when the frame loop
/// stops, so it is safe to call from the middle of one — including from a
/// gesture's settle animation, which is where the decision usually lands.
///
/// The backend drains it on its next turn of the loop, which is the following
/// frame for the usual caller. An app that calls this from another thread while
/// the loop is parked — nothing animating, no input — should wake it the same
/// way it would for a back request; registering
/// [`set_back_request_listener`] is enough, because this nudges that listener
/// too.
///
/// Platform behaviour, and where it does nothing:
///
/// - **Android**: finishes the activity, the same outcome as Compose's
/// `backDispatcher.onBackPressed()` on a screen with no `BackHandler`.
/// - **Desktop**: exits the event loop, closing the window.
/// - **iOS**: nothing. Apple's guidelines forbid an app terminating itself and
/// there is no supported API for it; the request is dropped rather than
/// faked, so an app can call this unconditionally.
/// - **Web**: nothing. A page cannot close a tab it did not open.
/// Whether an exit request is outstanding, without consuming it.
///
/// For a backend whose way of closing can fail. Consuming the flag and then
/// discovering the platform call did not land loses the app's only record that
/// it wanted to close: the app stays open, the gesture the user made did
/// nothing, and nothing will ever ask again. Such a backend tests with this and
/// calls [`take_exit_request`] once the request has actually been honoured.
///
/// This is also the cheap read for a loop that runs it every turn — see
/// [`take_exit_request`].
/// Take (and clear) a pending exit request. Drained by the platform backend.
///
/// Read before written: this runs on every turn of the platform's loop, and a
/// bare `swap` would dirty the cache line each time even with nothing to take.
/// The load is the common case by a very long way — an app asks to close once,
/// ever.