Skip to main content

freya_winit/
window.rs

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    /// Republishes the effective scale factor on [`Platform`] so consumers
391    /// (e.g. `ImageViewer`) pick it up on their next render.
392    pub fn sync_scale_factor(&mut self) {
393        self.platform
394            .scale_factor
395            .set(self.effective_scale_factor());
396    }
397
398    /// Sets `user_zoom`, clamped to `[MIN_USER_ZOOM, MAX_USER_ZOOM]`. On change,
399    /// resets layout/text caches and requests a redraw, mirroring `ScaleFactorChanged`.
400    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    /// Returns `true` when the combo matched. Releases are also consumed so
414    /// press/release pairs stay symmetrical for upstream listeners.
415    #[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}