i_slint_core/platform.rs
1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5The backend is the abstraction for crates that need to do the actual drawing and event loop
6*/
7
8#![warn(missing_docs)]
9
10use crate::SharedString;
11pub use crate::api::PlatformError;
12use crate::api::{LogicalPosition, LogicalSize};
13pub use crate::renderer::Renderer;
14#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
15use crate::unsafe_single_threaded::OnceCell;
16pub use crate::window::{LayoutConstraints, WindowAdapter, WindowProperties};
17use alloc::boxed::Box;
18use alloc::rc::Rc;
19use alloc::string::String;
20#[cfg(all(feature = "std", not(target_os = "android")))]
21use once_cell::sync::OnceCell;
22#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
23use std::time;
24#[cfg(target_arch = "wasm32")]
25use web_time as time;
26
27/// This trait defines the interface between Slint and platform APIs typically provided by operating and windowing systems.
28pub trait Platform {
29 /// Instantiate a window for a component.
30 fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError>;
31
32 /// Spins an event loop and renders the visible windows.
33 fn run_event_loop(&self) -> Result<(), PlatformError> {
34 Err(PlatformError::NoEventLoopProvider)
35 }
36
37 /// Spins an event loop for a specified period of time.
38 ///
39 /// This function is similar to `run_event_loop()` with two differences:
40 /// * The function is expected to return after the provided timeout, but
41 /// allow for subsequent invocations to resume the previous loop. The
42 /// function can return earlier if the loop was terminated otherwise,
43 /// for example by `quit_event_loop()` or a last-window-closed mechanism.
44 /// * If the timeout is zero, the implementation should merely peek and
45 /// process any pending events, but then return immediately.
46 ///
47 /// When the function returns `ControlFlow::Continue`, it is assumed that
48 /// the loop remains intact and that in the future the caller should call
49 /// `process_events()` again, to permit the user to continue to interact with
50 /// windows.
51 /// When the function returns `ControlFlow::Break`, it is assumed that the
52 /// event loop was terminated. Any subsequent calls to `process_events()`
53 /// will start the event loop afresh.
54 #[doc(hidden)]
55 fn process_events(
56 &self,
57 _timeout: core::time::Duration,
58 _: crate::InternalToken,
59 ) -> Result<core::ops::ControlFlow<()>, PlatformError> {
60 Err(PlatformError::NoEventLoopProvider)
61 }
62
63 #[doc(hidden)]
64 #[deprecated(
65 note = "i-slint-core takes care of closing behavior. Application should call run_event_loop_until_quit"
66 )]
67 /// This is being phased out, see #1499.
68 fn set_event_loop_quit_on_last_window_closed(&self, quit_on_last_window_closed: bool) {
69 assert!(!quit_on_last_window_closed);
70 crate::context::GLOBAL_CONTEXT
71 .with(|ctx| (*ctx.get().unwrap().0.window_count.borrow_mut()) += 1);
72 }
73
74 /// Return an [`EventLoopProxy`] that can be used to send event to the event loop
75 ///
76 /// If this function returns `None` (the default implementation), then it will
77 /// not be possible to send event to the event loop and the function
78 /// [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) and
79 /// [`slint::quit_event_loop()`](crate::api::quit_event_loop) will panic. These
80 /// functions are used internally by `slint::spawn_local()`
81 /// and features like live_preview. Implementing this function is necessary for
82 /// aforementioned functionalities to work.
83 fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
84 None
85 }
86
87 /// Returns the current time as a monotonic duration since the start of the program
88 ///
89 /// This is used by the animations and timer to compute the elapsed time.
90 ///
91 /// When the `std` feature is enabled, this function is implemented in terms of
92 /// [`std::time::Instant::now()`], but on `#![no_std]` platform, this function must
93 /// be implemented.
94 fn duration_since_start(&self) -> core::time::Duration {
95 #[cfg(feature = "std")]
96 {
97 let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
98 let now = time::Instant::now();
99 assert!(now >= the_beginning, "The platform's clock is not monotonic!");
100 now - the_beginning
101 }
102 #[cfg(not(feature = "std"))]
103 unimplemented!("The platform abstraction must implement `duration_since_start`")
104 }
105
106 /// Returns the current interval to internal measure the duration to send a double click event.
107 ///
108 /// A double click event is a series of two pointer clicks.
109 fn click_interval(&self) -> core::time::Duration {
110 // 500ms is the default delay according to https://en.wikipedia.org/wiki/Double-click#Speed_and_timing
111 core::time::Duration::from_millis(500)
112 }
113
114 /// Returns the current rate at which the text cursor should flash or blink.
115 ///
116 /// This is the length of the entire visible-hidden-visible cycle, so for a duration of 1000ms
117 /// it is visible for 500ms then hidden for 500ms, then visible again.
118 ///
119 /// If this value is `Duration::ZERO` then the cycle is disabled.
120 fn cursor_flash_cycle(&self) -> core::time::Duration {
121 core::time::Duration::from_millis(1000)
122 }
123
124 /// Sends the given text into the system clipboard.
125 ///
126 /// If the platform doesn't support the specified clipboard, this function should do nothing
127 fn set_clipboard_text(&self, _text: &str, _clipboard: Clipboard) {}
128
129 /// Returns a copy of text stored in the system clipboard, if any.
130 ///
131 /// If the platform doesn't support the specified clipboard, the function should return None
132 fn clipboard_text(&self, _clipboard: Clipboard) -> Option<String> {
133 None
134 }
135
136 /// This function is called when debug() is used in .slint files. The implementation
137 /// should direct the output to some developer visible terminal. The default implementation
138 /// uses stderr if available, or `console.log` when targeting wasm.
139 fn debug_log(&self, _arguments: core::fmt::Arguments) {
140 crate::tests::default_debug_log(_arguments);
141 }
142
143 /// Opens the given URL in an external browser.
144 ///
145 /// Returns [`PlatformError::Unsupported`] if the platform doesn't support opening URLs.
146 fn open_url(&self, _url: &str) -> Result<(), PlatformError> {
147 Err(PlatformError::Unsupported)
148 }
149
150 #[cfg(target_os = "android")]
151 #[doc(hidden)]
152 /// The long press interval before showing a context menu
153 fn long_press_interval(&self, _: crate::InternalToken) -> core::time::Duration {
154 core::time::Duration::from_millis(500)
155 }
156}
157
158/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]
159#[repr(u8)]
160#[non_exhaustive]
161#[derive(PartialEq, Clone, Default)]
162pub enum Clipboard {
163 /// This is the default clipboard used for text action for Ctrl+V, Ctrl+C.
164 /// Corresponds to the secondary clipboard on X11.
165 #[default]
166 DefaultClipboard = 0,
167
168 /// This is the clipboard that is used when text is selected
169 /// Corresponds to the primary clipboard on X11.
170 /// The Platform implementation should do nothing if copy on select is not supported on that platform.
171 SelectionClipboard = 1,
172}
173
174/// Trait that is returned by the [`Platform::new_event_loop_proxy`]
175///
176/// This are the implementation details for the function that may need to
177/// communicate with the eventloop from different thread
178pub trait EventLoopProxy: Send + Sync {
179 /// Exits the event loop.
180 ///
181 /// This is what is called by [`slint::quit_event_loop()`](crate::api::quit_event_loop)
182 fn quit_event_loop(&self) -> Result<(), crate::api::EventLoopError>;
183
184 /// Invoke the function from the event loop.
185 ///
186 /// This is what is called by [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop)
187 fn invoke_from_event_loop(
188 &self,
189 event: Box<dyn FnOnce() + Send>,
190 ) -> Result<(), crate::api::EventLoopError>;
191}
192
193#[cfg(feature = "std")]
194static INITIAL_INSTANT: once_cell::sync::OnceCell<time::Instant> = once_cell::sync::OnceCell::new();
195
196#[cfg(feature = "std")]
197impl std::convert::From<crate::animations::Instant> for time::Instant {
198 fn from(our_instant: crate::animations::Instant) -> Self {
199 let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
200 the_beginning + core::time::Duration::from_millis(our_instant.0)
201 }
202}
203
204#[cfg(not(target_os = "android"))]
205static EVENTLOOP_PROXY: OnceCell<Box<dyn EventLoopProxy + 'static>> = OnceCell::new();
206
207// On android, we allow the platform to be reset and the global eventloop proxy to be replaced.
208#[cfg(target_os = "android")]
209static EVENTLOOP_PROXY: std::sync::Mutex<Option<Box<dyn EventLoopProxy + 'static>>> =
210 std::sync::Mutex::new(None);
211
212pub(crate) fn with_event_loop_proxy<R>(f: impl FnOnce(Option<&dyn EventLoopProxy>) -> R) -> R {
213 #[cfg(not(target_os = "android"))]
214 return f(EVENTLOOP_PROXY.get().map(core::ops::Deref::deref));
215 #[cfg(target_os = "android")]
216 return f(EVENTLOOP_PROXY.lock().unwrap().as_ref().map(core::ops::Deref::deref));
217}
218
219/// This enum describes the different error scenarios that may occur when [`set_platform`]
220/// fails.
221#[derive(Debug, Clone, PartialEq)]
222#[repr(C)]
223#[non_exhaustive]
224pub enum SetPlatformError {
225 /// The platform has already been initialized in an earlier call to [`set_platform`].
226 AlreadySet,
227}
228
229impl core::fmt::Display for SetPlatformError {
230 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231 match self {
232 SetPlatformError::AlreadySet => {
233 f.write_str("The platform has already been initialized.")
234 }
235 }
236 }
237}
238
239#[cfg(feature = "std")]
240impl std::error::Error for SetPlatformError {}
241
242/// Set the Slint platform abstraction.
243///
244/// If the platform abstraction was already set this will return `Err`.
245pub fn set_platform(platform: Box<dyn Platform + 'static>) -> Result<(), SetPlatformError> {
246 crate::context::GLOBAL_CONTEXT.with(|instance| {
247 if instance.get().is_some() {
248 return Err(SetPlatformError::AlreadySet);
249 }
250 if let Some(proxy) = platform.new_event_loop_proxy() {
251 #[cfg(not(target_os = "android"))]
252 {
253 EVENTLOOP_PROXY.set(proxy).map_err(|_| SetPlatformError::AlreadySet)?;
254 }
255 #[cfg(target_os = "android")]
256 {
257 *EVENTLOOP_PROXY.lock().unwrap() = Some(proxy);
258 }
259 }
260 instance
261 .set(crate::SlintContext::new(platform))
262 .map_err(|_| SetPlatformError::AlreadySet)
263 .unwrap();
264 // Ensure a sane starting point for the animation tick.
265 update_timers_and_animations();
266 Ok(())
267 })
268}
269
270/// Call this function to update and potentially activate any pending timers, as well
271/// as advance the state of any active animations.
272///
273/// This function should be called before rendering or processing input event, at the
274/// beginning of each event loop iteration.
275pub fn update_timers_and_animations() {
276 crate::animations::update_animations();
277 crate::timers::TimerList::maybe_activate_timers(crate::animations::Instant::now());
278 crate::properties::ChangeTracker::run_change_handlers();
279}
280
281/// Returns the duration before the next timer is expected to be activated. This is the
282/// largest amount of time that you can wait before calling [`update_timers_and_animations()`].
283///
284/// `None` is returned if there is no active timer.
285///
286/// Call this in your own event loop implementation to know how long the current thread can
287/// go to sleep. Note that this does not take currently activate animations into account.
288/// Only go to sleep if [`Window::has_active_animations()`](crate::api::Window::has_active_animations())
289/// returns false.
290pub fn duration_until_next_timer_update() -> Option<core::time::Duration> {
291 crate::timers::TimerList::next_timeout().map(|timeout| {
292 let duration_since_start = crate::context::GLOBAL_CONTEXT
293 .with(|p| p.get().map(|p| p.platform().duration_since_start()))
294 .unwrap_or_default();
295 core::time::Duration::from_millis(
296 timeout.0.saturating_sub(duration_since_start.as_millis() as u64),
297 )
298 })
299}
300
301// reexport key enum to the public api
302pub use crate::input::PointerEventButton;
303pub use crate::input::key_codes::Key;
304
305/// A event that describes user input or windowing system events.
306///
307/// Slint backends typically receive events from the windowing system, translate them to this
308/// enum and deliver them to the scene of items via [`slint::Window::try_dispatch_event()`](`crate::api::Window::try_dispatch_event()`).
309///
310/// The pointer variants describe events originating from an input device such as a mouse
311/// or a contact point on a touch-enabled surface.
312///
313/// All position fields are in logical window coordinates.
314#[allow(missing_docs)]
315#[derive(Debug, Clone, PartialEq)]
316#[non_exhaustive]
317#[repr(u32)]
318pub enum WindowEvent {
319 /// A pointer was pressed.
320 PointerPressed {
321 position: LogicalPosition,
322 /// The button that was pressed.
323 button: PointerEventButton,
324 },
325 /// A pointer was released.
326 PointerReleased {
327 position: LogicalPosition,
328 /// The button that was released.
329 button: PointerEventButton,
330 },
331 /// The position of the pointer has changed.
332 PointerMoved { position: LogicalPosition },
333 /// The wheel button of a mouse was rotated to initiate scrolling.
334 PointerScrolled {
335 position: LogicalPosition,
336 /// The amount of logical pixels to scroll in the horizontal direction.
337 delta_x: f32,
338 /// The amount of logical pixels to scroll in the vertical direction.
339 delta_y: f32,
340 },
341 /// The pointer exited the window.
342 PointerExited,
343 /// A key was pressed.
344 KeyPressed {
345 /// The unicode representation of the key pressed.
346 ///
347 /// # Example
348 /// A specific key can be mapped to a unicode by using the [`Key`] enum
349 /// ```rust
350 /// let _ = slint::platform::WindowEvent::KeyPressed { text: slint::platform::Key::Shift.into() };
351 /// ```
352 text: SharedString,
353 },
354 /// A key press was auto-repeated.
355 KeyPressRepeated {
356 /// The unicode representation of the key pressed.
357 ///
358 /// # Example
359 /// A specific key can be mapped to a unicode by using the [`Key`] enum
360 /// ```rust
361 /// let _ = slint::platform::WindowEvent::KeyPressRepeated { text: slint::platform::Key::Shift.into() };
362 /// ```
363 text: SharedString,
364 },
365 /// A key was released.
366 KeyReleased {
367 /// The unicode representation of the key released.
368 ///
369 /// # Example
370 /// A specific key can be mapped to a unicode by using the [`Key`] enum
371 /// ```rust
372 /// let _ = slint::platform::WindowEvent::KeyReleased { text: slint::platform::Key::Shift.into() };
373 /// ```
374 text: SharedString,
375 },
376 /// The window's scale factor has changed. This can happen for example when the display's resolution
377 /// changes, the user selects a new scale factor in the system settings, or the window is moved to a
378 /// different screen.
379 /// Platform implementations should dispatch this event also right after the initial window creation,
380 /// to set the initial scale factor the windowing system provided for the window.
381 ScaleFactorChanged {
382 /// The window system provided scale factor to map logical pixels to physical pixels.
383 scale_factor: f32,
384 },
385 /// The window was resized.
386 ///
387 /// The backend must send this event to ensure that the `width` and `height` property of the root Window
388 /// element are properly set.
389 Resized {
390 /// The new logical size of the window
391 size: LogicalSize,
392 },
393 /// The user requested to close the window.
394 ///
395 /// The backend should send this event when the user tries to close the window,for example by pressing the close button.
396 ///
397 /// This will have the effect of invoking the callback set in [`Window::on_close_requested()`](`crate::api::Window::on_close_requested()`)
398 /// and then hiding the window depending on the return value of the callback.
399 CloseRequested,
400
401 /// The Window was activated or de-activated.
402 ///
403 /// The backend should dispatch this event with true when the window gains focus
404 /// and false when the window loses focus.
405 WindowActiveChanged(bool),
406}
407
408impl WindowEvent {
409 /// The position of the cursor for this event, if any
410 pub fn position(&self) -> Option<LogicalPosition> {
411 match self {
412 WindowEvent::PointerPressed { position, .. } => Some(*position),
413 WindowEvent::PointerReleased { position, .. } => Some(*position),
414 WindowEvent::PointerMoved { position } => Some(*position),
415 WindowEvent::PointerScrolled { position, .. } => Some(*position),
416 _ => None,
417 }
418 }
419}
420
421/**
422 * Test the animation tick is updated when a platform is set
423```rust
424use i_slint_core::platform::*;
425struct DummyBackend;
426impl Platform for DummyBackend {
427 fn create_window_adapter(
428 &self,
429 ) -> Result<std::rc::Rc<dyn WindowAdapter>, PlatformError> {
430 Err(PlatformError::Other("not implemented".into()))
431 }
432 fn duration_since_start(&self) -> core::time::Duration {
433 core::time::Duration::from_millis(100)
434 }
435}
436
437let start_time = i_slint_core::tests::slint_get_mocked_time();
438i_slint_core::platform::set_platform(Box::new(DummyBackend{}));
439let time_after_platform_init = i_slint_core::tests::slint_get_mocked_time();
440assert_ne!(time_after_platform_init, start_time);
441assert_eq!(time_after_platform_init, 100);
442```
443 */
444#[cfg(doctest)]
445const _ANIM_TICK_UPDATED_ON_PLATFORM_SET: () = ();