ferroui_headless/
ferro_headless_platform.rs1use 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 static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
30 static HEADLESS_RENDER_TIMER: RefCell<Option<Arc<HeadlessRenderTimer>>> = const { RefCell::new(None) };
33 static OPTIONS: RefCell<Option<SharedOptions>> = const { RefCell::new(None) };
36}
37
38pub(crate) type SharedOptions = Rc<RefCell<FerroHeadlessPlatformOptions>>;
40
41pub 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 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 pub(crate) fn compositor() -> Option<Rc<Compositor>> {
83 COMPOSITOR.with(|compositor| compositor.borrow().clone())
84 }
85
86 #[allow(dead_code)] 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 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 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#[derive(Clone, Debug, PartialEq)]
186pub struct FerroHeadlessPlatformOptions {
187 pub fps: i32,
190
191 pub should_render_on_ui_thread: bool,
195
196 pub use_headless_drawing: bool,
201
202 pub frame_buffer_format: PixelFormat,
204
205 pub overlay_popups: bool,
207
208 pub use_shared_mouse_device: Option<bool>,
212
213 pub supports_window_positioning: bool,
218
219 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
241pub trait FerroHeadlessPlatformExtensions {
243 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 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}