Skip to main content

ferroui_headless/
ferro_headless_platform.rs

1//! Port of the platform file of the upstream headless project: the
2//! headless platform, its options and the `use_headless` extension of the
3//! application builder.
4
5use crate::headless_platform_render_interface::HeadlessPlatformRenderInterface;
6use crate::headless_platform_stubs::{HeadlessClipboardImplStub, HeadlessCursorFactoryStub, HeadlessIconLoaderStub};
7use crate::headless_render_timer::HeadlessRenderTimer;
8use crate::headless_window_impl::HeadlessWindowImpl;
9use ferroui_base::input::platform::{
10    Clipboard, IClipboard, IClipboardImpl, KeyGestureFormatInfo, PlatformHotkeyConfiguration,
11};
12use ferroui_base::input::{IKeyboardDevice, KeyboardDevice, MouseDevice};
13use ferroui_base::platform::{DefaultPlatformSettings, ICursorFactory, IPlatformSettings, PixelFormat, PixelFormats};
14use ferroui_base::rendering::composition::Compositor;
15use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop, SleepLoopRenderTimer};
16use ferroui_base::threading::{Dispatcher, DispatcherFrame};
17use ferroui_base::{FerroLocator, LocatorExtensions};
18use ferroui_controls::platform::{IPlatformIconLoader, ITopLevelImpl, ITrayIconImpl, IWindowImpl, IWindowingPlatform};
19use ferroui_controls::AppBuilder;
20use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
21use std::cell::RefCell;
22use std::collections::HashMap;
23use std::rc::Rc;
24use std::sync::Arc;
25
26ferroui_base::ferro_thread_local! {
27    // The two static fields of the original. They are per thread: the compositor is an `Rc`,
28    // and a headless application lives on the one thread that initialised the platform.
29    static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
30    /// The render timer when it is the headless one (the UI thread timer
31    /// that can be ticked by force); `None` for the sleep loop timer.
32    static HEADLESS_RENDER_TIMER: RefCell<Option<Arc<HeadlessRenderTimer>>> = const { RefCell::new(None) };
33    /// The options the platform was initialised with, which its windows share and read on
34    /// every use (the options object of the original, which the windows hold by reference).
35    static OPTIONS: RefCell<Option<SharedOptions>> = const { RefCell::new(None) };
36}
37
38/// The options of the platform as its windows hold them.
39pub(crate) type SharedOptions = Rc<RefCell<FerroHeadlessPlatformOptions>>;
40
41/// The headless platform.
42pub struct FerroHeadlessPlatform;
43
44struct HeadlessWindowingPlatform {
45    options: SharedOptions,
46}
47
48impl IWindowingPlatform for HeadlessWindowingPlatform {
49    fn create_window(&self) -> Rc<dyn IWindowImpl> {
50        HeadlessWindowImpl::new(&self.options)
51    }
52
53    fn create_embeddable_top_level(&self) -> Rc<dyn ITopLevelImpl> {
54        let window: Rc<dyn ITopLevelImpl> = self.create_embeddable_window();
55        window
56    }
57
58    /// # Panics
59    /// Always: the headless platform has no embeddable windows
60    /// (`PlatformNotSupportedException`).
61    fn create_embeddable_window(&self) -> Rc<dyn IWindowImpl> {
62        panic!("Operation is not supported on this platform.");
63    }
64
65    fn create_tray_icon(&self) -> Option<Rc<dyn ITrayIconImpl>> {
66        None
67    }
68
69    fn get_windows_z_order(&self, windows: &[Rc<dyn IWindowImpl>], z_order: &mut [i64]) {
70        for (i, window) in windows.iter().enumerate() {
71            z_order[i] = window
72                .as_any()
73                .downcast_ref::<HeadlessWindowImpl>()
74                .map_or(0, |window| i64::from(window.z_order()));
75        }
76    }
77}
78
79impl FerroHeadlessPlatform {
80    /// The compositor of the platform; `None` before the platform is
81    /// initialised.
82    pub(crate) fn compositor() -> Option<Rc<Compositor>> {
83        COMPOSITOR.with(|compositor| compositor.borrow().clone())
84    }
85
86    /// Lets go of the compositor of the calling thread. Not in the
87    /// original, whose compositor is a static field that lives as long as
88    /// the process: here it belongs to the thread that initialised the
89    /// platform, and a thread that ends must drop it while it still has its
90    /// dispatcher. Left to the destructors of the thread, the compositor is
91    /// dropped after the dispatcher is gone, and its removal from the render
92    /// loop, which asserts the UI thread, panics there.
93    /// The options of the platform of the calling thread, which its windows read on every use;
94    /// `None` before the platform is initialised. A test changes them for its own duration (the
95    /// `Reconfigure` of the tests of the original changes the object the platform was given).
96    #[allow(dead_code)] // read by the tests of the crate
97    pub(crate) fn options() -> Option<SharedOptions> {
98        OPTIONS.with(|options| options.borrow().clone())
99    }
100
101    pub(crate) fn release_compositor() {
102        let compositor = COMPOSITOR.with(|slot| slot.borrow_mut().take());
103        drop(compositor);
104    }
105
106    pub(crate) fn initialize(opts: &FerroHeadlessPlatformOptions) {
107        if opts.use_shared_mouse_device == Some(true) {
108            MouseDevice::reset_primary_for_unit_tests();
109        }
110
111        let clipboard_impl: Rc<dyn IClipboardImpl> = Rc::new(HeadlessClipboardImplStub::default());
112        let clipboard: Rc<dyn IClipboard> = Clipboard::new(clipboard_impl.clone());
113
114        let (render_timer, headless_render_timer): (Arc<dyn IRenderTimer>, Option<Arc<HeadlessRenderTimer>>) =
115            if opts.should_render_on_ui_thread {
116                let timer = Arc::new(HeadlessRenderTimer::new(opts.fps));
117                (timer.clone(), Some(timer))
118            } else {
119                (Arc::new(SleepLoopRenderTimer::new(opts.fps)), None)
120            };
121        HEADLESS_RENDER_TIMER.with(|slot| *slot.borrow_mut() = headless_render_timer);
122
123        let keyboard_device: Rc<dyn IKeyboardDevice> = KeyboardDevice::new();
124        let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(render_timer);
125        let options: SharedOptions = Rc::new(RefCell::new(opts.clone()));
126        OPTIONS.with(|slot| *slot.borrow_mut() = Some(options.clone()));
127        let windowing_platform: Rc<dyn IWindowingPlatform> = Rc::new(HeadlessWindowingPlatform { options });
128
129        FerroLocator::current_mutable()
130            .bind::<dyn IClipboardImpl>()
131            .to_constant(clipboard_impl)
132            .bind::<dyn IClipboard>()
133            .to_constant(clipboard)
134            .bind::<dyn ICursorFactory>()
135            .to_singleton::<HeadlessCursorFactoryStub>(|instance| instance)
136            .bind::<dyn IPlatformSettings>()
137            .to_singleton::<DefaultPlatformSettings>(|instance| instance)
138            .bind::<dyn IPlatformIconLoader>()
139            .to_singleton::<HeadlessIconLoaderStub>(|instance| instance)
140            .bind::<dyn IKeyboardDevice>()
141            .to_constant(keyboard_device)
142            .bind::<Arc<dyn IRenderLoop>>()
143            .to_constant(Rc::new(render_loop))
144            .bind::<dyn IWindowingPlatform>()
145            .to_constant(windowing_platform)
146            .bind_to_self_singleton::<PlatformHotkeyConfiguration>()
147            .bind_to_self(Rc::new(KeyGestureFormatInfo::new(Some(HashMap::new()), "Cmd", "Ctrl", "Alt", "Shift")));
148
149        let compositor = Compositor::new(None, false);
150        COMPOSITOR.with(|slot| *slot.borrow_mut() = Some(compositor));
151    }
152
153    /// Forces renderer to process a rendering timer tick.
154    /// Use this method before calling
155    /// [`get_last_rendered_frame`](crate::HeadlessWindowExtensions::get_last_rendered_frame).
156    ///
157    /// `count` is the count of frames to be ticked on the timer (1 in the
158    /// original when it is not given).
159    ///
160    /// # Panics
161    /// Panics if the render timer is not the headless one and the
162    /// compositor is not initialized (`InvalidOperationException`).
163    pub fn force_render_timer_tick(count: i32) {
164        let timer = HEADLESS_RENDER_TIMER.with(|timer| timer.borrow().clone());
165        if let Some(timer) = timer {
166            for _ in 0..count {
167                timer.force_tick();
168            }
169        } else {
170            let Some(compositor) = Self::compositor() else {
171                panic!("Compositor is not initialized.");
172            };
173
174            let frame = DispatcherFrame::new();
175            // `RequestCommitAsync` of the original is the processed signal of the batch.
176            let batch = compositor.request_commit_async();
177            let continuation_frame = frame.clone();
178            batch.processed().on_completed(move || continuation_frame.set_continue(false));
179            Dispatcher::current_dispatcher().push_frame(&frame);
180        }
181    }
182}
183
184/// Options for configuring the headless platform.
185#[derive(Clone, Debug, PartialEq)]
186pub struct FerroHeadlessPlatformOptions {
187    /// Gets or sets the number of frames per second at which the renderer should run.
188    /// Default 60.
189    pub fps: i32,
190
191    /// Render directly on the UI thread instead of using a dedicated render thread.
192    /// This can be usable if your device doesn't have multiple cores to begin with.
193    /// This setting is true by default (the documentation of the original says false).
194    pub should_render_on_ui_thread: bool,
195
196    /// Gets or sets a value indicating whether to use headless drawing mode, which allows
197    /// rendering without creating an actual window.
198    ///
199    /// Disable this option if you are using the Skia backend or another drawing backend.
200    pub use_headless_drawing: bool,
201
202    /// Gets or sets the pixel format to be used for the headless window framebuffers.
203    pub frame_buffer_format: PixelFormat,
204
205    /// Embeds popups to the window when set to true. The default value is true.
206    pub overlay_popups: bool,
207
208    /// Shares a single mouse device between all top-levels when set to true, so that pointer
209    /// capture and click counting are global rather than per-window, as they are on the other
210    /// platforms. When `None` or false, every top-level gets its own mouse device.
211    pub use_shared_mouse_device: Option<bool>,
212
213    /// When false, top-level windows always report a [`PixelPoint`](ferroui_base::PixelPoint)
214    /// origin position and ignore moves, as on Wayland where the compositor owns window
215    /// placement. Popups still report their position relative to their parent. The default value
216    /// is true.
217    pub supports_window_positioning: bool,
218
219    /// When true, desktop scaling is always 1 and screen coordinates (window position,
220    /// `point_to_screen`, `point_to_client`) are logical units regardless of render scaling, as on
221    /// macOS and Wayland. The default value is false: screen coordinates are physical pixels, as
222    /// on Windows and X11.
223    pub use_logical_desktop_coordinates: bool,
224}
225
226impl Default for FerroHeadlessPlatformOptions {
227    fn default() -> Self {
228        Self {
229            fps: 60,
230            should_render_on_ui_thread: true,
231            use_headless_drawing: true,
232            frame_buffer_format: PixelFormats::RGBA8888,
233            overlay_popups: true,
234            use_shared_mouse_device: None,
235            supports_window_positioning: true,
236            use_logical_desktop_coordinates: false,
237        }
238    }
239}
240
241/// The `use_headless` extension of the application builder.
242pub trait FerroHeadlessPlatformExtensions {
243    /// Uses the headless platform: its windowing subsystem, the standard
244    /// runtime platform, the HarfBuzz text shaper and, with
245    /// [`use_headless_drawing`](FerroHeadlessPlatformOptions::use_headless_drawing),
246    /// its rendering subsystem.
247    fn use_headless(&self, opts: FerroHeadlessPlatformOptions) -> AppBuilder;
248}
249
250impl FerroHeadlessPlatformExtensions for AppBuilder {
251    fn use_headless(&self, opts: FerroHeadlessPlatformOptions) -> AppBuilder {
252        if opts.use_headless_drawing {
253            // The builder is a handle: the call configures the builder it is made on.
254            drop(self.use_rendering_subsystem(HeadlessPlatformRenderInterface::initialize, "Headless"));
255        }
256        self.use_standard_runtime_platform_subsystem()
257            .use_windowing_subsystem(move || FerroHeadlessPlatform::initialize(&opts), "Headless")
258            .use_harfbuzz()
259    }
260}