Skip to main content

inset_shell/
lib.rs

1//! The one [`EmbedderClient`] the framework provides: owns the [`AppCell`] and
2//! translates host pushes into binding methods.
3//!
4//! Dart has no type for this — the engine owns the isolate. [`App`] stays in
5//! foundation. This crate sits above scheduler and gestures so the isolate
6//! can name both.
7#![feature(arbitrary_self_types)]
8
9use std::cell::RefMut;
10use std::rc::Rc;
11use std::time::Duration;
12
13use inset_embedder::{
14    AppExitFuture, AppLifecycleState, DropData, EmbedderClient, Frame, Instant, KeyData,
15    PlatformRef, PointerDataPacket, TextEditingValue, TextInputAction, ViewFocusEvent, ViewId,
16};
17use inset_foundation::{App, AppCell};
18use inset_gestures::GestureBinding;
19use inset_painting::PaintingBinding;
20use inset_rendering::RendererBinding;
21use inset_scheduler::SchedulerBinding;
22use inset_services::{KeyEventManager, ServicesBinding, TextInput};
23
24/// Host-facing isolate: the [`AppCell`] plus the methods the embedder pushes.
25///
26/// Every push is one call into the app: the `App` is borrowed for the binding call and
27/// released before the cell's checkpoint, where the microtasks and futures the event queued
28/// run.
29pub struct Shell {
30    app: Rc<AppCell>,
31    platform: PlatformRef,
32    /// The platform time the app clock was last advanced to.
33    clock: Duration,
34    /// The platform's `now` at its `elapsed` zero, known once a frame has reported its `elapsed`; lets every push, not only a frame or wake, move the app clock to the platform's.
35    origin: Option<Instant>,
36}
37
38impl Shell {
39    /// Builds the heap from `platform`, then runs application setup.
40    ///
41    /// Setup (later widgets `run_app`) requests the first frame when it
42    /// installs work that needs one.
43    pub fn new(platform: PlatformRef, setup: impl FnOnce(&mut App)) -> Shell {
44        let shell = Shell {
45            app: AppCell::with_platform(platform.clone()),
46            platform,
47            clock: Duration::ZERO,
48            origin: None,
49        };
50        shell.handle_event(|app| {
51            // Flutter's engine collects the platform's fonts before the framework runs.
52            PaintingBinding::instance(app).install_platform_fonts(app);
53            setup(app);
54        });
55        shell
56    }
57
58    /// The `App`, borrowed until the guard drops. A host that needs the whole thing, a borrow
59    /// and then the checkpoint, takes [`cell`](Self::cell).
60    pub fn app(&self) -> RefMut<'_, App> {
61        self.app.borrow_mut()
62    }
63
64    pub fn cell(&self) -> &Rc<AppCell> {
65        &self.app
66    }
67
68    /// One platform event: `f` on the borrowed `App`, then the checkpoint.
69    fn handle_event<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
70        let result = f(&mut self.app.borrow_mut());
71        self.app.checkpoint();
72        result
73    }
74
75    /// An input or platform push: the app clock catches up with the platform's first, as Dart's
76    /// timers measure real time from the moment they are created, then `f` runs.
77    fn push<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
78        if let Some(origin) = self.origin {
79            let elapsed = self.platform.now().saturating_duration_since(origin);
80            self.advance_clock(elapsed);
81        }
82        self.handle_event(f)
83    }
84
85    /// Moves the app clock up to the platform's `elapsed`, firing the timers that came due.
86    fn advance_clock(&mut self, elapsed: Duration) {
87        if elapsed <= self.clock {
88            return;
89        }
90        self.app.elapse(elapsed - self.clock);
91        self.clock = elapsed;
92    }
93
94    /// dart:ui's `onMetricsChanged`, which the framework sets to
95    /// `RendererBinding.handleMetricsChanged`. `_addView` and `_removeView` invoke it as
96    /// `_updateWindowMetrics` does: a view coming or going reaches the app only this way.
97    fn metrics_changed(&mut self) {
98        self.push(|app| RendererBinding::instance(app).handle_metrics_changed(app));
99    }
100}
101
102impl EmbedderClient for Shell {
103    fn frame(&mut self, frame: Frame) {
104        self.origin = Some(self.platform.now() - frame.elapsed);
105        self.advance_clock(frame.elapsed);
106        // The engine runs `_beginFrame` and `_drawFrame` as two native tasks: two calls.
107        self.handle_event(|app| SchedulerBinding::on_begin_frame(app, Some(frame.elapsed)));
108        self.handle_event(SchedulerBinding::on_draw_frame);
109    }
110
111    fn view_added(&mut self, _id: ViewId) {
112        self.metrics_changed();
113    }
114
115    fn view_metrics_changed(&mut self, _id: ViewId) {
116        self.metrics_changed();
117    }
118
119    fn view_removed(&mut self, _id: ViewId) {
120        self.metrics_changed();
121    }
122
123    fn view_focus_changed(&mut self, event: ViewFocusEvent) {
124        self.push(|app| {
125            let callback = app.platform_callbacks().on_view_focus_change.clone();
126            if let Some(callback) = callback {
127                callback(app, event);
128            }
129        });
130    }
131
132    fn drop_data(&mut self, data: DropData) {
133        self.push(|app| {
134            let callback = app.platform_callbacks().on_drop.clone();
135            if let Some(callback) = callback {
136                callback(app, data);
137            }
138        });
139    }
140
141    fn pointer_data_packet(&mut self, packet: PointerDataPacket) {
142        self.push(|app| GestureBinding::instance(app).handle_pointer_data_packet(app, packet));
143    }
144
145    fn key_data(&mut self, data: KeyData) -> bool {
146        self.push(|app| KeyEventManager::instance(app).handle_key_data(app, data))
147    }
148
149    fn pop_route(&mut self) -> bool {
150        self.push(|app| {
151            let callback = app.platform_callbacks().on_pop_route.clone();
152            callback.is_some_and(|callback| callback(app))
153        })
154    }
155
156    /// The binding's task, whose checkpoint is this push's own: an answer the observers gave
157    /// at once is ready when the call returns.
158    fn request_app_exit(&mut self) -> AppExitFuture {
159        Box::pin(self.push(|app| ServicesBinding::instance(app).handle_request_app_exit(app)))
160    }
161
162    fn app_lifecycle_state_changed(&mut self, state: AppLifecycleState) {
163        self.push(|app| ServicesBinding::instance(app).handle_lifecycle_message(app, state));
164    }
165
166    fn platform_brightness_changed(&mut self) {
167        self.push(|app| {
168            let callback = app
169                .platform_callbacks()
170                .on_platform_brightness_changed
171                .clone();
172            if let Some(callback) = callback {
173                callback.call(app);
174            }
175        });
176    }
177
178    fn locales_changed(&mut self) {
179        self.push(|app| {
180            let callback = app.platform_callbacks().on_locale_changed.clone();
181            if let Some(callback) = callback {
182                callback.call(app);
183            }
184        });
185    }
186
187    fn system_fonts_changed(&mut self) {
188        self.push(|app| {
189            PaintingBinding::instance(app).handle_system_fonts_did_change(app);
190        });
191    }
192
193    fn wake(&mut self, elapsed: Duration) {
194        self.advance_clock(elapsed);
195        // A wake with no time behind it is a post from a native callback.
196        self.app.wake();
197    }
198
199    fn text_input_editing_value(&mut self, _view: ViewId, value: TextEditingValue) {
200        self.push(|app| TextInput::instance(app).update_editing_value(app, value));
201    }
202
203    fn text_input_action(&mut self, _view: ViewId, action: TextInputAction) {
204        self.push(|app| TextInput::instance(app).perform_action(app, action));
205    }
206
207    fn text_input_closed(&mut self, _view: ViewId) {
208        self.push(|app| TextInput::instance(app).connection_closed(app));
209    }
210
211    fn system_context_menu_hidden(&mut self) {
212        self.push(inset_services::SystemContextMenuController::dispatch_system_hide);
213    }
214
215    fn custom_context_menu_action(&mut self, callback_id: &str) {
216        let callback_id = callback_id.to_string();
217        self.push(move |app| {
218            inset_services::SystemContextMenuController::dispatch_custom_action(app, &callback_id);
219        });
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use std::cell::Cell;
226    use std::rc::Rc;
227    use std::sync::Arc;
228    use std::sync::atomic::{AtomicUsize, Ordering};
229    use std::task::{Context, Poll, Wake, Waker};
230    use std::time::Duration;
231
232    use inset_embedder::{
233        AppLifecycleState, EmbedderClient, Frame, KeyData, KeyEventType, PointerChange,
234        PointerData, PointerDataPacket, PointerDeviceKind, ViewId,
235    };
236    use inset_foundation::App;
237    use inset_gestures::{GestureBinding, PointerRoute};
238    use inset_scheduler::SchedulerBinding;
239    use inset_services::{HardwareKeyboard, KeyEvent, LogicalKeyboardKey, PhysicalKeyboardKey};
240    use inset_test::{TestPlatform, TestView};
241
242    use super::Shell;
243
244    /// A platform with the one view a pointer packet or a focus event is addressed to.
245    fn platform_with_a_view() -> TestPlatform {
246        TestPlatform::new().with_view(Rc::new(TestView::new(0.0, 0.0)))
247    }
248
249    #[test]
250    fn drop_data_reaches_the_platform_callback() {
251        use std::cell::RefCell;
252        use std::path::PathBuf;
253
254        use inset_embedder::{DropChange, DropData, ViewId};
255
256        let seen = Rc::new(RefCell::new(None));
257        let seen_slot = Rc::clone(&seen);
258        let platform = Rc::new(platform_with_a_view());
259        let mut shell = Shell::new(platform, move |app: &mut App| {
260            app.platform_callbacks_mut().on_drop = Some(Rc::new(move |_app, data| {
261                *seen_slot.borrow_mut() = Some(data);
262            }));
263        });
264        shell.drop_data(DropData {
265            view_id: ViewId(0),
266            change: DropChange::Dropped,
267            physical_position: None,
268            paths: vec![PathBuf::from("/a.wasm")],
269        });
270        let seen = seen.borrow();
271        let seen = seen.as_ref().expect("the callback ran");
272        assert_eq!(seen.change, DropChange::Dropped);
273        assert_eq!(seen.paths, [PathBuf::from("/a.wasm")]);
274    }
275
276    /// The answer to `shell.request_app_exit()` so far, polled as a host polls it; `woken`
277    /// counts the wakes of its waker.
278    fn poll_exit_answer(
279        answer: &mut inset_embedder::AppExitFuture,
280        woken: &Arc<WakeCounter>,
281    ) -> Option<inset_embedder::AppExitResponse> {
282        let waker = Waker::from(Arc::clone(woken));
283        match answer.as_mut().poll(&mut Context::from_waker(&waker)) {
284            Poll::Ready(response) => Some(response),
285            Poll::Pending => None,
286        }
287    }
288
289    #[derive(Default)]
290    struct WakeCounter(AtomicUsize);
291
292    impl WakeCounter {
293        fn wakes(&self) -> usize {
294            self.0.load(Ordering::Acquire)
295        }
296    }
297
298    impl Wake for WakeCounter {
299        fn wake(self: Arc<Self>) {
300            self.0.fetch_add(1, Ordering::AcqRel);
301        }
302    }
303
304    #[test]
305    fn a_lifecycle_state_the_host_reports_reaches_the_scheduler_with_the_states_between() {
306        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
307        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
308        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
309        let mut app = shell.app();
310        assert_eq!(
311            SchedulerBinding::lifecycle_state(&mut app),
312            Some(AppLifecycleState::Hidden)
313        );
314        assert!(!SchedulerBinding::frames_enabled(&mut app));
315    }
316
317    #[test]
318    fn request_app_exit_is_answered_by_the_services_binding() {
319        use inset_embedder::AppExitResponse;
320        use inset_foundation::{Handle, Task};
321        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
322
323        #[derive(Default)]
324        struct Refusing;
325
326        impl ServicesBindingOverridesObject for Refusing {
327            fn handle_request_app_exit(
328                self: Handle<Self>,
329                _app: &mut App,
330            ) -> Task<AppExitResponse> {
331                Task::ready(AppExitResponse::Cancel)
332            }
333        }
334
335        let woken = Arc::default();
336        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
337        let mut answer = shell.request_app_exit();
338        assert_eq!(
339            poll_exit_answer(&mut answer, &woken),
340            Some(AppExitResponse::Exit)
341        );
342
343        let refusing = shell.app().create(Refusing);
344        let binding = ServicesBinding::instance(&mut shell.app());
345        binding.set_overrides(&mut shell.app(), Rc::new(refusing));
346        let mut answer = shell.request_app_exit();
347        assert_eq!(
348            poll_exit_answer(&mut answer, &woken),
349            Some(AppExitResponse::Cancel)
350        );
351    }
352
353    /// The host's waker is woken inside the call that decides, and the answer is ready then.
354    #[test]
355    fn request_app_exit_answers_during_the_call_in_which_the_framework_decides() {
356        use inset_embedder::AppExitResponse;
357        use inset_foundation::{Completer, Handle, Task};
358        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
359
360        struct Deciding {
361            decision: Completer<AppExitResponse>,
362        }
363
364        impl ServicesBindingOverridesObject for Deciding {
365            fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
366                let decision = app.get(self).decision.future();
367                app.spawn(async move |_cx| decision.await)
368            }
369        }
370
371        let decision = Completer::new();
372        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
373        let deciding = shell.app().create(Deciding {
374            decision: decision.clone(),
375        });
376        let binding = ServicesBinding::instance(&mut shell.app());
377        binding.set_overrides(&mut shell.app(), Rc::new(deciding));
378
379        let woken: Arc<WakeCounter> = Arc::default();
380        let mut answer = shell.request_app_exit();
381        assert_eq!(poll_exit_answer(&mut answer, &woken), None);
382
383        decision.complete(&mut shell.app(), AppExitResponse::Exit);
384        assert_eq!(woken.wakes(), 0, "the task resumes at a checkpoint");
385        shell.wake(Duration::ZERO);
386        assert_eq!(woken.wakes(), 1, "the wake ran the checkpoint that decided");
387        assert_eq!(
388            poll_exit_answer(&mut answer, &woken),
389            Some(AppExitResponse::Exit)
390        );
391    }
392
393    #[test]
394    fn new_runs_setup_and_frame_runs_both_scheduler_phases() {
395        let platform = Rc::new(TestPlatform::new());
396        let mut setup_ran = false;
397        let mut shell = Shell::new(platform.clone(), |_app: &mut App| {
398            setup_ran = true;
399        });
400        assert!(setup_ran);
401        assert_eq!(platform.frames_requested(), 0);
402
403        SchedulerBinding::schedule_frame(&mut shell.app());
404        assert_eq!(platform.frames_requested(), 1);
405        shell.frame(Frame {
406            elapsed: Duration::from_millis(16),
407        });
408    }
409
410    #[test]
411    fn pointer_data_packet_reaches_gesture_binding() {
412        let ran = Rc::new(Cell::new(false));
413        let ran_flag = Rc::clone(&ran);
414        let platform = Rc::new(platform_with_a_view());
415        let mut shell = Shell::new(platform, |app| {
416            let binding = GestureBinding::instance(app);
417            binding.pointer_router(app).add_route(
418                app,
419                1,
420                PointerRoute::new(move |_app, _event| {
421                    ran_flag.set(true);
422                }),
423                None,
424            );
425        });
426
427        shell.pointer_data_packet(PointerDataPacket::new(vec![PointerData {
428            change: PointerChange::Down,
429            kind: PointerDeviceKind::Mouse,
430            buttons: 1,
431            pointer_identifier: 1,
432            ..PointerData::default()
433        }]));
434        assert!(ran.get());
435    }
436
437    #[test]
438    fn key_data_reaches_the_hardware_keyboard() {
439        let seen = Rc::new(Cell::new(0));
440        let seen_flag = Rc::clone(&seen);
441        let platform = Rc::new(TestPlatform::new());
442        let mut shell = Shell::new(platform, |app| {
443            HardwareKeyboard::instance(app).add_handler(
444                app,
445                Rc::new(move |_app: &mut App, _event: &KeyEvent| {
446                    seen_flag.set(seen_flag.get() + 1);
447                    true
448                }),
449            );
450        });
451
452        let key_a = KeyData {
453            event_type: KeyEventType::Down,
454            physical: PhysicalKeyboardKey::KEY_A.usb_hid_usage,
455            logical: LogicalKeyboardKey::KEY_A.key_id,
456            character: Some("a".to_owned()),
457            ..KeyData::default()
458        };
459        assert!(shell.key_data(key_a), "the handler claimed the event");
460        assert_eq!(seen.get(), 1);
461
462        let keyboard = HardwareKeyboard::instance(&mut shell.app());
463        assert!(keyboard.is_logical_key_pressed(&shell.app(), LogicalKeyboardKey::KEY_A));
464    }
465
466    #[test]
467    fn wake_and_frame_advance_the_app_clock_and_fire_due_timers() {
468        let platform = Rc::new(TestPlatform::new());
469        let fired = Rc::new(Cell::new(Vec::new()));
470        let mut shell = Shell::new(platform, |app| {
471            for (name, delay) in [("early", 10), ("late", 30)] {
472                let fired = Rc::clone(&fired);
473                inset_foundation::Timer::new(
474                    app,
475                    Duration::from_millis(delay),
476                    inset_foundation::Listener::new(move |_app| {
477                        let mut log = fired.take();
478                        log.push(name);
479                        fired.set(log);
480                    }),
481                );
482            }
483        });
484        assert!(fired.take().is_empty());
485
486        shell.wake(Duration::from_millis(15));
487        assert_eq!(fired.take(), vec!["early"]);
488
489        shell.frame(Frame {
490            elapsed: Duration::from_millis(32),
491        });
492        assert_eq!(fired.take(), vec!["late"]);
493    }
494
495    #[test]
496    fn a_view_coming_or_going_is_a_metrics_change() {
497        use inset_foundation::Handle;
498        use inset_rendering::{RendererBinding, RendererBindingOverridesObject};
499
500        struct CountingMetricsChanges {
501            count: Rc<Cell<usize>>,
502        }
503
504        impl RendererBindingOverridesObject for CountingMetricsChanges {
505            fn will_draw_frame(self: Handle<Self>, _app: &mut App) {}
506
507            fn did_draw_frame(self: Handle<Self>, _app: &mut App) {}
508
509            fn did_handle_metrics_changed(self: Handle<Self>, app: &mut App) {
510                let count = &app.get(self).count;
511                count.set(count.get() + 1);
512            }
513        }
514
515        let count = Rc::new(Cell::new(0));
516        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app| {});
517        let counting = shell.app().create(CountingMetricsChanges {
518            count: Rc::clone(&count),
519        });
520        let binding = RendererBinding::instance(&mut shell.app());
521        binding.set_overrides(&mut shell.app(), Rc::new(counting));
522
523        shell.view_added(ViewId(1));
524        assert_eq!(count.get(), 1, "a view added");
525        shell.view_removed(ViewId(1));
526        assert_eq!(count.get(), 2, "a view removed");
527    }
528
529    #[test]
530    fn view_focus_is_delivered_through_the_dispatcher_callback() {
531        use inset_embedder::{ViewFocusDirection, ViewFocusEvent, ViewFocusState};
532        let received = Rc::new(Cell::new(None));
533        let sink = received.clone();
534        let platform = Rc::new(platform_with_a_view());
535        let mut shell = Shell::new(platform, move |app| {
536            app.platform_callbacks_mut().on_view_focus_change = Some(Rc::new(move |app, event| {
537                let sink = sink.clone();
538                app.schedule_microtask(inset_foundation::Listener::new(move |_| {
539                    sink.set(Some(event))
540                }));
541            }));
542        });
543        shell.view_focus_changed(ViewFocusEvent {
544            view_id: ViewId(0),
545            state: ViewFocusState::Focused,
546            direction: ViewFocusDirection::Backward,
547        });
548        let event = received
549            .get()
550            .expect("the shell checkpoints the callback's microtask");
551        assert_eq!(event.view_id, ViewId(0));
552        assert_eq!(event.state, ViewFocusState::Focused);
553        assert_eq!(event.direction, ViewFocusDirection::Backward);
554    }
555}