1use std::{
2 borrow::Cow,
3 path::PathBuf,
4 rc::Rc,
5 sync::Arc,
6 task::Waker,
7};
8
9use accesskit_winit::Adapter;
10use freya_clipboard::copypasta::{
11 ClipboardContext,
12 ClipboardProvider,
13};
14use freya_components::{
15 cache::AssetCacher,
16 integration::integration,
17};
18use freya_core::{
19 integration::*,
20 prelude::Color,
21};
22use freya_engine::prelude::{
23 FontCollection,
24 FontMgr,
25};
26use futures_util::task::{
27 ArcWake,
28 waker,
29};
30use ragnarok::NodesState;
31use raw_window_handle::HasDisplayHandle;
32#[cfg(target_os = "linux")]
33use raw_window_handle::RawDisplayHandle;
34use torin::prelude::{
35 CursorPoint,
36 Size2D,
37};
38use winit::{
39 dpi::LogicalSize,
40 event::ElementState,
41 event_loop::{
42 ActiveEventLoop,
43 EventLoopProxy,
44 },
45 keyboard::ModifiersState,
46 window::{
47 Theme,
48 Window,
49 WindowAttributes,
50 WindowId,
51 },
52};
53
54use crate::{
55 accessibility::AccessibilityTask,
56 config::{
57 OnCloseHook,
58 WindowConfig,
59 },
60 drivers::GraphicsDriver,
61 plugins::{
62 PluginEvent,
63 PluginHandle,
64 PluginsManager,
65 },
66 renderer::{
67 NativeEvent,
68 NativeWindowEvent,
69 NativeWindowEventAction,
70 },
71};
72
73pub struct AppWindow {
74 pub(crate) runner: Runner,
75 pub(crate) tree: Tree,
76 pub(crate) driver: GraphicsDriver,
77 pub(crate) window: Window,
78 pub(crate) nodes_state: NodesState<NodeId>,
79
80 pub(crate) position: CursorPoint,
81 pub(crate) mouse_state: ElementState,
82 pub(crate) modifiers_state: ModifiersState,
83 pub(crate) just_focused: bool,
84
85 pub(crate) events_receiver: futures_channel::mpsc::UnboundedReceiver<EventsChunk>,
86 pub(crate) events_sender: futures_channel::mpsc::UnboundedSender<EventsChunk>,
87
88 pub(crate) accessibility: AccessibilityTree,
89 pub(crate) accessibility_adapter: accesskit_winit::Adapter,
90 pub(crate) accessibility_tasks_for_next_render: AccessibilityTask,
91
92 pub(crate) process_layout_on_next_render: bool,
93
94 pub(crate) waker: Waker,
95
96 pub(crate) ticker_sender: RenderingTickerSender,
97
98 pub(crate) platform: Platform,
99
100 pub(crate) animation_clock: AnimationClock,
101
102 pub(crate) background: Color,
103
104 pub(crate) dropped_file_paths: Vec<PathBuf>,
105
106 pub(crate) on_close: Option<OnCloseHook>,
107
108 pub(crate) window_attributes: WindowAttributes,
109 #[cfg(feature = "hotreload")]
110 pub(crate) hot_reload_pending: Arc<std::sync::atomic::AtomicBool>,
111}
112
113impl AppWindow {
114 #[allow(clippy::too_many_arguments)]
115 pub fn new(
116 mut window_config: WindowConfig,
117 active_event_loop: &ActiveEventLoop,
118 event_loop_proxy: &EventLoopProxy<NativeEvent>,
119 plugins: &mut PluginsManager,
120 font_collection: &mut FontCollection,
121 font_manager: &FontMgr,
122 fallback_fonts: &[Cow<'static, str>],
123 screen_reader: ScreenReader,
124 ) -> Self {
125 #[cfg(feature = "hotreload")]
126 let hot_reload_pending = Arc::new(std::sync::atomic::AtomicBool::new(false));
127 let mut window_attributes = Window::default_attributes()
128 .with_resizable(window_config.resizable)
129 .with_window_icon(window_config.icon.take())
130 .with_visible(false)
131 .with_title(window_config.title)
132 .with_decorations(window_config.decorations)
133 .with_transparent(window_config.transparent)
134 .with_inner_size(LogicalSize::<f64>::from(window_config.size));
135
136 if let Some(min_size) = window_config.min_size {
137 window_attributes =
138 window_attributes.with_min_inner_size(LogicalSize::<f64>::from(min_size));
139 }
140 if let Some(max_size) = window_config.max_size {
141 window_attributes =
142 window_attributes.with_max_inner_size(LogicalSize::<f64>::from(max_size));
143 }
144 #[cfg(target_os = "linux")]
145 if let Some(app_id) = window_config.app_id.take() {
146 use winit::platform::wayland::WindowAttributesExtWayland;
147 window_attributes = window_attributes.with_name(&app_id, &app_id);
148 }
149 if let Some(window_attributes_hook) = window_config.window_attributes_hook.take() {
150 window_attributes = window_attributes_hook(window_attributes, active_event_loop);
151 }
152 let (driver, mut window) =
153 GraphicsDriver::new(active_event_loop, window_attributes.clone());
154
155 if let Some(window_handle_hook) = window_config.window_handle_hook.take() {
156 window_handle_hook(&mut window);
157 }
158
159 let on_close = window_config.on_close.take();
160
161 let (events_sender, events_receiver) = futures_channel::mpsc::unbounded();
162
163 let app = window_config.app.clone();
164 let mut runner = Runner::new({
165 let plugins = plugins.clone();
166 move || {
167 let el = integration(app.clone()).into_element();
168 plugins.wrap_root(el)
169 }
170 });
171
172 runner.provide_root_context(|| screen_reader);
173
174 let (mut ticker_sender, ticker) = RenderingTicker::new();
175 ticker_sender.set_overflow(true);
176 runner.provide_root_context(|| ticker);
177
178 let animation_clock = AnimationClock::new();
179 runner.provide_root_context(|| animation_clock.clone());
180
181 runner.provide_root_context(AssetCacher::create);
182 let mut tree = Tree::default();
183
184 let window_size = window.inner_size();
185 let accent_color_preference = accent_color_preference();
186 let platform = runner.provide_root_context({
187 let event_loop_proxy = event_loop_proxy.clone();
188 let window_id = window.id();
189 let theme = match window.theme() {
190 Some(Theme::Dark) => PreferredTheme::Dark,
191 _ => PreferredTheme::Light,
192 };
193 move || Platform {
194 focused_accessibility_id: State::create(ACCESSIBILITY_ROOT_ID),
195 focused_accessibility_node: State::create(accesskit::Node::new(
196 accesskit::Role::Window,
197 )),
198 root_size: State::create(Size2D::new(
199 window_size.width as f32,
200 window_size.height as f32,
201 )),
202 navigation_mode: State::create(NavigationMode::NotKeyboard),
203 preferred_theme: State::create(theme),
204 accent_color: State::create(accent_color_preference.accent_color),
205 sender: Rc::new(move |user_event| {
206 event_loop_proxy
207 .send_event(NativeEvent::Window(NativeWindowEvent {
208 window_id,
209 action: NativeWindowEventAction::User(user_event),
210 }))
211 .unwrap();
212 }),
213 }
214 });
215
216 let clipboard = {
217 if let Ok(handle) = window.display_handle() {
218 #[allow(clippy::match_single_binding)]
219 match handle.as_raw() {
220 #[cfg(target_os = "linux")]
221 RawDisplayHandle::Wayland(handle) => {
222 let (_primary, clipboard) = unsafe {
223 use freya_clipboard::copypasta::wayland_clipboard;
224
225 wayland_clipboard::create_clipboards_from_external(
226 handle.display.as_ptr(),
227 )
228 };
229 let clipboard: Box<dyn ClipboardProvider> = Box::new(clipboard);
230 Some(clipboard)
231 }
232 _ => ClipboardContext::new().ok().map(|c| {
233 let clipboard: Box<dyn ClipboardProvider> = Box::new(c);
234 clipboard
235 }),
236 }
237 } else {
238 None
239 }
240 };
241
242 runner.provide_root_context(|| State::create(clipboard));
243
244 runner.provide_root_context(|| tree.accessibility_generator.clone());
245
246 runner.provide_root_context(|| tree.accessibility_generator.clone());
247
248 runner.provide_root_context(|| font_collection.clone());
249
250 plugins.send(
251 PluginEvent::RunnerCreated {
252 runner: &mut runner,
253 },
254 PluginHandle::new(event_loop_proxy),
255 );
256
257 let mutations = runner.sync_and_update();
258 tree.apply_mutations(mutations);
259 tree.measure_layout(
260 (
261 window.inner_size().width as f32,
262 window.inner_size().height as f32,
263 )
264 .into(),
265 font_collection,
266 font_manager,
267 &events_sender,
268 window.scale_factor(),
269 fallback_fonts,
270 );
271
272 let nodes_state = NodesState::default();
273
274 let accessibility_adapter =
275 Adapter::with_event_loop_proxy(active_event_loop, &window, event_loop_proxy.clone());
276
277 window.set_visible(true);
278
279 struct TreeHandle(EventLoopProxy<NativeEvent>, WindowId);
280
281 impl ArcWake for TreeHandle {
282 fn wake_by_ref(arc_self: &Arc<Self>) {
283 _ = arc_self
284 .0
285 .send_event(NativeEvent::Window(NativeWindowEvent {
286 window_id: arc_self.1,
287 action: NativeWindowEventAction::PollRunner,
288 }));
289 }
290 }
291
292 let waker = waker(Arc::new(TreeHandle(event_loop_proxy.clone(), window.id())));
293
294 #[cfg(feature = "hotreload")]
295 {
296 let event_loop_proxy = event_loop_proxy.clone();
297 let window_id = window.id();
298 let hot_reload_pending_handler = hot_reload_pending.clone();
299 freya_core::hotreload::subsecond::register_handler(Arc::new(move || {
300 hot_reload_pending_handler.store(true, std::sync::atomic::Ordering::Release);
301 let _ = event_loop_proxy.send_event(NativeEvent::Window(NativeWindowEvent {
302 window_id,
303 action: NativeWindowEventAction::PollRunner,
304 }));
305 }));
306 }
307
308 plugins.send(
309 PluginEvent::WindowCreated {
310 window: &window,
311 font_collection,
312 tree: &tree,
313 animation_clock: &animation_clock,
314 runner: &mut runner,
315 graphics_driver: driver.name(),
316 },
317 PluginHandle::new(event_loop_proxy),
318 );
319
320 AppWindow {
321 runner,
322 tree,
323 driver,
324 window,
325 nodes_state,
326
327 mouse_state: ElementState::Released,
328 position: CursorPoint::default(),
329 modifiers_state: ModifiersState::default(),
330 just_focused: false,
331
332 events_receiver,
333 events_sender,
334
335 accessibility: AccessibilityTree::default(),
336 accessibility_adapter,
337 accessibility_tasks_for_next_render: AccessibilityTask::ProcessUpdate { mode: None },
338
339 process_layout_on_next_render: true,
340
341 waker,
342
343 ticker_sender,
344
345 platform,
346
347 animation_clock,
348
349 background: window_config.background,
350
351 dropped_file_paths: Vec::new(),
352
353 on_close,
354
355 window_attributes,
356
357 #[cfg(feature = "hotreload")]
358 hot_reload_pending,
359 }
360 }
361
362 pub fn window(&self) -> &Window {
363 &self.window
364 }
365
366 pub fn window_mut(&mut self) -> &mut Window {
367 &mut self.window
368 }
369}
370
371fn accent_color_preference() -> mundy::Preferences {
372 use std::sync::OnceLock;
373 static PREFERENCE: OnceLock<mundy::Preferences> = OnceLock::new();
374 *PREFERENCE.get_or_init(|| {
375 mundy::Preferences::once_blocking(
376 mundy::Interest::AccentColor,
377 std::time::Duration::from_millis(200),
378 )
379 .unwrap_or_default()
380 })
381}