Skip to main content

ferroui_headless/
headless_window_extensions.rs

1//! Port of `HeadlessWindowExtensions.cs`: the extensions of a top-level
2//! that simulate input on a headless window and read what it rendered.
3
4use crate::ferro_headless_platform::FerroHeadlessPlatform;
5use crate::headless_platform_render_interface::HeadlessPlatformRenderInterface;
6use crate::headless_window_impl::HeadlessWindowImpl;
7use crate::i_headless_touch_pointer::IHeadlessTouchPointer;
8use crate::i_headless_window::IHeadlessWindow;
9use ferroui_base::input::raw::{RawDragEventType, RawPointerEventType};
10use ferroui_base::input::{DragDropEffects, IDataTransfer, Key, MouseButton, PhysicalKey, RawInputModifiers};
11use ferroui_base::media::imaging::WriteableBitmap;
12use ferroui_base::reactive::IDisposable;
13use ferroui_base::threading::{Dispatcher, DispatcherPriority};
14use ferroui_base::{Point, Ref, Vector};
15use ferroui_controls::TopLevel;
16use std::any::Any;
17use std::cell::Cell;
18use std::rc::Rc;
19use std::sync::atomic::{AtomicI64, Ordering};
20
21static NEXT_TOUCH_POINT_ID: AtomicI64 = AtomicI64::new(0);
22
23/// Set of extension methods to simplify usage of headless windows in the
24/// headless scenarios.
25///
26/// The parameters that are optional in the original
27/// (`RawInputModifiers.None`) are always given.
28pub trait HeadlessWindowExtensions {
29    /// Triggers a renderer timer tick and captures last rendered frame.
30    ///
31    /// Returns the bitmap with last rendered frame; `None` if nothing was
32    /// rendered.
33    fn capture_rendered_frame(&self) -> Option<WriteableBitmap>;
34
35    /// Reads last rendered frame.
36    /// Note, in order to trigger rendering timer,
37    /// [`FerroHeadlessPlatform::force_render_timer_tick`] method should be
38    /// called.
39    ///
40    /// Returns the bitmap with last rendered frame; `None` if nothing was
41    /// rendered.
42    ///
43    /// # Panics
44    /// Panics if the application uses the headless drawing
45    /// (`NotSupportedException`): it draws nothing.
46    fn get_last_rendered_frame(&self) -> Option<WriteableBitmap>;
47
48    /// Simulates a keyboard press on the headless window/toplevel.
49    fn key_press(&self, key: Key, modifiers: RawInputModifiers, physical_key: PhysicalKey, key_symbol: Option<&str>);
50
51    /// Simulates a keyboard press on the headless window/toplevel, as if
52    /// typed on a QWERTY keyboard.
53    fn key_press_qwerty(&self, physical_key: PhysicalKey, modifiers: RawInputModifiers);
54
55    /// Simulates a keyboard release on the headless window/toplevel.
56    fn key_release(&self, key: Key, modifiers: RawInputModifiers, physical_key: PhysicalKey, key_symbol: Option<&str>);
57
58    /// Simulates a keyboard release on the headless window/toplevel, as if
59    /// typed on a QWERTY keyboard.
60    fn key_release_qwerty(&self, physical_key: PhysicalKey, modifiers: RawInputModifiers);
61
62    /// Simulates a text input event on the headless window/toplevel.
63    ///
64    /// This event is independent of [`key_press`](Self::key_press) and
65    /// [`key_release`](Self::key_release). If you need to simulate text
66    /// input to a TextBox or a similar control, please use this method.
67    fn key_text_input(&self, text: &str);
68
69    /// Simulates a mouse down on the headless window/toplevel.
70    ///
71    /// In the headless platform, there is a single mouse pointer. There are
72    /// no helper methods for pen input; for touch, see
73    /// [`touch_begin`](Self::touch_begin).
74    fn mouse_down(&self, point: Point, button: MouseButton, modifiers: RawInputModifiers);
75
76    /// Simulates a mouse move on the headless window/toplevel.
77    fn mouse_move(&self, point: Point, modifiers: RawInputModifiers);
78
79    /// Simulates a mouse up on the headless window/toplevel.
80    fn mouse_up(&self, point: Point, button: MouseButton, modifiers: RawInputModifiers);
81
82    /// Simulates a mouse wheel on the headless window/toplevel.
83    fn mouse_wheel(&self, point: Point, delta: Vector, modifiers: RawInputModifiers);
84
85    /// Begins a simulated touch contact on the headless window/toplevel.
86    ///
87    /// Each call begins an independent contact with its own unique touch
88    /// id, so several contacts can be active at the same time to simulate
89    /// multi-touch gestures.
90    ///
91    /// Returns a touch pointer representing the contact. Pass it to
92    /// [`touch_move`](Self::touch_move) and [`touch_end`](Self::touch_end),
93    /// or dispose it to cancel the contact.
94    fn touch_begin(&self, point: Point, modifiers: RawInputModifiers) -> Rc<dyn IHeadlessTouchPointer>;
95
96    /// Moves a simulated touch contact started by
97    /// [`touch_begin`](Self::touch_begin).
98    fn touch_move(&self, touch_pointer: &dyn IHeadlessTouchPointer, point: Point, modifiers: RawInputModifiers);
99
100    /// Ends a simulated touch contact started by
101    /// [`touch_begin`](Self::touch_begin), as if the finger was lifted.
102    fn touch_end(&self, touch_pointer: &dyn IHeadlessTouchPointer, point: Point, modifiers: RawInputModifiers);
103
104    /// Simulates a drag and drop target event on the headless
105    /// window/toplevel. This event simulates a user moving files from
106    /// another app to the current app.
107    fn drag_drop(
108        &self,
109        point: Point,
110        type_: RawDragEventType,
111        data: Rc<dyn IDataTransfer>,
112        effects: DragDropEffects,
113        modifiers: RawInputModifiers,
114    );
115
116    /// Changes the render scaling (DPI scaling) of the headless
117    /// window/toplevel. This simulates a DPI change, triggering scaling
118    /// changed notifications and a layout pass.
119    fn set_render_scaling(&self, scaling: f64);
120}
121
122impl HeadlessWindowExtensions for TopLevel {
123    fn capture_rendered_frame(&self) -> Option<WriteableBitmap> {
124        let mut bitmap = None;
125        run_jobs_on_impl(self, |w| bitmap = w.get_last_rendered_frame());
126        bitmap
127    }
128
129    fn get_last_rendered_frame(&self) -> Option<WriteableBitmap> {
130        if HeadlessPlatformRenderInterface::is_current() {
131            panic!(
132                "To capture a rendered frame, make sure that headless application was initialized with the Skia backend and disabled 'use_headless_drawing' in the 'FerroHeadlessPlatformOptions'."
133            );
134        }
135
136        with_impl(self, |w| w.get_last_rendered_frame())
137    }
138
139    fn key_press(&self, key: Key, modifiers: RawInputModifiers, physical_key: PhysicalKey, key_symbol: Option<&str>) {
140        run_jobs_on_impl(self, |w| w.key_press(key, modifiers, physical_key, key_symbol));
141    }
142
143    fn key_press_qwerty(&self, physical_key: PhysicalKey, modifiers: RawInputModifiers) {
144        run_jobs_on_impl(self, |w| {
145            w.key_press(physical_key.to_qwerty_key(), modifiers, physical_key, physical_key.to_qwerty_key_symbol(false))
146        });
147    }
148
149    fn key_release(&self, key: Key, modifiers: RawInputModifiers, physical_key: PhysicalKey, key_symbol: Option<&str>) {
150        run_jobs_on_impl(self, |w| w.key_release(key, modifiers, physical_key, key_symbol));
151    }
152
153    fn key_release_qwerty(&self, physical_key: PhysicalKey, modifiers: RawInputModifiers) {
154        run_jobs_on_impl(self, |w| {
155            w.key_release(physical_key.to_qwerty_key(), modifiers, physical_key, physical_key.to_qwerty_key_symbol(false))
156        });
157    }
158
159    fn key_text_input(&self, text: &str) {
160        run_jobs_on_impl(self, |w| w.text_input(text));
161    }
162
163    fn mouse_down(&self, point: Point, button: MouseButton, modifiers: RawInputModifiers) {
164        run_jobs_on_impl(self, |w| w.mouse_down(point, button, modifiers));
165    }
166
167    fn mouse_move(&self, point: Point, modifiers: RawInputModifiers) {
168        run_jobs_on_impl(self, |w| w.mouse_move(point, modifiers));
169    }
170
171    fn mouse_up(&self, point: Point, button: MouseButton, modifiers: RawInputModifiers) {
172        run_jobs_on_impl(self, |w| w.mouse_up(point, button, modifiers));
173    }
174
175    fn mouse_wheel(&self, point: Point, delta: Vector, modifiers: RawInputModifiers) {
176        run_jobs_on_impl(self, |w| w.mouse_wheel(point, delta, modifiers));
177    }
178
179    fn touch_begin(&self, point: Point, modifiers: RawInputModifiers) -> Rc<dyn IHeadlessTouchPointer> {
180        let touch_point_id = NEXT_TOUCH_POINT_ID.fetch_add(1, Ordering::SeqCst) + 1;
181        run_jobs_on_impl(self, |w| w.touch(point, touch_point_id, RawPointerEventType::TouchBegin, modifiers));
182        Rc::new(HeadlessTouchPointer {
183            top_level: self.to_ref(),
184            touch_point_id,
185            position: Cell::new(point),
186            pressed: Cell::new(true),
187        })
188    }
189
190    fn touch_move(&self, touch_pointer: &dyn IHeadlessTouchPointer, point: Point, modifiers: RawInputModifiers) {
191        get_touch_pointer(self, touch_pointer).move_(point, modifiers);
192    }
193
194    fn touch_end(&self, touch_pointer: &dyn IHeadlessTouchPointer, point: Point, modifiers: RawInputModifiers) {
195        get_touch_pointer(self, touch_pointer).end(point, modifiers);
196    }
197
198    fn drag_drop(
199        &self,
200        point: Point,
201        type_: RawDragEventType,
202        data: Rc<dyn IDataTransfer>,
203        effects: DragDropEffects,
204        modifiers: RawInputModifiers,
205    ) {
206        run_jobs_on_impl(self, |w| w.drag_drop(point, type_, data, effects, modifiers));
207    }
208
209    fn set_render_scaling(&self, scaling: f64) {
210        run_jobs_on_impl(self, |w| w.set_render_scaling(scaling));
211    }
212}
213
214fn run_jobs_on_impl(top_level: &TopLevel, action: impl FnOnce(&HeadlessWindowImpl)) {
215    run_jobs_and_render();
216    with_impl(top_level, action);
217    run_jobs_and_render();
218}
219
220fn run_jobs_and_render() {
221    let dispatcher = Dispatcher::ui_thread();
222
223    // Run jobs and render frames until everything is stable.
224    // We use a simple approach: run jobs, render, and repeat until
225    // there are no more pending jobs. The render timer tick can schedule
226    // new jobs, so we loop until stable.
227    for _ in 0..10 {
228        dispatcher.run_jobs(None);
229        FerroHeadlessPlatform::force_render_timer_tick(1);
230
231        // `DispatcherPriority.MinimumActiveValue`, which is the system idle priority.
232        if !dispatcher.has_jobs_with_priority(DispatcherPriority::SYSTEM_IDLE) {
233            return;
234        }
235    }
236
237    // Final attempt: run remaining jobs without rendering
238    dispatcher.run_jobs(None);
239}
240
241/// `GetImpl`, for the duration of `f`.
242///
243/// # Panics
244/// Panics if the top-level is closed (`ObjectDisposedException`) or is not a
245/// headless window (`InvalidOperationException`).
246fn with_impl<R>(top_level: &TopLevel, f: impl FnOnce(&HeadlessWindowImpl) -> R) -> R {
247    let Some(platform_impl) = top_level.platform_impl() else {
248        panic!("Cannot access a disposed object.\nObject name: '{}'.", top_level.get_type().name());
249    };
250    match platform_impl.as_any().downcast_ref::<HeadlessWindowImpl>() {
251        Some(headless) => f(headless),
252        None => panic!("TopLevel must be a headless window."),
253    }
254}
255
256/// `topLevel.PlatformImpl is IHeadlessWindow`.
257fn has_headless_impl(top_level: &TopLevel) -> bool {
258    top_level
259        .platform_impl()
260        .is_some_and(|platform_impl| platform_impl.as_any().downcast_ref::<HeadlessWindowImpl>().is_some())
261}
262
263/// # Panics
264/// Panics if the touch pointer was not created by `touch_begin` or belongs
265/// to another top-level (`ArgumentException`).
266fn get_touch_pointer<'a>(top_level: &TopLevel, touch_pointer: &'a dyn IHeadlessTouchPointer) -> &'a HeadlessTouchPointer {
267    let Some(headless_touch_pointer) = touch_pointer.as_any().downcast_ref::<HeadlessTouchPointer>() else {
268        panic!("The touch pointer was not created by TouchBegin. (Parameter 'touchPointer')");
269    };
270    if headless_touch_pointer.top_level != top_level.to_ref() {
271        panic!("The touch pointer belongs to a different toplevel. (Parameter 'touchPointer')");
272    }
273    headless_touch_pointer
274}
275
276struct HeadlessTouchPointer {
277    top_level: Ref<TopLevel>,
278    touch_point_id: i64,
279    position: Cell<Point>,
280    pressed: Cell<bool>,
281}
282
283impl HeadlessTouchPointer {
284    fn move_(&self, point: Point, modifiers: RawInputModifiers) {
285        self.throw_if_released();
286        let touch_point_id = self.touch_point_id;
287        run_jobs_on_impl(&self.top_level, |w| w.touch(point, touch_point_id, RawPointerEventType::TouchUpdate, modifiers));
288        self.position.set(point);
289    }
290
291    fn end(&self, point: Point, modifiers: RawInputModifiers) {
292        self.throw_if_released();
293        self.pressed.set(false);
294        let touch_point_id = self.touch_point_id;
295        run_jobs_on_impl(&self.top_level, |w| w.touch(point, touch_point_id, RawPointerEventType::TouchEnd, modifiers));
296    }
297
298    /// # Panics
299    /// Panics if the contact was released (`InvalidOperationException`).
300    fn throw_if_released(&self) {
301        if !self.pressed.get() {
302            panic!("The touch pointer has already been released.");
303        }
304    }
305}
306
307impl IDisposable for HeadlessTouchPointer {
308    fn dispose(&self) {
309        if !self.pressed.get() {
310            return;
311        }
312        self.pressed.set(false);
313
314        // The toplevel might have been closed already, cancelling all of its touch pointers.
315        if has_headless_impl(&self.top_level) {
316            let position = self.position.get();
317            let touch_point_id = self.touch_point_id;
318            run_jobs_on_impl(&self.top_level, |w| {
319                w.touch(position, touch_point_id, RawPointerEventType::TouchCancel, RawInputModifiers::empty())
320            });
321        }
322    }
323}
324
325impl IHeadlessTouchPointer for HeadlessTouchPointer {
326    fn as_any(&self) -> &dyn Any {
327        self
328    }
329}