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