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
84 pub(crate) events_receiver: futures_channel::mpsc::UnboundedReceiver<EventsChunk>,
85 pub(crate) events_sender: futures_channel::mpsc::UnboundedSender<EventsChunk>,
86
87 pub(crate) accessibility: AccessibilityTree,
88 pub(crate) accessibility_adapter: accesskit_winit::Adapter,
89 pub(crate) accessibility_tasks_for_next_render: AccessibilityTask,
90
91 pub(crate) process_layout_on_next_render: bool,
92
93 pub(crate) waker: Waker,
94
95 pub(crate) ticker_sender: RenderingTickerSender,
96
97 pub(crate) platform: Platform,
98
99 pub(crate) animation_clock: AnimationClock,
100
101 pub(crate) background: Color,
102
103 pub(crate) dropped_file_paths: Vec<PathBuf>,
104
105 pub(crate) on_close: Option<OnCloseHook>,
106
107 pub(crate) window_attributes: WindowAttributes,
108
109 pub(crate) user_zoom: f32,
110 #[cfg(feature = "hotreload")]
111 pub(crate) hot_reload_pending: Arc<std::sync::atomic::AtomicBool>,
112}
113
114pub(crate) const MIN_USER_ZOOM: f32 = 0.25;
115pub(crate) const MAX_USER_ZOOM: f32 = 5.0;
116
117#[cfg(feature = "zoom-shortcuts")]
118pub(crate) const ZOOM_STEP: f32 = 0.10;
119
120impl AppWindow {
121 #[allow(clippy::too_many_arguments)]
122 pub fn new(
123 mut window_config: WindowConfig,
124 active_event_loop: &ActiveEventLoop,
125 event_loop_proxy: &EventLoopProxy<NativeEvent>,
126 plugins: &mut PluginsManager,
127 font_collection: &mut FontCollection,
128 font_manager: &FontMgr,
129 fallback_fonts: &[Cow<'static, str>],
130 screen_reader: ScreenReader,
131 gpu_resource_cache_limit: usize,
132 ) -> Self {
133 #[cfg(feature = "hotreload")]
134 let hot_reload_pending = Arc::new(std::sync::atomic::AtomicBool::new(false));
135 let mut window_attributes = Window::default_attributes()
136 .with_resizable(window_config.resizable)
137 .with_window_icon(window_config.icon.take())
138 .with_visible(false)
139 .with_title(window_config.title)
140 .with_decorations(window_config.decorations)
141 .with_transparent(window_config.transparent)
142 .with_inner_size(LogicalSize::<f64>::from(window_config.size));
143
144 if let Some(min_size) = window_config.min_size {
145 window_attributes =
146 window_attributes.with_min_inner_size(LogicalSize::<f64>::from(min_size));
147 }
148 if let Some(max_size) = window_config.max_size {
149 window_attributes =
150 window_attributes.with_max_inner_size(LogicalSize::<f64>::from(max_size));
151 }
152 #[cfg(target_os = "linux")]
153 if let Some(app_id) = window_config.app_id.take() {
154 use winit::platform::wayland::WindowAttributesExtWayland;
155 window_attributes = window_attributes.with_name(&app_id, &app_id);
156 }
157 if let Some(window_attributes_hook) = window_config.window_attributes_hook.take() {
158 window_attributes = window_attributes_hook(window_attributes, active_event_loop);
159 }
160 let (driver, mut window) = GraphicsDriver::new(
161 active_event_loop,
162 window_attributes.clone(),
163 gpu_resource_cache_limit,
164 );
165
166 if let Some(window_handle_hook) = window_config.window_handle_hook.take() {
167 window_handle_hook(&mut window);
168 }
169
170 let on_close = window_config.on_close.take();
171
172 let (events_sender, events_receiver) = futures_channel::mpsc::unbounded();
173
174 let app = window_config.app.clone();
175 let mut runner = Runner::new({
176 let plugins = plugins.clone();
177 move || {
178 let el = integration(app.clone()).into_element();
179 plugins.wrap_root(el)
180 }
181 });
182
183 runner.provide_root_context(|| screen_reader);
184
185 let (mut ticker_sender, ticker) = RenderingTicker::new();
186 ticker_sender.set_overflow(true);
187 runner.provide_root_context(|| ticker);
188
189 let animation_clock = AnimationClock::new();
190 runner.provide_root_context(|| animation_clock.clone());
191
192 runner.provide_root_context(AssetCacher::create);
193 let mut tree = Tree::default();
194
195 let window_size = window.inner_size();
196 let accent_color_preference = accent_color_preference();
197 let platform = runner.provide_root_context({
198 let event_loop_proxy = event_loop_proxy.clone();
199 let window_id = window.id();
200 let theme = match window.theme() {
201 Some(Theme::Dark) => PreferredTheme::Dark,
202 _ => PreferredTheme::Light,
203 };
204 let is_app_focused = window.has_focus();
205 let scale_factor = window.scale_factor();
206 move || Platform {
207 focused_accessibility_id: State::create(ACCESSIBILITY_ROOT_ID),
208 focused_accessibility_node: State::create(accesskit::Node::new(
209 accesskit::Role::Window,
210 )),
211 root_size: State::create(Size2D::new(
212 window_size.width as f32,
213 window_size.height as f32,
214 )),
215 scale_factor: State::create(scale_factor),
216 navigation_mode: State::create(NavigationMode::NotKeyboard),
217 preferred_theme: State::create(theme),
218 is_app_focused: State::create(is_app_focused),
219 accent_color: State::create(accent_color_preference.accent_color),
220 sender: Rc::new(move |user_event| {
221 event_loop_proxy
222 .send_event(NativeEvent::Window(NativeWindowEvent {
223 window_id,
224 action: NativeWindowEventAction::User(user_event),
225 }))
226 .unwrap();
227 }),
228 }
229 });
230
231 let clipboard = {
232 if let Ok(handle) = window.display_handle() {
233 #[allow(clippy::match_single_binding)]
234 match handle.as_raw() {
235 #[cfg(target_os = "linux")]
236 RawDisplayHandle::Wayland(handle) => {
237 let (_primary, clipboard) = unsafe {
238 use freya_clipboard::copypasta::wayland_clipboard;
239
240 wayland_clipboard::create_clipboards_from_external(
241 handle.display.as_ptr(),
242 )
243 };
244 let clipboard: Box<dyn ClipboardProvider> = Box::new(clipboard);
245 Some(clipboard)
246 }
247 _ => ClipboardContext::new().ok().map(|c| {
248 let clipboard: Box<dyn ClipboardProvider> = Box::new(c);
249 clipboard
250 }),
251 }
252 } else {
253 None
254 }
255 };
256
257 runner.provide_root_context(|| State::create(clipboard));
258
259 runner.provide_root_context(|| tree.accessibility_generator.clone());
260
261 runner.provide_root_context(|| tree.accessibility_generator.clone());
262
263 runner.provide_root_context(|| font_collection.clone());
264
265 plugins.send(
266 PluginEvent::RunnerCreated {
267 runner: &mut runner,
268 },
269 PluginHandle::new(event_loop_proxy),
270 );
271
272 let mutations = runner.sync_and_update();
273 tree.apply_mutations(mutations);
274 tree.measure_layout(
275 (
276 window.inner_size().width as f32,
277 window.inner_size().height as f32,
278 )
279 .into(),
280 font_collection,
281 font_manager,
282 &events_sender,
283 window.scale_factor(),
284 fallback_fonts,
285 );
286
287 let nodes_state = NodesState::default();
288
289 let accessibility_adapter =
290 Adapter::with_event_loop_proxy(active_event_loop, &window, event_loop_proxy.clone());
291
292 window.set_visible(true);
293
294 struct TreeHandle(EventLoopProxy<NativeEvent>, WindowId);
295
296 impl ArcWake for TreeHandle {
297 fn wake_by_ref(arc_self: &Arc<Self>) {
298 _ = arc_self
299 .0
300 .send_event(NativeEvent::Window(NativeWindowEvent {
301 window_id: arc_self.1,
302 action: NativeWindowEventAction::PollRunner,
303 }));
304 }
305 }
306
307 let waker = waker(Arc::new(TreeHandle(event_loop_proxy.clone(), window.id())));
308
309 #[cfg(feature = "hotreload")]
310 {
311 let event_loop_proxy = event_loop_proxy.clone();
312 let window_id = window.id();
313 let hot_reload_pending_handler = hot_reload_pending.clone();
314 freya_core::hotreload::subsecond::register_handler(Arc::new(move || {
315 hot_reload_pending_handler.store(true, std::sync::atomic::Ordering::Release);
316 let _ = event_loop_proxy.send_event(NativeEvent::Window(NativeWindowEvent {
317 window_id,
318 action: NativeWindowEventAction::PollRunner,
319 }));
320 }));
321 }
322
323 plugins.send(
324 PluginEvent::WindowCreated {
325 window: &window,
326 font_collection,
327 tree: &tree,
328 animation_clock: &animation_clock,
329 runner: &mut runner,
330 graphics_driver: driver.name(),
331 },
332 PluginHandle::new(event_loop_proxy),
333 );
334
335 AppWindow {
336 runner,
337 tree,
338 driver,
339 window,
340 nodes_state,
341
342 mouse_state: ElementState::Released,
343 position: CursorPoint::default(),
344 modifiers_state: ModifiersState::default(),
345
346 events_receiver,
347 events_sender,
348
349 accessibility: AccessibilityTree::default(),
350 accessibility_adapter,
351 accessibility_tasks_for_next_render: AccessibilityTask::ProcessUpdate { mode: None },
352
353 process_layout_on_next_render: true,
354
355 waker,
356
357 ticker_sender,
358
359 platform,
360
361 animation_clock,
362
363 background: window_config.background,
364
365 dropped_file_paths: Vec::new(),
366
367 on_close,
368
369 window_attributes,
370
371 user_zoom: 1.0,
372
373 #[cfg(feature = "hotreload")]
374 hot_reload_pending,
375 }
376 }
377
378 pub fn window(&self) -> &Window {
379 &self.window
380 }
381
382 pub fn window_mut(&mut self) -> &mut Window {
383 &mut self.window
384 }
385
386 pub fn effective_scale_factor(&self) -> f64 {
387 self.window.scale_factor() * self.user_zoom as f64
388 }
389
390 pub fn sync_scale_factor(&mut self) {
393 self.platform
394 .scale_factor
395 .set(self.effective_scale_factor());
396 }
397
398 pub fn set_user_zoom(&mut self, zoom: f32) {
401 let clamped = zoom.clamp(MIN_USER_ZOOM, MAX_USER_ZOOM);
402 if (clamped - self.user_zoom).abs() < f32::EPSILON {
403 return;
404 }
405 self.user_zoom = clamped;
406 self.sync_scale_factor();
407 self.process_layout_on_next_render = true;
408 self.tree.layout.reset();
409 self.tree.text_cache.reset();
410 self.window.request_redraw();
411 }
412
413 #[cfg(feature = "zoom-shortcuts")]
416 pub fn try_handle_zoom_shortcut(
417 &mut self,
418 key: &keyboard_types::Key,
419 modifiers: keyboard_types::Modifiers,
420 is_pressed: bool,
421 ) -> bool {
422 use keyboard_types::{
423 Key,
424 Modifiers,
425 };
426 if !modifiers.contains(Modifiers::ctrl_or_meta()) {
427 return false;
428 }
429 let new_zoom = match key {
430 Key::Character(c) if c == "+" || c == "=" => self.user_zoom + ZOOM_STEP,
431 Key::Character(c) if c == "-" => self.user_zoom - ZOOM_STEP,
432 Key::Character(c) if c == "0" => 1.0,
433 _ => return false,
434 };
435 if is_pressed {
436 self.set_user_zoom(new_zoom);
437 }
438 true
439 }
440}
441
442fn accent_color_preference() -> mundy::Preferences {
443 use std::sync::OnceLock;
444 static PREFERENCE: OnceLock<mundy::Preferences> = OnceLock::new();
445 *PREFERENCE.get_or_init(|| {
446 mundy::Preferences::once_blocking(
447 mundy::Interest::AccentColor,
448 std::time::Duration::from_millis(200),
449 )
450 .unwrap_or_default()
451 })
452}