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