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::platform::PlatformError;
6use i_slint_core::renderer::Renderer;
7use i_slint_core::window::{InputMethodRequest, WindowAdapter, WindowAdapterInternal};
8use i_slint_renderer_skia::SkiaRenderer;
9
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12use std::sync::Mutex;
13
14pub struct HeadlessBackend {
15 clipboard: Mutex<Option<String>>,
16 queue: Option<Queue>,
17}
18
19impl HeadlessBackend {
20 pub fn new() -> Self {
21 eprintln!("Running headless!");
22 Self {
23 clipboard: Mutex::default(),
24 queue: Some(Queue(Default::default(), std::thread::current())),
25 }
26 }
27}
28
29impl Default for HeadlessBackend {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl i_slint_core::platform::Platform for HeadlessBackend {
36 fn create_window_adapter(
37 &self,
38 ) -> Result<Rc<dyn WindowAdapter>, i_slint_core::platform::PlatformError> {
39 Ok(Rc::new_cyclic(|self_weak| HeadlessWindow {
40 window: i_slint_core::api::Window::new(self_weak.clone() as _),
41 size: Default::default(),
42 ime_requests: Default::default(),
43 mouse_cursor: Default::default(),
44 renderer: SkiaRenderer::default_software(),
45 }))
46 }
47
48 fn duration_since_start(&self) -> core::time::Duration {
49 static INITIAL_INSTANT: std::sync::OnceLock<std::time::Instant> =
50 std::sync::OnceLock::new();
51 let the_beginning = *INITIAL_INSTANT.get_or_init(std::time::Instant::now);
52 std::time::Instant::now() - the_beginning
53 }
54
55 fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
56 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
57 *self.clipboard.lock().unwrap() = Some(text.into());
58 }
59 }
60
61 fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
62 if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
63 self.clipboard.lock().unwrap().clone()
64 } else {
65 None
66 }
67 }
68
69 fn run_event_loop(&self) -> Result<(), PlatformError> {
70 let queue = match self.queue.as_ref() {
71 Some(queue) => queue.clone(),
72 None => return Err(PlatformError::NoEventLoopProvider),
73 };
74
75 loop {
76 let e = queue.0.lock().unwrap().pop_front();
77 i_slint_core::platform::update_timers_and_animations();
78 match e {
79 Some(Event::Quit) => break Ok(()),
80 Some(Event::Event(e)) => e(),
81 None => {
82 i_slint_core::platform::duration_until_next_timer_update();
83 std::thread::park();
84 }
85 }
86 }
87 }
88
89 fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
90 self.queue
91 .as_ref()
92 .map(|q| Box::new(q.clone()) as Box<dyn i_slint_core::platform::EventLoopProxy>)
93 }
94}
95
96pub struct HeadlessWindow {
97 window: i_slint_core::api::Window,
98 size: Cell<PhysicalSize>,
99 pub ime_requests: RefCell<Vec<InputMethodRequest>>,
100 pub mouse_cursor: Cell<i_slint_core::items::MouseCursor>,
101 renderer: SkiaRenderer,
102}
103
104impl WindowAdapterInternal for HeadlessWindow {
105 fn as_any(&self) -> &dyn std::any::Any {
106 self
107 }
108
109 fn input_method_request(&self, request: i_slint_core::window::InputMethodRequest) {
110 self.ime_requests.borrow_mut().push(request)
111 }
112
113 fn set_mouse_cursor(&self, cursor: i_slint_core::items::MouseCursor) {
114 self.mouse_cursor.set(val:cursor);
115 }
116}
117
118impl WindowAdapter for HeadlessWindow {
119 fn window(&self) -> &i_slint_core::api::Window {
120 &self.window
121 }
122
123 fn size(&self) -> PhysicalSize {
124 if self.size.get().width == 0 {
125 PhysicalSize::new(800, 600)
126 } else {
127 self.size.get()
128 }
129 }
130
131 fn set_size(&self, size: i_slint_core::api::WindowSize) {
132 self.window.dispatch_event(i_slint_core::platform::WindowEvent::Resized {
133 size: size.to_logical(self.window().scale_factor()),
134 });
135 self.size.set(size.to_physical(self.window().scale_factor()))
136 }
137
138 fn renderer(&self) -> &dyn Renderer {
139 &self.renderer
140 }
141
142 fn update_window_properties(&self, properties: i_slint_core::window::WindowProperties<'_>) {
143 if self.size.get().width == 0 {
144 let c = properties.layout_constraints();
145 self.size.set(c.preferred.to_physical(self.window.scale_factor()));
146 }
147 }
148
149 fn internal(&self, _: i_slint_core::InternalToken) -> Option<&dyn WindowAdapterInternal> {
150 Some(self)
151 }
152}
153
154enum Event {
155 Quit,
156 Event(Box<dyn FnOnce() + Send>),
157}
158
159#[derive(Clone)]
160struct Queue(
161 std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>,
162 std::thread::Thread,
163);
164
165impl i_slint_core::platform::EventLoopProxy for Queue {
166 fn quit_event_loop(&self) -> Result<(), i_slint_core::api::EventLoopError> {
167 self.0.lock().unwrap().push_back(Event::Quit);
168 self.1.unpark();
169 Ok(())
170 }
171
172 fn invoke_from_event_loop(
173 &self,
174 event: Box<dyn FnOnce() + Send>,
175 ) -> Result<(), i_slint_core::api::EventLoopError> {
176 self.0.lock().unwrap().push_back(Event::Event(event));
177 self.1.unpark();
178 Ok(())
179 }
180}
181
182pub fn init() {
183 i_slint_core::platform::set_platform(Box::new(HeadlessBackend::default()))
184 .expect(msg:"platform already initialized");
185}
186
187pub fn set_window_scale_factor(window: &slint_interpreter::Window, factor: f32) {
188 window.dispatch_event(i_slint_core::platform::WindowEvent::ScaleFactorChanged {
189 scale_factor: factor,
190 });
191}
192