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    DropData, EmbedderClient, Frame, Instant, KeyData, PlatformRef, PointerDataPacket,
15    TextEditingValue, TextInputAction, ViewFocusEvent, ViewId,
16};
17use inset_foundation::{App, AppCell};
18use inset_gestures::GestureBinding;
19use inset_painting::PaintingBinding;
20use inset_rendering::RendererBinding;
21use inset_scheduler::SchedulerBinding;
22use inset_services::{KeyEventManager, TextInput};
23
24/// Host-facing isolate: the [`AppCell`] plus the methods the embedder pushes.
25///
26/// Every push is one call into the app: the `App` is borrowed for the binding call and
27/// released before the cell's checkpoint, where the microtasks and futures the event queued
28/// run.
29pub struct Shell {
30    app: Rc<AppCell>,
31    platform: PlatformRef,
32    /// The platform time the app clock was last advanced to.
33    clock: Duration,
34    /// 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.
35    origin: Option<Instant>,
36}
37
38impl Shell {
39    /// Builds the heap from `platform`, then runs application setup.
40    ///
41    /// Setup (later widgets `run_app`) requests the first frame when it
42    /// installs work that needs one.
43    pub fn new(platform: PlatformRef, setup: impl FnOnce(&mut App)) -> Shell {
44        let shell = Shell {
45            app: AppCell::with_platform(platform.clone()),
46            platform,
47            clock: Duration::ZERO,
48            origin: None,
49        };
50        shell.handle_event(|app| {
51            // Flutter's engine collects the platform's fonts before the framework runs.
52            PaintingBinding::instance(app).install_platform_fonts(app);
53            setup(app);
54        });
55        shell
56    }
57
58    /// The `App`, borrowed until the guard drops. A host that needs the whole thing, a borrow
59    /// and then the checkpoint, takes [`cell`](Self::cell).
60    pub fn app(&self) -> RefMut<'_, App> {
61        self.app.borrow_mut()
62    }
63
64    pub fn cell(&self) -> &Rc<AppCell> {
65        &self.app
66    }
67
68    /// One platform event: `f` on the borrowed `App`, then the checkpoint.
69    fn handle_event<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
70        let result = f(&mut self.app.borrow_mut());
71        self.app.checkpoint();
72        result
73    }
74
75    /// An input or platform push: the app clock catches up with the platform's first, as Dart's
76    /// timers measure real time from the moment they are created, then `f` runs.
77    fn push<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
78        if let Some(origin) = self.origin {
79            let elapsed = self.platform.now().saturating_duration_since(origin);
80            self.advance_clock(elapsed);
81        }
82        self.handle_event(f)
83    }
84
85    /// Moves the app clock up to the platform's `elapsed`, firing the timers that came due.
86    fn advance_clock(&mut self, elapsed: Duration) {
87        if elapsed <= self.clock {
88            return;
89        }
90        self.app.elapse(elapsed - self.clock);
91        self.clock = elapsed;
92    }
93}
94
95impl EmbedderClient for Shell {
96    fn frame(&mut self, frame: Frame) {
97        self.origin = Some(self.platform.now() - frame.elapsed);
98        self.advance_clock(frame.elapsed);
99        // The engine runs `_beginFrame` and `_drawFrame` as two native tasks: two calls.
100        self.handle_event(|app| SchedulerBinding::on_begin_frame(app, Some(frame.elapsed)));
101        self.handle_event(SchedulerBinding::on_draw_frame);
102    }
103
104    fn view_added(&mut self, _id: ViewId) {}
105
106    fn view_metrics_changed(&mut self, _id: ViewId) {
107        self.push(|app| RendererBinding::instance(app).handle_metrics_changed(app));
108    }
109
110    fn view_removed(&mut self, _id: ViewId) {}
111
112    fn view_focus_changed(&mut self, event: ViewFocusEvent) {
113        self.push(|app| {
114            let callback = app.platform_callbacks().on_view_focus_change.clone();
115            if let Some(callback) = callback {
116                callback(app, event);
117            }
118        });
119    }
120
121    fn drop_data(&mut self, data: DropData) {
122        self.push(|app| {
123            let callback = app.platform_callbacks().on_drop.clone();
124            if let Some(callback) = callback {
125                callback(app, data);
126            }
127        });
128    }
129
130    fn pointer_data_packet(&mut self, packet: PointerDataPacket) {
131        self.push(|app| GestureBinding::instance(app).handle_pointer_data_packet(app, packet));
132    }
133
134    fn key_data(&mut self, data: KeyData) -> bool {
135        self.push(|app| KeyEventManager::instance(app).handle_key_data(app, data))
136    }
137
138    fn pop_route(&mut self) -> bool {
139        self.push(|app| {
140            let callback = app.platform_callbacks().on_pop_route.clone();
141            callback.is_some_and(|callback| callback(app))
142        })
143    }
144
145    fn platform_brightness_changed(&mut self) {
146        self.push(|app| {
147            let callback = app
148                .platform_callbacks()
149                .on_platform_brightness_changed
150                .clone();
151            if let Some(callback) = callback {
152                callback.call(app);
153            }
154        });
155    }
156
157    fn locales_changed(&mut self) {
158        self.push(|app| {
159            let callback = app.platform_callbacks().on_locale_changed.clone();
160            if let Some(callback) = callback {
161                callback.call(app);
162            }
163        });
164    }
165
166    fn system_fonts_changed(&mut self) {
167        self.push(|app| {
168            PaintingBinding::instance(app).handle_system_fonts_did_change(app);
169        });
170    }
171
172    fn wake(&mut self, elapsed: Duration) {
173        self.advance_clock(elapsed);
174        // A wake with no time behind it is a post from a native callback.
175        self.app.wake();
176    }
177
178    fn text_input_editing_value(&mut self, _view: ViewId, value: TextEditingValue) {
179        self.push(|app| TextInput::instance(app).update_editing_value(app, value));
180    }
181
182    fn text_input_action(&mut self, _view: ViewId, action: TextInputAction) {
183        self.push(|app| TextInput::instance(app).perform_action(app, action));
184    }
185
186    fn text_input_closed(&mut self, _view: ViewId) {
187        self.push(|app| TextInput::instance(app).connection_closed(app));
188    }
189
190    fn system_context_menu_hidden(&mut self) {
191        self.push(inset_services::SystemContextMenuController::dispatch_system_hide);
192    }
193
194    fn custom_context_menu_action(&mut self, callback_id: &str) {
195        let callback_id = callback_id.to_string();
196        self.push(move |app| {
197            inset_services::SystemContextMenuController::dispatch_custom_action(app, &callback_id);
198        });
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use std::cell::Cell;
205    use std::rc::Rc;
206    use std::time::Duration;
207
208    use inset_embedder::{
209        EmbedderClient, Frame, KeyData, KeyEventType, PointerChange, PointerData,
210        PointerDataPacket, PointerDeviceKind, ViewId,
211    };
212    use inset_foundation::App;
213    use inset_gestures::{GestureBinding, PointerRoute};
214    use inset_scheduler::SchedulerBinding;
215    use inset_services::{HardwareKeyboard, KeyEvent, LogicalKeyboardKey, PhysicalKeyboardKey};
216    use inset_test::{TestPlatform, TestView};
217
218    use super::Shell;
219
220    /// A platform with the one view a pointer packet or a focus event is addressed to.
221    fn platform_with_a_view() -> TestPlatform {
222        TestPlatform::new().with_view(Rc::new(TestView::new(0.0, 0.0)))
223    }
224
225    #[test]
226    fn drop_data_reaches_the_platform_callback() {
227        use std::cell::RefCell;
228        use std::path::PathBuf;
229
230        use inset_embedder::{DropChange, DropData, ViewId};
231
232        let seen = Rc::new(RefCell::new(None));
233        let seen_slot = Rc::clone(&seen);
234        let platform = Rc::new(platform_with_a_view());
235        let mut shell = Shell::new(platform, move |app: &mut App| {
236            app.platform_callbacks_mut().on_drop = Some(Rc::new(move |_app, data| {
237                *seen_slot.borrow_mut() = Some(data);
238            }));
239        });
240        shell.drop_data(DropData {
241            view_id: ViewId(0),
242            change: DropChange::Dropped,
243            physical_position: None,
244            paths: vec![PathBuf::from("/a.wasm")],
245        });
246        let seen = seen.borrow();
247        let seen = seen.as_ref().expect("the callback ran");
248        assert_eq!(seen.change, DropChange::Dropped);
249        assert_eq!(seen.paths, [PathBuf::from("/a.wasm")]);
250    }
251
252    #[test]
253    fn new_runs_setup_and_frame_runs_both_scheduler_phases() {
254        let platform = Rc::new(TestPlatform::new());
255        let mut setup_ran = false;
256        let mut shell = Shell::new(platform.clone(), |_app: &mut App| {
257            setup_ran = true;
258        });
259        assert!(setup_ran);
260        assert_eq!(platform.frames_requested(), 0);
261
262        SchedulerBinding::schedule_frame(&mut shell.app());
263        assert_eq!(platform.frames_requested(), 1);
264        shell.frame(Frame {
265            elapsed: Duration::from_millis(16),
266        });
267    }
268
269    #[test]
270    fn pointer_data_packet_reaches_gesture_binding() {
271        let ran = Rc::new(Cell::new(false));
272        let ran_flag = Rc::clone(&ran);
273        let platform = Rc::new(platform_with_a_view());
274        let mut shell = Shell::new(platform, |app| {
275            let binding = GestureBinding::instance(app);
276            binding.pointer_router(app).add_route(
277                app,
278                1,
279                PointerRoute::new(move |_app, _event| {
280                    ran_flag.set(true);
281                }),
282                None,
283            );
284        });
285
286        shell.pointer_data_packet(PointerDataPacket::new(vec![PointerData {
287            change: PointerChange::Down,
288            kind: PointerDeviceKind::Mouse,
289            buttons: 1,
290            pointer_identifier: 1,
291            ..PointerData::default()
292        }]));
293        assert!(ran.get());
294    }
295
296    #[test]
297    fn key_data_reaches_the_hardware_keyboard() {
298        let seen = Rc::new(Cell::new(0));
299        let seen_flag = Rc::clone(&seen);
300        let platform = Rc::new(TestPlatform::new());
301        let mut shell = Shell::new(platform, |app| {
302            HardwareKeyboard::instance(app).add_handler(
303                app,
304                Rc::new(move |_app: &mut App, _event: &KeyEvent| {
305                    seen_flag.set(seen_flag.get() + 1);
306                    true
307                }),
308            );
309        });
310
311        let key_a = KeyData {
312            event_type: KeyEventType::Down,
313            physical: PhysicalKeyboardKey::KEY_A.usb_hid_usage,
314            logical: LogicalKeyboardKey::KEY_A.key_id,
315            character: Some("a".to_owned()),
316            ..KeyData::default()
317        };
318        assert!(shell.key_data(key_a), "the handler claimed the event");
319        assert_eq!(seen.get(), 1);
320
321        let keyboard = HardwareKeyboard::instance(&mut shell.app());
322        assert!(keyboard.is_logical_key_pressed(&shell.app(), LogicalKeyboardKey::KEY_A));
323    }
324
325    #[test]
326    fn wake_and_frame_advance_the_app_clock_and_fire_due_timers() {
327        let platform = Rc::new(TestPlatform::new());
328        let fired = Rc::new(Cell::new(Vec::new()));
329        let mut shell = Shell::new(platform, |app| {
330            for (name, delay) in [("early", 10), ("late", 30)] {
331                let fired = Rc::clone(&fired);
332                inset_foundation::Timer::new(
333                    app,
334                    Duration::from_millis(delay),
335                    inset_foundation::Listener::new(move |_app| {
336                        let mut log = fired.take();
337                        log.push(name);
338                        fired.set(log);
339                    }),
340                );
341            }
342        });
343        assert!(fired.take().is_empty());
344
345        shell.wake(Duration::from_millis(15));
346        assert_eq!(fired.take(), vec!["early"]);
347
348        shell.frame(Frame {
349            elapsed: Duration::from_millis(32),
350        });
351        assert_eq!(fired.take(), vec!["late"]);
352    }
353
354    #[test]
355    fn view_focus_is_delivered_through_the_dispatcher_callback() {
356        use inset_embedder::{ViewFocusDirection, ViewFocusEvent, ViewFocusState};
357        let received = Rc::new(Cell::new(None));
358        let sink = received.clone();
359        let platform = Rc::new(platform_with_a_view());
360        let mut shell = Shell::new(platform, move |app| {
361            app.platform_callbacks_mut().on_view_focus_change = Some(Rc::new(move |app, event| {
362                let sink = sink.clone();
363                app.schedule_microtask(inset_foundation::Listener::new(move |_| {
364                    sink.set(Some(event))
365                }));
366            }));
367        });
368        shell.view_focus_changed(ViewFocusEvent {
369            view_id: ViewId(0),
370            state: ViewFocusState::Focused,
371            direction: ViewFocusDirection::Backward,
372        });
373        let event = received
374            .get()
375            .expect("the shell checkpoints the callback's microtask");
376        assert_eq!(event.view_id, ViewId(0));
377        assert_eq!(event.state, ViewFocusState::Focused);
378        assert_eq!(event.direction, ViewFocusDirection::Backward);
379    }
380}