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#![doc = include_str!("README.md")]
5#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
6
7use i_slint_core::api::PhysicalSize;
8use i_slint_core::graphics::euclid::{Point2D, Size2D};
9use i_slint_core::graphics::FontRequest;
10use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
11use i_slint_core::platform::PlatformError;
12use i_slint_core::renderer::{Renderer, RendererSealed};
13use i_slint_core::window::{InputMethodRequest, WindowAdapter, WindowAdapterInternal};
14
15use std::cell::{Cell, RefCell};
16use std::pin::Pin;
17use std::rc::Rc;
18use std::sync::Mutex;
19
20pub struct TestingBackend {
21 clipboard: Mutex<Option<String>>,
22 queue: Option<Queue>,
23}
24
25impl TestingBackend {
26 pub fn new() -> Self {
27 Self {
28 queue: Some(Queue(Default::default(), std::thread::current())),
29 ..Self::new_no_thread()
30 }
31 }
32
33 pub fn new_no_thread() -> Self {
34 Self { clipboard: Mutex::default(), queue: None }
35 }
36}
37
38impl i_slint_core::platform::Platform for TestingBackend {
39 fn create_window_adapter(
40 &self,
41 ) -> Result<Rc<dyn WindowAdapter>, i_slint_core::platform::PlatformError> {
42 Ok(Rc::new_cyclic(|self_weak| TestingWindow {
43 window: i_slint_core::api::Window::new(self_weak.clone() as _),
44 size: Default::default(),
45 ime_requests: Default::default(),
46 mouse_cursor: Default::default(),
47 }))
48 }
49
50 fn duration_since_start(&self) -> core::time::Duration {
51 // The slint::testing::mock_elapsed_time updates the animation tick directly
52 core::time::Duration::from_millis(i_slint_core::animations::current_tick().0)
53 }
54
55 fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
56 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
57 *self.clipboard.lock().unwrap() = Some(text.into());
58 }
59 }
60
61 fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
62 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
63 self.clipboard.lock().unwrap().clone()
64 } else {
65 None
66 }
67 }
68
69 fn run_event_loop(&self) -> Result<(), PlatformError> {
70 let queue = match self.queue.as_ref() {
71 Some(queue) => queue.clone(),
72 None => return Err(PlatformError::NoEventLoopProvider),
73 };
74
75 loop {
76 let e = queue.0.lock().unwrap().pop_front();
77 match e {
78 Some(Event::Quit) => break Ok(()),
79 Some(Event::Event(e)) => e(),
80 None => std::thread::park(),
81 }
82 }
83 }
84
85 fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
86 self.queue
87 .as_ref()
88 .map(|q| Box::new(q.clone()) as Box<dyn i_slint_core::platform::EventLoopProxy>)
89 }
90}
91
92pub struct TestingWindow {
93 window: i_slint_core::api::Window,
94 size: Cell<PhysicalSize>,
95 pub ime_requests: RefCell<Vec<InputMethodRequest>>,
96 pub mouse_cursor: Cell<i_slint_core::items::MouseCursor>,
97}
98
99impl WindowAdapterInternal for TestingWindow {
100 fn as_any(&self) -> &dyn std::any::Any {
101 self
102 }
103
104 fn input_method_request(&self, request: i_slint_core::window::InputMethodRequest) {
105 self.ime_requests.borrow_mut().push(request)
106 }
107
108 fn set_mouse_cursor(&self, cursor: i_slint_core::items::MouseCursor) {
109 self.mouse_cursor.set(val:cursor);
110 }
111}
112
113impl WindowAdapter for TestingWindow {
114 fn window(&self) -> &i_slint_core::api::Window {
115 &self.window
116 }
117
118 fn size(&self) -> PhysicalSize {
119 if self.size.get().width == 0 {
120 PhysicalSize::new(800, 600)
121 } else {
122 self.size.get()
123 }
124 }
125
126 fn set_size(&self, size: i_slint_core::api::WindowSize) {
127 self.window.dispatch_event(i_slint_core::platform::WindowEvent::Resized {
128 size: size.to_logical(1.),
129 });
130 self.size.set(size.to_physical(1.))
131 }
132
133 fn renderer(&self) -> &dyn Renderer {
134 self
135 }
136
137 fn update_window_properties(&self, properties: i_slint_core::window::WindowProperties<'_>) {
138 if self.size.get().width == 0 {
139 let c = properties.layout_constraints();
140 self.size.set(c.preferred.to_physical(self.window.scale_factor()));
141 }
142 }
143
144 fn internal(&self, _: i_slint_core::InternalToken) -> Option<&dyn WindowAdapterInternal> {
145 Some(self)
146 }
147}
148
149impl RendererSealed for TestingWindow {
150 fn text_size(
151 &self,
152 _font_request: i_slint_core::graphics::FontRequest,
153 text: &str,
154 _max_width: Option<LogicalLength>,
155 _scale_factor: ScaleFactor,
156 ) -> LogicalSize {
157 LogicalSize::new(text.len() as f32 * 10., 10.)
158 }
159
160 // this works only for single line text
161 fn text_input_byte_offset_for_position(
162 &self,
163 text_input: Pin<&i_slint_core::items::TextInput>,
164 pos: LogicalPoint,
165 _font_request: FontRequest,
166 _scale_factor: ScaleFactor,
167 ) -> usize {
168 let text_len = text_input.text().len();
169 let result = pos.x / 10.;
170 result.min(text_len as f32).max(0.) as usize
171 }
172
173 // this works only for single line text
174 fn text_input_cursor_rect_for_byte_offset(
175 &self,
176 _text_input: Pin<&i_slint_core::items::TextInput>,
177 byte_offset: usize,
178 _font_request: FontRequest,
179 _scale_factor: ScaleFactor,
180 ) -> LogicalRect {
181 LogicalRect::new(Point2D::new(byte_offset as f32 * 10., 0.), Size2D::new(1., 10.))
182 }
183
184 fn register_font_from_memory(
185 &self,
186 _data: &'static [u8],
187 ) -> Result<(), Box<dyn std::error::Error>> {
188 Ok(())
189 }
190
191 fn register_font_from_path(
192 &self,
193 _path: &std::path::Path,
194 ) -> Result<(), Box<dyn std::error::Error>> {
195 Ok(())
196 }
197
198 fn default_font_size(&self) -> LogicalLength {
199 LogicalLength::new(10.)
200 }
201
202 fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>) {
203 // No-op since TestingWindow is also the WindowAdapter
204 }
205}
206
207enum Event {
208 Quit,
209 Event(Box<dyn FnOnce() + Send>),
210}
211#[derive(Clone)]
212struct Queue(
213 std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>,
214 std::thread::Thread,
215);
216
217impl i_slint_core::platform::EventLoopProxy for Queue {
218 fn quit_event_loop(&self) -> Result<(), i_slint_core::api::EventLoopError> {
219 self.0.lock().unwrap().push_back(Event::Quit);
220 self.1.unpark();
221 Ok(())
222 }
223
224 fn invoke_from_event_loop(
225 &self,
226 event: Box<dyn FnOnce() + Send>,
227 ) -> Result<(), i_slint_core::api::EventLoopError> {
228 self.0.lock().unwrap().push_back(Event::Event(event));
229 self.1.unpark();
230 Ok(())
231 }
232}
233
234/// Initialize the testing backend.
235/// Must be called before any call that would otherwise initialize the rendering backend.
236/// Calling it when the rendering backend is already initialized will have no effects
237pub fn init() {
238 i_slint_core::platform::set_platform(Box::new(TestingBackend::new_no_thread()))
239 .expect(msg:"platform already initialized");
240}
241
242/// Initialize the testing backend with support for simple event loop.
243/// This function can only be called once per process, so make sure to use integration
244/// tests with one `#[test]` function.
245pub fn init_with_event_loop() {
246 i_slint_core::platform::set_platform(Box::new(TestingBackend::new()))
247 .expect(msg:"platform already initialized");
248}
249
250/// This module contains functions useful for unit tests
251mod for_unit_test {
252 use i_slint_core::api::ComponentHandle;
253 use i_slint_core::platform::WindowEvent;
254 pub use i_slint_core::tests::slint_get_mocked_time as get_mocked_time;
255 pub use i_slint_core::tests::slint_mock_elapsed_time as mock_elapsed_time;
256 use i_slint_core::window::WindowInner;
257 use i_slint_core::SharedString;
258
259 /// Simulate a mouse click
260 pub fn send_mouse_click<
261 X: vtable::HasStaticVTable<i_slint_core::item_tree::ItemTreeVTable> + 'static,
262 Component: Into<vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, X>> + ComponentHandle,
263 >(
264 component: &Component,
265 x: f32,
266 y: f32,
267 ) {
268 i_slint_core::tests::slint_send_mouse_click(
269 x,
270 y,
271 &WindowInner::from_pub(component.window()).window_adapter(),
272 );
273 }
274
275 /// Simulate entering a sequence of ascii characters key by (pressed or released).
276 pub fn send_keyboard_char<
277 X: vtable::HasStaticVTable<i_slint_core::item_tree::ItemTreeVTable>,
278 Component: Into<vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, X>> + ComponentHandle,
279 >(
280 component: &Component,
281 string: char,
282 pressed: bool,
283 ) {
284 i_slint_core::tests::slint_send_keyboard_char(
285 &SharedString::from(string),
286 pressed,
287 &WindowInner::from_pub(component.window()).window_adapter(),
288 )
289 }
290
291 /// Simulate entering a sequence of ascii characters key by key.
292 pub fn send_keyboard_string_sequence<
293 X: vtable::HasStaticVTable<i_slint_core::item_tree::ItemTreeVTable>,
294 Component: Into<vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, X>> + ComponentHandle,
295 >(
296 component: &Component,
297 sequence: &str,
298 ) {
299 i_slint_core::tests::send_keyboard_string_sequence(
300 &SharedString::from(sequence),
301 &WindowInner::from_pub(component.window()).window_adapter(),
302 )
303 }
304
305 /// Applies the specified scale factor to the window that's associated with the given component.
306 /// This overrides the value provided by the windowing system.
307 pub fn set_window_scale_factor<
308 X: vtable::HasStaticVTable<i_slint_core::item_tree::ItemTreeVTable>,
309 Component: Into<vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, X>> + ComponentHandle,
310 >(
311 component: &Component,
312 factor: f32,
313 ) {
314 component.window().dispatch_event(WindowEvent::ScaleFactorChanged { scale_factor: factor });
315 }
316}
317
318pub fn access_testing_window<R>(
319 window: &i_slint_core::api::Window,
320 callback: impl FnOnce(&TestingWindow) -> R,
321) -> R {
322 i_slint_core::window::WindowInner::from_pub(window)
323 .window_adapter()
324 .internal(i_slint_core::InternalToken)
325 .and_then(|wa| wa.as_any().downcast_ref::<TestingWindow>())
326 .map(callback)
327 .expect(msg:"access_testing_window called without testing backend/adapter")
328}
329
330pub use for_unit_test::*;
331