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