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, EditCommand, EmbedderClient, Frame,
15    Instant, KeyData, MenuId, NotificationActivation, OpenRequest, PlatformRef, PointerData,
16    PointerDataPacket, PointerDataPacketConverter, PointerReport, RequestId, TextEditingValue,
17    TextInputAction, TrayIconEvent, TrayIconId, ViewError, ViewFocusEvent, ViewId,
18};
19use inset_foundation::{App, AppCell};
20use inset_gestures::GestureBinding;
21use inset_painting::PaintingBinding;
22use inset_rendering::RendererBinding;
23use inset_scheduler::{FrameTarget, SchedulerBinding};
24use inset_services::{KeyEventManager, ServicesBinding, TextInput};
25
26/// Host-facing isolate: the [`AppCell`] plus the methods the embedder pushes.
27///
28/// Every push is one call into the app: the `App` is borrowed for the binding call and
29/// released before the cell's checkpoint, where the microtasks and futures the event queued
30/// run.
31pub struct Shell {
32    app: Rc<AppCell>,
33    platform: PlatformRef,
34    /// The platform time the app clock was last advanced to.
35    clock: Duration,
36    /// 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.
37    origin: Option<Instant>,
38    /// What the framework was last told of each pointing device, as `RuntimeController` keeps it.
39    pointer_data_packet_converter: PointerDataPacketConverter,
40}
41
42impl Shell {
43    /// Builds the heap from `platform`, then runs application setup.
44    ///
45    /// Setup (later widgets `run_app`) requests the first frame when it
46    /// installs work that needs one.
47    pub fn new(platform: PlatformRef, setup: impl FnOnce(&mut App)) -> Shell {
48        let shell = Shell {
49            app: AppCell::with_platform(platform.clone()),
50            platform,
51            clock: Duration::ZERO,
52            origin: None,
53            pointer_data_packet_converter: PointerDataPacketConverter::new(),
54        };
55        shell.handle_event(|app| {
56            // Flutter's engine collects the platform's fonts before the framework runs.
57            PaintingBinding::instance(app).install_platform_fonts(app);
58            setup(app);
59        });
60        shell
61    }
62
63    /// The `App`, borrowed until the guard drops. A host that needs the whole thing, a borrow
64    /// and then the checkpoint, takes [`cell`](Self::cell).
65    pub fn app(&self) -> RefMut<'_, App> {
66        self.app.borrow_mut()
67    }
68
69    pub fn cell(&self) -> &Rc<AppCell> {
70        &self.app
71    }
72
73    /// One platform event: `f` on the borrowed `App`, then the checkpoint.
74    fn handle_event<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
75        let result = f(&mut self.app.borrow_mut());
76        self.app.checkpoint();
77        result
78    }
79
80    /// An input or platform push: the app clock catches up with the platform's first, as Dart's
81    /// timers measure real time from the moment they are created, then `f` runs.
82    fn push<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
83        if let Some(origin) = self.origin {
84            let elapsed = self.platform.now().saturating_duration_since(origin);
85            self.advance_clock(elapsed);
86        }
87        self.handle_event(f)
88    }
89
90    /// Moves the app clock up to the platform's `elapsed`, firing the timers that came due.
91    fn advance_clock(&mut self, elapsed: Duration) {
92        if elapsed <= self.clock {
93            return;
94        }
95        self.app.elapse(elapsed - self.clock);
96        self.clock = elapsed;
97    }
98
99    /// dart:ui's `onMetricsChanged`, which the framework sets to
100    /// `RendererBinding.handleMetricsChanged`. `_addView`, `_removeView` and `_updateDisplays`
101    /// invoke it as `_updateWindowMetrics` does: a view coming or going, and a change to the
102    /// screens, reach the app only this way.
103    fn metrics_changed(&mut self) {
104        self.push(|app| RendererBinding::instance(app).handle_metrics_changed(app));
105    }
106}
107
108impl EmbedderClient for Shell {
109    fn frame(&mut self, views: &[ViewId], frame: Frame) {
110        self.origin = Some(self.platform.now() - frame.elapsed);
111        self.advance_clock(frame.elapsed);
112        // The engine runs `_beginFrame` and `_drawFrame` as two native tasks: two calls.
113        self.handle_event(|app| SchedulerBinding::on_begin_frame(app, views, Some(frame.elapsed)));
114        self.handle_event(|app| SchedulerBinding::on_draw_frame(app, views));
115    }
116
117    /// The framework hears which view came, then "metrics changed", as `_addView` invokes it.
118    fn view_added(&mut self, id: ViewId) {
119        self.push(|app| {
120            let callback = app.platform_callbacks().on_view_added.clone();
121            if let Some(callback) = callback {
122                callback(app, id);
123            }
124        });
125        self.metrics_changed();
126    }
127
128    fn view_metrics_changed(&mut self, _id: ViewId) {
129        self.metrics_changed();
130    }
131
132    /// The framework hears which view went, then "metrics changed", as `_removeView` invokes
133    /// it.
134    fn view_removed(&mut self, id: ViewId) {
135        self.push(|app| {
136            let callback = app.platform_callbacks().on_view_removed.clone();
137            if let Some(callback) = callback {
138                callback(app, id);
139            }
140        });
141        self.metrics_changed();
142    }
143
144    /// "Metrics changed", as `_updateDisplays` invokes it.
145    fn screens_changed(&mut self) {
146        self.metrics_changed();
147    }
148
149    fn view_focus_changed(&mut self, event: ViewFocusEvent) {
150        self.push(|app| {
151            let callback = app.platform_callbacks().on_view_focus_change.clone();
152            if let Some(callback) = callback {
153                callback(app, event);
154            }
155        });
156    }
157
158    fn drop_data(&mut self, data: DropData) {
159        self.push(|app| {
160            let callback = app.platform_callbacks().on_drop.clone();
161            if let Some(callback) = callback {
162                callback(app, data);
163            }
164        });
165    }
166
167    fn clipboard_event(&mut self, view: ViewId, event: ClipboardEvent) {
168        self.push(|app| {
169            let callback = app.platform_callbacks().on_clipboard_event.clone();
170            if let Some(callback) = callback {
171                callback(app, view, event);
172            }
173        });
174    }
175
176    fn edit_command(&mut self, view: ViewId, command: EditCommand) {
177        self.push(|app| {
178            let callback = app.platform_callbacks().on_edit_command.clone();
179            if let Some(callback) = callback {
180                callback(app, view, command);
181            }
182        });
183    }
184
185    fn menu_selected(&mut self, id: MenuId) {
186        self.push(|app| {
187            let callback = app.platform_callbacks().on_menu_selected.clone();
188            if let Some(callback) = callback {
189                callback(app, id);
190            }
191        });
192    }
193
194    fn dock_menu_selected(&mut self, id: MenuId) {
195        self.push(|app| {
196            let callback = app.platform_callbacks().on_dock_menu_selected.clone();
197            if let Some(callback) = callback {
198                callback(app, id);
199            }
200        });
201    }
202
203    fn tray_icon_event(&mut self, icon: TrayIconId, event: TrayIconEvent) {
204        self.push(|app| {
205            let callback = app.platform_callbacks().on_tray_icon_event.clone();
206            if let Some(callback) = callback {
207                callback(app, icon, event);
208            }
209        });
210    }
211
212    fn open_requested(&mut self, request: OpenRequest) {
213        self.push(|app| app.open_requested(request));
214    }
215
216    fn notification_activated(&mut self, activation: NotificationActivation) {
217        self.push(|app| app.notification_activated(activation));
218    }
219
220    fn pointer_reports(&mut self, reports: Vec<PointerReport>) {
221        let packet = PointerDataPacket::new(reports.into_iter().map(PointerData::from).collect());
222        let platform = &self.platform;
223        let packet = self
224            .pointer_data_packet_converter
225            .convert(&packet, |view_id| platform.view(view_id).is_some());
226        self.push(|app| GestureBinding::instance(app).handle_pointer_data_packet(app, packet));
227    }
228
229    fn key_data(&mut self, data: KeyData) -> bool {
230        self.push(|app| KeyEventManager::instance(app).handle_key_data(app, data))
231    }
232
233    fn pop_route(&mut self) -> bool {
234        self.push(|app| {
235            let callback = app.platform_callbacks().on_pop_route.clone();
236            callback.is_some_and(|callback| callback(app))
237        })
238    }
239
240    fn view_close_requested(&mut self, view: ViewId) {
241        self.push(|app| {
242            let callback = app.platform_callbacks().on_view_close_requested.clone();
243            if let Some(callback) = callback {
244                callback(app, view);
245            }
246        });
247    }
248
249    fn window_changed(&mut self, view: ViewId) {
250        self.push(|app| {
251            let callback = app.platform_callbacks().on_window_changed.clone();
252            if let Some(callback) = callback {
253                callback(app, view);
254            }
255        });
256    }
257
258    fn view_request_failed(&mut self, request: RequestId, error: ViewError) {
259        self.push(|app| {
260            let callback = app.platform_callbacks().on_view_request_failed.clone();
261            if let Some(callback) = callback {
262                callback(app, request, error);
263            }
264        });
265    }
266
267    /// The binding's answer goes to `reply` at the checkpoint of the call in which the
268    /// observers decide: this push's own when they answer at once.
269    fn request_app_exit(&mut self, reply: AppExitReply) {
270        self.push(|app| {
271            let answer = ServicesBinding::instance(app).handle_request_app_exit(app);
272            app.spawn(async move |_cx| reply.answer(answer.await));
273        });
274    }
275
276    /// On `resumed` or `inactive`, every view is marked to present and asked for a frame before
277    /// the bindings hear the state: a view that did not change while the app was away would
278    /// otherwise present nothing.
279    fn app_lifecycle_state_changed(&mut self, state: AppLifecycleState) {
280        self.push(|app| {
281            if matches!(
282                state,
283                AppLifecycleState::Resumed | AppLifecycleState::Inactive
284            ) {
285                RendererBinding::instance(app).handle_mark_all_views_need_render(app);
286                SchedulerBinding::schedule_forced_frame(app, FrameTarget::EveryView);
287            }
288            ServicesBinding::instance(app).handle_lifecycle_message(app, state);
289        });
290    }
291
292    fn platform_brightness_changed(&mut self) {
293        self.push(|app| {
294            let callback = app
295                .platform_callbacks()
296                .on_platform_brightness_changed
297                .clone();
298            if let Some(callback) = callback {
299                callback.call(app);
300            }
301        });
302    }
303
304    fn locales_changed(&mut self) {
305        self.push(|app| {
306            let callback = app.platform_callbacks().on_locale_changed.clone();
307            if let Some(callback) = callback {
308                callback.call(app);
309            }
310        });
311    }
312
313    fn system_fonts_changed(&mut self) {
314        self.push(|app| {
315            PaintingBinding::instance(app).handle_system_fonts_did_change(app);
316        });
317    }
318
319    fn clipboard_changed(&mut self) {
320        self.push(|app| ServicesBinding::instance(app).handle_clipboard_changed(app));
321    }
322
323    fn wake(&mut self, elapsed: Duration) {
324        self.advance_clock(elapsed);
325        // A wake with no time behind it is a post from a native callback.
326        self.app.wake();
327    }
328
329    fn text_input_editing_value(&mut self, _view: ViewId, value: TextEditingValue) {
330        self.push(|app| TextInput::instance(app).update_editing_value(app, value));
331    }
332
333    fn text_input_action(&mut self, _view: ViewId, action: TextInputAction) {
334        self.push(|app| TextInput::instance(app).perform_action(app, action));
335    }
336
337    fn text_input_closed(&mut self, _view: ViewId) {
338        self.push(|app| TextInput::instance(app).connection_closed(app));
339    }
340
341    fn system_context_menu_hidden(&mut self) {
342        self.push(inset_services::SystemContextMenuController::dispatch_system_hide);
343    }
344
345    fn custom_context_menu_action(&mut self, callback_id: &str) {
346        let callback_id = callback_id.to_string();
347        self.push(move |app| {
348            inset_services::SystemContextMenuController::dispatch_custom_action(app, &callback_id);
349        });
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use std::cell::Cell;
356    use std::rc::Rc;
357    use std::time::Duration;
358
359    use inset_embedder::{
360        AppLifecycleState, Contact, EmbedderClient, Frame, KeyData, KeyEventType,
361        PointerDeviceKind, PointerReport, ReportedChange, ViewId,
362    };
363    use inset_foundation::App;
364    use inset_gestures::{GestureBinding, PointerRoute};
365    use inset_scheduler::{FrameTarget, SchedulerBinding};
366    use inset_services::{HardwareKeyboard, KeyEvent, LogicalKeyboardKey, PhysicalKeyboardKey};
367    use inset_test::{IMPLICIT_VIEW_ID, TestPlatform};
368
369    use super::Shell;
370
371    /// A platform with the one view a pointer packet, a focus event or a frame is addressed to.
372    fn platform_with_a_view() -> TestPlatform {
373        TestPlatform::new().with_views(&[IMPLICIT_VIEW_ID])
374    }
375
376    #[test]
377    fn drop_data_reaches_the_platform_callback() {
378        use std::cell::RefCell;
379        use std::path::PathBuf;
380
381        use inset_embedder::{DropChange, DropData};
382
383        let seen = Rc::new(RefCell::new(None));
384        let seen_slot = Rc::clone(&seen);
385        let platform = Rc::new(platform_with_a_view());
386        let mut shell = Shell::new(platform, move |app: &mut App| {
387            app.platform_callbacks_mut().on_drop = Some(Rc::new(move |_app, data| {
388                *seen_slot.borrow_mut() = Some(data);
389            }));
390        });
391        shell.drop_data(DropData {
392            view_id: IMPLICIT_VIEW_ID,
393            change: DropChange::Dropped,
394            physical_position: None,
395            paths: vec![PathBuf::from("/a.wasm")],
396        });
397        let seen = seen.borrow();
398        let seen = seen.as_ref().expect("the callback ran");
399        assert_eq!(seen.change, DropChange::Dropped);
400        assert_eq!(seen.paths, [PathBuf::from("/a.wasm")]);
401    }
402
403    #[test]
404    fn a_clipboard_event_reaches_the_platform_callback_with_its_view() {
405        use inset_embedder::{ClipboardEvent, CopyRequest};
406
407        let seen = Rc::new(Cell::new(None));
408        let seen_slot = Rc::clone(&seen);
409        let mut shell = Shell::new(Rc::new(platform_with_a_view()), move |app: &mut App| {
410            app.platform_callbacks_mut().on_clipboard_event =
411                Some(Rc::new(move |_app, view, event| {
412                    seen_slot.set(Some((view, matches!(event, ClipboardEvent::Cut(_)))));
413                }));
414        });
415        let request = CopyRequest::new(|_items, _options| Box::pin(std::future::ready(Ok(()))));
416        shell.clipboard_event(IMPLICIT_VIEW_ID, ClipboardEvent::Cut(request));
417        assert_eq!(seen.get(), Some((IMPLICIT_VIEW_ID, true)));
418    }
419
420    #[test]
421    fn an_open_request_waits_for_the_apps_handler() {
422        use std::cell::RefCell;
423
424        use inset_embedder::OpenRequest;
425
426        let seen = Rc::new(RefCell::new(Vec::new()));
427        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app: &mut App| {});
428        shell.open_requested(OpenRequest {
429            files: Vec::new(),
430            links: vec!["https://example.org/launch".to_owned()],
431        });
432        let slot = Rc::clone(&seen);
433        shell
434            .app
435            .borrow_mut()
436            .set_open_requested_handler(Some(Rc::new(move |_app, request: OpenRequest| {
437                slot.borrow_mut().extend(request.links)
438            })));
439        shell.open_requested(OpenRequest {
440            files: Vec::new(),
441            links: vec!["https://example.org/later".to_owned()],
442        });
443        assert_eq!(
444            *seen.borrow(),
445            ["https://example.org/launch", "https://example.org/later"],
446            "the launch's request waited for the handler"
447        );
448    }
449
450    #[test]
451    fn a_notification_click_waits_for_the_apps_handler() {
452        use std::cell::RefCell;
453
454        use inset_embedder::NotificationActivation;
455
456        let seen = Rc::new(RefCell::new(Vec::new()));
457        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app: &mut App| {});
458        let click = |action: Option<&str>| NotificationActivation {
459            id: "message-7".to_owned(),
460            action: action.map(str::to_owned),
461            reply: None,
462        };
463        shell.notification_activated(click(None));
464        let slot = Rc::clone(&seen);
465        shell
466            .app
467            .borrow_mut()
468            .set_notification_activated_handler(Some(Rc::new(move |_app, activation| {
469                slot.borrow_mut().push(activation)
470            })));
471        shell.notification_activated(click(Some("later")));
472        assert_eq!(
473            *seen.borrow(),
474            [click(None), click(Some("later"))],
475            "the click that started the app waited for the handler"
476        );
477    }
478
479    #[test]
480    fn menu_choices_edit_commands_and_tray_icon_events_reach_their_callbacks() {
481        use std::cell::RefCell;
482
483        use inset_embedder::{EditCommand, Offset, TrayIconEvent};
484
485        let seen = Rc::new(RefCell::new(Vec::new()));
486        let slot = Rc::clone(&seen);
487        let mut shell = Shell::new(Rc::new(platform_with_a_view()), move |app: &mut App| {
488            let callbacks = app.platform_callbacks_mut();
489            let menu = Rc::clone(&slot);
490            callbacks.on_menu_selected = Some(Rc::new(move |_app, id| {
491                menu.borrow_mut().push(format!("menu {id}"));
492            }));
493            let dock = Rc::clone(&slot);
494            callbacks.on_dock_menu_selected = Some(Rc::new(move |_app, id| {
495                dock.borrow_mut().push(format!("dock {id}"));
496            }));
497            let edit = Rc::clone(&slot);
498            callbacks.on_edit_command = Some(Rc::new(move |_app, view, command| {
499                edit.borrow_mut().push(format!("{view:?} {command:?}"));
500            }));
501            let tray = Rc::clone(&slot);
502            callbacks.on_tray_icon_event = Some(Rc::new(move |_app, icon, event| {
503                tray.borrow_mut().push(format!("tray {icon} {event:?}"));
504            }));
505        });
506        shell.menu_selected(3);
507        shell.dock_menu_selected(4);
508        shell.edit_command(IMPLICIT_VIEW_ID, EditCommand::Undo);
509        shell.tray_icon_event(
510            7,
511            TrayIconEvent::Activated {
512                position: Offset::new(1.0, 2.0),
513            },
514        );
515        assert_eq!(
516            *seen.borrow(),
517            [
518                "menu 3".to_owned(),
519                "dock 4".to_owned(),
520                format!("{IMPLICIT_VIEW_ID:?} Undo"),
521                format!(
522                    "tray 7 {:?}",
523                    TrayIconEvent::Activated {
524                        position: Offset::new(1.0, 2.0)
525                    }
526                ),
527            ]
528        );
529    }
530
531    #[test]
532    fn a_change_of_the_clipboard_reaches_the_services_bindings_listenable() {
533        use inset_foundation::{Listenable, Listener};
534        use inset_services::ServicesBinding;
535
536        let heard = Rc::new(Cell::new(0));
537        let counter = Rc::clone(&heard);
538        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
539            let clipboard = ServicesBinding::instance(app).clipboard(app);
540            clipboard.add_listener(
541                app,
542                Listener::new(move |_app| counter.set(counter.get() + 1)),
543            );
544        });
545        shell.clipboard_changed();
546        assert_eq!(heard.get(), 1);
547    }
548
549    /// A reply to `shell.request_app_exit`, and the answer it was given so far.
550    fn recorded_reply() -> (
551        inset_embedder::AppExitReply,
552        Rc<Cell<Option<inset_embedder::AppExitResponse>>>,
553    ) {
554        let answer = Rc::new(Cell::new(None));
555        let given = Rc::clone(&answer);
556        let reply = inset_embedder::AppExitReply::new(move |response| given.set(Some(response)));
557        (reply, answer)
558    }
559
560    #[test]
561    fn a_view_added_or_removed_reaches_the_framework_with_its_id() {
562        let heard = Rc::new(std::cell::RefCell::new(Vec::new()));
563        let (added, removed) = (Rc::clone(&heard), Rc::clone(&heard));
564        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
565            let callbacks = app.platform_callbacks_mut();
566            callbacks.on_view_added = Some(Rc::new(move |_app, view| {
567                added.borrow_mut().push(("added", view))
568            }));
569            callbacks.on_view_removed = Some(Rc::new(move |_app, view| {
570                removed.borrow_mut().push(("removed", view))
571            }));
572        });
573        shell.view_added(ViewId(4));
574        shell.view_removed(ViewId(4));
575        assert_eq!(
576            *heard.borrow(),
577            [("added", ViewId(4)), ("removed", ViewId(4))]
578        );
579    }
580
581    #[test]
582    fn a_view_close_request_reaches_the_framework() {
583        let heard = Rc::new(Cell::new(None));
584        let heard_slot = Rc::clone(&heard);
585        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
586            app.platform_callbacks_mut().on_view_close_requested =
587                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
588        });
589        shell.view_close_requested(ViewId(3));
590        assert_eq!(heard.get(), Some(ViewId(3)));
591    }
592
593    #[test]
594    fn a_window_change_reaches_the_framework() {
595        let heard = Rc::new(Cell::new(None));
596        let heard_slot = Rc::clone(&heard);
597        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
598            app.platform_callbacks_mut().on_window_changed =
599                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
600        });
601        shell.window_changed(ViewId(5));
602        assert_eq!(heard.get(), Some(ViewId(5)));
603    }
604
605    #[test]
606    fn a_lifecycle_state_the_host_reports_reaches_the_scheduler_with_the_states_between() {
607        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
608        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
609        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
610        let mut app = shell.app();
611        assert_eq!(
612            SchedulerBinding::lifecycle_state(&mut app),
613            Some(AppLifecycleState::Hidden)
614        );
615        assert!(!SchedulerBinding::frames_enabled(&mut app));
616    }
617
618    #[test]
619    fn resuming_marks_every_view_to_be_rendered_and_asks_each_for_a_frame() {
620        use inset_rendering::RendererBinding;
621
622        let platform = Rc::new(TestPlatform::new().with_views(&[IMPLICIT_VIEW_ID, ViewId(1)]));
623        let first = platform.test_view(IMPLICIT_VIEW_ID);
624        let mut shell = Shell::new(platform.clone(), |_app| {});
625        let render_view = {
626            let mut app = shell.app();
627            let binding = RendererBinding::instance(&mut app);
628            binding.init_render_view(&mut app, first.clone())
629        };
630        let at = |ms| Frame {
631            elapsed: Duration::from_millis(ms),
632        };
633        shell.frame(&[IMPLICIT_VIEW_ID], at(16));
634        shell.frame(&[IMPLICIT_VIEW_ID], at(32));
635        assert_eq!(
636            first.presented(),
637            1,
638            "nothing changed after the first frame"
639        );
640
641        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
642        platform.take_frame_requests();
643        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
644        assert_eq!(
645            platform.take_frame_requests(),
646            [IMPLICIT_VIEW_ID, ViewId(1)]
647        );
648        assert!(render_view.needs_composite_frame(&shell.app()));
649        shell.frame(&[IMPLICIT_VIEW_ID], at(48));
650        assert_eq!(first.presented(), 2, "drawn again though nothing changed");
651    }
652
653    #[test]
654    fn request_app_exit_is_answered_by_the_services_binding() {
655        use inset_embedder::AppExitResponse;
656        use inset_foundation::{Handle, Task};
657        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
658
659        #[derive(Default)]
660        struct Refusing;
661
662        impl ServicesBindingOverridesObject for Refusing {
663            fn handle_request_app_exit(
664                self: Handle<Self>,
665                _app: &mut App,
666            ) -> Task<AppExitResponse> {
667                Task::ready(AppExitResponse::Cancel)
668            }
669        }
670
671        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
672        let (reply, answer) = recorded_reply();
673        shell.request_app_exit(reply);
674        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
675
676        let refusing = shell.app().create(Refusing);
677        let binding = ServicesBinding::instance(&mut shell.app());
678        binding.set_overrides(&mut shell.app(), Rc::new(refusing));
679        let (reply, answer) = recorded_reply();
680        shell.request_app_exit(reply);
681        assert_eq!(answer.get(), Some(AppExitResponse::Cancel));
682    }
683
684    /// The reply is answered inside the call that decides.
685    #[test]
686    fn request_app_exit_answers_during_the_call_in_which_the_framework_decides() {
687        use inset_embedder::AppExitResponse;
688        use inset_foundation::{Completer, Handle, Task};
689        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
690
691        struct Deciding {
692            decision: Completer<AppExitResponse>,
693        }
694
695        impl ServicesBindingOverridesObject for Deciding {
696            fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
697                let decision = app.get(self).decision.future();
698                app.spawn(async move |_cx| decision.await)
699            }
700        }
701
702        let decision = Completer::new();
703        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
704        let deciding = shell.app().create(Deciding {
705            decision: decision.clone(),
706        });
707        let binding = ServicesBinding::instance(&mut shell.app());
708        binding.set_overrides(&mut shell.app(), Rc::new(deciding));
709
710        let (reply, answer) = recorded_reply();
711        shell.request_app_exit(reply);
712        assert_eq!(answer.get(), None);
713
714        decision.complete(&mut shell.app(), AppExitResponse::Exit);
715        assert_eq!(answer.get(), None, "the task resumes at a checkpoint");
716        shell.wake(Duration::ZERO);
717        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
718    }
719
720    #[test]
721    fn new_runs_setup_and_frame_runs_both_scheduler_phases() {
722        let platform = Rc::new(platform_with_a_view());
723        let mut setup_ran = false;
724        let mut shell = Shell::new(platform.clone(), |_app: &mut App| {
725            setup_ran = true;
726        });
727        assert!(setup_ran);
728        assert!(platform.frame_requests().is_empty());
729
730        SchedulerBinding::schedule_frame(&mut shell.app(), FrameTarget::View(IMPLICIT_VIEW_ID));
731        assert_eq!(platform.frame_requests(), [IMPLICIT_VIEW_ID]);
732        shell.frame(
733            &[IMPLICIT_VIEW_ID],
734            Frame {
735                elapsed: Duration::from_millis(16),
736            },
737        );
738    }
739
740    #[test]
741    fn a_reported_press_reaches_gesture_binding_numbered_by_the_converter() {
742        let ran = Rc::new(Cell::new(false));
743        let ran_flag = Rc::clone(&ran);
744        let platform = Rc::new(platform_with_a_view());
745        let mut shell = Shell::new(platform, |app| {
746            let binding = GestureBinding::instance(app);
747            binding.pointer_router(app).add_route(
748                app,
749                1,
750                PointerRoute::new(move |_app, _event| {
751                    ran_flag.set(true);
752                }),
753                None,
754            );
755        });
756
757        shell.pointer_reports(vec![PointerReport {
758            view_id: IMPLICIT_VIEW_ID,
759            time_stamp: Duration::ZERO,
760            device: 0,
761            kind: PointerDeviceKind::Mouse,
762            position: [0.0, 0.0],
763            change: ReportedChange::Down { buttons: 1 },
764            contact: Contact::default(),
765        }]);
766        assert!(ran.get(), "the first press is pointer 1");
767    }
768
769    #[test]
770    fn key_data_reaches_the_hardware_keyboard() {
771        let seen = Rc::new(Cell::new(0));
772        let seen_flag = Rc::clone(&seen);
773        let platform = Rc::new(TestPlatform::new());
774        let mut shell = Shell::new(platform, |app| {
775            HardwareKeyboard::instance(app).add_handler(
776                app,
777                Rc::new(move |_app: &mut App, _event: &KeyEvent| {
778                    seen_flag.set(seen_flag.get() + 1);
779                    true
780                }),
781            );
782        });
783
784        let key_a = KeyData {
785            event_type: KeyEventType::Down,
786            physical: PhysicalKeyboardKey::KEY_A.usb_hid_usage,
787            logical: LogicalKeyboardKey::KEY_A.key_id,
788            character: Some("a".to_owned()),
789            ..KeyData::default()
790        };
791        assert!(shell.key_data(key_a), "the handler claimed the event");
792        assert_eq!(seen.get(), 1);
793
794        let keyboard = HardwareKeyboard::instance(&mut shell.app());
795        assert!(keyboard.is_logical_key_pressed(&shell.app(), LogicalKeyboardKey::KEY_A));
796    }
797
798    #[test]
799    fn wake_and_frame_advance_the_app_clock_and_fire_due_timers() {
800        let platform = Rc::new(TestPlatform::new());
801        let fired = Rc::new(Cell::new(Vec::new()));
802        let mut shell = Shell::new(platform, |app| {
803            for (name, delay) in [("early", 10), ("late", 30)] {
804                let fired = Rc::clone(&fired);
805                inset_foundation::Timer::new(
806                    app,
807                    Duration::from_millis(delay),
808                    inset_foundation::Listener::new(move |_app| {
809                        let mut log = fired.take();
810                        log.push(name);
811                        fired.set(log);
812                    }),
813                );
814            }
815        });
816        assert!(fired.take().is_empty());
817
818        shell.wake(Duration::from_millis(15));
819        assert_eq!(fired.take(), vec!["early"]);
820
821        shell.frame(
822            &[IMPLICIT_VIEW_ID],
823            Frame {
824                elapsed: Duration::from_millis(32),
825            },
826        );
827        assert_eq!(fired.take(), vec!["late"]);
828    }
829
830    #[test]
831    fn a_view_coming_or_going_and_a_change_to_the_screens_are_metrics_changes() {
832        use inset_foundation::Handle;
833        use inset_rendering::{RendererBinding, RendererBindingOverridesObject};
834
835        struct CountingMetricsChanges {
836            count: Rc<Cell<usize>>,
837        }
838
839        impl RendererBindingOverridesObject for CountingMetricsChanges {
840            fn will_draw_frame(self: Handle<Self>, _app: &mut App) {}
841
842            fn did_draw_frame(self: Handle<Self>, _app: &mut App) {}
843
844            fn did_handle_metrics_changed(self: Handle<Self>, app: &mut App) {
845                let count = &app.get(self).count;
846                count.set(count.get() + 1);
847            }
848        }
849
850        let count = Rc::new(Cell::new(0));
851        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app| {});
852        let counting = shell.app().create(CountingMetricsChanges {
853            count: Rc::clone(&count),
854        });
855        let binding = RendererBinding::instance(&mut shell.app());
856        binding.set_overrides(&mut shell.app(), Rc::new(counting));
857
858        shell.view_added(ViewId(1));
859        assert_eq!(count.get(), 1, "a view added");
860        shell.view_removed(ViewId(1));
861        assert_eq!(count.get(), 2, "a view removed");
862        shell.screens_changed();
863        assert_eq!(count.get(), 3, "the screens changed");
864    }
865
866    #[test]
867    fn view_focus_is_delivered_through_the_dispatcher_callback() {
868        use inset_embedder::{ViewFocusDirection, ViewFocusEvent, ViewFocusState};
869        let received = Rc::new(Cell::new(None));
870        let sink = received.clone();
871        let platform = Rc::new(platform_with_a_view());
872        let mut shell = Shell::new(platform, move |app| {
873            app.platform_callbacks_mut().on_view_focus_change = Some(Rc::new(move |app, event| {
874                let sink = sink.clone();
875                app.schedule_microtask(inset_foundation::Listener::new(move |_| {
876                    sink.set(Some(event))
877                }));
878            }));
879        });
880        shell.view_focus_changed(ViewFocusEvent {
881            view_id: IMPLICIT_VIEW_ID,
882            state: ViewFocusState::Focused,
883            direction: ViewFocusDirection::Backward,
884        });
885        let event = received
886            .get()
887            .expect("the shell checkpoints the callback's microtask");
888        assert_eq!(event.view_id, IMPLICIT_VIEW_ID);
889        assert_eq!(event.state, ViewFocusState::Focused);
890        assert_eq!(event.direction, ViewFocusDirection::Backward);
891    }
892}