// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 /*! The backend is the abstraction for crates that need to do the actual drawing and event loop */ #![warn(missing_docs)] use crate::SharedString; pub use crate::api::PlatformError; use crate::api::{LogicalPosition, LogicalSize}; pub use crate::renderer::Renderer; #[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))] use crate::unsafe_single_threaded::OnceCell; pub use crate::window::{LayoutConstraints, WindowAdapter, WindowProperties}; use alloc::boxed::Box; use alloc::rc::Rc; use alloc::string::String; #[cfg(all(feature = "std", not(target_os = "android")))] use once_cell::sync::OnceCell; #[cfg(all(feature = "std", not(target_arch = "wasm32")))] use std::time; #[cfg(target_arch = "wasm32")] use web_time as time; /// This trait defines the interface between Slint and platform APIs typically provided by operating and windowing systems. pub trait Platform { /// Instantiate a window for a component. fn create_window_adapter(&self) -> Result, PlatformError>; /// Spins an event loop and renders the visible windows. fn run_event_loop(&self) -> Result<(), PlatformError> { Err(PlatformError::NoEventLoopProvider) } /// Processes pending events and waits for new ones up to the given timeout. /// /// This function is similar to `run_event_loop()` with two differences: /// * It processes any pending events, /// then blocks waiting for new events for up to `timeout`. /// It may return earlier than the timeout if events were received, /// if the loop was terminated via `quit_event_loop()`, /// or through a last-window-closed mechanism. /// Callers shouldn't assume the full timeout has elapsed when the function returns. /// * If the timeout is `None`, the implementation should wait /// indefinitely for events. /// * If the timeout is `Some(Duration::ZERO)`, /// the implementation should merely peek and process any pending events, /// then return immediately. /// /// When the function returns `ControlFlow::Continue`, it is assumed that /// the loop remains intact and that in the future the caller should call /// `process_events()` again, to permit the user to continue to interact with /// windows. /// When the function returns `ControlFlow::Break`, it is assumed that the /// event loop was terminated. Any subsequent calls to `process_events()` /// will start the event loop afresh. #[doc(hidden)] fn process_events( &self, _timeout: Option, _: crate::InternalToken, ) -> Result, PlatformError> { Err(PlatformError::NoEventLoopProvider) } /// Called once by [`crate::SlintContext::new`], as the context that owns this platform /// finishes construction, to give the platform a weak handle to it. Platforms can stash /// the handle and later use it to spawn futures or write context-wide state without /// going through a window adapter. The default impl drops the handle. /// /// Every context binds its own platform, not only the one installed as the thread's /// global context, so a backend driving a context can always find it. #[doc(hidden)] fn bind_context(&self, _ctx: crate::SlintContextWeak, _: crate::InternalToken) {} #[doc(hidden)] #[deprecated( note = "i-slint-core takes care of closing behavior. Application should call run_event_loop_until_quit" )] /// This is being phased out, see #1499. fn set_event_loop_quit_on_last_window_closed(&self, quit_on_last_window_closed: bool) { assert!(!quit_on_last_window_closed); crate::context::GLOBAL_CONTEXT .with(|ctx| (*ctx.get().unwrap().0.window_count.borrow_mut()) += 1); } /// Return an [`EventLoopProxy`] that can be used to send event to the event loop /// /// If this function returns `None` (the default implementation), then it will /// not be possible to send event to the event loop and the function /// [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) and /// [`slint::quit_event_loop()`](crate::api::quit_event_loop) will panic. These /// functions are used internally by `slint::spawn_local()` /// and features like live_preview. Implementing this function is necessary for /// aforementioned functionalities to work. fn new_event_loop_proxy(&self) -> Option> { None } /// Returns the current time as a monotonic duration since the start of the program /// /// This is used by the animations and timer to compute the elapsed time. /// /// When the `std` feature is enabled, this function is implemented in terms of /// [`std::time::Instant::now()`], but on `#![no_std]` platform, this function must /// be implemented. fn duration_since_start(&self) -> core::time::Duration { #[cfg(feature = "std")] { let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now); let now = time::Instant::now(); assert!(now >= the_beginning, "The platform's clock is not monotonic!"); now - the_beginning } #[cfg(not(feature = "std"))] unimplemented!("The platform abstraction must implement `duration_since_start`") } /// Returns the current interval to internal measure the duration to send a double click event. /// /// A double click event is a series of two pointer clicks. fn click_interval(&self) -> core::time::Duration { // 500ms is the default delay according to https://en.wikipedia.org/wiki/Double-click#Speed_and_timing core::time::Duration::from_millis(500) } /// Returns the current rate at which the text cursor should flash or blink. /// /// This is the length of the entire visible-hidden-visible cycle, so for a duration of 1000ms /// it is visible for 500ms then hidden for 500ms, then visible again. /// /// If this value is `Duration::ZERO` then the cycle is disabled. fn cursor_flash_cycle(&self) -> core::time::Duration { core::time::Duration::from_millis(1000) } /// Sends the given text into the system clipboard. /// /// If the platform doesn't support the specified clipboard, this function should do nothing fn set_clipboard_text(&self, _text: &str, _clipboard: Clipboard) {} /// Returns a copy of text stored in the system clipboard, if any. /// /// If the platform doesn't support the specified clipboard, the function should return None fn clipboard_text(&self, _clipboard: Clipboard) -> Option { None } /// This function is called when debug() is used in .slint files. The implementation /// should direct the output to some developer visible terminal. The default implementation /// uses stderr if available, or `console.log` when targeting wasm. fn debug_log(&self, _arguments: core::fmt::Arguments) { crate::debug_log::default_log_message(_arguments); } /// Opens the given URL in an external browser. /// /// Returns [`PlatformError::Unsupported`] if the platform doesn't support opening URLs. fn open_url(&self, _url: &str) -> Result<(), PlatformError> { Err(PlatformError::Unsupported) } #[cfg(target_os = "android")] #[doc(hidden)] /// The long press interval before showing a context menu fn long_press_interval(&self, _: crate::InternalToken) -> core::time::Duration { core::time::Duration::from_millis(500) } } /// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`] #[repr(u8)] #[non_exhaustive] #[derive(Debug, PartialEq, Clone, Default)] pub enum Clipboard { /// This is the default clipboard used for text action for Ctrl+V, Ctrl+C. /// Corresponds to the secondary clipboard on X11. #[default] DefaultClipboard = 0, /// This is the clipboard that is used when text is selected /// Corresponds to the primary clipboard on X11. /// The Platform implementation should do nothing if copy on select is not supported on that platform. SelectionClipboard = 1, } /// Trait that is returned by the [`Platform::new_event_loop_proxy`] /// /// This are the implementation details for the function that may need to /// communicate with the eventloop from different thread pub trait EventLoopProxy: Send + Sync { /// Exits the event loop. /// /// This is what is called by [`slint::quit_event_loop()`](crate::api::quit_event_loop) fn quit_event_loop(&self) -> Result<(), crate::api::EventLoopError>; /// Invoke the function from the event loop. /// /// This is what is called by [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) fn invoke_from_event_loop( &self, event: Box, ) -> Result<(), crate::api::EventLoopError>; } #[cfg(feature = "std")] static INITIAL_INSTANT: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); #[cfg(feature = "std")] impl std::convert::From for time::Instant { fn from(our_instant: crate::animations::Instant) -> Self { let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now); the_beginning + core::time::Duration::from_millis(our_instant.0) } } #[cfg(not(target_os = "android"))] static EVENTLOOP_PROXY: OnceCell> = OnceCell::new(); // On android, we allow the platform to be reset and the global eventloop proxy to be replaced. #[cfg(target_os = "android")] static EVENTLOOP_PROXY: std::sync::Mutex>> = std::sync::Mutex::new(None); pub(crate) fn with_event_loop_proxy(f: impl FnOnce(Option<&dyn EventLoopProxy>) -> R) -> R { #[cfg(not(target_os = "android"))] return f(EVENTLOOP_PROXY.get().map(core::ops::Deref::deref)); #[cfg(target_os = "android")] return f(EVENTLOOP_PROXY.lock().unwrap().as_ref().map(core::ops::Deref::deref)); } /// This enum describes the different error scenarios that may occur when [`set_platform`] /// fails. #[derive(Debug, Clone, PartialEq)] #[repr(C)] #[non_exhaustive] pub enum SetPlatformError { /// The platform has already been initialized in an earlier call to [`set_platform`]. AlreadySet, } impl core::fmt::Display for SetPlatformError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { SetPlatformError::AlreadySet => { f.write_str("The platform has already been initialized.") } } } } impl core::error::Error for SetPlatformError {} /// Set the Slint platform abstraction. /// /// If the platform abstraction was already set this will return `Err`. pub fn set_platform(platform: Box) -> Result<(), SetPlatformError> { crate::context::GLOBAL_CONTEXT.with(|instance| { if instance.get().is_some() { return Err(SetPlatformError::AlreadySet); } if let Some(proxy) = platform.new_event_loop_proxy() { #[cfg(not(target_os = "android"))] { EVENTLOOP_PROXY.set(proxy).map_err(|_| SetPlatformError::AlreadySet)?; } #[cfg(target_os = "android")] { *EVENTLOOP_PROXY.lock().unwrap() = Some(proxy); } } // The slot is free, so this claims it. The returned handle is dropped here; the // context stays alive because the slot holds it. drop(crate::SlintContext::new(platform)); debug_assert!(instance.get().is_some(), "SlintContext::new claims a free slot"); // Ensure a sane starting point for the animation tick. update_timers_and_animations(); Ok(()) }) } /// Call this function to update and potentially activate any pending timers, as well /// as advance the state of any active animations. /// /// This function should be called before rendering or processing input event, at the /// beginning of each event loop iteration. pub fn update_timers_and_animations() { match crate::context::GLOBAL_CONTEXT.with(|ctx| ctx.get().cloned()) { Some(ctx) => ctx.update_timers_and_animations(), None => { // Pre-platform behavior: with no context there is no clock either, so only // zero-duration timers in the pending list are due. crate::animations::update_animations(Default::default()); crate::timers::TimerList::maybe_activate_timers(Default::default()); crate::properties::ChangeTracker::run_change_handlers(); } } } /// Returns the duration before the next timer is expected to be activated. This is the /// largest amount of time that you can wait before calling [`update_timers_and_animations()`]. /// /// `None` is returned if there is no active timer. /// /// Call this in your own event loop implementation to know how long the current thread can /// go to sleep. Note that this does not take currently activate animations into account. /// Only go to sleep if [`Window::has_active_animations()`](crate::api::Window::has_active_animations()) /// returns false. pub fn duration_until_next_timer_update() -> Option { match crate::context::GLOBAL_CONTEXT.with(|ctx| ctx.get().cloned()) { Some(ctx) => ctx.duration_until_next_timer_update(), // No context, hence no clock: the deadline is measured from a zero origin. None => crate::timers::TimerList::next_timeout() .map(|timeout| core::time::Duration::from_millis(timeout.0)), } } // reexport key enum to the public api pub use crate::input::PointerEventButton; pub use crate::input::key_codes::Key; /// Result of dispatching a window event through Slint's runtime with /// [`Window::dispatch_event_with_result()`](crate::api::Window::dispatch_event_with_result). #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub enum WindowEventDispatchResult { /// The event was handled. For example, a key handler consumed a key press, or /// the window acted on a resize or close request. Accepted, /// The event wasn't handled: no element consumed it, or a handler actively refused it, /// such as a `close-requested` callback returning `reject` to keep the window open. Rejected, } impl From for WindowEventDispatchResult { fn from(value: crate::input::KeyEventResult) -> Self { match value { crate::input::KeyEventResult::EventAccepted => Self::Accepted, crate::input::KeyEventResult::EventIgnored => Self::Rejected, } } } impl From> for WindowEventDispatchResult { /// `None` (no component to dispatch to) and `accepted: false` both map to `Rejected`. fn from(value: Option) -> Self { if value.is_some_and(|r| r.accepted) { Self::Accepted } else { Self::Rejected } } } // api/node/build.rs parses this enum to generate the Node.js window event types. /// A event that describes user input or windowing system events. /// /// Slint backends typically receive events from the windowing system, translate them to this /// enum and deliver them to the scene of items via [`slint::Window::dispatch_event_with_result()`](`crate::api::Window::dispatch_event_with_result()`). /// /// The pointer variants describe events originating from an input device such as a mouse /// or a contact point on a touch-enabled surface. /// /// All position fields are in logical window coordinates. #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] #[repr(u32)] pub enum WindowEvent { /// A pointer was pressed. PointerPressed { /// The position of the pointer, in logical pixels relative to the top left corner of the window. position: LogicalPosition, /// The button that was pressed. button: PointerEventButton, }, /// A pointer was released. PointerReleased { /// The position of the pointer, in logical pixels relative to the top left corner of the window. position: LogicalPosition, /// The button that was released. button: PointerEventButton, }, /// The position of the pointer has changed. PointerMoved { /// The new position of the pointer, in logical pixels relative to the top left corner of the window. position: LogicalPosition, }, /// The wheel button of a mouse was rotated to initiate scrolling. PointerScrolled { /// The position of the pointer when the scroll occurred. position: LogicalPosition, /// The amount of logical pixels to scroll in the horizontal direction. delta_x: f32, /// The amount of logical pixels to scroll in the vertical direction. delta_y: f32, }, /// The pointer exited the window. /// /// Always reported as [`Accepted`](WindowEventDispatchResult::Accepted). PointerExited, /// A key was pressed. KeyPressed { /// The unicode representation of the key pressed. /// /// # Example /// A specific key can be mapped to a unicode by using the [`Key`] enum /// ```rust /// let _ = slint::platform::WindowEvent::KeyPressed { text: slint::platform::Key::Shift.into() }; /// ``` text: SharedString, }, /// A key press was auto-repeated. KeyPressRepeated { /// The unicode representation of the key pressed. /// /// # Example /// A specific key can be mapped to a unicode by using the [`Key`] enum /// ```rust /// let _ = slint::platform::WindowEvent::KeyPressRepeated { text: slint::platform::Key::Shift.into() }; /// ``` text: SharedString, }, /// A key was released. KeyReleased { /// The unicode representation of the key released. /// /// # Example /// A specific key can be mapped to a unicode by using the [`Key`] enum /// ```rust /// let _ = slint::platform::WindowEvent::KeyReleased { text: slint::platform::Key::Shift.into() }; /// ``` text: SharedString, }, /// The window's scale factor has changed. This can happen for example when the display's resolution /// changes, the user selects a new scale factor in the system settings, or the window is moved to a /// different screen. /// Platform implementations should dispatch this event also right after the initial window creation, /// to set the initial scale factor the windowing system provided for the window. ScaleFactorChanged { /// The window system provided scale factor to map logical pixels to physical pixels. scale_factor: f32, }, /// The window was resized. /// /// The backend must send this event to ensure that the `width` and `height` property of the root Window /// element are properly set. Resized { /// The new logical size of the window. size: LogicalSize, }, /// The user requested to close the window. /// /// The backend should send this event when the user tries to close the window,for example by pressing the close button. /// /// This will have the effect of invoking the callback set in [`Window::on_close_requested()`](`crate::api::Window::on_close_requested()`) /// and then hiding the window depending on the return value of the callback. CloseRequested, /// The Window was activated or de-activated. /// /// The backend should dispatch this event with true when the window gains focus /// and false when the window loses focus. WindowActiveChanged(bool), /// An event that one of Slint's own backends delivers in the runtime's internal representation. /// /// This isn't public API, use [`WindowEvent::internal()`] to construct it. #[doc(hidden)] Internal(InternalEventBox), } impl WindowEvent { /// The position of the cursor for this event, if any pub fn position(&self) -> Option { match self { WindowEvent::PointerPressed { position, .. } => Some(*position), WindowEvent::PointerReleased { position, .. } => Some(*position), WindowEvent::PointerMoved { position } => Some(*position), WindowEvent::PointerScrolled { position, .. } => Some(*position), WindowEvent::Internal(event) => event.position(), _ => None, } } /// Wraps an event in the runtime's internal representation, /// for Slint's own backends to deliver via [`Window::dispatch_event_with_result()`](crate::api::Window::dispatch_event_with_result). #[doc(hidden)] pub fn internal(event: impl Into) -> Self { Self::Internal(InternalEventBox::new(event.into())) } } /// Owning pointer to an [`InternalEvent`]. /// /// The payload lives behind a pointer, so that the size of [`WindowEvent`] and the way the C++ /// bindings represent it don't depend on the internal event. /// It's a dedicated type rather than a `Box`, because a `Box` of an unsized type is two pointers /// wide, which the generated C++ struct wouldn't match. #[doc(hidden)] #[repr(transparent)] pub struct InternalEventBox(core::ptr::NonNull); // Safety: the box exclusively owns the event it points to, which the assertion below keeps // `Send` and `Sync`. #[allow(unsafe_code)] unsafe impl Send for InternalEventBox {} #[allow(unsafe_code)] unsafe impl Sync for InternalEventBox {} // A backend may build events on one thread and dispatch them from the event loop's thread. const _: () = { const fn assert_send_sync() {} assert_send_sync::(); assert_send_sync::(); }; #[allow(unsafe_code)] impl InternalEventBox { fn new(event: InternalEvent) -> Self { // Safety: `Box::into_raw` never returns null. Self(unsafe { core::ptr::NonNull::new_unchecked(Box::into_raw(Box::new(event))) }) } /// Takes the event out of the box. pub(crate) fn into_inner(self) -> InternalEvent { let this = core::mem::ManuallyDrop::new(self); // Safety: the pointer comes from `Box::into_raw` in `new()`, and `ManuallyDrop` keeps // `Drop` from freeing it a second time. *unsafe { Box::from_raw(this.0.as_ptr()) } } } #[allow(unsafe_code)] impl Drop for InternalEventBox { fn drop(&mut self) { // Safety: the pointer comes from `Box::into_raw` in `new()` and is freed only here or in // `into_inner()`, which doesn't run this. drop(unsafe { Box::from_raw(self.0.as_ptr()) }); } } #[allow(unsafe_code)] impl core::ops::Deref for InternalEventBox { type Target = InternalEvent; fn deref(&self) -> &InternalEvent { // Safety: the pointer comes from `Box::into_raw` in `new()` and stays valid until `Drop`. unsafe { self.0.as_ref() } } } impl Clone for InternalEventBox { fn clone(&self) -> Self { Self::new(InternalEvent::clone(self)) } } impl PartialEq for InternalEventBox { fn eq(&self, other: &Self) -> bool { **self == **other } } impl core::fmt::Debug for InternalEventBox { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { InternalEvent::fmt(self, f) } } /// An event that one of Slint's own backends delivers in the representation the runtime uses internally. /// /// These carry information that the [`WindowEvent`] variants can't express, /// such as the touch finger id, the click count, the gesture phase, drag payloads or input method composition. /// Backends wrap them with [`WindowEvent::internal()`] and dispatch them like any other event, /// so that all input takes the same path into the runtime and is observed by the window event hook. /// /// This isn't public API: the variants and their payload change without notice. #[doc(hidden)] #[derive(Debug, Clone, PartialEq)] pub enum InternalEvent { /// A pointer event, including the ones that have no public representation, such as gestures. Mouse(crate::input::BackendMouseEvent), /// A key event, including input method composition updates. Key(crate::input::InternalKeyEvent), /// A touch point update, which the runtime turns into pointer or gesture events. Touch { /// The id of the finger that produced the event. /// Must be non-negative and distinct among the fingers currently down. /// Ids may be reused once a finger lifts. id: i32, /// The position of the finger, in logical coordinates. position: crate::lengths::LogicalPoint, /// Whether the finger was put down, moved, lifted or cancelled. phase: crate::input::TouchPhase, }, } impl From for InternalEvent { fn from(event: crate::input::BackendMouseEvent) -> Self { Self::Mouse(event) } } impl From for InternalEvent { fn from(event: crate::input::InternalKeyEvent) -> Self { Self::Key(event) } } impl InternalEvent { /// The public event this event corresponds to, if any. /// /// This is what the window event hook observes, /// so that hooks only ever see events they could dispatch themselves. /// Events without a public representation aren't reported: /// gestures, touch and input method composition. pub(crate) fn public_representation(&self) -> Option { use crate::input::{BackendMouseEvent, KeyEventType}; use crate::lengths::logical_position_to_api; match self { Self::Mouse(event) => match event { BackendMouseEvent::Pressed { position, button, .. } => { Some(WindowEvent::PointerPressed { position: logical_position_to_api(*position), button: *button, }) } BackendMouseEvent::Released { position, button, .. } => { Some(WindowEvent::PointerReleased { position: logical_position_to_api(*position), button: *button, }) } BackendMouseEvent::Moved { position, .. } => { Some(WindowEvent::PointerMoved { position: logical_position_to_api(*position) }) } BackendMouseEvent::Wheel { position, delta_x, delta_y, .. } => { Some(WindowEvent::PointerScrolled { position: logical_position_to_api(*position), delta_x: *delta_x as f32, delta_y: *delta_y as f32, }) } BackendMouseEvent::Exit => Some(WindowEvent::PointerExited), BackendMouseEvent::PinchGesture { .. } | BackendMouseEvent::RotationGesture { .. } => None, }, Self::Key(event) => { let text = event.key_event.text.clone(); match event.event_type { KeyEventType::KeyPressed if event.key_event.repeat => { Some(WindowEvent::KeyPressRepeated { text }) } KeyEventType::KeyPressed => Some(WindowEvent::KeyPressed { text }), KeyEventType::KeyReleased => Some(WindowEvent::KeyReleased { text }), KeyEventType::UpdateComposition | KeyEventType::CommitComposition => None, } } // There's no public touch event, and the pointer events the runtime synthesizes from // a touch point carry a finger id that `WindowEvent` can't express. Self::Touch { .. } => None, } } /// The position of the pointer or finger for this event, if any. fn position(&self) -> Option { match self { Self::Mouse(event) => crate::input::MouseEvent::from(*event) .position() .map(crate::lengths::logical_position_to_api), Self::Key(_) => None, Self::Touch { position, .. } => { Some(crate::lengths::logical_position_to_api(*position)) } } } } /** * Test the animation tick is updated when a platform is set ```rust use i_slint_core::platform::*; struct DummyBackend; impl Platform for DummyBackend { fn create_window_adapter( &self, ) -> Result, PlatformError> { Err(PlatformError::Other("not implemented".into())) } fn duration_since_start(&self) -> core::time::Duration { core::time::Duration::from_millis(100) } } let start_time = i_slint_backend_testing::get_mocked_time(); i_slint_core::platform::set_platform(Box::new(DummyBackend{})); let time_after_platform_init = i_slint_backend_testing::get_mocked_time(); assert_ne!(time_after_platform_init, start_time); assert_eq!(time_after_platform_init, 100); ``` */ #[cfg(doctest)] const _ANIM_TICK_UPDATED_ON_PLATFORM_SET: () = ();