| 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 | |
| 4 | use i_slint_core::api::PhysicalSize; |
| 5 | use i_slint_core::platform::PlatformError; |
| 6 | use i_slint_core::renderer::Renderer; |
| 7 | use i_slint_core::window::{InputMethodRequest, WindowAdapter, WindowAdapterInternal}; |
| 8 | use i_slint_renderer_skia::SkiaRenderer; |
| 9 | |
| 10 | use std::cell::{Cell, RefCell}; |
| 11 | use std::rc::Rc; |
| 12 | use std::sync::Mutex; |
| 13 | |
| 14 | pub struct HeadlessBackend { |
| 15 | clipboard: Mutex<Option<String>>, |
| 16 | queue: Option<Queue>, |
| 17 | } |
| 18 | |
| 19 | impl 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 | |
| 29 | impl Default for HeadlessBackend { |
| 30 | fn default() -> Self { |
| 31 | Self::new() |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | impl 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 | |
| 96 | pub 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 | |
| 104 | impl 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 | |
| 118 | impl 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 | |
| 154 | enum Event { |
| 155 | Quit, |
| 156 | Event(Box<dyn FnOnce() + Send>), |
| 157 | } |
| 158 | |
| 159 | #[derive (Clone)] |
| 160 | struct Queue( |
| 161 | std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<Event>>>, |
| 162 | std::thread::Thread, |
| 163 | ); |
| 164 | |
| 165 | impl 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 | |
| 182 | pub fn init() { |
| 183 | i_slint_core::platform::set_platform(Box::new(HeadlessBackend::default())) |
| 184 | .expect(msg:"platform already initialized" ); |
| 185 | } |
| 186 | |
| 187 | pub 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 | |