i_slint_backend_testing/
testing_backend.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use i_slint_core::api::PhysicalSize;
5use i_slint_core::graphics::euclid::{Point2D, Size2D};
6use i_slint_core::graphics::FontRequest;
7use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
8use i_slint_core::platform::PlatformError;
9use i_slint_core::renderer::{Renderer, RendererSealed};
10use i_slint_core::window::{InputMethodRequest, WindowAdapter, WindowAdapterInternal};
11
12use i_slint_core::items::TextWrap;
13use std::cell::{Cell, RefCell};
14use std::pin::Pin;
15use std::rc::Rc;
16use std::sync::Mutex;
17
18#[derive(Default)]
19pub struct TestingBackendOptions {
20    pub mock_time: bool,
21    pub threading: bool,
22}
23
24pub struct TestingBackend {
25    clipboard: Mutex<Option<String>>,
26    queue: Option<Queue>,
27    mock_time: bool,
28}
29
30impl TestingBackend {
31    pub fn new(options: TestingBackendOptions) -> Self {
32        Self {
33            clipboard: Mutex::default(),
34            queue: options.threading.then(|| Queue(Default::default(), std::thread::current())),
35            mock_time: options.mock_time,
36        }
37    }
38}
39
40impl i_slint_core::platform::Platform for TestingBackend {
41    fn create_window_adapter(
42        &self,
43    ) -> Result<Rc<dyn WindowAdapter>, i_slint_core::platform::PlatformError> {
44        Ok(Rc::new_cyclic(|self_weak| TestingWindow {
45            window: i_slint_core::api::Window::new(self_weak.clone() as _),
46            size: Default::default(),
47            ime_requests: Default::default(),
48            mouse_cursor: Default::default(),
49        }))
50    }
51
52    fn duration_since_start(&self) -> core::time::Duration {
53        if self.mock_time {
54            // The slint::testing::mock_elapsed_time updates the animation tick directly
55            core::time::Duration::from_millis(i_slint_core::animations::current_tick().0)
56        } else {
57            static INITIAL_INSTANT: std::sync::OnceLock<std::time::Instant> =
58                std::sync::OnceLock::new();
59            let the_beginning = *INITIAL_INSTANT.get_or_init(std::time::Instant::now);
60            std::time::Instant::now() - the_beginning
61        }
62    }
63
64    fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
65        if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
66            *self.clipboard.lock().unwrap() = Some(text.into());
67        }
68    }
69
70    fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
71        if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
72            self.clipboard.lock().unwrap().clone()
73        } else {
74            None
75        }
76    }
77
78    fn run_event_loop(&self) -> Result<(), PlatformError> {
79        let queue = match self.queue.as_ref() {
80            Some(queue) => queue.clone(),
81            None => return Err(PlatformError::NoEventLoopProvider),
82        };
83
84        loop {
85            let e = queue.0.lock().unwrap().pop_front();
86            if !self.mock_time {
87                i_slint_core::platform::update_timers_and_animations();
88            }
89            match e {
90                Some(Event::Quit) => break Ok(()),
91                Some(Event::Event(e)) => e(),
92                None => match i_slint_core::platform::duration_until_next_timer_update() {
93                    Some(duration) if !self.mock_time => std::thread::park_timeout(duration),
94                    _ => std::thread::park(),
95                },
96            }
97        }
98    }
99
100    fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
101        self.queue
102            .as_ref()
103            .map(|q| Box::new(q.clone()) as Box<dyn i_slint_core::platform::EventLoopProxy>)
104    }
105}
106
107pub struct TestingWindow {
108    window: i_slint_core::api::Window,
109    size: Cell<PhysicalSize>,
110    pub ime_requests: RefCell<Vec<InputMethodRequest>>,
111    pub mouse_cursor: Cell<i_slint_core::items::MouseCursor>,
112}
113
114impl WindowAdapterInternal for TestingWindow {
115    fn as_any(&self) -> &dyn std::any::Any {
116        self
117    }
118
119    fn input_method_request(&self, request: i_slint_core::window::InputMethodRequest) {
120        self.ime_requests.borrow_mut().push(request)
121    }
122
123    fn set_mouse_cursor(&self, cursor: i_slint_core::items::MouseCursor) {
124        self.mouse_cursor.set(cursor);
125    }
126}
127
128impl WindowAdapter for TestingWindow {
129    fn window(&self) -> &i_slint_core::api::Window {
130        &self.window
131    }
132
133    fn size(&self) -> PhysicalSize {
134        if self.size.get().width == 0 {
135            PhysicalSize::new(800, 600)
136        } else {
137            self.size.get()
138        }
139    }
140
141    fn set_size(&self, size: i_slint_core::api::WindowSize) {
142        self.window.dispatch_event(i_slint_core::platform::WindowEvent::Resized {
143            size: size.to_logical(1.),
144        });
145        self.size.set(size.to_physical(1.))
146    }
147
148    fn renderer(&self) -> &dyn Renderer {
149        self
150    }
151
152    fn update_window_properties(&self, properties: i_slint_core::window::WindowProperties<'_>) {
153        if self.size.get().width == 0 {
154            let c = properties.layout_constraints();
155            self.size.set(c.preferred.to_physical(self.window.scale_factor()));
156        }
157    }
158
159    fn internal(&self, _: i_slint_core::InternalToken) -> Option<&dyn WindowAdapterInternal> {
160        Some(self)
161    }
162}
163
164impl RendererSealed for TestingWindow {
165    fn text_size(
166        &self,
167        _font_request: i_slint_core::graphics::FontRequest,
168        text: &str,
169        _max_width: Option<LogicalLength>,
170        _scale_factor: ScaleFactor,
171        _text_wrap: TextWrap,
172    ) -> LogicalSize {
173        LogicalSize::new(text.len() as f32 * 10., 10.)
174    }
175
176    fn font_metrics(
177        &self,
178        font_request: i_slint_core::graphics::FontRequest,
179        _scale_factor: ScaleFactor,
180    ) -> i_slint_core::items::FontMetrics {
181        let pixel_size = font_request.pixel_size.unwrap_or(LogicalLength::new(10.));
182        i_slint_core::items::FontMetrics {
183            ascent: pixel_size.get() * 0.7,
184            descent: -pixel_size.get() * 0.3,
185            x_height: 3.,
186            cap_height: 7.,
187        }
188    }
189
190    fn text_input_byte_offset_for_position(
191        &self,
192        text_input: Pin<&i_slint_core::items::TextInput>,
193        pos: LogicalPoint,
194        _font_request: FontRequest,
195        _scale_factor: ScaleFactor,
196    ) -> usize {
197        let text = text_input.text();
198        if pos.y < 0. {
199            return 0;
200        }
201        let line = (pos.y / 10.) as usize;
202        let offset =
203            if line >= 1 { text.split('\n').take(line - 1).map(|l| l.len() + 1).sum() } else { 0 };
204        let Some(line) = text.split('\n').nth(line) else {
205            return text.len();
206        };
207        let column = ((pos.x / 10.).max(0.) as usize).min(line.len());
208        offset + column
209    }
210
211    fn text_input_cursor_rect_for_byte_offset(
212        &self,
213        text_input: Pin<&i_slint_core::items::TextInput>,
214        byte_offset: usize,
215        _font_request: FontRequest,
216        _scale_factor: ScaleFactor,
217    ) -> LogicalRect {
218        let text = text_input.text();
219        let line = text[..byte_offset].chars().filter(|c| *c == '\n').count();
220        let column = text[..byte_offset].split('\n').nth(line).unwrap_or("").len();
221        LogicalRect::new(Point2D::new(column as f32 * 10., line as f32 * 10.), Size2D::new(1., 10.))
222    }
223
224    fn register_font_from_memory(
225        &self,
226        _data: &'static [u8],
227    ) -> Result<(), Box<dyn std::error::Error>> {
228        Ok(())
229    }
230
231    fn register_font_from_path(
232        &self,
233        _path: &std::path::Path,
234    ) -> Result<(), Box<dyn std::error::Error>> {
235        Ok(())
236    }
237
238    fn default_font_size(&self) -> LogicalLength {
239        LogicalLength::new(10.)
240    }
241
242    fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>) {
243        // No-op since TestingWindow is also the WindowAdapter
244    }
245}
246
247enum Event {
248    Quit,
249    Event(Box<dyn FnOnce() + Send>),
250}
251#[derive(Clone)]
252struct Queue(
253    std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>,
254    std::thread::Thread,
255);
256
257impl i_slint_core::platform::EventLoopProxy for Queue {
258    fn quit_event_loop(&self) -> Result<(), i_slint_core::api::EventLoopError> {
259        self.0.lock().unwrap().push_back(Event::Quit);
260        self.1.unpark();
261        Ok(())
262    }
263
264    fn invoke_from_event_loop(
265        &self,
266        event: Box<dyn FnOnce() + Send>,
267    ) -> Result<(), i_slint_core::api::EventLoopError> {
268        self.0.lock().unwrap().push_back(Event::Event(event));
269        self.1.unpark();
270        Ok(())
271    }
272}