inset-services 0.18.0

Keyboard, text input, and system services for Inset
Documentation
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
//! Flutter counterpart: `services/binding.dart` (`ServicesBinding`).
//!
//! Only the app life cycle, the application exit and the clipboard's changes are here: the
//! lifecycle messages the host sends, which reach
//! `SchedulerBinding::handle_app_lifecycle_state_changed`, `handleRequestAppExit`, which the
//! widgets binding overrides, `exitApplication`, and the host's word that the clipboard changed,
//! which Flutter's engines do not send. The keyboard and the restoration manager are singletons
//! of their own (`KeyEventManager`, `RestorationManager`); the rest waits with platform channels.

use std::rc::Rc;

use inset_embedder::{AppExitResponse, AppExitType, AppLifecycleState};
use inset_foundation::{App, ChangeNotifierData, Handle, Task};
use inset_scheduler::SchedulerBinding;

use crate::SystemChrome;

/// The object side of Flutter's `WidgetsBinding` override of
/// `ServicesBinding.handleRequestAppExit`, which asks the widget layer's observers. Implement it
/// on the binding above and register `Rc::new(handle)` with [`ServicesBinding::set_overrides`];
/// `Handle<T>` is then [`ServicesBindingOverrides`].
pub trait ServicesBindingOverridesObject: Sized + 'static {
    /// Answers in place of [`ServicesBinding::handle_request_app_exit`]'s own answer.
    fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse>;
}

/// What the binding above this one overrides; kept erased.
pub trait ServicesBindingOverrides {
    /// See [`ServicesBindingOverridesObject::handle_request_app_exit`].
    fn handle_request_app_exit(&self, app: &mut App) -> Task<AppExitResponse>;
}

impl<T: ServicesBindingOverridesObject> ServicesBindingOverrides for Handle<T> {
    fn handle_request_app_exit(&self, app: &mut App) -> Task<AppExitResponse> {
        T::handle_request_app_exit(*self, app)
    }
}

/// Hands the host's lifecycle messages to the scheduler and the layers above, answers the host's
/// requests to exit the application, and asks the host to exit it.
#[derive(Default)]
pub struct ServicesBinding {
    overrides: Option<Rc<dyn ServicesBindingOverrides>>,
    /// Created with the binding so that what shows the clipboard can listen.
    clipboard: Option<Handle<ChangeNotifierData>>,
    /// Whether `initInstances`'s work is done, which the first `instance` does.
    initialized: bool,
}

impl ServicesBinding {
    /// The current [`ServicesBinding`], created on first use.
    ///
    /// Creating it reads the lifecycle state the host started in, as Dart's `initInstances`
    /// does.
    pub fn instance(app: &mut App) -> Handle<ServicesBinding> {
        let this: Handle<ServicesBinding> = app.singleton();
        if !app.get(this).initialized {
            app.get_mut(this).initialized = true;
            let clipboard = app.create(ChangeNotifierData::new());
            app.get_mut(this).clipboard = Some(clipboard);
            this.read_initial_lifecycle_state_from_native_window(app);
        }
        this
    }

    /// Listenable that notifies when another app has changed what is on the clipboard, or a copy
    /// of this app's expired and was cleared, as far as the host can tell
    /// (`EmbedderClient::clipboard_changed`). The app's own writes are not told, but where the
    /// host's system does not tell them apart, as a browser's does not.
    ///
    /// What shows whether the clipboard can be pasted, such as a toolbar's Paste button, should
    /// listen to this and look again.
    pub fn clipboard(self: Handle<Self>, app: &App) -> Handle<ChangeNotifierData> {
        app.get(self)
            .clipboard
            .expect("ServicesBinding::instance creates the clipboard listenable")
    }

    /// Notifies [`clipboard`](Self::clipboard): the host says what is on the clipboard changed.
    pub fn handle_clipboard_changed(self: Handle<Self>, app: &mut App) {
        self.clipboard(app).notify_listeners(app);
    }

    /// Registers the binding layered above this one, as Flutter's `WidgetsBinding` does by
    /// overriding `handleRequestAppExit`.
    pub fn set_overrides(
        self: Handle<Self>,
        app: &mut App,
        overrides: Rc<dyn ServicesBindingOverrides>,
    ) {
        app.get_mut(self).overrides = Some(overrides);
    }

    // App life cycle

    /// Initializes the [`SchedulerBinding::lifecycle_state`] with the
    /// `Platform::initial_lifecycle_state`.
    ///
    /// Once the lifecycle state is populated through any means (including this method), this
    /// method will do nothing. This is because the `Platform::initial_lifecycle_state` may
    /// already be stale and it no longer makes sense to use the initial state at startup as the
    /// current state anymore.
    ///
    /// The latest state should be obtained by subscribing to
    /// `WidgetsBindingObserver::did_change_app_lifecycle_state`.
    pub fn read_initial_lifecycle_state_from_native_window(self: Handle<Self>, app: &mut App) {
        let initial_lifecycle_state = app.platform().initial_lifecycle_state();
        let Some(initial_lifecycle_state) = initial_lifecycle_state else {
            return;
        };
        if SchedulerBinding::lifecycle_state(app).is_some() {
            return;
        }
        self.handle_lifecycle_message(app, initial_lifecycle_state);
    }

    /// Handles a lifecycle state the host reports: Dart's `_handleLifecycleMessage`, which
    /// `SystemChannels.lifecycle` calls with the state's name, and which the host reaches through
    /// `EmbedderClient::app_lifecycle_state_changed`. The states between the one the app is in
    /// and `state` are handled first, in order.
    pub fn handle_lifecycle_message(self: Handle<Self>, app: &mut App, state: AppLifecycleState) {
        let generated = generate_state_transitions(SchedulerBinding::lifecycle_state(app), state);
        for state_change in generated {
            SchedulerBinding::handle_app_lifecycle_state_changed(app, state_change);
            SystemChrome::handle_app_lifecycle_state_changed(app, state_change);
        }
    }

    /// Handles any requests for application exit that the host sends through
    /// `EmbedderClient::request_app_exit`.
    ///
    /// By default, returns [`AppExitResponse::Exit`].
    ///
    /// Not all exits are cancelable, so not all exits will call this function. Do not rely on
    /// this function as a place to save critical data, because you will be disappointed. There
    /// are a number of ways that the application can exit without letting the application know
    /// first: power can be unplugged, the battery removed, the application can be killed in a
    /// task manager or command line, or the device could have a rapid unplanned disassembly
    /// (i.e. it could explode). In all of those cases (and probably others), no notification
    /// will be given to the application that it is about to exit.
    ///
    /// See also:
    ///
    /// * `WidgetsBindingObserver::did_request_app_exit`, which can be overridden to respond to
    ///   this message.
    /// * `WidgetsBinding`'s override of this method, which notifies its observers.
    pub fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
        let overrides = app.get(self).overrides.clone();
        match overrides {
            Some(overrides) => overrides.handle_request_app_exit(app),
            None => Task::ready(AppExitResponse::Exit),
        }
    }

    /// Exits the application by calling the native application API method for exiting an
    /// application cleanly.
    ///
    /// This differs from calling [`std::process::exit`] in that it gives the engine a chance
    /// to clean up resources so that it doesn't crash on exit, so calling this is always
    /// preferred over calling [`std::process::exit`]. It also optionally gives handlers of
    /// [`handle_request_app_exit`](Self::handle_request_app_exit) a chance to cancel the
    /// application exit.
    ///
    /// The `exit_type` indicates what kind of exit to perform. For
    /// [`AppExitType::Cancelable`] exits, the application is queried through a call to
    /// [`handle_request_app_exit`](Self::handle_request_app_exit), where the application can
    /// optionally cancel the request for exit. If the `exit_type` is
    /// [`AppExitType::Required`], then the application exits immediately without querying the
    /// application.
    ///
    /// For [`AppExitType::Cancelable`] exits, the returned response value is the response
    /// obtained from the application as to whether the exit was canceled or not. Practically,
    /// the response will never be [`AppExitResponse::Exit`], since the application will have
    /// already exited by the time the result would have been received.
    ///
    /// The `exit_code` argument will be used as the application exit code on platforms where
    /// an exit code is supported. On other platforms it may be ignored. Dart's default is
    /// zero.
    ///
    /// Flutter's engine asks the framework back for a cancelable exit. Here the framework asks
    /// itself, on a later turn as the engine's request would come, and only a decided exit
    /// reaches the host, as [`AppExit::quit`](inset_embedder::AppExit::quit); a host that cannot
    /// quit cancels every exit.
    ///
    /// See also:
    ///
    /// * `WidgetsBindingObserver::did_request_app_exit` for a handler you can override on a
    ///   `WidgetsBindingObserver` to receive exit requests.
    pub fn exit_application(
        self: Handle<Self>,
        app: &App,
        exit_type: AppExitType,
        exit_code: i32,
    ) -> Task<AppExitResponse> {
        let platform = app.platform();
        let Some(app_exit) = platform.app_exit() else {
            return Task::ready(AppExitResponse::Cancel);
        };
        match exit_type {
            AppExitType::Required => {
                app_exit.quit(exit_code);
                Task::ready(AppExitResponse::Exit)
            }
            AppExitType::Cancelable => app.spawn(async move |cx| {
                let response = cx.update(|app| self.handle_request_app_exit(app)).await;
                if response == AppExitResponse::Exit {
                    cx.update(|app| quit(app, exit_code));
                }
                response
            }),
        }
    }
}

/// Quits through the host, where it can quit.
fn quit(app: &App, exit_code: i32) {
    if let Some(app_exit) = app.platform().app_exit() {
        app_exit.quit(exit_code);
    }
}

/// The states from `previous_state` to `state`, in the order the state machine goes through
/// them, `state` last; `state` alone from no state, and none to the state already held.
fn generate_state_transitions(
    previous_state: Option<AppLifecycleState>,
    state: AppLifecycleState,
) -> Vec<AppLifecycleState> {
    if previous_state == Some(state) {
        return Vec::new();
    }
    let mut state_changes = Vec::new();
    match previous_state {
        // If there was no previous state, just jump directly to the new state.
        None => state_changes.push(state),
        Some(previous_state) => {
            let previous_state_index = state_index(previous_state);
            let state_index = state_index(state);
            if state == AppLifecycleState::Detached {
                state_changes.extend(STATE_ORDER[previous_state_index + 1..].iter().copied());
                state_changes.push(AppLifecycleState::Detached);
            } else if previous_state_index > state_index {
                state_changes.extend(STATE_ORDER[state_index..previous_state_index].iter().rev());
            } else {
                state_changes.extend(&STATE_ORDER[previous_state_index + 1..=state_index]);
            }
        }
    }
    debug_assert!(
        {
            let mut starting = previous_state;
            state_changes.iter().all(|&ending| {
                let valid = debug_verify_lifecycle_change(starting, ending);
                starting = Some(ending);
                valid
            })
        },
        "Invalid lifecycle state transition generated from {previous_state:?} to {state:?} (generated {state_changes:?})"
    );
    state_changes
}

/// Dart's `AppLifecycleState.values`: the order the state machine goes through.
const STATE_ORDER: [AppLifecycleState; 5] = [
    AppLifecycleState::Detached,
    AppLifecycleState::Resumed,
    AppLifecycleState::Inactive,
    AppLifecycleState::Hidden,
    AppLifecycleState::Paused,
];

fn state_index(state: AppLifecycleState) -> usize {
    STATE_ORDER
        .iter()
        .position(|&ordered| ordered == state)
        .expect("every state is in the order")
}

fn debug_verify_lifecycle_change(
    starting: Option<AppLifecycleState>,
    ending: AppLifecycleState,
) -> bool {
    let Some(starting) = starting else {
        // Any transition from null is fine, since it is initializing the state.
        return true;
    };
    if starting == ending {
        // Any transition to itself shouldn't happen.
        return false;
    }
    match starting {
        // Can't go from resumed to detached directly (must go through paused).
        AppLifecycleState::Resumed => ending == AppLifecycleState::Inactive,
        AppLifecycleState::Detached => {
            ending == AppLifecycleState::Resumed || ending == AppLifecycleState::Paused
        }
        AppLifecycleState::Inactive => {
            ending == AppLifecycleState::Resumed || ending == AppLifecycleState::Hidden
        }
        AppLifecycleState::Hidden => {
            ending == AppLifecycleState::Paused || ending == AppLifecycleState::Inactive
        }
        AppLifecycleState::Paused => {
            ending == AppLifecycleState::Hidden || ending == AppLifecycleState::Detached
        }
    }
}

#[cfg(test)]
mod tests {
    use std::cell::Cell;
    use std::rc::Rc;

    use std::future::Future;

    use inset_embedder::AppExit;
    use inset_foundation::{AppCell, Completer};
    use inset_test::TestPlatform;

    use super::*;

    /// A host that records the exit code it was told to quit with.
    #[derive(Default)]
    struct RecordingAppExit {
        quit_with: Cell<Option<i32>>,
    }

    impl AppExit for RecordingAppExit {
        fn quit(&self, exit_code: i32) {
            self.quit_with.set(Some(exit_code));
        }
    }

    /// An app whose host quits through `host`.
    fn quitting_through(host: &Rc<RecordingAppExit>) -> Rc<AppCell> {
        AppCell::with_platform(Rc::new(
            TestPlatform::new().with_app_exit(Rc::clone(host) as Rc<dyn AppExit>),
        ))
    }

    /// Makes `binding` the one above the services binding's own answer.
    fn answer_with<T: ServicesBindingOverridesObject + 'static>(cell: &Rc<AppCell>, binding: T) {
        let mut app = cell.borrow_mut();
        let binding = app.create(binding);
        ServicesBinding::instance(&mut app).set_overrides(&mut app, Rc::new(binding));
    }

    /// Awaits `answer` in a task, and runs the checkpoint that task needs; `None` while the
    /// answer is still on its way.
    fn awaited(
        cell: &Rc<AppCell>,
        answer: impl Future<Output = AppExitResponse> + 'static,
    ) -> Rc<Cell<Option<AppExitResponse>>> {
        let awaited = Rc::new(Cell::new(None));
        let out = Rc::clone(&awaited);
        cell.borrow()
            .spawn(async move |_cx| out.set(Some(answer.await)));
        cell.checkpoint();
        awaited
    }

    fn exit_application(cell: &Rc<AppCell>, exit_type: AppExitType) -> AppExitResponse {
        let response = {
            let mut app = cell.borrow_mut();
            ServicesBinding::instance(&mut app).exit_application(&app, exit_type, 0)
        };
        let answer = awaited(cell, response).get();
        answer.expect("the exit was answered")
    }

    fn handle_request_app_exit(cell: &Rc<AppCell>) -> Rc<Cell<Option<AppExitResponse>>> {
        let response = {
            let mut app = cell.borrow_mut();
            ServicesBinding::instance(&mut app).handle_request_app_exit(&mut app)
        };
        awaited(cell, response)
    }

    // widgets/binding_test.dart 'handleLifecycleStateChanged callback'.
    #[test]
    fn the_states_between_are_generated_in_order() {
        use AppLifecycleState::*;
        let steps = [
            (None, Paused, vec![Paused]),
            (Some(Paused), Resumed, vec![Hidden, Inactive, Resumed]),
            (Some(Resumed), Paused, vec![Inactive, Hidden, Paused]),
            (Some(Paused), Inactive, vec![Hidden, Inactive]),
            (Some(Inactive), Hidden, vec![Hidden]),
            (Some(Hidden), Paused, vec![Paused]),
            (Some(Paused), Detached, vec![Detached]),
            (Some(Detached), Resumed, vec![Resumed]),
            (
                Some(Resumed),
                Detached,
                vec![Inactive, Hidden, Paused, Detached],
            ),
            (Some(Hidden), Hidden, vec![]),
        ];
        for (previous_state, state, generated) in steps {
            assert_eq!(
                generate_state_transitions(previous_state, state),
                generated,
                "from {previous_state:?} to {state:?}"
            );
        }
    }

    #[test]
    fn the_initial_state_is_read_when_the_binding_is_made() {
        let cell = AppCell::with_platform(Rc::new(
            TestPlatform::new().with_initial_lifecycle_state(AppLifecycleState::Hidden),
        ));
        let mut app = cell.borrow_mut();
        ServicesBinding::instance(&mut app);
        assert_eq!(
            SchedulerBinding::lifecycle_state(&mut app),
            Some(AppLifecycleState::Hidden)
        );
        assert!(!SchedulerBinding::frames_enabled(&mut app));
    }

    #[test]
    fn a_host_without_an_initial_state_leaves_the_app_without_one() {
        let cell = AppCell::new();
        let mut app = cell.borrow_mut();
        ServicesBinding::instance(&mut app);
        assert_eq!(SchedulerBinding::lifecycle_state(&mut app), None);
    }

    #[test]
    fn a_required_exit_quits_without_asking() {
        let host = Rc::default();
        let cell = quitting_through(&host);
        answer_with(&cell, Refusing);
        let response = exit_application(&cell, AppExitType::Required);
        assert_eq!(host.quit_with.get(), Some(0));
        assert_eq!(response, AppExitResponse::Exit);
    }

    #[test]
    fn a_cancelable_exit_quits_once_the_app_agrees() {
        let host = Rc::default();
        let cell = quitting_through(&host);
        let response = exit_application(&cell, AppExitType::Cancelable);
        assert_eq!(host.quit_with.get(), Some(0));
        assert_eq!(response, AppExitResponse::Exit);
    }

    #[test]
    fn a_cancelable_exit_the_app_refuses_does_not_quit() {
        let host = Rc::default();
        let cell = quitting_through(&host);
        answer_with(&cell, Refusing);
        let response = exit_application(&cell, AppExitType::Cancelable);
        assert_eq!(host.quit_with.get(), None);
        assert_eq!(response, AppExitResponse::Cancel);
    }

    #[test]
    fn exit_application_is_canceled_where_the_host_cannot_exit() {
        let cell = AppCell::with_platform(Rc::new(TestPlatform::new()));
        let response = exit_application(&cell, AppExitType::Cancelable);
        assert_eq!(response, AppExitResponse::Cancel);
    }

    #[test]
    fn default_handle_request_app_exit_returns_exit() {
        let cell = AppCell::new();
        let response = handle_request_app_exit(&cell);
        assert_eq!(response.get(), Some(AppExitResponse::Exit));
    }

    /// Refuses every exit, as a binding whose observer cancels.
    struct Refusing;

    impl ServicesBindingOverridesObject for Refusing {
        fn handle_request_app_exit(self: Handle<Self>, _app: &mut App) -> Task<AppExitResponse> {
            Task::ready(AppExitResponse::Cancel)
        }
    }

    /// Answers once `decision` completes, as a binding whose observers ask the user.
    struct Deciding {
        decision: Completer<AppExitResponse>,
    }

    impl ServicesBindingOverridesObject for Deciding {
        fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
            let decision = app.get(self).decision.future();
            app.spawn(async move |_cx| decision.await)
        }
    }

    #[test]
    fn the_binding_above_answers_in_place_of_the_default_once_it_has_decided() {
        let cell = AppCell::new();
        let decision = Completer::new();
        answer_with(
            &cell,
            Deciding {
                decision: decision.clone(),
            },
        );
        let response = handle_request_app_exit(&cell);
        assert_eq!(response.get(), None, "still deciding");

        decision.complete(&mut cell.borrow_mut(), AppExitResponse::Cancel);
        cell.checkpoint();
        assert_eq!(response.get(), Some(AppExitResponse::Cancel));
    }
}