1use i_slint_core::api::PhysicalSize;
5use i_slint_core::graphics::euclid::{Point2D, Size2D};
6use i_slint_core::item_rendering::HasFont;
7use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize};
8use i_slint_core::platform::PlatformError;
9use i_slint_core::renderer::{Renderer, RendererSealed};
10use i_slint_core::textlayout::sharedparley;
11use i_slint_core::window::{
12 InputMethodRequest, WindowAdapter, WindowAdapterInternal, WindowInner, WindowKind,
13};
14
15use i_slint_core::SharedString;
16use i_slint_core::items::TextWrap;
17use std::cell::{Cell, RefCell};
18use std::collections::HashMap;
19use std::pin::Pin;
20use std::rc::{Rc, Weak};
21use std::sync::Mutex;
22
23std::thread_local! {
24 static ALL_TESTING_WINDOWS: RefCell<Vec<Weak<TestingWindow>>> =
26 const { RefCell::new(Vec::new()) }
27}
28
29fn ensure_all_tracked_trees_instantiated() {
32 let live: Vec<Rc<TestingWindow>> = ALL_TESTING_WINDOWS.with(|list| {
33 let mut list = list.borrow_mut();
34 list.retain(|w| w.upgrade().is_some());
35 list.iter().filter_map(|w| w.upgrade()).collect()
36 });
37 for tw in live {
38 WindowInner::from_pub(&tw.window).ensure_tree_instantiated();
39 }
40}
41
42pub fn mock_elapsed_time(time_in_ms: u64) {
46 let tick = i_slint_core::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
47 let mut tick = driver.current_tick();
48 tick += core::time::Duration::from_millis(time_in_ms);
49 driver.update_animations(tick);
50 tick
51 });
52 i_slint_core::timers::TimerList::maybe_activate_timers(tick);
53 i_slint_core::properties::ChangeTracker::run_change_handlers();
54 ensure_all_tracked_trees_instantiated();
55}
56
57pub fn get_mocked_time() -> u64 {
59 i_slint_core::animations::CURRENT_ANIMATION_DRIVER
60 .with(|driver| driver.current_tick())
61 .as_millis()
62}
63
64#[cfg(any(feature = "internal", feature = "ffi"))]
65pub fn send_mouse_click(x: f32, y: f32, window_adapter: &i_slint_core::window::WindowAdapterRc) {
67 use i_slint_core::api::LogicalPosition;
68 use i_slint_core::items::PointerEventButton;
69 use i_slint_core::platform::WindowEvent;
70
71 let position = LogicalPosition::new(x, y);
72 let button = PointerEventButton::Left;
73
74 window_adapter.window().dispatch_event(WindowEvent::PointerMoved { position });
75 window_adapter.window().dispatch_event(WindowEvent::PointerPressed { position, button });
76 mock_elapsed_time(50);
77 window_adapter.window().dispatch_event(WindowEvent::PointerReleased { position, button });
78}
79
80#[cfg(any(feature = "internal", feature = "ffi"))]
81pub fn send_keyboard_key_text(
83 text: &i_slint_core::SharedString,
84 pressed: bool,
85 window_adapter: &i_slint_core::window::WindowAdapterRc,
86) {
87 use i_slint_core::platform::WindowEvent;
88 window_adapter.window().dispatch_event(if pressed {
89 WindowEvent::KeyPressed { text: text.clone() }
90 } else {
91 WindowEvent::KeyReleased { text: text.clone() }
92 })
93}
94
95#[cfg(feature = "ffi")]
96pub fn send_keyboard_char(
98 string: &i_slint_core::SharedString,
99 pressed: bool,
100 window_adapter: &i_slint_core::window::WindowAdapterRc,
101) {
102 for ch in string.chars() {
103 send_keyboard_key_text(&ch.into(), pressed, window_adapter);
104 }
105}
106
107#[cfg(any(feature = "internal", feature = "ffi"))]
108pub fn send_keyboard_string_sequence(
110 sequence: &i_slint_core::SharedString,
111 window_adapter: &i_slint_core::window::WindowAdapterRc,
112) {
113 use i_slint_core::input::key_codes::Key;
114 use i_slint_core::platform::WindowEvent;
115
116 for ch in sequence.chars() {
117 if ch.is_ascii_uppercase() {
118 window_adapter
119 .window()
120 .dispatch_event(WindowEvent::KeyPressed { text: Key::Shift.into() });
121 }
122
123 let text: i_slint_core::SharedString = ch.into();
124 window_adapter.window().dispatch_event(WindowEvent::KeyPressed { text: text.clone() });
125 window_adapter.window().dispatch_event(WindowEvent::KeyReleased { text });
126
127 if ch.is_ascii_uppercase() {
128 window_adapter
129 .window()
130 .dispatch_event(WindowEvent::KeyReleased { text: Key::Shift.into() });
131 }
132 }
133}
134
135const FIXED_TEST_FONT: &str = "FixedTestFont";
136
137fn is_fixed_test_font(family: &Option<SharedString>) -> bool {
138 family.as_ref().is_some_and(|f| f == FIXED_TEST_FONT)
139}
140
141#[derive(Default)]
142pub struct TestingBackendOptions {
143 pub mock_time: bool,
144 pub threading: bool,
145 #[cfg(supports_headless)]
151 pub renderer_name: Option<SharedString>,
152}
153
154pub struct TestingBackend {
155 clipboard: Mutex<Option<String>>,
156 queue: Option<Queue>,
157 mock_time: bool,
158 pub open_url: Rc<RefCell<Option<SharedString>>>,
159 pub debug_logs: Rc<RefCell<Vec<String>>>,
160 #[cfg(supports_headless)]
161 renderer_name: Option<SharedString>,
162}
163
164impl TestingBackend {
165 pub fn new(options: TestingBackendOptions) -> Self {
166 Self {
167 clipboard: Mutex::default(),
168 queue: options.threading.then(|| Queue(Default::default(), std::thread::current())),
169 mock_time: options.mock_time,
170 open_url: Default::default(),
171 debug_logs: Default::default(),
172 #[cfg(supports_headless)]
173 renderer_name: options.renderer_name,
174 }
175 }
176}
177
178impl i_slint_core::platform::Platform for TestingBackend {
179 fn create_window_adapter(
180 &self,
181 ) -> Result<Rc<dyn WindowAdapter>, i_slint_core::platform::PlatformError> {
182 #[cfg(supports_headless)]
183 let renderer =
184 self.renderer_name.as_ref().map(|name| create_headless_renderer(name)).transpose()?;
185 let window = Rc::new_cyclic(|self_weak| TestingWindow {
186 window: i_slint_core::api::Window::new(self_weak.clone() as _),
187 size: Default::default(),
188 ime_requests: Default::default(),
189 mouse_cursor: Default::default(),
190 all_item_trees: Default::default(),
191 open_url: self.open_url.clone(),
192 debug_logs: self.debug_logs.clone(),
193 native_popup: Cell::new(false),
194 #[cfg(supports_headless)]
195 renderer_name: self.renderer_name.clone(),
196 #[cfg(supports_headless)]
197 renderer,
198 });
199 ALL_TESTING_WINDOWS.with(|list| list.borrow_mut().push(Rc::downgrade(&window)));
200 Ok(window)
201 }
202
203 fn duration_since_start(&self) -> core::time::Duration {
204 if self.mock_time {
205 core::time::Duration::from_millis(i_slint_core::animations::current_tick().0)
207 } else {
208 static INITIAL_INSTANT: std::sync::OnceLock<std::time::Instant> =
209 std::sync::OnceLock::new();
210 let the_beginning = *INITIAL_INSTANT.get_or_init(std::time::Instant::now);
211 std::time::Instant::now() - the_beginning
212 }
213 }
214
215 fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
216 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
217 *self.clipboard.lock().unwrap() = Some(text.into());
218 }
219 }
220
221 fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
222 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
223 self.clipboard.lock().unwrap().clone()
224 } else {
225 None
226 }
227 }
228
229 fn run_event_loop(&self) -> Result<(), PlatformError> {
230 let queue = match self.queue.as_ref() {
231 Some(queue) => queue.clone(),
232 None => return Err(PlatformError::NoEventLoopProvider),
233 };
234
235 loop {
236 let e = queue.0.lock().unwrap().pop_front();
237 if !self.mock_time {
238 i_slint_core::platform::update_timers_and_animations();
239 }
240 match e {
241 Some(Event::Quit) => break Ok(()),
242 Some(Event::Event(e)) => e(),
243 None => match i_slint_core::platform::duration_until_next_timer_update() {
244 Some(duration) if !self.mock_time => std::thread::park_timeout(duration),
245 _ => std::thread::park(),
246 },
247 }
248 }
249 }
250
251 fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
252 self.queue
253 .as_ref()
254 .map(|q| Box::new(q.clone()) as Box<dyn i_slint_core::platform::EventLoopProxy>)
255 }
256
257 fn open_url(&self, url: &str) -> Result<(), PlatformError> {
258 *self.open_url.borrow_mut() = Some(url.into());
259 Ok(())
260 }
261
262 fn debug_log(&self, arguments: core::fmt::Arguments) {
263 self.debug_logs.borrow_mut().push(arguments.to_string());
264 i_slint_core::debug_log::default_log_message(arguments);
265 }
266}
267
268#[derive(Default)]
269struct CheckAllItemTreesUnregistered(RefCell<HashMap<*const u8, SharedString>>);
270
271impl Drop for CheckAllItemTreesUnregistered {
272 fn drop(&mut self) {
273 if !std::thread::panicking() {
274 assert!(
275 self.0.borrow().is_empty(),
276 "Some item trees were not unregistered: {:?}",
277 self.0.borrow().values()
278 );
279 }
280 }
281}
282
283pub struct TestingWindow {
284 window: i_slint_core::api::Window,
285 size: Cell<PhysicalSize>,
286 pub ime_requests: RefCell<Vec<InputMethodRequest>>,
287 mouse_cursor: Cell<i_slint_core::items::MouseCursor>,
288 all_item_trees: CheckAllItemTreesUnregistered,
289 pub open_url: Rc<RefCell<Option<SharedString>>>,
290 pub debug_logs: Rc<RefCell<Vec<String>>>,
291 native_popup: Cell<bool>,
292 #[cfg(supports_headless)]
294 renderer_name: Option<SharedString>,
295 #[cfg(supports_headless)]
299 renderer: Option<Box<dyn Renderer>>,
300}
301
302impl TestingWindow {
303 pub fn use_native_popup(&self, native: bool) {
304 self.native_popup.set(native);
305 }
306
307 #[allow(dead_code)] pub fn mouse_cursor(&self) -> i_slint_core::items::MouseCursor {
309 self.mouse_cursor.get()
310 }
311
312 #[allow(dead_code)]
313 pub fn open_url(&self) -> Option<SharedString> {
314 self.open_url.borrow().clone()
315 }
316
317 pub fn take_debug_log(&self) -> Vec<String> {
319 self.debug_logs.borrow_mut().drain(..).collect()
320 }
321}
322
323impl WindowAdapterInternal for TestingWindow {
324 fn input_method_request(&self, request: i_slint_core::window::InputMethodRequest) {
325 self.ime_requests.borrow_mut().push(request)
326 }
327
328 fn set_mouse_cursor(&self, cursor: i_slint_core::items::MouseCursor) {
329 self.mouse_cursor.set(cursor);
330 }
331
332 fn register_item_tree(&self, item_tree: i_slint_core::item_tree::ItemTreeRefPin) {
333 let mut debug = SharedString::new();
334 item_tree.as_ref().item_element_infos(0, &mut debug);
335 assert_eq!(
336 self.all_item_trees.0.borrow_mut().insert(item_tree.as_ptr(), debug.clone()),
337 None,
338 "Item tree already registered {debug:?}"
339 );
340 }
341
342 fn unregister_item_tree(
343 &self,
344 item_tree: i_slint_core::item_tree::ItemTreeRef,
345 _items: &mut dyn Iterator<Item = Pin<i_slint_core::items::ItemRef<'_>>>,
346 ) {
347 self.all_item_trees.0.borrow_mut().remove(&item_tree.as_ptr());
348 }
349
350 fn create_child_window_adapter(&self, _kind: WindowKind) -> Option<Rc<dyn WindowAdapter>> {
351 if self.native_popup.get() {
352 #[cfg(supports_headless)]
353 let renderer = self
354 .renderer_name
355 .as_ref()
356 .map(|name| create_headless_renderer(name))
357 .transpose()
358 .ok()?;
359 let window = Rc::new_cyclic(|self_weak| TestingWindow {
360 window: i_slint_core::api::Window::new(self_weak.clone() as _),
361 size: Default::default(),
362 ime_requests: Default::default(),
363 mouse_cursor: Default::default(),
364 all_item_trees: Default::default(),
365 open_url: self.open_url.clone(),
366 debug_logs: self.debug_logs.clone(),
367 native_popup: self.native_popup.clone(),
368 #[cfg(supports_headless)]
369 renderer_name: self.renderer_name.clone(),
370 #[cfg(supports_headless)]
371 renderer,
372 });
373 Some(window)
374 } else {
375 None
376 }
377 }
378}
379
380impl WindowAdapter for TestingWindow {
381 fn window(&self) -> &i_slint_core::api::Window {
382 &self.window
383 }
384
385 fn size(&self) -> PhysicalSize {
386 if self.size.get().width == 0 { PhysicalSize::new(800, 600) } else { self.size.get() }
387 }
388
389 fn set_size(&self, size: i_slint_core::api::WindowSize) {
390 self.window.dispatch_event(i_slint_core::platform::WindowEvent::Resized {
391 size: size.to_logical(1.),
392 });
393 self.size.set(size.to_physical(1.))
394 }
395
396 fn renderer(&self) -> &dyn Renderer {
397 #[cfg(supports_headless)]
398 if let Some(renderer) = &self.renderer {
399 return &**renderer;
400 }
401 self
402 }
403
404 fn update_window_properties(&self, properties: i_slint_core::window::WindowProperties<'_>) {
405 if self.size.get().width == 0 {
406 let c = properties.layout_constraints();
407 self.size.set(c.preferred.to_physical(self.window.scale_factor()));
408 }
409 }
410
411 fn internal(&self, _: i_slint_core::InternalToken) -> Option<&dyn WindowAdapterInternal> {
412 Some(self)
413 }
414}
415
416impl RendererSealed for TestingWindow {
417 fn text_size(
418 &self,
419 text_item: Pin<&dyn i_slint_core::item_rendering::RenderString>,
420 item_rc: &i_slint_core::item_tree::ItemRc,
421 max_width: Option<LogicalLength>,
422 text_wrap: TextWrap,
423 ) -> LogicalSize {
424 let font_request = text_item.font_request(item_rc);
425 if is_fixed_test_font(&font_request.family) {
426 let pixel_size = font_request.pixel_size.map_or(10., |s| s.get());
427 let text: String = match text_item.text() {
428 i_slint_core::item_rendering::PlainOrStyledText::Plain(s) => s.to_string(),
429 i_slint_core::item_rendering::PlainOrStyledText::Styled(s) => {
430 i_slint_core::styled_text::get_raw_text(&s).into_owned()
431 }
432 };
433 let max_line_len = text.lines().map(|l: &str| l.len()).max().unwrap_or(0);
434 let num_lines = text.lines().count().max(1);
435 let width = max_line_len as f32 * pixel_size;
436 let height = num_lines as f32 * pixel_size;
437 LogicalSize::new(width, height)
438 } else {
439 sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap, None)
440 .unwrap_or_default()
441 }
442 }
443
444 fn char_size(
445 &self,
446 text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>,
447 item_rc: &i_slint_core::item_tree::ItemRc,
448 ch: char,
449 ) -> LogicalSize {
450 let font_request = text_item.font_request(item_rc);
451 if is_fixed_test_font(&font_request.family) {
452 let pixel_size = font_request.pixel_size.map_or(10., |s| s.get());
453 LogicalSize::new(pixel_size, pixel_size)
454 } else {
455 let Some(ctx) = self.slint_context() else {
456 return LogicalSize::default();
457 };
458 let mut font_ctx = ctx.font_context().borrow_mut();
459 sharedparley::char_size(&mut font_ctx, text_item, item_rc, ch).unwrap_or_default()
460 }
461 }
462
463 fn font_metrics(
464 &self,
465 font_request: i_slint_core::graphics::FontRequest,
466 ) -> i_slint_core::items::FontMetrics {
467 if is_fixed_test_font(&font_request.family) {
468 let pixel_size = font_request.pixel_size.map_or(10., |s| s.get());
469 i_slint_core::items::FontMetrics {
470 ascent: pixel_size * 0.7,
471 descent: -pixel_size * 0.3,
472 x_height: 3.,
473 cap_height: 7.,
474 }
475 } else {
476 let Some(ctx) = self.slint_context() else {
477 return Default::default();
478 };
479 let mut font_ctx = ctx.font_context().borrow_mut();
480 sharedparley::font_metrics(&mut font_ctx, font_request)
481 }
482 }
483
484 fn text_input_byte_offset_for_position(
485 &self,
486 text_input: Pin<&i_slint_core::items::TextInput>,
487 item_rc: &i_slint_core::item_tree::ItemRc,
488 pos: LogicalPoint,
489 ) -> usize {
490 let font_request = text_input.font_request(item_rc);
491 if is_fixed_test_font(&font_request.family) {
492 let pixel_size = font_request.pixel_size.map_or(10., |s| s.get());
493 let text = text_input.text();
494 if pos.y < 0. {
495 return 0;
496 }
497 let line = (pos.y / pixel_size) as usize;
498 let offset = if line >= 1 {
499 text.split('\n').take(line - 1).map(|l| l.len() + 1).sum()
500 } else {
501 0
502 };
503 let Some(line) = text.split('\n').nth(line) else {
504 return text.len();
505 };
506 let column = ((pos.x / pixel_size).max(0.) as usize).min(line.len());
507 offset + column
508 } else {
509 sharedparley::text_input_byte_offset_for_position(self, text_input, item_rc, pos)
510 }
511 }
512
513 fn text_input_cursor_rect_for_byte_offset(
514 &self,
515 text_input: Pin<&i_slint_core::items::TextInput>,
516 item_rc: &i_slint_core::item_tree::ItemRc,
517 byte_offset: usize,
518 ) -> LogicalRect {
519 let font_request = text_input.font_request(item_rc);
520 if is_fixed_test_font(&font_request.family) {
521 let pixel_size = font_request.pixel_size.map_or(10., |s| s.get());
522 let text = text_input.text();
523 let line = text[..byte_offset].chars().filter(|c| *c == '\n').count();
524 let column = text[..byte_offset].split('\n').nth(line).unwrap_or("").len();
525 LogicalRect::new(
526 Point2D::new(column as f32 * pixel_size, line as f32 * pixel_size),
527 Size2D::new(1., pixel_size),
528 )
529 } else {
530 sharedparley::text_input_cursor_rect_for_byte_offset(
531 self,
532 text_input,
533 item_rc,
534 byte_offset,
535 )
536 }
537 }
538
539 fn register_font_from_memory(
540 &self,
541 data: &'static [u8],
542 ) -> Result<(), Box<dyn std::error::Error>> {
543 let ctx = self.slint_context().ok_or("slint platform not initialized")?;
544 ctx.font_context().borrow_mut().register_static_font(data);
545 Ok(())
546 }
547
548 fn register_font_from_path(
549 &self,
550 path: &std::path::Path,
551 ) -> Result<(), Box<dyn std::error::Error>> {
552 let requested_path = path.canonicalize().unwrap_or_else(|_| path.into());
553 let contents = std::fs::read(requested_path)?;
554 let ctx = self.slint_context().ok_or("slint platform not initialized")?;
555 ctx.font_context().borrow_mut().collection.register_fonts(contents.into(), None);
556 Ok(())
557 }
558
559 fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>) {
560 }
562
563 fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>> {
564 Some(WindowInner::from_pub(&self.window).window_adapter())
565 }
566
567 fn supports_transformations(&self) -> bool {
568 true
569 }
570}
571
572#[cfg(supports_headless)]
576fn create_headless_renderer(name: &str) -> Result<Box<dyn Renderer>, PlatformError> {
577 match name {
578 #[cfg(skia_headless)]
579 "" | "default" | "skia" | "skia-software" => {
580 std::thread_local! {
581 static SHARED_CONTEXT: i_slint_renderer_skia::SkiaSharedContext =
583 Default::default();
584 }
585 SHARED_CONTEXT.with(|context| {
586 Ok(Box::new(i_slint_renderer_skia::SkiaRenderer::default_software(context)) as _)
587 })
588 }
589 #[cfg(all(feature = "renderer-software", not(skia_headless)))]
590 "" | "default" => Ok(Box::new(i_slint_renderer_software::SoftwareRenderer::new())),
591 #[cfg(feature = "renderer-software")]
592 "sw" | "software" => Ok(Box::new(i_slint_renderer_software::SoftwareRenderer::new())),
593 other => {
594 let available: &[&str] = &[
595 #[cfg(feature = "renderer-software")]
596 "software",
597 #[cfg(skia_headless)]
598 "skia",
599 ];
600 Err(PlatformError::Other(format!(
601 "Unknown headless renderer {other:?} (available: {})",
602 available.join(", ")
603 )))
604 }
605 }
606}
607
608enum Event {
609 Quit,
610 Event(Box<dyn FnOnce() + Send>),
611}
612#[derive(Clone)]
613struct Queue(
614 std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>,
615 std::thread::Thread,
616);
617
618impl i_slint_core::platform::EventLoopProxy for Queue {
619 fn quit_event_loop(&self) -> Result<(), i_slint_core::api::EventLoopError> {
620 self.0.lock().unwrap().push_back(Event::Quit);
621 self.1.unpark();
622 Ok(())
623 }
624
625 fn invoke_from_event_loop(
626 &self,
627 event: Box<dyn FnOnce() + Send>,
628 ) -> Result<(), i_slint_core::api::EventLoopError> {
629 self.0.lock().unwrap().push_back(Event::Event(event));
630 self.1.unpark();
631 Ok(())
632 }
633}