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    /// 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    #[cfg(target_os = "android")]
144    #[doc(hidden)]
145    /// The long press interval before showing a context menu
146    fn long_press_interval(&self, _: crate::InternalToken) -> core::time::Duration {
147        core::time::Duration::from_millis(500)
148    }
149}
150
151/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]
152#[repr(u8)]
153#[non_exhaustive]
154#[derive(PartialEq, Clone, Default)]
155pub enum Clipboard {
156    /// This is the default clipboard used for text action for Ctrl+V,  Ctrl+C.
157    /// Corresponds to the secondary clipboard on X11.
158    #[default]
159    DefaultClipboard = 0,
160
161    /// This is the clipboard that is used when text is selected
162    /// Corresponds to the primary clipboard on X11.
163    /// The Platform implementation should do nothing if copy on select is not supported on that platform.
164    SelectionClipboard = 1,
165}
166
167/// Trait that is returned by the [`Platform::new_event_loop_proxy`]
168///
169/// This are the implementation details for the function that may need to
170/// communicate with the eventloop from different thread
171pub trait EventLoopProxy: Send + Sync {
172    /// Exits the event loop.
173    ///
174    /// This is what is called by [`slint::quit_event_loop()`](crate::api::quit_event_loop)
175    fn quit_event_loop(&self) -> Result<(), crate::api::EventLoopError>;
176
177    /// Invoke the function from the event loop.
178    ///
179    /// This is what is called by [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop)
180    fn invoke_from_event_loop(
181        &self,
182        event: Box<dyn FnOnce() + Send>,
183    ) -> Result<(), crate::api::EventLoopError>;
184}
185
186#[cfg(feature = "std")]
187static INITIAL_INSTANT: once_cell::sync::OnceCell<time::Instant> = once_cell::sync::OnceCell::new();
188
189#[cfg(feature = "std")]
190impl std::convert::From<crate::animations::Instant> for time::Instant {
191    fn from(our_instant: crate::animations::Instant) -> Self {
192        let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
193        the_beginning + core::time::Duration::from_millis(our_instant.0)
194    }
195}
196
197#[cfg(not(target_os = "android"))]
198static EVENTLOOP_PROXY: OnceCell<Box<dyn EventLoopProxy + 'static>> = OnceCell::new();
199
200// On android, we allow the platform to be reset and the global eventloop proxy to be replaced.
201#[cfg(target_os = "android")]
202static EVENTLOOP_PROXY: std::sync::Mutex<Option<Box<dyn EventLoopProxy + 'static>>> =
203    std::sync::Mutex::new(None);
204
205pub(crate) fn with_event_loop_proxy<R>(f: impl FnOnce(Option<&dyn EventLoopProxy>) -> R) -> R {
206    #[cfg(not(target_os = "android"))]
207    return f(EVENTLOOP_PROXY.get().map(core::ops::Deref::deref));
208    #[cfg(target_os = "android")]
209    return f(EVENTLOOP_PROXY.lock().unwrap().as_ref().map(core::ops::Deref::deref));
210}
211
212/// This enum describes the different error scenarios that may occur when [`set_platform`]
213/// fails.
214#[derive(Debug, Clone, PartialEq)]
215#[repr(C)]
216#[non_exhaustive]
217pub enum SetPlatformError {
218    /// The platform has already been initialized in an earlier call to [`set_platform`].
219    AlreadySet,
220}
221
222impl core::fmt::Display for SetPlatformError {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        match self {
225            SetPlatformError::AlreadySet => {
226                f.write_str("The platform has already been initialized.")
227            }
228        }
229    }
230}
231
232#[cfg(feature = "std")]
233impl std::error::Error for SetPlatformError {}
234
235/// Set the Slint platform abstraction.
236///
237/// If the platform abstraction was already set this will return `Err`.
238pub fn set_platform(platform: Box<dyn Platform + 'static>) -> Result<(), SetPlatformError> {
239    crate::context::GLOBAL_CONTEXT.with(|instance| {
240        if instance.get().is_some() {
241            return Err(SetPlatformError::AlreadySet);
242        }
243        if let Some(proxy) = platform.new_event_loop_proxy() {
244            #[cfg(not(target_os = "android"))]
245            {
246                EVENTLOOP_PROXY.set(proxy).map_err(|_| SetPlatformError::AlreadySet)?;
247            }
248            #[cfg(target_os = "android")]
249            {
250                *EVENTLOOP_PROXY.lock().unwrap() = Some(proxy);
251            }
252        }
253        instance
254            .set(crate::SlintContext::new(platform))
255            .map_err(|_| SetPlatformError::AlreadySet)
256            .unwrap();
257        // Ensure a sane starting point for the animation tick.
258        update_timers_and_animations();
259        Ok(())
260    })
261}
262
263/// Call this function to update and potentially activate any pending timers, as well
264/// as advance the state of any active animations.
265///
266/// This function should be called before rendering or processing input event, at the
267/// beginning of each event loop iteration.
268pub fn update_timers_and_animations() {
269    crate::animations::update_animations();
270    crate::timers::TimerList::maybe_activate_timers(crate::animations::Instant::now());
271    crate::properties::ChangeTracker::run_change_handlers();
272}
273
274/// Returns the duration before the next timer is expected to be activated. This is the
275/// largest amount of time that you can wait before calling [`update_timers_and_animations()`].
276///
277/// `None` is returned if there is no active timer.
278///
279/// Call this in your own event loop implementation to know how long the current thread can
280/// go to sleep. Note that this does not take currently activate animations into account.
281/// Only go to sleep if [`Window::has_active_animations()`](crate::api::Window::has_active_animations())
282/// returns false.
283pub fn duration_until_next_timer_update() -> Option<core::time::Duration> {
284    crate::timers::TimerList::next_timeout().map(|timeout| {
285        let duration_since_start = crate::context::GLOBAL_CONTEXT
286            .with(|p| p.get().map(|p| p.platform().duration_since_start()))
287            .unwrap_or_default();
288        core::time::Duration::from_millis(
289            timeout.0.saturating_sub(duration_since_start.as_millis() as u64),
290        )
291    })
292}
293
294// reexport key enum to the public api
295pub use crate::input::PointerEventButton;
296pub use crate::input::key_codes::Key;
297
298/// A event that describes user input or windowing system events.
299///
300/// Slint backends typically receive events from the windowing system, translate them to this
301/// enum and deliver them to the scene of items via [`slint::Window::try_dispatch_event()`](`crate::api::Window::try_dispatch_event()`).
302///
303/// The pointer variants describe events originating from an input device such as a mouse
304/// or a contact point on a touch-enabled surface.
305///
306/// All position fields are in logical window coordinates.
307#[allow(missing_docs)]
308#[derive(Debug, Clone, PartialEq)]
309#[non_exhaustive]
310#[repr(u32)]
311pub enum WindowEvent {
312    /// A pointer was pressed.
313    PointerPressed {
314        position: LogicalPosition,
315        /// The button that was pressed.
316        button: PointerEventButton,
317    },
318    /// A pointer was released.
319    PointerReleased {
320        position: LogicalPosition,
321        /// The button that was released.
322        button: PointerEventButton,
323    },
324    /// The position of the pointer has changed.
325    PointerMoved { position: LogicalPosition },
326    /// The wheel button of a mouse was rotated to initiate scrolling.
327    PointerScrolled {
328        position: LogicalPosition,
329        /// The amount of logical pixels to scroll in the horizontal direction.
330        delta_x: f32,
331        /// The amount of logical pixels to scroll in the vertical direction.
332        delta_y: f32,
333    },
334    /// The pointer exited the window.
335    PointerExited,
336    /// A key was pressed.
337    KeyPressed {
338        /// The unicode representation of the key pressed.
339        ///
340        /// # Example
341        /// A specific key can be mapped to a unicode by using the [`Key`] enum
342        /// ```rust
343        /// let _ = slint::platform::WindowEvent::KeyPressed { text: slint::platform::Key::Shift.into() };
344        /// ```
345        text: SharedString,
346    },
347    /// A key press was auto-repeated.
348    KeyPressRepeated {
349        /// The unicode representation of the key pressed.
350        ///
351        /// # Example
352        /// A specific key can be mapped to a unicode by using the [`Key`] enum
353        /// ```rust
354        /// let _ = slint::platform::WindowEvent::KeyPressRepeated { text: slint::platform::Key::Shift.into() };
355        /// ```
356        text: SharedString,
357    },
358    /// A key was released.
359    KeyReleased {
360        /// The unicode representation of the key released.
361        ///
362        /// # Example
363        /// A specific key can be mapped to a unicode by using the [`Key`] enum
364        /// ```rust
365        /// let _ = slint::platform::WindowEvent::KeyReleased { text: slint::platform::Key::Shift.into() };
366        /// ```
367        text: SharedString,
368    },
369    /// The window's scale factor has changed. This can happen for example when the display's resolution
370    /// changes, the user selects a new scale factor in the system settings, or the window is moved to a
371    /// different screen.
372    /// Platform implementations should dispatch this event also right after the initial window creation,
373    /// to set the initial scale factor the windowing system provided for the window.
374    ScaleFactorChanged {
375        /// The window system provided scale factor to map logical pixels to physical pixels.
376        scale_factor: f32,
377    },
378    /// The window was resized.
379    ///
380    /// The backend must send this event to ensure that the `width` and `height` property of the root Window
381    /// element are properly set.
382    Resized {
383        /// The new logical size of the window
384        size: LogicalSize,
385    },
386    /// The user requested to close the window.
387    ///
388    /// The backend should send this event when the user tries to close the window,for example by pressing the close button.
389    ///
390    /// This will have the effect of invoking the callback set in [`Window::on_close_requested()`](`crate::api::Window::on_close_requested()`)
391    /// and then hiding the window depending on the return value of the callback.
392    CloseRequested,
393
394    /// The Window was activated or de-activated.
395    ///
396    /// The backend should dispatch this event with true when the window gains focus
397    /// and false when the window loses focus.
398    WindowActiveChanged(bool),
399}
400
401impl WindowEvent {
402    /// The position of the cursor for this event, if any
403    pub fn position(&self) -> Option<LogicalPosition> {
404        match self {
405            WindowEvent::PointerPressed { position, .. } => Some(*position),
406            WindowEvent::PointerReleased { position, .. } => Some(*position),
407            WindowEvent::PointerMoved { position } => Some(*position),
408            WindowEvent::PointerScrolled { position, .. } => Some(*position),
409            _ => None,
410        }
411    }
412}
413
414/**
415 * Test the animation tick is updated when a platform is set
416```rust
417use i_slint_core::platform::*;
418struct DummyBackend;
419impl Platform for DummyBackend {
420     fn create_window_adapter(
421        &self,
422    ) -> Result<std::rc::Rc<dyn WindowAdapter>, PlatformError> {
423        Err(PlatformError::Other("not implemented".into()))
424    }
425    fn duration_since_start(&self) -> core::time::Duration {
426        core::time::Duration::from_millis(100)
427    }
428}
429
430let start_time = i_slint_core::tests::slint_get_mocked_time();
431i_slint_core::platform::set_platform(Box::new(DummyBackend{}));
432let time_after_platform_init = i_slint_core::tests::slint_get_mocked_time();
433assert_ne!(time_after_platform_init, start_time);
434assert_eq!(time_after_platform_init, 100);
435```
436 */
437#[cfg(doctest)]
438const _ANIM_TICK_UPDATED_ON_PLATFORM_SET: () = ();