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