Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
703 lines
29 KiB
Rust
703 lines
29 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)]
|
|
|
|
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<Rc<dyn WindowAdapter>, 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<core::time::Duration>,
|
|
_: crate::InternalToken,
|
|
) -> Result<core::ops::ControlFlow<()>, 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<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);
|
|
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<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::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<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.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<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);
|
|
}
|
|
}
|
|
// 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<core::time::Duration> {
|
|
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<crate::input::KeyEventResult> 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<Option<crate::window::MouseDispatchResult>> for WindowEventDispatchResult {
|
|
/// `None` (no component to dispatch to) and `accepted: false` both map to `Rejected`.
|
|
fn from(value: Option<crate::window::MouseDispatchResult>) -> 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<LogicalPosition> {
|
|
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<InternalEvent>) -> 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<InternalEvent>);
|
|
|
|
// 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<T: Send + Sync>() {}
|
|
assert_send_sync::<WindowEvent>();
|
|
assert_send_sync::<InternalEvent>();
|
|
};
|
|
|
|
#[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<crate::input::BackendMouseEvent> for InternalEvent {
|
|
fn from(event: crate::input::BackendMouseEvent) -> Self {
|
|
Self::Mouse(event)
|
|
}
|
|
}
|
|
|
|
impl From<crate::input::InternalKeyEvent> 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<WindowEvent> {
|
|
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<LogicalPosition> {
|
|
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<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_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: () = ();
|