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 | |
7 | use i_slint_core::api::PhysicalSize; |
8 | use i_slint_core::graphics::euclid::{Point2D, Size2D}; |
9 | use i_slint_core::graphics::FontRequest; |
10 | use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor}; |
11 | use i_slint_core::platform::PlatformError; |
12 | use i_slint_core::renderer::{Renderer, RendererSealed}; |
13 | use i_slint_core::window::{InputMethodRequest, WindowAdapter, WindowAdapterInternal}; |
14 | |
15 | use std::cell::{Cell, RefCell}; |
16 | use std::pin::Pin; |
17 | use std::rc::Rc; |
18 | use std::sync::Mutex; |
19 | |
20 | pub struct TestingBackend { |
21 | clipboard: Mutex<Option<String>>, |
22 | queue: Option<Queue>, |
23 | } |
24 | |
25 | impl 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 | |
38 | impl 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 | |
92 | pub 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 | |
99 | impl 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 | |
113 | impl 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 | |
149 | impl 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 | |
207 | enum Event { |
208 | Quit, |
209 | Event(Box<dyn FnOnce() + Send>), |
210 | } |
211 | #[derive (Clone)] |
212 | struct Queue( |
213 | std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>, |
214 | std::thread::Thread, |
215 | ); |
216 | |
217 | impl 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 |
237 | pub 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. |
245 | pub 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 |
251 | mod 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 | |
318 | pub 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 | |
330 | pub use for_unit_test::*; |
331 | |