inset_embedder/platform.rs
1//! Host capabilities the framework calls at runtime.
2//!
3//! Unlike Dart's isolate-global `PlatformDispatcher`, this object is supplied
4//! by the embedder and held by `App`. Incoming host events travel through
5//! `EmbedderClient`, so this interface contains requests and state only.
6
7use std::any::Any;
8use std::future::Future;
9use std::path::PathBuf;
10use std::pin::Pin;
11use std::rc::Rc;
12use std::sync::Arc;
13use std::time::Duration;
14
15use web_time::Instant;
16
17use crate::fonts::FontSource;
18use crate::geometry::{Color, Offset, Rect};
19use crate::mouse_cursor::SystemMouseCursorKind;
20use crate::restoration::{RestorationMap, RestorationUpdate};
21use crate::system_context_menu::SystemContextMenuItem;
22use crate::{View, ViewFocusDirection, ViewFocusState, ViewId};
23
24pub type PlatformRef = Rc<dyn Platform>;
25
26pub type ViewRef = Rc<dyn View>;
27
28/// Specifies a description of the application that is pertinent to the
29/// embedder's application switcher (also known as "recent tasks") user
30/// interface.
31///
32/// Flutter counterpart: `ApplicationSwitcherDescription` (`services/system_chrome.dart`).
33/// It lives here because [`Platform`] names it.
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
35pub struct ApplicationSwitcherDescription {
36 /// A label and description of the current state of the application.
37 pub label: Option<String>,
38 /// The application's primary color, as a 32-bit ARGB value.
39 ///
40 /// This may influence the color that the operating system uses to represent
41 /// the application.
42 pub primary_color: Option<u32>,
43}
44
45/// Specifies a system overlay style for a portion of the app.
46///
47/// Flutter counterpart: `SystemUiOverlayStyle` (`services/system_chrome.dart`).
48/// It lives here because [`Platform`] names it.
49#[derive(Clone, Copy, Debug, Default, PartialEq)]
50pub struct SystemUiOverlayStyle {
51 /// The color of the system bottom navigation bar.
52 ///
53 /// Only honored in Android versions O and greater.
54 pub system_navigation_bar_color: Option<Color>,
55
56 /// The color of the divider between the system's bottom navigation bar and the app's
57 /// content.
58 ///
59 /// Only honored in Android versions P and greater.
60 pub system_navigation_bar_divider_color: Option<Color>,
61
62 /// The brightness of the system navigation bar icons.
63 ///
64 /// Only honored in Android versions O and greater.
65 /// When set to [`Brightness::Light`], the system navigation bar icons are light.
66 /// When set to [`Brightness::Dark`], the system navigation bar icons are dark.
67 pub system_navigation_bar_icon_brightness: Option<Brightness>,
68
69 /// Overrides the contrast enforcement when setting a transparent navigation bar.
70 ///
71 /// When setting a transparent navigation bar in SDK 29+, or Android 10 and up, a
72 /// translucent body scrim may be applied behind the button navigation bar to ensure
73 /// contrast with buttons and the background of the application.
74 ///
75 /// SDK 28-, or Android P and lower, will not apply this body scrim.
76 ///
77 /// Setting this to false overrides the default body scrim.
78 pub system_navigation_bar_contrast_enforced: Option<bool>,
79
80 /// The color of top status bar.
81 ///
82 /// Only honored in Android version M and greater.
83 pub status_bar_color: Option<Color>,
84
85 /// The brightness of top status bar.
86 ///
87 /// Only honored in iOS.
88 pub status_bar_brightness: Option<Brightness>,
89
90 /// The brightness of the top status bar icons.
91 ///
92 /// Only honored in Android version M and greater.
93 pub status_bar_icon_brightness: Option<Brightness>,
94
95 /// Overrides the contrast enforcement when setting a transparent status bar.
96 ///
97 /// When setting a transparent status bar in SDK 29+, or Android 10 and up, a translucent
98 /// body scrim may be applied to ensure contrast with icons and the background of the
99 /// application.
100 ///
101 /// SDK 28-, or Android P and lower, will not apply this body scrim.
102 ///
103 /// Setting this to false overrides the default body scrim.
104 pub system_status_bar_contrast_enforced: Option<bool>,
105}
106
107impl SystemUiOverlayStyle {
108 /// System overlays should be drawn with a light color. Intended for applications with a
109 /// dark background.
110 pub const LIGHT: SystemUiOverlayStyle = SystemUiOverlayStyle {
111 system_navigation_bar_color: Some(Color::new(0xFF000000)),
112 system_navigation_bar_icon_brightness: Some(Brightness::Light),
113 status_bar_icon_brightness: Some(Brightness::Light),
114 status_bar_brightness: Some(Brightness::Dark),
115 system_navigation_bar_divider_color: None,
116 system_navigation_bar_contrast_enforced: None,
117 status_bar_color: None,
118 system_status_bar_contrast_enforced: None,
119 };
120
121 /// System overlays should be drawn with a dark color. Intended for applications with a
122 /// light background.
123 pub const DARK: SystemUiOverlayStyle = SystemUiOverlayStyle {
124 system_navigation_bar_color: Some(Color::new(0xFF000000)),
125 system_navigation_bar_icon_brightness: Some(Brightness::Light),
126 status_bar_icon_brightness: Some(Brightness::Dark),
127 status_bar_brightness: Some(Brightness::Light),
128 system_navigation_bar_divider_color: None,
129 system_navigation_bar_contrast_enforced: None,
130 status_bar_color: None,
131 system_status_bar_contrast_enforced: None,
132 };
133
134 /// Creates a new [`SystemUiOverlayStyle`]; Dart's named arguments are the setters.
135 pub const fn new() -> SystemUiOverlayStyle {
136 SystemUiOverlayStyle {
137 system_navigation_bar_color: None,
138 system_navigation_bar_divider_color: None,
139 system_navigation_bar_icon_brightness: None,
140 system_navigation_bar_contrast_enforced: None,
141 status_bar_color: None,
142 status_bar_brightness: None,
143 status_bar_icon_brightness: None,
144 system_status_bar_contrast_enforced: None,
145 }
146 }
147
148 /// Dart `SystemUiOverlayStyle(systemNavigationBarColor:)`.
149 pub fn system_navigation_bar_color(mut self, color: Color) -> SystemUiOverlayStyle {
150 self.system_navigation_bar_color = Some(color);
151 self
152 }
153
154 /// Dart `SystemUiOverlayStyle(systemNavigationBarDividerColor:)`.
155 pub fn system_navigation_bar_divider_color(mut self, color: Color) -> SystemUiOverlayStyle {
156 self.system_navigation_bar_divider_color = Some(color);
157 self
158 }
159
160 /// Dart `SystemUiOverlayStyle(systemNavigationBarIconBrightness:)`.
161 pub fn system_navigation_bar_icon_brightness(
162 mut self,
163 brightness: Brightness,
164 ) -> SystemUiOverlayStyle {
165 self.system_navigation_bar_icon_brightness = Some(brightness);
166 self
167 }
168
169 /// Dart `SystemUiOverlayStyle(systemNavigationBarContrastEnforced:)`.
170 pub fn system_navigation_bar_contrast_enforced(
171 mut self,
172 enforced: bool,
173 ) -> SystemUiOverlayStyle {
174 self.system_navigation_bar_contrast_enforced = Some(enforced);
175 self
176 }
177
178 /// Dart `SystemUiOverlayStyle(statusBarColor:)`.
179 pub fn status_bar_color(mut self, color: Color) -> SystemUiOverlayStyle {
180 self.status_bar_color = Some(color);
181 self
182 }
183
184 /// Dart `SystemUiOverlayStyle(statusBarBrightness:)`.
185 pub fn status_bar_brightness(mut self, brightness: Brightness) -> SystemUiOverlayStyle {
186 self.status_bar_brightness = Some(brightness);
187 self
188 }
189
190 /// Dart `SystemUiOverlayStyle(statusBarIconBrightness:)`.
191 pub fn status_bar_icon_brightness(mut self, brightness: Brightness) -> SystemUiOverlayStyle {
192 self.status_bar_icon_brightness = Some(brightness);
193 self
194 }
195
196 /// Dart `SystemUiOverlayStyle(systemStatusBarContrastEnforced:)`.
197 pub fn system_status_bar_contrast_enforced(mut self, enforced: bool) -> SystemUiOverlayStyle {
198 self.system_status_bar_contrast_enforced = Some(enforced);
199 self
200 }
201
202 /// Creates a copy of this theme with the given fields replaced with new values.
203 pub fn copy_with(&self) -> SystemUiOverlayStyle {
204 *self
205 }
206}
207
208/// The kind of haptic feedback to play.
209///
210/// Flutter counterpart: the `HapticFeedbackType.*` argument Dart sends with the
211/// `HapticFeedback.vibrate` message on `SystemChannels.platform`. It lives here
212/// because [`Platform`] names it.
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
214pub enum HapticFeedbackType {
215 /// Vibration for a short duration, the argument-less `HapticFeedback.vibrate`.
216 Vibrate,
217 /// A collision impact with a light mass.
218 LightImpact,
219 /// A collision impact with a medium mass.
220 MediumImpact,
221 /// A collision impact with a heavy mass.
222 HeavyImpact,
223 /// A selection changing through discrete values.
224 SelectionClick,
225 /// A task or action completed successfully.
226 SuccessNotification,
227 /// A task or action produced a warning.
228 WarningNotification,
229 /// A task or action failed.
230 ErrorNotification,
231}
232
233/// Describes the contrast of a theme or color palette.
234///
235/// Flutter counterpart: `Brightness` (`dart:ui` `window.dart`).
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
237pub enum Brightness {
238 /// The color is dark and will require a light text color to achieve readable
239 /// contrast.
240 ///
241 /// For example, the color might be dark grey, requiring white text.
242 Dark,
243
244 /// The color is light and will require a dark text color to achieve readable
245 /// contrast.
246 ///
247 /// For example, the color might be bright white, requiring black text.
248 Light,
249}
250
251/// States that an application can be in once it is running.
252///
253/// States not supported on a platform are synthesized so the state machine stays the same
254/// everywhere. For example, [`Hidden`](AppLifecycleState::Hidden) is synthesized on mobile
255/// before [`Paused`](AppLifecycleState::Paused) when coming from
256/// [`Inactive`](AppLifecycleState::Inactive), and before [`Inactive`](AppLifecycleState::Inactive)
257/// when coming from [`Paused`](AppLifecycleState::Paused).
258///
259/// The values below are listed in the expected state machine transition order. The initial
260/// state is [`Detached`](AppLifecycleState::Detached).
261///
262/// Flutter counterpart: `AppLifecycleState` (`dart:ui` `platform_dispatcher.dart`).
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
264pub enum AppLifecycleState {
265 /// The application is still hosted by a Flutter engine but is detached from any host
266 /// views.
267 ///
268 /// The application defaults to this state before it initializes, and can be in this
269 /// state (applicable on Android, iOS, and web) after all views have been detached.
270 ///
271 /// When the application is in this state, the engine is running without a view.
272 ///
273 /// This state is only entered on iOS, Android, and web, although on all platforms it is
274 /// the default state before the application begins running.
275 Detached,
276
277 /// On all platforms, this state indicates that the application is in the default
278 /// running mode for a running application that has input focus and is visible.
279 ///
280 /// On Android, this state corresponds to the Flutter host view having focus while in
281 /// Android's "resumed" state. It is possible for the Flutter app to be in the
282 /// [`Inactive`](AppLifecycleState::Inactive) state while still being in Android's
283 /// "onResume" state if the app has lost focus but hasn't had `Activity.onPause` called
284 /// on it.
285 ///
286 /// On iOS and macOS, this corresponds to the app running in the foreground active
287 /// state.
288 Resumed,
289
290 /// At least one view of the application is visible, but none have input focus. The
291 /// application is otherwise running normally.
292 ///
293 /// On non-web desktop platforms, this corresponds to an application that is not in the
294 /// foreground, but still has visible windows.
295 ///
296 /// On the web, this corresponds to an application that is running in a window or tab
297 /// that does not have input focus.
298 ///
299 /// On iOS and macOS, this state corresponds to the Flutter host view running in the
300 /// foreground inactive state. Apps transition to this state when in a phone call, when
301 /// responding to a TouchID request, when entering the app switcher or the control
302 /// center, or when the UIViewController hosting the Flutter app is transitioning.
303 ///
304 /// On Android, this corresponds to the Flutter host view running in Android's paused
305 /// state (i.e. `Activity.onPause` has been called), or in Android's "resumed" state
306 /// (i.e. `Activity.onResume` has been called) but does not have window focus.
307 ///
308 /// On Android and iOS, apps in this state should assume that they may be
309 /// [`Hidden`](AppLifecycleState::Hidden) and [`Paused`](AppLifecycleState::Paused) at any
310 /// time.
311 Inactive,
312
313 /// All views of an application are hidden, either because the application is about to
314 /// be paused (on iOS and Android), or because it has been minimized or placed on a
315 /// desktop that is no longer visible (on non-web desktop), or is running in a window or
316 /// tab that is no longer visible (on the web).
317 ///
318 /// On iOS and Android, in order to keep the state machine the same on all platforms, a
319 /// transition to this state is synthesized before the [`Paused`](AppLifecycleState::Paused)
320 /// state is entered when coming from [`Inactive`](AppLifecycleState::Inactive), and
321 /// before the [`Inactive`](AppLifecycleState::Inactive) state is entered when coming
322 /// from [`Paused`](AppLifecycleState::Paused). This allows cross-platform implementations
323 /// that want to know when an app is conceptually "hidden" to only write one handler.
324 Hidden,
325
326 /// The application is not currently visible to the user, and not responding to user
327 /// input.
328 ///
329 /// When the application is in this state, the engine will not call
330 /// `PlatformDispatcher.onBeginFrame` and `PlatformDispatcher.onDrawFrame`.
331 ///
332 /// This state is only entered on iOS and Android.
333 Paused,
334}
335
336/// The possible responses to a request to exit the application.
337///
338/// The request is typically responded to by creating an `AppLifecycleListener` and supplying
339/// an `on_exit_requested` callback, or by overriding
340/// `WidgetsBindingObserver::did_request_app_exit`.
341///
342/// Flutter counterpart: `AppExitResponse` (`dart:ui` `platform_dispatcher.dart`).
343#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
344pub enum AppExitResponse {
345 /// Exiting the application can proceed.
346 Exit,
347
348 /// Cancel the exit: do not exit the application.
349 Cancel,
350}
351
352/// The answer to one request to exit, which the host made through
353/// [`EmbedderClient::request_app_exit`](crate::EmbedderClient::request_app_exit): Flutter's reply
354/// to `System.requestAppExit`. It is answered once; dropped unanswered, it answers
355/// [`AppExitResponse::Exit`], since an application that lets go of the question has nothing to
356/// keep.
357pub struct AppExitReply(Option<Box<dyn FnOnce(AppExitResponse)>>);
358
359impl AppExitReply {
360 /// A reply that hands its answer to `send`. The answer is given inside a call into the
361 /// client, so `send` may only post it to the host's own loop, never call the client.
362 pub fn new(send: impl FnOnce(AppExitResponse) + 'static) -> AppExitReply {
363 AppExitReply(Some(Box::new(send)))
364 }
365
366 pub fn answer(mut self, response: AppExitResponse) {
367 self.send(response);
368 }
369
370 fn send(&mut self, response: AppExitResponse) {
371 if let Some(send) = self.0.take() {
372 send(response);
373 }
374 }
375}
376
377impl Drop for AppExitReply {
378 fn drop(&mut self) {
379 self.send(AppExitResponse::Exit);
380 }
381}
382
383impl std::fmt::Debug for AppExitReply {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 f.debug_struct("AppExitReply")
386 .field("answered", &self.0.is_none())
387 .finish()
388 }
389}
390
391/// The type of application exit to perform when calling `ServicesBinding::exit_application`.
392///
393/// Flutter counterpart: `AppExitType` (`dart:ui` `platform_dispatcher.dart`).
394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
395pub enum AppExitType {
396 /// Requests that the application start an orderly exit, sending a request back to the
397 /// framework through the `WidgetsBinding`. If that responds with [`AppExitResponse::Exit`],
398 /// then proceed with the same steps as a [`Required`](AppExitType::Required) exit. If that
399 /// responds with [`AppExitResponse::Cancel`], then the exit request is canceled and the
400 /// application continues executing normally.
401 Cancelable,
402
403 /// A non-cancelable orderly exit request. The engine will shut down the engine and call
404 /// the native UI toolkit's exit API.
405 ///
406 /// If you need an even faster and more dangerous exit, then call
407 /// [`std::process::exit`] directly, and even the native toolkit's exit API won't be
408 /// called. This is quite dangerous, though, since it's possible that the engine will
409 /// crash because it hasn't been properly shut down, causing the app to crash on exit.
410 Required,
411}
412
413/// The platform that user interaction should adapt to target.
414///
415/// Flutter counterpart: `TargetPlatform` (`foundation/platform.dart`). Flutter
416/// reads the current one from a library global that consults `dart:io`. Here it
417/// is a plain value type, and the App-owned [`Platform`] reports which one it
418/// is, so two Apps in one process can differ.
419#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
420pub enum TargetPlatform {
421 /// Android: <https://www.android.com/>
422 Android,
423 /// Fuchsia: <https://fuchsia.dev/fuchsia-src/concepts>
424 Fuchsia,
425 /// iOS: <https://www.apple.com/ios/>
426 IOS,
427 /// Linux: <https://www.linux.org>
428 Linux,
429 /// macOS: <https://www.apple.com/macos>
430 MacOS,
431 /// Windows: <https://www.windows.com>
432 Windows,
433}
434
435/// The OS this build targets, as Flutter names it; `None` for one Flutter does not name, such as
436/// a web page's or a WebAssembly component's.
437const THIS_OS: Option<TargetPlatform> = if cfg!(target_os = "android") {
438 Some(TargetPlatform::Android)
439} else if cfg!(target_os = "fuchsia") {
440 Some(TargetPlatform::Fuchsia)
441} else if cfg!(target_os = "ios") {
442 Some(TargetPlatform::IOS)
443} else if cfg!(target_os = "linux") {
444 Some(TargetPlatform::Linux)
445} else if cfg!(target_os = "macos") {
446 Some(TargetPlatform::MacOS)
447} else if cfg!(target_os = "windows") {
448 Some(TargetPlatform::Windows)
449} else {
450 None
451};
452
453/// One line of a popup menu the host shows with its own menu system.
454#[derive(Clone, Debug, PartialEq, Eq)]
455pub enum PopupMenuEntry {
456 Item {
457 label: String,
458 enabled: bool,
459 /// Drawn with a check mark, for a setting the menu toggles.
460 checked: bool,
461 },
462 Separator,
463}
464
465impl PopupMenuEntry {
466 pub fn item(label: impl Into<String>) -> PopupMenuEntry {
467 PopupMenuEntry::Item {
468 label: label.into(),
469 enabled: true,
470 checked: false,
471 }
472 }
473
474 pub fn checked(self, checked: bool) -> PopupMenuEntry {
475 match self {
476 PopupMenuEntry::Item { label, enabled, .. } => PopupMenuEntry::Item {
477 label,
478 enabled,
479 checked,
480 },
481 separator => separator,
482 }
483 }
484
485 pub fn enabled(self, enabled: bool) -> PopupMenuEntry {
486 match self {
487 PopupMenuEntry::Item { label, checked, .. } => PopupMenuEntry::Item {
488 label,
489 enabled,
490 checked,
491 },
492 separator => separator,
493 }
494 }
495}
496
497/// The host's threads as any thread reaches them: the two things the framework asks of them.
498/// gpui's `PlatformDispatcher`. Flutter's embedder API has the first as `post_task_callback`,
499/// with its target time, and keeps the second — the engine's own worker threads — to itself.
500pub trait Dispatcher: Send + Sync {
501 /// Sets when the host should next wake the application for a timer.
502 ///
503 /// The host keeps one such time. When it arrives, the host calls
504 /// [`EmbedderClient::wake`](crate::EmbedderClient::wake) on the main thread.
505 ///
506 /// `None` means no timer is waiting, so the host should not wake for one.
507 ///
508 /// Each call replaces the previous one. The new time can be later than the old one, not
509 /// only earlier. Call this only from the thread the application runs on.
510 fn wake_at(&self, timer_wakeup: Option<Instant>);
511
512 /// Asks the host to wake the application as soon as it can, because a task became ready
513 /// to run.
514 ///
515 /// The next wake clears this request. It does not change the timer wakeup set by
516 /// [`wake_at`](Self::wake_at), which is still waiting for its own time. Call this from
517 /// any thread.
518 fn wake_now(&self);
519
520 /// Runs `work` off the main thread, on whatever the host has for that, such as a system
521 /// queue or a worker thread. The framework's `run_in_background` sends its work here.
522 fn dispatch(&self, work: Box<dyn FnOnce() + Send>);
523}
524
525/// A dispatcher for tests, which pump the application by hand: it never wakes the application,
526/// and work runs at once on the calling thread, so its result is there at the next checkpoint.
527pub struct InertDispatcher;
528
529impl Dispatcher for InertDispatcher {
530 fn wake_at(&self, _timer_wakeup: Option<Instant>) {}
531
532 fn wake_now(&self) {}
533
534 fn dispatch(&self, work: Box<dyn FnOnce() + Send>) {
535 work();
536 }
537}
538
539/// The long-lived host object held by the application.
540///
541/// Implementations must queue requests and return. They must not synchronously
542/// re-enter the [`EmbedderClient`](crate::EmbedderClient) while `App` is active.
543///
544/// A host may offer more than this interface — a picture from a buffer only its system
545/// has, a handle to its renderer. An app that knows its host reaches that through
546/// [`downcast_ref`](dyn Platform::downcast_ref); the interface itself names nothing of any
547/// one system.
548pub trait Platform: Any {
549 /// Which host this is, for behaviour that follows platform convention
550 /// (Flutter `defaultTargetPlatform`).
551 ///
552 /// Required rather than defaulted: an embedder must say what it is, and a
553 /// default would let one silently claim the wrong conventions.
554 fn target_platform(&self) -> TargetPlatform;
555
556 /// Asks for a frame of `view`: Flutter's `PlatformDispatcher.scheduleFrame`, for one view.
557 ///
558 /// `view` is in the next [`EmbedderClient::frame`](crate::EmbedderClient::frame) of the
559 /// screen it is on once its window is in sight; the host holds the request while the window
560 /// is minimized or occluded. Asked again before then, it is in that refresh once.
561 fn request_frame(&self, view: ViewId);
562
563 /// The host clock used for frame and timer timestamps.
564 fn now(&self) -> Instant;
565
566 /// The host's threads, for waking the application or running work off the main thread: see
567 /// [`Dispatcher`].
568 fn dispatcher(&self) -> Arc<dyn Dispatcher>;
569
570 /// Every view the app has now: those the platform gave it, such as a phone's screen or an
571 /// embedder's views, and those it asked for. An app may run with none, as one on a desktop
572 /// does until it asks.
573 fn views(&self) -> Vec<ViewRef>;
574
575 /// Looks up one of the app's views.
576 fn view(&self, id: ViewId) -> Option<ViewRef>;
577
578 /// Asks for a view. A desktop makes a window for it; a platform that gives the app its
579 /// views and makes none, a phone or an embedder, answers [`ViewError::Unsupported`](crate::ViewError::Unsupported).
580 /// The view arrives later, through `EmbedderClient::view_added`, carrying the id answered
581 /// here; a request that fails on the way is reported through
582 /// `EmbedderClient::view_request_failed`.
583 fn request_view(
584 &self,
585 request: crate::ViewRequest,
586 ) -> Result<crate::RequestId, crate::ViewError> {
587 let _ = request;
588 Err(crate::ViewError::Unsupported)
589 }
590
591 /// Flutter `PlatformDispatcher.requestViewFocusChange`: asks the host to move view focus.
592 /// A host without native focus support ignores the request.
593 fn request_view_focus_change(
594 &self,
595 view_id: ViewId,
596 state: ViewFocusState,
597 direction: ViewFocusDirection,
598 ) {
599 let _ = (view_id, state, direction);
600 }
601
602 /// The full system-reported supported locales of the device (Flutter
603 /// `PlatformDispatcher.locales`), in order of preference; empty until the host
604 /// reports them.
605 fn locales(&self) -> Vec<crate::Locale> {
606 Vec::new()
607 }
608
609 /// Tells the host which locale the application resolved to (Flutter
610 /// `PlatformDispatcher.setApplicationLocale`); the default drops it.
611 fn set_application_locale(&self, locale: &crate::Locale) {
612 let _ = locale;
613 }
614
615 /// The platform's light/dark preference (Flutter
616 /// `PlatformDispatcher.platformBrightness`).
617 ///
618 /// Defaults to [`Brightness::Light`], matching Flutter's view configuration
619 /// default. A live host that can see the OS theme overrides this.
620 fn platform_brightness(&self) -> Brightness {
621 Brightness::Light
622 }
623
624 /// The route the application was started with (Flutter
625 /// `PlatformDispatcher.defaultRouteName`).
626 ///
627 /// Defaults to `"/"`, Flutter's value for a host that was not asked to open a
628 /// particular route; a host that receives a deep link answers it here.
629 fn default_route_name(&self) -> String {
630 String::from("/")
631 }
632
633 /// The lifecycle state immediately after the application started (Flutter
634 /// `PlatformDispatcher.initialLifecycleState`).
635 ///
636 /// This property will not be updated as the lifecycle changes: a host reports each change
637 /// through `EmbedderClient::app_lifecycle_state_changed`.
638 ///
639 /// It is used to initialize `SchedulerBinding::lifecycle_state` at startup. Defaults to
640 /// `None`, Flutter's empty string: a host that has no state to report yet.
641 fn initial_lifecycle_state(&self) -> Option<AppLifecycleState> {
642 None
643 }
644
645 /// The platform's own font lookup: faces by family name and by codepoint, the way
646 /// Flutter's engine asks the OS. `None` for a host without one; text then shapes only
647 /// against fonts the application registers.
648 fn font_source(&self) -> Option<Box<dyn FontSource>> {
649 None
650 }
651
652 /// Flutter's `txt::GetDefaultFontFamilies()`: the families text uses when its style names
653 /// none, first match first.
654 ///
655 /// Defaults to the engine's answer on the OS this build targets
656 /// ([`default_font_families`](crate::default_font_families)). A host that learns its OS at
657 /// run time answers for that OS, and one whose fonts are not the OS's, as a web page's are
658 /// not, answers its own.
659 fn default_font_families(&self) -> Vec<String> {
660 match THIS_OS {
661 Some(os) => crate::default_font_families(os),
662 // `txt/platform.cc`: the engine's answer on an OS it has no file of its own for.
663 None => vec![String::from("Arial")],
664 }
665 }
666
667 /// Open an encoded image, answering with a codec for its frames.
668 ///
669 /// Flutter dart:ui `instantiateImageCodec`. The host decodes with whatever its platform
670 /// provides and uploads each frame, so the framework never sees pixels and the set of
671 /// readable formats is the platform's rather than the framework's.
672 ///
673 /// The default answers a failure at once, so a host with no decoder leaves nobody waiting.
674 fn open_image_codec(&self, bytes: std::sync::Arc<[u8]>) -> crate::ImageCodecFuture {
675 let _ = bytes;
676 Box::pin(std::future::ready(Err(crate::ImageDecodeError::NoDecoder)))
677 }
678
679 /// An image from pixels the app already holds, with no codec in between: Flutter's
680 /// `decodeImageFromPixels`. `None` when the host has no renderer to put it on yet.
681 fn import_pixels(&self, pixels: valo::PixelBuffer) -> Option<crate::Image> {
682 let _ = pixels;
683 None
684 }
685
686 /// The host's compositor, where it has one: native surfaces shown beside the
687 /// framework's picture. `None` on the web, on X11, on the inert host, and on an
688 /// embedder that chose not to have one.
689 fn compositor(&self) -> Option<Arc<dyn crate::Compositor>> {
690 None
691 }
692
693 /// Flutter's `VsyncWaiter`, for any screen: the refreshes the host draws the framework's
694 /// frames at, for a producer that paces by them, as a native surface's does. `None` for a
695 /// host without screen ticks.
696 fn vsync_waiter(&self) -> Option<Arc<dyn crate::VsyncWaiter>> {
697 None
698 }
699
700 /// The screens the app's views are on, and where the pointer is on them (Flutter
701 /// `PlatformDispatcher.displays`), or `None` where the host has none to report. Every host
702 /// with screens answers, a phone with its one included; a view's metrics name the screen it
703 /// is on, and a host that hears the screens change says so through
704 /// `EmbedderClient::screens_changed`.
705 fn screens(&self) -> Option<&dyn Screens> {
706 None
707 }
708
709 /// The system clipboard, or `None` where the host has none.
710 fn clipboard(&self) -> Option<&dyn Clipboard> {
711 None
712 }
713
714 /// The system UI drawn around the app, or `None` where the host has none.
715 fn system_chrome(&self) -> Option<&dyn SystemChrome> {
716 None
717 }
718
719 /// Haptic feedback on the device, or `None` where the host has none.
720 fn haptics(&self) -> Option<&dyn Haptics> {
721 None
722 }
723
724 /// The system's own menu over a text selection, or `None` where the host has none.
725 fn system_context_menu(&self) -> Option<&dyn SystemContextMenu> {
726 None
727 }
728
729 /// Native pop-up menus shown at the pointer, or `None` where the host has none.
730 fn popup_menus(&self) -> Option<&dyn PopupMenus> {
731 None
732 }
733
734 /// The system services a selection toolbar offers, or `None` where the host has none.
735 fn text_services(&self) -> Option<&dyn TextServices> {
736 None
737 }
738
739 /// The system cursor shown for a pointing device, or `None` where the host has none.
740 fn mouse_cursor(&self) -> Option<&dyn MouseCursor> {
741 None
742 }
743
744 /// Application state kept across relaunches, or `None` where the host has none.
745 fn restoration(&self) -> Option<&dyn Restoration> {
746 None
747 }
748
749 /// Quitting at the application's own request, or `None` where the host cannot quit
750 /// itself: a phone, a browser tab.
751 fn app_exit(&self) -> Option<&dyn AppExit> {
752 None
753 }
754
755 /// The files the system asks the app to open, or `None` where the system never asks: it
756 /// starts the app with them on its command line instead, or has no files to open.
757 fn file_opening(&self) -> Option<&dyn FileOpening> {
758 None
759 }
760}
761
762impl dyn Platform {
763 /// The host as its own type, for what it offers beyond the interface.
764 pub fn downcast_ref<T: Platform>(&self) -> Option<&T> {
765 (self as &dyn Any).downcast_ref::<T>()
766 }
767}
768
769/// The system clipboard: Flutter's `Clipboard` over the platform channel. Every call answers
770/// later, as Flutter's does: a host may have to ask its user, or another process, first.
771pub trait Clipboard {
772 /// Stores plain text on the system clipboard (Flutter's `Clipboard.setData`), and completes
773 /// once it is stored.
774 fn set_text(&self, text: &str) -> ClipboardSetTextFuture;
775
776 /// Retrieves plain text from the system clipboard (Flutter's `Clipboard.getData`); `None`
777 /// when it holds none, or the host could not read it.
778 fn text(&self) -> ClipboardTextFuture;
779
780 /// Whether the clipboard contains string data (Flutter's `Clipboard.hasStrings`).
781 fn has_strings(&self) -> ClipboardHasStringsFuture;
782}
783
784/// The clipboard's text being stored. Flutter's `Future<void>` from `Clipboard.setData`.
785pub type ClipboardSetTextFuture = Pin<Box<dyn Future<Output = ()>>>;
786
787/// The clipboard's text on its way. Flutter's `Future<ClipboardData?>`.
788pub type ClipboardTextFuture = Pin<Box<dyn Future<Output = Option<String>>>>;
789
790/// Whether the clipboard holds text, on its way. Flutter's `Future<bool>`.
791pub type ClipboardHasStringsFuture = Pin<Box<dyn Future<Output = bool>>>;
792
793/// Flutter's `SystemChrome`: the system UI around the app.
794pub trait SystemChrome {
795 /// Styles the system overlays the host draws over the app — the status bar, and on
796 /// Android the system navigation bar (Flutter `SystemChrome.setSystemUIOverlayStyle`).
797 fn set_overlay_style(&self, style: &SystemUiOverlayStyle);
798
799 /// Describes the app in the host's application switcher (Flutter
800 /// `SystemChrome.setApplicationSwitcherDescription`).
801 fn set_application_switcher_description(&self, description: &ApplicationSwitcherDescription);
802}
803
804/// Flutter's `HapticFeedback`.
805pub trait Haptics {
806 /// Plays haptic feedback on the device (Flutter's `HapticFeedback.vibrate`
807 /// message on `SystemChannels.platform`).
808 fn feedback(&self, kind: HapticFeedbackType);
809}
810
811/// Flutter's `SystemContextMenu`: the system's own menu over a text selection.
812pub trait SystemContextMenu {
813 /// Shows the system context menu (Flutter's `ContextMenu.showSystemContextMenu`).
814 ///
815 /// `items` is `None` for the deprecated call that lets the platform pick default
816 /// buttons. Built-in items are performed by the host; custom items are reported
817 /// through [`crate::EmbedderClient::custom_context_menu_action`].
818 fn show(&self, target_rect: Rect, items: Option<&[SystemContextMenuItem]>);
819
820 /// Hides the system context menu (Flutter's `ContextMenu.hideSystemContextMenu`).
821 fn hide(&self);
822}
823
824/// A native pop-up menu shown at the pointer, answered with the index chosen.
825pub trait PopupMenus {
826 /// Shows `entries` as the platform's own popup menu at the pointer; answers the index of the
827 /// entry chosen once it closes, `None` when it closed with none chosen. Not a Flutter call:
828 /// Flutter's context menus draw inside the view, and a desktop panel narrower than its menu
829 /// needs the system's, which is its own window.
830 ///
831 /// The host opens the menu after this call returns and runs the menu's own loop outside every
832 /// call into the app, so the app's frames, timers and events go on while it is open, as they
833 /// do in Flutter, whose platform calls answer later.
834 fn show(&self, entries: &[PopupMenuEntry]) -> PopupMenuFuture;
835}
836
837/// A popup menu on screen: the index of the entry chosen, once it closes. Flutter's `Future<int?>`
838/// from a platform call.
839pub type PopupMenuFuture = Pin<Box<dyn Future<Output = Option<usize>>>>;
840
841/// The screens the app's views are on: what drawing on each needs, as Flutter's
842/// `PlatformDispatcher.displays` lists them, and where each is, for an app that places a window
843/// against something on them, such as an edge or its own icon, which Flutter's `Display` has no
844/// place for. Positions are in the coordinates of [`HostWindow::frame`](crate::HostWindow::frame):
845/// logical points from the top-left corner of the main screen, y growing down.
846pub trait Screens {
847 /// The screens, the main one first: the one positions start from.
848 fn screens(&self) -> Vec<Screen>;
849
850 /// Where the pointer is now, over a window of the app or not. `None` when the host cannot
851 /// say.
852 fn pointer_location(&self) -> Option<Offset>;
853}
854
855/// Names a screen: Flutter's `Display.id`, which the embedder API calls a
856/// `FlutterEngineDisplayId`. Each host numbers its screens its own way; a screen keeps its
857/// number while it stays connected.
858#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
859pub struct ScreenId(pub u64);
860
861/// A monitor: where it is (the frames, for placing windows) and what drawing on it needs
862/// (Flutter's `Display`: `id`, `devicePixelRatio`, `refreshRate`; `Display.size` is the frame's
863/// size at `scale`).
864#[derive(Clone, Debug, PartialEq)]
865pub struct Screen {
866 /// Flutter `Display.id`; stable while the screen stays connected.
867 pub id: ScreenId,
868 /// What the system calls it: "Built-in Retina Display". Empty where the host has no name
869 /// for it.
870 pub name: String,
871 /// All of it, in points, placed on the desktop.
872 pub frame: Rect,
873 /// What the system's own bars leave free: the menu bar and the Dock, or the taskbar. The
874 /// whole frame where the host cannot say.
875 pub visible_frame: Rect,
876 /// Flutter `Display.devicePixelRatio`: physical pixels per point, as on every view on the
877 /// screen.
878 pub scale: f64,
879 /// Flutter `Display.refreshRate`, in hertz; none where the host cannot say.
880 pub refresh_rate: Option<f64>,
881}
882
883/// The text services a selection toolbar offers: Flutter's LookUp, SearchWeb and Share
884/// platform-channel calls.
885pub trait TextServices {
886 /// Looks up the given text (Flutter's `LookUp.invoke`).
887 fn look_up(&self, text: &str);
888
889 /// Searches the web for the given text (Flutter's `SearchWeb.invoke`).
890 fn search_web(&self, text: &str);
891
892 /// Shares the given text (Flutter's `Share.invoke`).
893 fn share(&self, text: &str);
894}
895
896/// Flutter's mouse cursor channel.
897pub trait MouseCursor {
898 /// Shows a system cursor for a pointing device on `view`, the one the device is over
899 /// (Flutter's `activateSystemCursor` message on `SystemChannels.mouseCursor`, which
900 /// carries no view: its embedders map the device to one themselves). A host with one
901 /// cursor for the whole process ignores the view.
902 fn activate_system_cursor(&self, view: ViewId, device: i64, kind: SystemMouseCursorKind);
903}
904
905/// Flutter's restoration channel: state kept across relaunches.
906pub trait Restoration {
907 /// The restoration data the host kept for this application (the `get` message of
908 /// Flutter's `SystemChannels.restoration`).
909 fn get(&self) -> Option<RestorationUpdate>;
910
911 /// Hands the host the current restoration data (the `put` message of Flutter's
912 /// `SystemChannels.restoration`), which keeps it until the operating system asks for it.
913 fn put(&self, data: RestorationMap);
914}
915
916/// The application quitting itself: the `System.exitApplication` message of Flutter's
917/// `SystemChannels.platform`, for a required exit. A cancelable exit never reaches the host: the
918/// framework asks its own listeners first (`ServicesBinding::exit_application`), and quits here
919/// once they agree.
920pub trait AppExit {
921 /// Quits the application the way the system quits one, as Flutter's embedders do, on a
922 /// later turn of the host's loop, never inside this call.
923 ///
924 /// `exit_code` is the process's exit status where the system takes one; Flutter's macOS
925 /// and Linux embedders ignore it, and a host may too.
926 fn quit(&self, exit_code: i32);
927}
928
929/// The files the system asks the app to open: documents opened from the system's file browser, or
930/// dropped on the app's icon, at launch or while it runs. Flutter's macOS embedder hands them to
931/// its plugins (`FlutterAppLifecycleDelegate.handleOpenURLs:`), outside the framework.
932pub trait FileOpening {
933 /// What happens with the files the system asks the app to open. The host calls `handler`
934 /// with each request's files on the main thread, outside every framework call, so it may
935 /// post to the app. Files asked for before a handler is set wait for one: a launch asks for
936 /// its files before the app has started.
937 fn set_open_files_handler(&self, handler: Option<OpenFilesHandler>);
938}
939
940/// A handler for the files the system asks the app to open.
941pub type OpenFilesHandler = Rc<dyn Fn(Vec<PathBuf>)>;
942
943/// Platform for hand-pumped tests.
944pub struct InertPlatform;
945
946impl Platform for InertPlatform {
947 /// Android, which is what Flutter's own test binding defaults
948 /// `debugDefaultTargetPlatform` to: a bare App has no host to speak of,
949 /// and a deterministic answer beats the machine's.
950 fn target_platform(&self) -> TargetPlatform {
951 TargetPlatform::Android
952 }
953
954 fn request_frame(&self, _view: ViewId) {}
955
956 fn now(&self) -> Instant {
957 Instant::now()
958 }
959
960 fn dispatcher(&self) -> Arc<dyn Dispatcher> {
961 Arc::new(InertDispatcher)
962 }
963
964 fn views(&self) -> Vec<ViewRef> {
965 Vec::new()
966 }
967
968 fn view(&self, _id: ViewId) -> Option<ViewRef> {
969 None
970 }
971}
972
973/// A refresh of a screen, as the host delivers it.
974#[derive(Clone, Copy, Debug, PartialEq, Eq)]
975pub struct Frame {
976 pub elapsed: Duration,
977}
978
979#[cfg(test)]
980mod tests {
981 use super::{Brightness, InertPlatform, Platform, TargetPlatform};
982
983 #[test]
984 fn inert_platform_is_android() {
985 assert_eq!(InertPlatform.target_platform(), TargetPlatform::Android);
986 }
987
988 #[test]
989 fn inert_platform_brightness_is_light() {
990 assert_eq!(InertPlatform.platform_brightness(), Brightness::Light);
991 }
992
993 #[test]
994 fn a_platform_downcasts_to_its_own_type_and_no_other() {
995 struct Other;
996 impl Platform for Other {
997 fn target_platform(&self) -> TargetPlatform {
998 TargetPlatform::Linux
999 }
1000 fn request_frame(&self, _view: super::ViewId) {}
1001 fn now(&self) -> super::Instant {
1002 super::Instant::now()
1003 }
1004 fn dispatcher(&self) -> std::sync::Arc<dyn super::Dispatcher> {
1005 std::sync::Arc::new(super::InertDispatcher)
1006 }
1007 fn views(&self) -> Vec<super::ViewRef> {
1008 Vec::new()
1009 }
1010 fn view(&self, _id: super::ViewId) -> Option<super::ViewRef> {
1011 None
1012 }
1013 }
1014 let platform: super::PlatformRef = std::rc::Rc::new(InertPlatform);
1015 assert!(platform.downcast_ref::<InertPlatform>().is_some());
1016 assert!(platform.downcast_ref::<Other>().is_none());
1017 }
1018}