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