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, ClipboardEvent, DropData, EmbedderClient, Frame, Instant,
15    KeyData, PlatformRef, PointerDataPacket, RequestId, TextEditingValue, TextInputAction,
16    ViewError, ViewFocusEvent, ViewId,
17};
18use inset_foundation::{App, AppCell};
19use inset_gestures::GestureBinding;
20use inset_painting::PaintingBinding;
21use inset_rendering::RendererBinding;
22use inset_scheduler::{FrameTarget, 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`, `_removeView` and `_updateDisplays`
97    /// invoke it as `_updateWindowMetrics` does: a view coming or going, and a change to the
98    /// screens, reach the app only this way.
99    fn metrics_changed(&mut self) {
100        self.push(|app| RendererBinding::instance(app).handle_metrics_changed(app));
101    }
102}
103
104impl EmbedderClient for Shell {
105    fn frame(&mut self, views: &[ViewId], frame: Frame) {
106        self.origin = Some(self.platform.now() - frame.elapsed);
107        self.advance_clock(frame.elapsed);
108        // The engine runs `_beginFrame` and `_drawFrame` as two native tasks: two calls.
109        self.handle_event(|app| SchedulerBinding::on_begin_frame(app, views, Some(frame.elapsed)));
110        self.handle_event(|app| SchedulerBinding::on_draw_frame(app, views));
111    }
112
113    /// The framework hears which view came, then "metrics changed", as `_addView` invokes it.
114    fn view_added(&mut self, id: ViewId) {
115        self.push(|app| {
116            let callback = app.platform_callbacks().on_view_added.clone();
117            if let Some(callback) = callback {
118                callback(app, id);
119            }
120        });
121        self.metrics_changed();
122    }
123
124    fn view_metrics_changed(&mut self, _id: ViewId) {
125        self.metrics_changed();
126    }
127
128    /// The framework hears which view went, then "metrics changed", as `_removeView` invokes
129    /// it.
130    fn view_removed(&mut self, id: ViewId) {
131        self.push(|app| {
132            let callback = app.platform_callbacks().on_view_removed.clone();
133            if let Some(callback) = callback {
134                callback(app, id);
135            }
136        });
137        self.metrics_changed();
138    }
139
140    /// "Metrics changed", as `_updateDisplays` invokes it.
141    fn screens_changed(&mut self) {
142        self.metrics_changed();
143    }
144
145    fn view_focus_changed(&mut self, event: ViewFocusEvent) {
146        self.push(|app| {
147            let callback = app.platform_callbacks().on_view_focus_change.clone();
148            if let Some(callback) = callback {
149                callback(app, event);
150            }
151        });
152    }
153
154    fn drop_data(&mut self, data: DropData) {
155        self.push(|app| {
156            let callback = app.platform_callbacks().on_drop.clone();
157            if let Some(callback) = callback {
158                callback(app, data);
159            }
160        });
161    }
162
163    fn clipboard_event(&mut self, view: ViewId, event: ClipboardEvent) {
164        self.push(|app| {
165            let callback = app.platform_callbacks().on_clipboard_event.clone();
166            if let Some(callback) = callback {
167                callback(app, view, event);
168            }
169        });
170    }
171
172    fn pointer_data_packet(&mut self, packet: PointerDataPacket) {
173        self.push(|app| GestureBinding::instance(app).handle_pointer_data_packet(app, packet));
174    }
175
176    fn key_data(&mut self, data: KeyData) -> bool {
177        self.push(|app| KeyEventManager::instance(app).handle_key_data(app, data))
178    }
179
180    fn pop_route(&mut self) -> bool {
181        self.push(|app| {
182            let callback = app.platform_callbacks().on_pop_route.clone();
183            callback.is_some_and(|callback| callback(app))
184        })
185    }
186
187    fn view_close_requested(&mut self, view: ViewId) {
188        self.push(|app| {
189            let callback = app.platform_callbacks().on_view_close_requested.clone();
190            if let Some(callback) = callback {
191                callback(app, view);
192            }
193        });
194    }
195
196    fn window_changed(&mut self, view: ViewId) {
197        self.push(|app| {
198            let callback = app.platform_callbacks().on_window_changed.clone();
199            if let Some(callback) = callback {
200                callback(app, view);
201            }
202        });
203    }
204
205    fn view_request_failed(&mut self, request: RequestId, error: ViewError) {
206        self.push(|app| {
207            let callback = app.platform_callbacks().on_view_request_failed.clone();
208            if let Some(callback) = callback {
209                callback(app, request, error);
210            }
211        });
212    }
213
214    /// The binding's answer goes to `reply` at the checkpoint of the call in which the
215    /// observers decide: this push's own when they answer at once.
216    fn request_app_exit(&mut self, reply: AppExitReply) {
217        self.push(|app| {
218            let answer = ServicesBinding::instance(app).handle_request_app_exit(app);
219            app.spawn(async move |_cx| reply.answer(answer.await));
220        });
221    }
222
223    /// On `resumed` or `inactive`, every view is marked to present and asked for a frame before
224    /// the bindings hear the state: a view that did not change while the app was away would
225    /// otherwise present nothing.
226    fn app_lifecycle_state_changed(&mut self, state: AppLifecycleState) {
227        self.push(|app| {
228            if matches!(
229                state,
230                AppLifecycleState::Resumed | AppLifecycleState::Inactive
231            ) {
232                RendererBinding::instance(app).handle_mark_all_views_need_render(app);
233                SchedulerBinding::schedule_forced_frame(app, FrameTarget::EveryView);
234            }
235            ServicesBinding::instance(app).handle_lifecycle_message(app, state);
236        });
237    }
238
239    fn platform_brightness_changed(&mut self) {
240        self.push(|app| {
241            let callback = app
242                .platform_callbacks()
243                .on_platform_brightness_changed
244                .clone();
245            if let Some(callback) = callback {
246                callback.call(app);
247            }
248        });
249    }
250
251    fn locales_changed(&mut self) {
252        self.push(|app| {
253            let callback = app.platform_callbacks().on_locale_changed.clone();
254            if let Some(callback) = callback {
255                callback.call(app);
256            }
257        });
258    }
259
260    fn system_fonts_changed(&mut self) {
261        self.push(|app| {
262            PaintingBinding::instance(app).handle_system_fonts_did_change(app);
263        });
264    }
265
266    fn clipboard_changed(&mut self) {
267        self.push(|app| ServicesBinding::instance(app).handle_clipboard_changed(app));
268    }
269
270    fn wake(&mut self, elapsed: Duration) {
271        self.advance_clock(elapsed);
272        // A wake with no time behind it is a post from a native callback.
273        self.app.wake();
274    }
275
276    fn text_input_editing_value(&mut self, _view: ViewId, value: TextEditingValue) {
277        self.push(|app| TextInput::instance(app).update_editing_value(app, value));
278    }
279
280    fn text_input_action(&mut self, _view: ViewId, action: TextInputAction) {
281        self.push(|app| TextInput::instance(app).perform_action(app, action));
282    }
283
284    fn text_input_closed(&mut self, _view: ViewId) {
285        self.push(|app| TextInput::instance(app).connection_closed(app));
286    }
287
288    fn system_context_menu_hidden(&mut self) {
289        self.push(inset_services::SystemContextMenuController::dispatch_system_hide);
290    }
291
292    fn custom_context_menu_action(&mut self, callback_id: &str) {
293        let callback_id = callback_id.to_string();
294        self.push(move |app| {
295            inset_services::SystemContextMenuController::dispatch_custom_action(app, &callback_id);
296        });
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use std::cell::Cell;
303    use std::rc::Rc;
304    use std::time::Duration;
305
306    use inset_embedder::{
307        AppLifecycleState, EmbedderClient, Frame, KeyData, KeyEventType, PointerChange,
308        PointerData, PointerDataPacket, PointerDeviceKind, ViewId,
309    };
310    use inset_foundation::App;
311    use inset_gestures::{GestureBinding, PointerRoute};
312    use inset_scheduler::{FrameTarget, SchedulerBinding};
313    use inset_services::{HardwareKeyboard, KeyEvent, LogicalKeyboardKey, PhysicalKeyboardKey};
314    use inset_test::{IMPLICIT_VIEW_ID, TestPlatform};
315
316    use super::Shell;
317
318    /// A platform with the one view a pointer packet, a focus event or a frame is addressed to.
319    fn platform_with_a_view() -> TestPlatform {
320        TestPlatform::new().with_views(&[IMPLICIT_VIEW_ID])
321    }
322
323    #[test]
324    fn drop_data_reaches_the_platform_callback() {
325        use std::cell::RefCell;
326        use std::path::PathBuf;
327
328        use inset_embedder::{DropChange, DropData};
329
330        let seen = Rc::new(RefCell::new(None));
331        let seen_slot = Rc::clone(&seen);
332        let platform = Rc::new(platform_with_a_view());
333        let mut shell = Shell::new(platform, move |app: &mut App| {
334            app.platform_callbacks_mut().on_drop = Some(Rc::new(move |_app, data| {
335                *seen_slot.borrow_mut() = Some(data);
336            }));
337        });
338        shell.drop_data(DropData {
339            view_id: IMPLICIT_VIEW_ID,
340            change: DropChange::Dropped,
341            physical_position: None,
342            paths: vec![PathBuf::from("/a.wasm")],
343        });
344        let seen = seen.borrow();
345        let seen = seen.as_ref().expect("the callback ran");
346        assert_eq!(seen.change, DropChange::Dropped);
347        assert_eq!(seen.paths, [PathBuf::from("/a.wasm")]);
348    }
349
350    #[test]
351    fn a_clipboard_event_reaches_the_platform_callback_with_its_view() {
352        use inset_embedder::{ClipboardEvent, CopyRequest};
353
354        let seen = Rc::new(Cell::new(None));
355        let seen_slot = Rc::clone(&seen);
356        let mut shell = Shell::new(Rc::new(platform_with_a_view()), move |app: &mut App| {
357            app.platform_callbacks_mut().on_clipboard_event =
358                Some(Rc::new(move |_app, view, event| {
359                    seen_slot.set(Some((view, matches!(event, ClipboardEvent::Cut(_)))));
360                }));
361        });
362        let request = CopyRequest::new(|_items, _options| Box::pin(std::future::ready(Ok(()))));
363        shell.clipboard_event(IMPLICIT_VIEW_ID, ClipboardEvent::Cut(request));
364        assert_eq!(seen.get(), Some((IMPLICIT_VIEW_ID, true)));
365    }
366
367    #[test]
368    fn a_change_of_the_clipboard_reaches_the_services_bindings_listenable() {
369        use inset_foundation::{Listenable, Listener};
370        use inset_services::ServicesBinding;
371
372        let heard = Rc::new(Cell::new(0));
373        let counter = Rc::clone(&heard);
374        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
375            let clipboard = ServicesBinding::instance(app).clipboard(app);
376            clipboard.add_listener(
377                app,
378                Listener::new(move |_app| counter.set(counter.get() + 1)),
379            );
380        });
381        shell.clipboard_changed();
382        assert_eq!(heard.get(), 1);
383    }
384
385    /// A reply to `shell.request_app_exit`, and the answer it was given so far.
386    fn recorded_reply() -> (
387        inset_embedder::AppExitReply,
388        Rc<Cell<Option<inset_embedder::AppExitResponse>>>,
389    ) {
390        let answer = Rc::new(Cell::new(None));
391        let given = Rc::clone(&answer);
392        let reply = inset_embedder::AppExitReply::new(move |response| given.set(Some(response)));
393        (reply, answer)
394    }
395
396    #[test]
397    fn a_view_added_or_removed_reaches_the_framework_with_its_id() {
398        let heard = Rc::new(std::cell::RefCell::new(Vec::new()));
399        let (added, removed) = (Rc::clone(&heard), Rc::clone(&heard));
400        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
401            let callbacks = app.platform_callbacks_mut();
402            callbacks.on_view_added = Some(Rc::new(move |_app, view| {
403                added.borrow_mut().push(("added", view))
404            }));
405            callbacks.on_view_removed = Some(Rc::new(move |_app, view| {
406                removed.borrow_mut().push(("removed", view))
407            }));
408        });
409        shell.view_added(ViewId(4));
410        shell.view_removed(ViewId(4));
411        assert_eq!(
412            *heard.borrow(),
413            [("added", ViewId(4)), ("removed", ViewId(4))]
414        );
415    }
416
417    #[test]
418    fn a_view_close_request_reaches_the_framework() {
419        let heard = Rc::new(Cell::new(None));
420        let heard_slot = Rc::clone(&heard);
421        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
422            app.platform_callbacks_mut().on_view_close_requested =
423                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
424        });
425        shell.view_close_requested(ViewId(3));
426        assert_eq!(heard.get(), Some(ViewId(3)));
427    }
428
429    #[test]
430    fn a_window_change_reaches_the_framework() {
431        let heard = Rc::new(Cell::new(None));
432        let heard_slot = Rc::clone(&heard);
433        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
434            app.platform_callbacks_mut().on_window_changed =
435                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
436        });
437        shell.window_changed(ViewId(5));
438        assert_eq!(heard.get(), Some(ViewId(5)));
439    }
440
441    #[test]
442    fn a_lifecycle_state_the_host_reports_reaches_the_scheduler_with_the_states_between() {
443        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
444        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
445        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
446        let mut app = shell.app();
447        assert_eq!(
448            SchedulerBinding::lifecycle_state(&mut app),
449            Some(AppLifecycleState::Hidden)
450        );
451        assert!(!SchedulerBinding::frames_enabled(&mut app));
452    }
453
454    #[test]
455    fn resuming_marks_every_view_to_be_rendered_and_asks_each_for_a_frame() {
456        use inset_rendering::RendererBinding;
457
458        let platform = Rc::new(TestPlatform::new().with_views(&[IMPLICIT_VIEW_ID, ViewId(1)]));
459        let first = platform.test_view(IMPLICIT_VIEW_ID);
460        let mut shell = Shell::new(platform.clone(), |_app| {});
461        let render_view = {
462            let mut app = shell.app();
463            let binding = RendererBinding::instance(&mut app);
464            binding.init_render_view(&mut app, first.clone())
465        };
466        let at = |ms| Frame {
467            elapsed: Duration::from_millis(ms),
468        };
469        shell.frame(&[IMPLICIT_VIEW_ID], at(16));
470        shell.frame(&[IMPLICIT_VIEW_ID], at(32));
471        assert_eq!(
472            first.presented(),
473            1,
474            "nothing changed after the first frame"
475        );
476
477        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
478        platform.take_frame_requests();
479        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
480        assert_eq!(
481            platform.take_frame_requests(),
482            [IMPLICIT_VIEW_ID, ViewId(1)]
483        );
484        assert!(render_view.needs_composite_frame(&shell.app()));
485        shell.frame(&[IMPLICIT_VIEW_ID], at(48));
486        assert_eq!(first.presented(), 2, "drawn again though nothing changed");
487    }
488
489    #[test]
490    fn request_app_exit_is_answered_by_the_services_binding() {
491        use inset_embedder::AppExitResponse;
492        use inset_foundation::{Handle, Task};
493        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
494
495        #[derive(Default)]
496        struct Refusing;
497
498        impl ServicesBindingOverridesObject for Refusing {
499            fn handle_request_app_exit(
500                self: Handle<Self>,
501                _app: &mut App,
502            ) -> Task<AppExitResponse> {
503                Task::ready(AppExitResponse::Cancel)
504            }
505        }
506
507        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
508        let (reply, answer) = recorded_reply();
509        shell.request_app_exit(reply);
510        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
511
512        let refusing = shell.app().create(Refusing);
513        let binding = ServicesBinding::instance(&mut shell.app());
514        binding.set_overrides(&mut shell.app(), Rc::new(refusing));
515        let (reply, answer) = recorded_reply();
516        shell.request_app_exit(reply);
517        assert_eq!(answer.get(), Some(AppExitResponse::Cancel));
518    }
519
520    /// The reply is answered inside the call that decides.
521    #[test]
522    fn request_app_exit_answers_during_the_call_in_which_the_framework_decides() {
523        use inset_embedder::AppExitResponse;
524        use inset_foundation::{Completer, Handle, Task};
525        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
526
527        struct Deciding {
528            decision: Completer<AppExitResponse>,
529        }
530
531        impl ServicesBindingOverridesObject for Deciding {
532            fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
533                let decision = app.get(self).decision.future();
534                app.spawn(async move |_cx| decision.await)
535            }
536        }
537
538        let decision = Completer::new();
539        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
540        let deciding = shell.app().create(Deciding {
541            decision: decision.clone(),
542        });
543        let binding = ServicesBinding::instance(&mut shell.app());
544        binding.set_overrides(&mut shell.app(), Rc::new(deciding));
545
546        let (reply, answer) = recorded_reply();
547        shell.request_app_exit(reply);
548        assert_eq!(answer.get(), None);
549
550        decision.complete(&mut shell.app(), AppExitResponse::Exit);
551        assert_eq!(answer.get(), None, "the task resumes at a checkpoint");
552        shell.wake(Duration::ZERO);
553        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
554    }
555
556    #[test]
557    fn new_runs_setup_and_frame_runs_both_scheduler_phases() {
558        let platform = Rc::new(platform_with_a_view());
559        let mut setup_ran = false;
560        let mut shell = Shell::new(platform.clone(), |_app: &mut App| {
561            setup_ran = true;
562        });
563        assert!(setup_ran);
564        assert!(platform.frame_requests().is_empty());
565
566        SchedulerBinding::schedule_frame(&mut shell.app(), FrameTarget::View(IMPLICIT_VIEW_ID));
567        assert_eq!(platform.frame_requests(), [IMPLICIT_VIEW_ID]);
568        shell.frame(
569            &[IMPLICIT_VIEW_ID],
570            Frame {
571                elapsed: Duration::from_millis(16),
572            },
573        );
574    }
575
576    #[test]
577    fn pointer_data_packet_reaches_gesture_binding() {
578        let ran = Rc::new(Cell::new(false));
579        let ran_flag = Rc::clone(&ran);
580        let platform = Rc::new(platform_with_a_view());
581        let mut shell = Shell::new(platform, |app| {
582            let binding = GestureBinding::instance(app);
583            binding.pointer_router(app).add_route(
584                app,
585                1,
586                PointerRoute::new(move |_app, _event| {
587                    ran_flag.set(true);
588                }),
589                None,
590            );
591        });
592
593        shell.pointer_data_packet(PointerDataPacket::new(vec![PointerData {
594            change: PointerChange::Down,
595            kind: PointerDeviceKind::Mouse,
596            buttons: 1,
597            pointer_identifier: 1,
598            ..PointerData::default()
599        }]));
600        assert!(ran.get());
601    }
602
603    #[test]
604    fn key_data_reaches_the_hardware_keyboard() {
605        let seen = Rc::new(Cell::new(0));
606        let seen_flag = Rc::clone(&seen);
607        let platform = Rc::new(TestPlatform::new());
608        let mut shell = Shell::new(platform, |app| {
609            HardwareKeyboard::instance(app).add_handler(
610                app,
611                Rc::new(move |_app: &mut App, _event: &KeyEvent| {
612                    seen_flag.set(seen_flag.get() + 1);
613                    true
614                }),
615            );
616        });
617
618        let key_a = KeyData {
619            event_type: KeyEventType::Down,
620            physical: PhysicalKeyboardKey::KEY_A.usb_hid_usage,
621            logical: LogicalKeyboardKey::KEY_A.key_id,
622            character: Some("a".to_owned()),
623            ..KeyData::default()
624        };
625        assert!(shell.key_data(key_a), "the handler claimed the event");
626        assert_eq!(seen.get(), 1);
627
628        let keyboard = HardwareKeyboard::instance(&mut shell.app());
629        assert!(keyboard.is_logical_key_pressed(&shell.app(), LogicalKeyboardKey::KEY_A));
630    }
631
632    #[test]
633    fn wake_and_frame_advance_the_app_clock_and_fire_due_timers() {
634        let platform = Rc::new(TestPlatform::new());
635        let fired = Rc::new(Cell::new(Vec::new()));
636        let mut shell = Shell::new(platform, |app| {
637            for (name, delay) in [("early", 10), ("late", 30)] {
638                let fired = Rc::clone(&fired);
639                inset_foundation::Timer::new(
640                    app,
641                    Duration::from_millis(delay),
642                    inset_foundation::Listener::new(move |_app| {
643                        let mut log = fired.take();
644                        log.push(name);
645                        fired.set(log);
646                    }),
647                );
648            }
649        });
650        assert!(fired.take().is_empty());
651
652        shell.wake(Duration::from_millis(15));
653        assert_eq!(fired.take(), vec!["early"]);
654
655        shell.frame(
656            &[IMPLICIT_VIEW_ID],
657            Frame {
658                elapsed: Duration::from_millis(32),
659            },
660        );
661        assert_eq!(fired.take(), vec!["late"]);
662    }
663
664    #[test]
665    fn a_view_coming_or_going_and_a_change_to_the_screens_are_metrics_changes() {
666        use inset_foundation::Handle;
667        use inset_rendering::{RendererBinding, RendererBindingOverridesObject};
668
669        struct CountingMetricsChanges {
670            count: Rc<Cell<usize>>,
671        }
672
673        impl RendererBindingOverridesObject for CountingMetricsChanges {
674            fn will_draw_frame(self: Handle<Self>, _app: &mut App) {}
675
676            fn did_draw_frame(self: Handle<Self>, _app: &mut App) {}
677
678            fn did_handle_metrics_changed(self: Handle<Self>, app: &mut App) {
679                let count = &app.get(self).count;
680                count.set(count.get() + 1);
681            }
682        }
683
684        let count = Rc::new(Cell::new(0));
685        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app| {});
686        let counting = shell.app().create(CountingMetricsChanges {
687            count: Rc::clone(&count),
688        });
689        let binding = RendererBinding::instance(&mut shell.app());
690        binding.set_overrides(&mut shell.app(), Rc::new(counting));
691
692        shell.view_added(ViewId(1));
693        assert_eq!(count.get(), 1, "a view added");
694        shell.view_removed(ViewId(1));
695        assert_eq!(count.get(), 2, "a view removed");
696        shell.screens_changed();
697        assert_eq!(count.get(), 3, "the screens changed");
698    }
699
700    #[test]
701    fn view_focus_is_delivered_through_the_dispatcher_callback() {
702        use inset_embedder::{ViewFocusDirection, ViewFocusEvent, ViewFocusState};
703        let received = Rc::new(Cell::new(None));
704        let sink = received.clone();
705        let platform = Rc::new(platform_with_a_view());
706        let mut shell = Shell::new(platform, move |app| {
707            app.platform_callbacks_mut().on_view_focus_change = Some(Rc::new(move |app, event| {
708                let sink = sink.clone();
709                app.schedule_microtask(inset_foundation::Listener::new(move |_| {
710                    sink.set(Some(event))
711                }));
712            }));
713        });
714        shell.view_focus_changed(ViewFocusEvent {
715            view_id: IMPLICIT_VIEW_ID,
716            state: ViewFocusState::Focused,
717            direction: ViewFocusDirection::Backward,
718        });
719        let event = received
720            .get()
721            .expect("the shell checkpoints the callback's microtask");
722        assert_eq!(event.view_id, IMPLICIT_VIEW_ID);
723        assert_eq!(event.state, ViewFocusState::Focused);
724        assert_eq!(event.direction, ViewFocusDirection::Backward);
725    }
726}