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, DragReport, EditCommand, EmbedderClient,
15    Frame, 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 drag_report(&mut self, report: DragReport) {
159        self.push(|app| {
160            let callback = app.platform_callbacks().on_drag.clone();
161            if let Some(callback) = callback {
162                callback(app, report);
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 a_read_started_by_a_task_the_drop_spawns_is_in_time() {
378        use std::cell::RefCell;
379
380        use inset_embedder::{
381            ClipboardReadFuture, DataTransferItem, DragChange, DragReport, DraggedItems,
382        };
383
384        struct Text([String; 1]);
385
386        impl DataTransferItem for Text {
387            fn types(&self) -> &[String] {
388                &self.0
389            }
390
391            fn read(&self, _mime: &str) -> ClipboardReadFuture {
392                Box::pin(std::future::ready(Ok(b"hi".to_vec())))
393            }
394        }
395
396        let read = Rc::new(RefCell::new(None));
397        let read_slot = Rc::clone(&read);
398        let platform = Rc::new(platform_with_a_view());
399        let mut shell = Shell::new(platform, move |app: &mut App| {
400            app.platform_callbacks_mut().on_drag = Some(Rc::new(move |app, report| {
401                let read_slot = Rc::clone(&read_slot);
402                let item = Rc::clone(&report.items[0]);
403                // Dropping the task lets it run, as a Dart future does.
404                let _reading = app.spawn(async move |_cx| {
405                    *read_slot.borrow_mut() = Some(item.read("text/plain").await);
406                });
407            }));
408        });
409        let dragged = DraggedItems::new(vec![Box::new(Text(["text/plain".to_owned()]))]);
410        let drop = DragReport {
411            view_id: IMPLICIT_VIEW_ID,
412            change: DragChange::Dropped,
413            physical_position: None,
414            items: dragged.items(),
415        };
416        dragged.report_drop(|| shell.drag_report(drop));
417        assert_eq!(*read.borrow(), Some(Ok(b"hi".to_vec())));
418    }
419
420    #[test]
421    fn a_clipboard_event_reaches_the_platform_callback_with_its_view() {
422        use inset_embedder::{ClipboardEvent, CopyRequest};
423
424        let seen = Rc::new(Cell::new(None));
425        let seen_slot = Rc::clone(&seen);
426        let mut shell = Shell::new(Rc::new(platform_with_a_view()), move |app: &mut App| {
427            app.platform_callbacks_mut().on_clipboard_event =
428                Some(Rc::new(move |_app, view, event| {
429                    seen_slot.set(Some((view, matches!(event, ClipboardEvent::Cut(_)))));
430                }));
431        });
432        let request = CopyRequest::new(|_items, _options| Box::pin(std::future::ready(Ok(()))));
433        shell.clipboard_event(IMPLICIT_VIEW_ID, ClipboardEvent::Cut(request));
434        assert_eq!(seen.get(), Some((IMPLICIT_VIEW_ID, true)));
435    }
436
437    #[test]
438    fn an_open_request_waits_for_the_apps_handler() {
439        use std::cell::RefCell;
440
441        use inset_embedder::OpenRequest;
442
443        let seen = Rc::new(RefCell::new(Vec::new()));
444        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app: &mut App| {});
445        shell.open_requested(OpenRequest {
446            files: Vec::new(),
447            links: vec!["https://example.org/launch".to_owned()],
448        });
449        let slot = Rc::clone(&seen);
450        shell
451            .app
452            .borrow_mut()
453            .set_open_requested_handler(Some(Rc::new(move |_app, request: OpenRequest| {
454                slot.borrow_mut().extend(request.links)
455            })));
456        shell.open_requested(OpenRequest {
457            files: Vec::new(),
458            links: vec!["https://example.org/later".to_owned()],
459        });
460        assert_eq!(
461            *seen.borrow(),
462            ["https://example.org/launch", "https://example.org/later"],
463            "the launch's request waited for the handler"
464        );
465    }
466
467    #[test]
468    fn a_notification_click_waits_for_the_apps_handler() {
469        use std::cell::RefCell;
470
471        use inset_embedder::NotificationActivation;
472
473        let seen = Rc::new(RefCell::new(Vec::new()));
474        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app: &mut App| {});
475        let click = |action: Option<&str>| NotificationActivation {
476            id: "message-7".to_owned(),
477            action: action.map(str::to_owned),
478            reply: None,
479        };
480        shell.notification_activated(click(None));
481        let slot = Rc::clone(&seen);
482        shell
483            .app
484            .borrow_mut()
485            .set_notification_activated_handler(Some(Rc::new(move |_app, activation| {
486                slot.borrow_mut().push(activation)
487            })));
488        shell.notification_activated(click(Some("later")));
489        assert_eq!(
490            *seen.borrow(),
491            [click(None), click(Some("later"))],
492            "the click that started the app waited for the handler"
493        );
494    }
495
496    #[test]
497    fn menu_choices_edit_commands_and_tray_icon_events_reach_their_callbacks() {
498        use std::cell::RefCell;
499
500        use inset_embedder::{EditCommand, Offset, TrayIconEvent};
501
502        let seen = Rc::new(RefCell::new(Vec::new()));
503        let slot = Rc::clone(&seen);
504        let mut shell = Shell::new(Rc::new(platform_with_a_view()), move |app: &mut App| {
505            let callbacks = app.platform_callbacks_mut();
506            let menu = Rc::clone(&slot);
507            callbacks.on_menu_selected = Some(Rc::new(move |_app, id| {
508                menu.borrow_mut().push(format!("menu {id}"));
509            }));
510            let dock = Rc::clone(&slot);
511            callbacks.on_dock_menu_selected = Some(Rc::new(move |_app, id| {
512                dock.borrow_mut().push(format!("dock {id}"));
513            }));
514            let edit = Rc::clone(&slot);
515            callbacks.on_edit_command = Some(Rc::new(move |_app, view, command| {
516                edit.borrow_mut().push(format!("{view:?} {command:?}"));
517            }));
518            let tray = Rc::clone(&slot);
519            callbacks.on_tray_icon_event = Some(Rc::new(move |_app, icon, event| {
520                tray.borrow_mut().push(format!("tray {icon} {event:?}"));
521            }));
522        });
523        shell.menu_selected(3);
524        shell.dock_menu_selected(4);
525        shell.edit_command(IMPLICIT_VIEW_ID, EditCommand::Undo);
526        shell.tray_icon_event(
527            7,
528            TrayIconEvent::Activated {
529                position: Offset::new(1.0, 2.0),
530            },
531        );
532        assert_eq!(
533            *seen.borrow(),
534            [
535                "menu 3".to_owned(),
536                "dock 4".to_owned(),
537                format!("{IMPLICIT_VIEW_ID:?} Undo"),
538                format!(
539                    "tray 7 {:?}",
540                    TrayIconEvent::Activated {
541                        position: Offset::new(1.0, 2.0)
542                    }
543                ),
544            ]
545        );
546    }
547
548    #[test]
549    fn a_change_of_the_clipboard_reaches_the_services_bindings_listenable() {
550        use inset_foundation::{Listenable, Listener};
551        use inset_services::ServicesBinding;
552
553        let heard = Rc::new(Cell::new(0));
554        let counter = Rc::clone(&heard);
555        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
556            let clipboard = ServicesBinding::instance(app).clipboard(app);
557            clipboard.add_listener(
558                app,
559                Listener::new(move |_app| counter.set(counter.get() + 1)),
560            );
561        });
562        shell.clipboard_changed();
563        assert_eq!(heard.get(), 1);
564    }
565
566    /// A reply to `shell.request_app_exit`, and the answer it was given so far.
567    fn recorded_reply() -> (
568        inset_embedder::AppExitReply,
569        Rc<Cell<Option<inset_embedder::AppExitResponse>>>,
570    ) {
571        let answer = Rc::new(Cell::new(None));
572        let given = Rc::clone(&answer);
573        let reply = inset_embedder::AppExitReply::new(move |response| given.set(Some(response)));
574        (reply, answer)
575    }
576
577    #[test]
578    fn a_view_added_or_removed_reaches_the_framework_with_its_id() {
579        let heard = Rc::new(std::cell::RefCell::new(Vec::new()));
580        let (added, removed) = (Rc::clone(&heard), Rc::clone(&heard));
581        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
582            let callbacks = app.platform_callbacks_mut();
583            callbacks.on_view_added = Some(Rc::new(move |_app, view| {
584                added.borrow_mut().push(("added", view))
585            }));
586            callbacks.on_view_removed = Some(Rc::new(move |_app, view| {
587                removed.borrow_mut().push(("removed", view))
588            }));
589        });
590        shell.view_added(ViewId(4));
591        shell.view_removed(ViewId(4));
592        assert_eq!(
593            *heard.borrow(),
594            [("added", ViewId(4)), ("removed", ViewId(4))]
595        );
596    }
597
598    #[test]
599    fn a_view_close_request_reaches_the_framework() {
600        let heard = Rc::new(Cell::new(None));
601        let heard_slot = Rc::clone(&heard);
602        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
603            app.platform_callbacks_mut().on_view_close_requested =
604                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
605        });
606        shell.view_close_requested(ViewId(3));
607        assert_eq!(heard.get(), Some(ViewId(3)));
608    }
609
610    #[test]
611    fn a_window_change_reaches_the_framework() {
612        let heard = Rc::new(Cell::new(None));
613        let heard_slot = Rc::clone(&heard);
614        let mut shell = Shell::new(Rc::new(TestPlatform::new()), move |app: &mut App| {
615            app.platform_callbacks_mut().on_window_changed =
616                Some(Rc::new(move |_app, view| heard_slot.set(Some(view))));
617        });
618        shell.window_changed(ViewId(5));
619        assert_eq!(heard.get(), Some(ViewId(5)));
620    }
621
622    #[test]
623    fn a_lifecycle_state_the_host_reports_reaches_the_scheduler_with_the_states_between() {
624        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
625        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
626        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
627        let mut app = shell.app();
628        assert_eq!(
629            SchedulerBinding::lifecycle_state(&mut app),
630            Some(AppLifecycleState::Hidden)
631        );
632        assert!(!SchedulerBinding::frames_enabled(&mut app));
633    }
634
635    #[test]
636    fn resuming_marks_every_view_to_be_rendered_and_asks_each_for_a_frame() {
637        use inset_rendering::RendererBinding;
638
639        let platform = Rc::new(TestPlatform::new().with_views(&[IMPLICIT_VIEW_ID, ViewId(1)]));
640        let first = platform.test_view(IMPLICIT_VIEW_ID);
641        let mut shell = Shell::new(platform.clone(), |_app| {});
642        let render_view = {
643            let mut app = shell.app();
644            let binding = RendererBinding::instance(&mut app);
645            binding.init_render_view(&mut app, first.clone())
646        };
647        let at = |ms| Frame {
648            elapsed: Duration::from_millis(ms),
649        };
650        shell.frame(&[IMPLICIT_VIEW_ID], at(16));
651        shell.frame(&[IMPLICIT_VIEW_ID], at(32));
652        assert_eq!(
653            first.presented(),
654            1,
655            "nothing changed after the first frame"
656        );
657
658        shell.app_lifecycle_state_changed(AppLifecycleState::Hidden);
659        platform.take_frame_requests();
660        shell.app_lifecycle_state_changed(AppLifecycleState::Resumed);
661        assert_eq!(
662            platform.take_frame_requests(),
663            [IMPLICIT_VIEW_ID, ViewId(1)]
664        );
665        assert!(render_view.needs_composite_frame(&shell.app()));
666        shell.frame(&[IMPLICIT_VIEW_ID], at(48));
667        assert_eq!(first.presented(), 2, "drawn again though nothing changed");
668    }
669
670    #[test]
671    fn request_app_exit_is_answered_by_the_services_binding() {
672        use inset_embedder::AppExitResponse;
673        use inset_foundation::{Handle, Task};
674        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
675
676        #[derive(Default)]
677        struct Refusing;
678
679        impl ServicesBindingOverridesObject for Refusing {
680            fn handle_request_app_exit(
681                self: Handle<Self>,
682                _app: &mut App,
683            ) -> Task<AppExitResponse> {
684                Task::ready(AppExitResponse::Cancel)
685            }
686        }
687
688        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
689        let (reply, answer) = recorded_reply();
690        shell.request_app_exit(reply);
691        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
692
693        let refusing = shell.app().create(Refusing);
694        let binding = ServicesBinding::instance(&mut shell.app());
695        binding.set_overrides(&mut shell.app(), Rc::new(refusing));
696        let (reply, answer) = recorded_reply();
697        shell.request_app_exit(reply);
698        assert_eq!(answer.get(), Some(AppExitResponse::Cancel));
699    }
700
701    /// The reply is answered inside the call that decides.
702    #[test]
703    fn request_app_exit_answers_during_the_call_in_which_the_framework_decides() {
704        use inset_embedder::AppExitResponse;
705        use inset_foundation::{Completer, Handle, Task};
706        use inset_services::{ServicesBinding, ServicesBindingOverridesObject};
707
708        struct Deciding {
709            decision: Completer<AppExitResponse>,
710        }
711
712        impl ServicesBindingOverridesObject for Deciding {
713            fn handle_request_app_exit(self: Handle<Self>, app: &mut App) -> Task<AppExitResponse> {
714                let decision = app.get(self).decision.future();
715                app.spawn(async move |_cx| decision.await)
716            }
717        }
718
719        let decision = Completer::new();
720        let mut shell = Shell::new(Rc::new(TestPlatform::new()), |_app| {});
721        let deciding = shell.app().create(Deciding {
722            decision: decision.clone(),
723        });
724        let binding = ServicesBinding::instance(&mut shell.app());
725        binding.set_overrides(&mut shell.app(), Rc::new(deciding));
726
727        let (reply, answer) = recorded_reply();
728        shell.request_app_exit(reply);
729        assert_eq!(answer.get(), None);
730
731        decision.complete(&mut shell.app(), AppExitResponse::Exit);
732        assert_eq!(answer.get(), None, "the task resumes at a checkpoint");
733        shell.wake(Duration::ZERO);
734        assert_eq!(answer.get(), Some(AppExitResponse::Exit));
735    }
736
737    #[test]
738    fn new_runs_setup_and_frame_runs_both_scheduler_phases() {
739        let platform = Rc::new(platform_with_a_view());
740        let mut setup_ran = false;
741        let mut shell = Shell::new(platform.clone(), |_app: &mut App| {
742            setup_ran = true;
743        });
744        assert!(setup_ran);
745        assert!(platform.frame_requests().is_empty());
746
747        SchedulerBinding::schedule_frame(&mut shell.app(), FrameTarget::View(IMPLICIT_VIEW_ID));
748        assert_eq!(platform.frame_requests(), [IMPLICIT_VIEW_ID]);
749        shell.frame(
750            &[IMPLICIT_VIEW_ID],
751            Frame {
752                elapsed: Duration::from_millis(16),
753            },
754        );
755    }
756
757    #[test]
758    fn a_reported_press_reaches_gesture_binding_numbered_by_the_converter() {
759        let ran = Rc::new(Cell::new(false));
760        let ran_flag = Rc::clone(&ran);
761        let platform = Rc::new(platform_with_a_view());
762        let mut shell = Shell::new(platform, |app| {
763            let binding = GestureBinding::instance(app);
764            binding.pointer_router(app).add_route(
765                app,
766                1,
767                PointerRoute::new(move |_app, _event| {
768                    ran_flag.set(true);
769                }),
770                None,
771            );
772        });
773
774        shell.pointer_reports(vec![PointerReport {
775            view_id: IMPLICIT_VIEW_ID,
776            time_stamp: Duration::ZERO,
777            device: 0,
778            kind: PointerDeviceKind::Mouse,
779            position: [0.0, 0.0],
780            change: ReportedChange::Down { buttons: 1 },
781            contact: Contact::default(),
782        }]);
783        assert!(ran.get(), "the first press is pointer 1");
784    }
785
786    #[test]
787    fn key_data_reaches_the_hardware_keyboard() {
788        let seen = Rc::new(Cell::new(0));
789        let seen_flag = Rc::clone(&seen);
790        let platform = Rc::new(TestPlatform::new());
791        let mut shell = Shell::new(platform, |app| {
792            HardwareKeyboard::instance(app).add_handler(
793                app,
794                Rc::new(move |_app: &mut App, _event: &KeyEvent| {
795                    seen_flag.set(seen_flag.get() + 1);
796                    true
797                }),
798            );
799        });
800
801        let key_a = KeyData {
802            event_type: KeyEventType::Down,
803            physical: PhysicalKeyboardKey::KEY_A.usb_hid_usage,
804            logical: LogicalKeyboardKey::KEY_A.key_id,
805            character: Some("a".to_owned()),
806            ..KeyData::default()
807        };
808        assert!(shell.key_data(key_a), "the handler claimed the event");
809        assert_eq!(seen.get(), 1);
810
811        let keyboard = HardwareKeyboard::instance(&mut shell.app());
812        assert!(keyboard.is_logical_key_pressed(&shell.app(), LogicalKeyboardKey::KEY_A));
813    }
814
815    #[test]
816    fn wake_and_frame_advance_the_app_clock_and_fire_due_timers() {
817        let platform = Rc::new(TestPlatform::new());
818        let fired = Rc::new(Cell::new(Vec::new()));
819        let mut shell = Shell::new(platform, |app| {
820            for (name, delay) in [("early", 10), ("late", 30)] {
821                let fired = Rc::clone(&fired);
822                inset_foundation::Timer::new(
823                    app,
824                    Duration::from_millis(delay),
825                    inset_foundation::Listener::new(move |_app| {
826                        let mut log = fired.take();
827                        log.push(name);
828                        fired.set(log);
829                    }),
830                );
831            }
832        });
833        assert!(fired.take().is_empty());
834
835        shell.wake(Duration::from_millis(15));
836        assert_eq!(fired.take(), vec!["early"]);
837
838        shell.frame(
839            &[IMPLICIT_VIEW_ID],
840            Frame {
841                elapsed: Duration::from_millis(32),
842            },
843        );
844        assert_eq!(fired.take(), vec!["late"]);
845    }
846
847    #[test]
848    fn a_view_coming_or_going_and_a_change_to_the_screens_are_metrics_changes() {
849        use inset_foundation::Handle;
850        use inset_rendering::{RendererBinding, RendererBindingOverridesObject};
851
852        struct CountingMetricsChanges {
853            count: Rc<Cell<usize>>,
854        }
855
856        impl RendererBindingOverridesObject for CountingMetricsChanges {
857            fn will_draw_frame(self: Handle<Self>, _app: &mut App) {}
858
859            fn did_draw_frame(self: Handle<Self>, _app: &mut App) {}
860
861            fn did_handle_metrics_changed(self: Handle<Self>, app: &mut App) {
862                let count = &app.get(self).count;
863                count.set(count.get() + 1);
864            }
865        }
866
867        let count = Rc::new(Cell::new(0));
868        let mut shell = Shell::new(Rc::new(platform_with_a_view()), |_app| {});
869        let counting = shell.app().create(CountingMetricsChanges {
870            count: Rc::clone(&count),
871        });
872        let binding = RendererBinding::instance(&mut shell.app());
873        binding.set_overrides(&mut shell.app(), Rc::new(counting));
874
875        shell.view_added(ViewId(1));
876        assert_eq!(count.get(), 1, "a view added");
877        shell.view_removed(ViewId(1));
878        assert_eq!(count.get(), 2, "a view removed");
879        shell.screens_changed();
880        assert_eq!(count.get(), 3, "the screens changed");
881    }
882
883    #[test]
884    fn view_focus_is_delivered_through_the_dispatcher_callback() {
885        use inset_embedder::{ViewFocusDirection, ViewFocusEvent, ViewFocusState};
886        let received = Rc::new(Cell::new(None));
887        let sink = received.clone();
888        let platform = Rc::new(platform_with_a_view());
889        let mut shell = Shell::new(platform, move |app| {
890            app.platform_callbacks_mut().on_view_focus_change = Some(Rc::new(move |app, event| {
891                let sink = sink.clone();
892                app.schedule_microtask(inset_foundation::Listener::new(move |_| {
893                    sink.set(Some(event))
894                }));
895            }));
896        });
897        shell.view_focus_changed(ViewFocusEvent {
898            view_id: IMPLICIT_VIEW_ID,
899            state: ViewFocusState::Focused,
900            direction: ViewFocusDirection::Backward,
901        });
902        let event = received
903            .get()
904            .expect("the shell checkpoints the callback's microtask");
905        assert_eq!(event.view_id, IMPLICIT_VIEW_ID);
906        assert_eq!(event.state, ViewFocusState::Focused);
907        assert_eq!(event.direction, ViewFocusDirection::Backward);
908    }
909}