1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | /*! |
5 | The backend is the abstraction for crates that need to do the actual drawing and event loop |
6 | */ |
7 | |
8 | #![warn (missing_docs)] |
9 | |
10 | pub use crate::api::PlatformError; |
11 | use crate::api::{LogicalPosition, LogicalSize}; |
12 | pub use crate::renderer::Renderer; |
13 | #[cfg (feature = "software-renderer" )] |
14 | pub use crate::software_renderer; |
15 | #[cfg (all(not(feature = "std" ), feature = "unsafe-single-threaded" ))] |
16 | use crate::unsafe_single_threaded::OnceCell; |
17 | pub use crate::window::{LayoutConstraints, WindowAdapter, WindowProperties}; |
18 | use crate::SharedString; |
19 | #[cfg (not(feature = "std" ))] |
20 | use alloc::boxed::Box; |
21 | use alloc::rc::Rc; |
22 | #[cfg (not(feature = "std" ))] |
23 | use alloc::string::String; |
24 | #[cfg (feature = "std" )] |
25 | use once_cell::sync::OnceCell; |
26 | #[cfg (all(feature = "std" , not(target_arch = "wasm32" )))] |
27 | use std::time; |
28 | #[cfg (target_arch = "wasm32" )] |
29 | use web_time as time; |
30 | |
31 | /// This trait defines the interface between Slint and platform APIs typically provided by operating and windowing systems. |
32 | pub trait Platform { |
33 | /// Instantiate a window for a component. |
34 | fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError>; |
35 | |
36 | /// Spins an event loop and renders the visible windows. |
37 | fn run_event_loop(&self) -> Result<(), PlatformError> { |
38 | Err(PlatformError::NoEventLoopProvider) |
39 | } |
40 | |
41 | /// Spins an event loop for a specified period of time. |
42 | /// |
43 | /// This function is similar to `run_event_loop()` with two differences: |
44 | /// * The function is expected to return after the provided timeout, but |
45 | /// allow for subsequent invocations to resume the previous loop. The |
46 | /// function can return earlier if the loop was terminated otherwise, |
47 | /// for example by `quit_event_loop()` or a last-window-closed mechanism. |
48 | /// * If the timeout is zero, the implementation should merely peek and |
49 | /// process any pending events, but then return immediately. |
50 | /// |
51 | /// When the function returns `ControlFlow::Continue`, it is assumed that |
52 | /// the loop remains intact and that in the future the caller should call |
53 | /// `process_events()` again, to permit the user to continue to interact with |
54 | /// windows. |
55 | /// When the function returns `ControlFlow::Break`, it is assumed that the |
56 | /// event loop was terminated. Any subsequent calls to `process_events()` |
57 | /// will start the event loop afresh. |
58 | #[doc (hidden)] |
59 | fn process_events( |
60 | &self, |
61 | _timeout: core::time::Duration, |
62 | _: crate::InternalToken, |
63 | ) -> Result<core::ops::ControlFlow<()>, PlatformError> { |
64 | Err(PlatformError::NoEventLoopProvider) |
65 | } |
66 | |
67 | #[doc (hidden)] |
68 | #[deprecated ( |
69 | note = "i-slint-core takes care of closing behavior. Application should call run_event_loop_until_quit" |
70 | )] |
71 | /// This is being phased out, see #1499. |
72 | fn set_event_loop_quit_on_last_window_closed(&self, quit_on_last_window_closed: bool) { |
73 | assert!(!quit_on_last_window_closed); |
74 | crate::context::GLOBAL_CONTEXT |
75 | .with(|ctx| (*ctx.get().unwrap().0.window_count.borrow_mut()) += 1); |
76 | } |
77 | |
78 | /// Return an [`EventLoopProxy`] that can be used to send event to the event loop |
79 | /// |
80 | /// If this function returns `None` (the default implementation), then it will |
81 | /// not be possible to send event to the event loop and the function |
82 | /// [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) and |
83 | /// [`slint::quit_event_loop()`](crate::api::quit_event_loop) will panic |
84 | fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> { |
85 | None |
86 | } |
87 | |
88 | /// Returns the current time as a monotonic duration since the start of the program |
89 | /// |
90 | /// This is used by the animations and timer to compute the elapsed time. |
91 | /// |
92 | /// When the `std` feature is enabled, this function is implemented in terms of |
93 | /// [`std::time::Instant::now()`], but on `#![no_std]` platform, this function must |
94 | /// be implemented. |
95 | fn duration_since_start(&self) -> core::time::Duration { |
96 | #[cfg (feature = "std" )] |
97 | { |
98 | let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now); |
99 | time::Instant::now() - the_beginning |
100 | } |
101 | #[cfg (not(feature = "std" ))] |
102 | unimplemented!("The platform abstraction must implement `duration_since_start`" ) |
103 | } |
104 | |
105 | /// Returns the current interval to internal measure the duration to send a double click event. |
106 | /// |
107 | /// A double click event is a series of two pointer clicks. |
108 | fn click_interval(&self) -> core::time::Duration { |
109 | // 500ms is the default delay according to https://en.wikipedia.org/wiki/Double-click#Speed_and_timing |
110 | core::time::Duration::from_millis(500) |
111 | } |
112 | |
113 | /// Sends the given text into the system clipboard. |
114 | /// |
115 | /// If the platform doesn't support the specified clipboard, this function should do nothing |
116 | fn set_clipboard_text(&self, _text: &str, _clipboard: Clipboard) {} |
117 | |
118 | /// Returns a copy of text stored in the system clipboard, if any. |
119 | /// |
120 | /// If the platform doesn't support the specified clipboard, the function should return None |
121 | fn clipboard_text(&self, _clipboard: Clipboard) -> Option<String> { |
122 | None |
123 | } |
124 | |
125 | /// This function is called when debug() is used in .slint files. The implementation |
126 | /// should direct the output to some developer visible terminal. The default implementation |
127 | /// uses stderr if available, or `console.log` when targeting wasm. |
128 | fn debug_log(&self, _arguments: core::fmt::Arguments) { |
129 | crate::tests::default_debug_log(_arguments); |
130 | } |
131 | } |
132 | |
133 | /// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`] |
134 | #[repr (u8)] |
135 | #[non_exhaustive ] |
136 | #[derive (PartialEq, Clone, Default)] |
137 | pub enum Clipboard { |
138 | /// This is the default clipboard used for text action for Ctrl+V, Ctrl+C. |
139 | /// Corresponds to the secondary clipboard on X11. |
140 | #[default] |
141 | DefaultClipboard = 0, |
142 | |
143 | /// This is the clipboard that is used when text is selected |
144 | /// Corresponds to the primary clipboard on X11. |
145 | /// The Platform implementation should do nothing if copy on select is not supported on that platform. |
146 | SelectionClipboard = 1, |
147 | } |
148 | |
149 | /// Trait that is returned by the [`Platform::new_event_loop_proxy`] |
150 | /// |
151 | /// This are the implementation details for the function that may need to |
152 | /// communicate with the eventloop from different thread |
153 | pub trait EventLoopProxy: Send + Sync { |
154 | /// Exits the event loop. |
155 | /// |
156 | /// This is what is called by [`slint::quit_event_loop()`](crate::api::quit_event_loop) |
157 | fn quit_event_loop(&self) -> Result<(), crate::api::EventLoopError>; |
158 | |
159 | /// Invoke the function from the event loop. |
160 | /// |
161 | /// This is what is called by [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) |
162 | fn invoke_from_event_loop( |
163 | &self, |
164 | event: Box<dyn FnOnce() + Send>, |
165 | ) -> Result<(), crate::api::EventLoopError>; |
166 | } |
167 | |
168 | #[cfg (feature = "std" )] |
169 | static INITIAL_INSTANT: once_cell::sync::OnceCell<time::Instant> = once_cell::sync::OnceCell::new(); |
170 | |
171 | #[cfg (feature = "std" )] |
172 | impl std::convert::From<crate::animations::Instant> for time::Instant { |
173 | fn from(our_instant: crate::animations::Instant) -> Self { |
174 | let the_beginning: Instant = *INITIAL_INSTANT.get_or_init(time::Instant::now); |
175 | the_beginning + core::time::Duration::from_millis(our_instant.0) |
176 | } |
177 | } |
178 | |
179 | static EVENTLOOP_PROXY: OnceCell<Box<dyn EventLoopProxy + 'static>> = OnceCell::new(); |
180 | |
181 | pub(crate) fn event_loop_proxy() -> Option<&'static dyn EventLoopProxy> { |
182 | EVENTLOOP_PROXY.get().map(core::ops::Deref::deref) |
183 | } |
184 | |
185 | /// This enum describes the different error scenarios that may occur when [`set_platform`] |
186 | /// fails. |
187 | #[derive (Debug, Clone, PartialEq)] |
188 | #[repr (C)] |
189 | #[non_exhaustive ] |
190 | pub enum SetPlatformError { |
191 | /// The platform has been initialized in an earlier call to [`set_platform`]. |
192 | AlreadySet, |
193 | } |
194 | |
195 | /// Set the Slint platform abstraction. |
196 | /// |
197 | /// If the platform abstraction was already set this will return `Err`. |
198 | pub fn set_platform(platform: Box<dyn Platform + 'static>) -> Result<(), SetPlatformError> { |
199 | crate::context::GLOBAL_CONTEXT.with(|instance: &OnceCell| { |
200 | if instance.get().is_some() { |
201 | return Err(SetPlatformError::AlreadySet); |
202 | } |
203 | if let Some(proxy: Box) = platform.new_event_loop_proxy() { |
204 | EVENTLOOP_PROXY.set(proxy).map_err(|_| SetPlatformError::AlreadySet)? |
205 | } |
206 | instanceResult<(), SetPlatformError> |
207 | .set(crate::SlintContext::new(platform)) |
208 | .map_err(|_| SetPlatformError::AlreadySet) |
209 | .unwrap(); |
210 | // Ensure a sane starting point for the animation tick. |
211 | update_timers_and_animations(); |
212 | Ok(()) |
213 | }) |
214 | } |
215 | |
216 | /// Call this function to update and potentially activate any pending timers, as well |
217 | /// as advance the state of any active animations. |
218 | /// |
219 | /// This function should be called before rendering or processing input event, at the |
220 | /// beginning of each event loop iteration. |
221 | pub fn update_timers_and_animations() { |
222 | crate::animations::update_animations(); |
223 | crate::timers::TimerList::maybe_activate_timers(crate::animations::Instant::now()); |
224 | } |
225 | |
226 | /// Returns the duration before the next timer is expected to be activated. This is the |
227 | /// largest amount of time that you can wait before calling [`update_timers_and_animations()`]. |
228 | /// |
229 | /// `None` is returned if there is no active timer. |
230 | /// |
231 | /// Call this in your own event loop implementation to know how long the current thread can |
232 | /// go to sleep. Note that this does not take currently activate animations into account. |
233 | /// Only go to sleep if [`Window::has_active_animations()`](crate::api::Window::has_active_animations()) |
234 | /// returns false. |
235 | pub fn duration_until_next_timer_update() -> Option<core::time::Duration> { |
236 | crate::timers::TimerList::next_timeout().map(|timeout: Instant| { |
237 | let duration_since_start: Duration = crateOption::context::GLOBAL_CONTEXT |
238 | .with(|p: &OnceCell| p.get().map(|p: &SlintContext| p.0.platform.duration_since_start())) |
239 | .unwrap_or_default(); |
240 | core::time::Duration::from_millis( |
241 | timeout.0.saturating_sub(duration_since_start.as_millis() as u64), |
242 | ) |
243 | }) |
244 | } |
245 | |
246 | // reexport key enum to the public api |
247 | pub use crate::input::key_codes::Key; |
248 | pub use crate::input::PointerEventButton; |
249 | |
250 | /// A event that describes user input or windowing system events. |
251 | /// |
252 | /// Slint backends typically receive events from the windowing system, translate them to this |
253 | /// enum and deliver them to the scene of items via [`slint::Window::dispatch_event()`](`crate::api::Window::dispatch_event()`). |
254 | /// |
255 | /// The pointer variants describe events originating from an input device such as a mouse |
256 | /// or a contact point on a touch-enabled surface. |
257 | /// |
258 | /// All position fields are in logical window coordinates. |
259 | #[allow (missing_docs)] |
260 | #[derive (Debug, Clone, PartialEq)] |
261 | #[non_exhaustive ] |
262 | #[repr (u32)] |
263 | pub enum WindowEvent { |
264 | /// A pointer was pressed. |
265 | PointerPressed { |
266 | position: LogicalPosition, |
267 | /// The button that was pressed. |
268 | button: PointerEventButton, |
269 | }, |
270 | /// A pointer was released. |
271 | PointerReleased { |
272 | position: LogicalPosition, |
273 | /// The button that was released. |
274 | button: PointerEventButton, |
275 | }, |
276 | /// The position of the pointer has changed. |
277 | PointerMoved { position: LogicalPosition }, |
278 | /// The wheel button of a mouse was rotated to initiate scrolling. |
279 | PointerScrolled { |
280 | position: LogicalPosition, |
281 | /// The amount of logical pixels to scroll in the horizontal direction. |
282 | delta_x: f32, |
283 | /// The amount of logical pixels to scroll in the vertical direction. |
284 | delta_y: f32, |
285 | }, |
286 | /// The pointer exited the window. |
287 | PointerExited, |
288 | /// A key was pressed. |
289 | KeyPressed { |
290 | /// The unicode representation of the key pressed. |
291 | /// |
292 | /// # Example |
293 | /// A specific key can be mapped to a unicode by using the [`Key`] enum |
294 | /// ```rust |
295 | /// let _ = slint::platform::WindowEvent::KeyPressed { text: slint::platform::Key::Shift.into() }; |
296 | /// ``` |
297 | text: SharedString, |
298 | }, |
299 | /// A key press was auto-repeated. |
300 | KeyPressRepeated { |
301 | /// The unicode representation of the key pressed. |
302 | /// |
303 | /// # Example |
304 | /// A specific key can be mapped to a unicode by using the [`Key`] enum |
305 | /// ```rust |
306 | /// let _ = slint::platform::WindowEvent::KeyPressRepeated { text: slint::platform::Key::Shift.into() }; |
307 | /// ``` |
308 | text: SharedString, |
309 | }, |
310 | /// A key was released. |
311 | KeyReleased { |
312 | /// The unicode representation of the key released. |
313 | /// |
314 | /// # Example |
315 | /// A specific key can be mapped to a unicode by using the [`Key`] enum |
316 | /// ```rust |
317 | /// let _ = slint::platform::WindowEvent::KeyReleased { text: slint::platform::Key::Shift.into() }; |
318 | /// ``` |
319 | text: SharedString, |
320 | }, |
321 | /// The window's scale factor has changed. This can happen for example when the display's resolution |
322 | /// changes, the user selects a new scale factor in the system settings, or the window is moved to a |
323 | /// different screen. |
324 | /// Platform implementations should dispatch this event also right after the initial window creation, |
325 | /// to set the initial scale factor the windowing system provided for the window. |
326 | ScaleFactorChanged { |
327 | /// The window system provided scale factor to map logical pixels to physical pixels. |
328 | scale_factor: f32, |
329 | }, |
330 | /// The window was resized. |
331 | /// |
332 | /// The backend must send this event to ensure that the `width` and `height` property of the root Window |
333 | /// element are properly set. |
334 | Resized { |
335 | /// The new logical size of the window |
336 | size: LogicalSize, |
337 | }, |
338 | /// The user requested to close the window. |
339 | /// |
340 | /// The backend should send this event when the user tries to close the window,for example by pressing the close button. |
341 | /// |
342 | /// This will have the effect of invoking the callback set in [`Window::on_close_requested()`](`crate::api::Window::on_close_requested()`) |
343 | /// and then hiding the window depending on the return value of the callback. |
344 | CloseRequested, |
345 | |
346 | /// The Window was activated or de-activated. |
347 | /// |
348 | /// The backend should dispatch this event with true when the window gains focus |
349 | /// and false when the window loses focus. |
350 | WindowActiveChanged(bool), |
351 | } |
352 | |
353 | impl WindowEvent { |
354 | /// The position of the cursor for this event, if any |
355 | pub fn position(&self) -> Option<LogicalPosition> { |
356 | match self { |
357 | WindowEvent::PointerPressed { position: &LogicalPosition, .. } => Some(*position), |
358 | WindowEvent::PointerReleased { position: &LogicalPosition, .. } => Some(*position), |
359 | WindowEvent::PointerMoved { position: &LogicalPosition } => Some(*position), |
360 | WindowEvent::PointerScrolled { position: &LogicalPosition, .. } => Some(*position), |
361 | _ => None, |
362 | } |
363 | } |
364 | } |
365 | |
366 | /** |
367 | * Test the animation tick is updated when a platform is set |
368 | ```rust |
369 | use i_slint_core::platform::*; |
370 | struct DummyBackend; |
371 | impl Platform for DummyBackend { |
372 | fn create_window_adapter( |
373 | &self, |
374 | ) -> Result<std::rc::Rc<dyn WindowAdapter>, PlatformError> { |
375 | Err(PlatformError::Other("not implemented".into())) |
376 | } |
377 | fn duration_since_start(&self) -> core::time::Duration { |
378 | core::time::Duration::from_millis(100) |
379 | } |
380 | } |
381 | |
382 | let start_time = i_slint_core::tests::slint_get_mocked_time(); |
383 | i_slint_core::platform::set_platform(Box::new(DummyBackend{})); |
384 | let time_after_platform_init = i_slint_core::tests::slint_get_mocked_time(); |
385 | assert_ne!(time_after_platform_init, start_time); |
386 | assert_eq!(time_after_platform_init, 100); |
387 | ``` |
388 | */ |
389 | #[cfg (doctest)] |
390 | const _ANIM_TICK_UPDATED_ON_PLATFORM_SET: () = (); |
391 | |