mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-13 17:25:27 +00:00
And call `extern crate std` when the feature is enabled. I've read this is the good practice on how to do it. So that the std prelude is no longer included automatically. There is then less difference between std and and no-std build which should avoid surprises in the CI when we use things from the prelude. The downside is that there is a bit of churn in the tests
418 lines
17 KiB
Rust
418 lines
17 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// 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)]
|
|
|
|
pub use crate::api::PlatformError;
|
|
use crate::api::{LogicalPosition, LogicalSize};
|
|
pub use crate::renderer::Renderer;
|
|
#[cfg(feature = "software-renderer")]
|
|
pub use crate::software_renderer;
|
|
#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
|
|
use crate::unsafe_single_threaded::OnceCell;
|
|
pub use crate::window::{LayoutConstraints, WindowAdapter, WindowProperties};
|
|
use crate::SharedString;
|
|
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<Rc<dyn WindowAdapter>, PlatformError>;
|
|
|
|
/// Spins an event loop and renders the visible windows.
|
|
fn run_event_loop(&self) -> Result<(), PlatformError> {
|
|
Err(PlatformError::NoEventLoopProvider)
|
|
}
|
|
|
|
/// Spins an event loop for a specified period of time.
|
|
///
|
|
/// This function is similar to `run_event_loop()` with two differences:
|
|
/// * The function is expected to return after the provided timeout, but
|
|
/// allow for subsequent invocations to resume the previous loop. The
|
|
/// function can return earlier if the loop was terminated otherwise,
|
|
/// for example by `quit_event_loop()` or a last-window-closed mechanism.
|
|
/// * If the timeout is zero, the implementation should merely peek and
|
|
/// process any pending events, but 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: core::time::Duration,
|
|
_: crate::InternalToken,
|
|
) -> Result<core::ops::ControlFlow<()>, PlatformError> {
|
|
Err(PlatformError::NoEventLoopProvider)
|
|
}
|
|
|
|
#[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
|
|
fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
|
|
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);
|
|
time::Instant::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)
|
|
}
|
|
|
|
/// 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<String> {
|
|
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::tests::default_debug_log(_arguments);
|
|
}
|
|
}
|
|
|
|
/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]
|
|
#[repr(u8)]
|
|
#[non_exhaustive]
|
|
#[derive(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<dyn FnOnce() + Send>,
|
|
) -> Result<(), crate::api::EventLoopError>;
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
static INITIAL_INSTANT: once_cell::sync::OnceCell<time::Instant> = once_cell::sync::OnceCell::new();
|
|
|
|
#[cfg(feature = "std")]
|
|
impl std::convert::From<crate::animations::Instant> 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<Box<dyn EventLoopProxy + 'static>> = 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<Option<Box<dyn EventLoopProxy + 'static>>> =
|
|
std::sync::Mutex::new(None);
|
|
|
|
pub(crate) fn with_event_loop_proxy<R>(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.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl std::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<dyn Platform + 'static>) -> 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);
|
|
}
|
|
}
|
|
instance
|
|
.set(crate::SlintContext::new(platform))
|
|
.map_err(|_| SetPlatformError::AlreadySet)
|
|
.unwrap();
|
|
// 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() {
|
|
crate::animations::update_animations();
|
|
crate::timers::TimerList::maybe_activate_timers(crate::animations::Instant::now());
|
|
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<core::time::Duration> {
|
|
crate::timers::TimerList::next_timeout().map(|timeout| {
|
|
let duration_since_start = crate::context::GLOBAL_CONTEXT
|
|
.with(|p| p.get().map(|p| p.platform().duration_since_start()))
|
|
.unwrap_or_default();
|
|
core::time::Duration::from_millis(
|
|
timeout.0.saturating_sub(duration_since_start.as_millis() as u64),
|
|
)
|
|
})
|
|
}
|
|
|
|
// reexport key enum to the public api
|
|
pub use crate::input::key_codes::Key;
|
|
pub use crate::input::PointerEventButton;
|
|
|
|
/// 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::try_dispatch_event()`](`crate::api::Window::try_dispatch_event()`).
|
|
///
|
|
/// 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 {
|
|
position: LogicalPosition,
|
|
/// The button that was pressed.
|
|
button: PointerEventButton,
|
|
},
|
|
/// A pointer was released.
|
|
PointerReleased {
|
|
position: LogicalPosition,
|
|
/// The button that was released.
|
|
button: PointerEventButton,
|
|
},
|
|
/// The position of the pointer has changed.
|
|
PointerMoved { position: LogicalPosition },
|
|
/// The wheel button of a mouse was rotated to initiate scrolling.
|
|
PointerScrolled {
|
|
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.
|
|
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),
|
|
}
|
|
|
|
impl WindowEvent {
|
|
/// The position of the cursor for this event, if any
|
|
pub fn position(&self) -> Option<LogicalPosition> {
|
|
match self {
|
|
WindowEvent::PointerPressed { position, .. } => Some(*position),
|
|
WindowEvent::PointerReleased { position, .. } => Some(*position),
|
|
WindowEvent::PointerMoved { position } => Some(*position),
|
|
WindowEvent::PointerScrolled { position, .. } => Some(*position),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<std::rc::Rc<dyn WindowAdapter>, 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_core::tests::slint_get_mocked_time();
|
|
i_slint_core::platform::set_platform(Box::new(DummyBackend{}));
|
|
let time_after_platform_init = i_slint_core::tests::slint_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: () = ();
|