Skip to main content

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