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