Skip to main content

ravenrs_platform_native/
lib.rs

1//! Desktop backend for RavenRS: one winit window per app, rendered with wgpu
2//! (Metal on macOS, DX12/Vulkan on Windows, Vulkan/GL on Linux).
3//!
4//! Frames are produced on demand: the event loop sleeps until input arrives or
5//! reactive state changes, so an idle app uses no CPU or GPU time.
6//!
7//! Screen readers see the UI through AccessKit: the accessibility tree is
8//! built only while one is running.
9
10mod access;
11mod diagnostics;
12mod files;
13mod notice;
14mod sound;
15mod tray;
16
17/// The system clipboard through `arboard`. When it cannot be opened (no
18/// display server, say), copy and paste stay inside the app.
19struct SystemClipboard(std::cell::RefCell<arboard::Clipboard>);
20
21impl ravenrs_widgets::ClipboardProvider for SystemClipboard {
22    fn read_text(&self) -> Option<String> {
23        self.0.borrow_mut().get_text().ok()
24    }
25
26    fn write_text(&self, text: &str) {
27        if let Err(e) = self.0.borrow_mut().set_text(text) {
28            tracing::warn!(error = %e, "could not write to the clipboard");
29        }
30    }
31}
32
33use std::fmt;
34use std::sync::Arc;
35use std::sync::atomic::{AtomicBool, Ordering};
36
37use std::rc::Rc;
38
39use ravenrs_core::{
40    AccessTree, CursorIcon, Event, FrameSample, ImageData, Key, Modifiers, Point, PointerButton, Size,
41};
42use ravenrs_render::{GpuContext, GpuError, Renderer, preferred_surface_format, surface_report, wgpu};
43use ravenrs_style::Theme;
44use ravenrs_widgets::{Diagnostics, DiagnosticsHost, Labels, Localized, Ui, View};
45use winit::application::ApplicationHandler;
46use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
47use winit::event::StartCause;
48use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
49use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy, OwnedDisplayHandle};
50use winit::keyboard::{Key as WinitKey, ModifiersState, NamedKey};
51use winit::window::{Icon, Window, WindowAttributes, WindowId};
52
53pub use diagnostics::diagnose;
54pub use files::{OpenDialog, SaveDialog, open_url, open_with_system};
55pub use notice::SystemNotifier;
56pub use tray::SystemTray;
57
58/// Wheel pixels that ask for twice the size, with Ctrl held.
59const WHEEL_PER_ZOOM: f32 = 400.0;
60/// Set to anything, the diagnostics panel is open from the start.
61const DIAGNOSTICS_VARIABLE: &str = "RAVENRS_DIAG";
62
63/// What reaches the event loop from outside it.
64#[derive(Debug)]
65enum UserEvent {
66    /// From the screen reader.
67    Access(accesskit_winit::Event),
68    /// A [`UiWaker`](ravenrs_widgets::UiWaker) fired, on some other thread.
69    Wake,
70    /// A touchpad pinch on Wayland, which winit does not deliver.
71    Pinch(f32),
72    /// From the icon beside the clock, or from opening the app a second time.
73    Tray(tray::TrayAction),
74}
75
76impl From<accesskit_winit::Event> for UserEvent {
77    fn from(event: accesskit_winit::Event) -> Self {
78        Self::Access(event)
79    }
80}
81
82/// How the main window opens.
83#[derive(Clone, Debug)]
84pub struct WindowOptions {
85    /// Window title.
86    pub title: String,
87    /// Initial inner size in logical pixels.
88    pub size: Size,
89    /// Smallest inner size the user can resize to, in logical pixels.
90    pub min_size: Option<Size>,
91    /// Theme the UI starts with.
92    pub theme: Theme,
93}
94
95impl Default for WindowOptions {
96    fn default() -> Self {
97        Self {
98            title: "RavenRS".into(),
99            size: Size::new(960.0, 640.0),
100            min_size: Some(Size::new(320.0, 240.0)),
101            theme: Theme::default(),
102        }
103    }
104}
105
106impl WindowOptions {
107    /// Default options with the given window title.
108    ///
109    /// ```
110    /// use ravenrs_platform_native::WindowOptions;
111    /// use ravenrs_style::Theme;
112    ///
113    /// let options = WindowOptions::new("Notes").size(800.0, 600.0).theme(Theme::raven_light());
114    /// assert_eq!(options.size.width, 800.0);
115    /// ```
116    pub fn new(title: impl Into<String>) -> Self {
117        Self { title: title.into(), ..Self::default() }
118    }
119
120    /// Sets the initial inner size in logical pixels.
121    pub fn size(mut self, width: f32, height: f32) -> Self {
122        self.size = Size::new(width, height);
123        self
124    }
125
126    /// Sets the theme the UI starts with.
127    pub fn theme(mut self, theme: Theme) -> Self {
128        self.theme = theme;
129        self
130    }
131}
132
133/// Failure to start or run the desktop app.
134#[derive(Debug)]
135pub enum PlatformError {
136    /// The event loop could not be created or exited with an error.
137    EventLoop(String),
138    /// The window could not be created.
139    Window(String),
140    /// GPU setup or surface configuration failed.
141    Gpu(GpuError),
142}
143
144impl fmt::Display for PlatformError {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            Self::EventLoop(e) => write!(f, "event loop error: {e}"),
148            Self::Window(e) => write!(f, "window could not be created: {e}"),
149            Self::Gpu(e) => write!(f, "{e}"),
150        }
151    }
152}
153
154impl std::error::Error for PlatformError {}
155
156impl From<GpuError> for PlatformError {
157    fn from(e: GpuError) -> Self {
158        Self::Gpu(e)
159    }
160}
161
162/// Opens a window showing `root` and runs until it is closed.
163pub fn run(root: impl View + 'static, options: WindowOptions) -> Result<(), PlatformError> {
164    run_window(root, options, AppIdentity::anonymous(None))
165}
166
167/// Who the app is to the system, for finding its icon and its menu entry.
168///
169/// On Linux the id is what ties the window to the app's `.desktop` file. Wayland
170/// has no way for a window to carry an icon: GNOME shows the one in the
171/// `.desktop` file whose name, without the extension, is the window's id. On
172/// X11 the id must match the file's `StartupWMClass`.
173///
174/// ```
175/// use ravenrs_platform_native::AppIdentity;
176///
177/// // For a file named `notes.desktop`.
178/// let identity = AppIdentity::new("notes");
179/// ```
180#[derive(Clone, Debug)]
181pub struct AppIdentity {
182    id: String,
183    icon: Option<ImageData>,
184    labels: Option<Labels>,
185    diagnostics: Option<Diagnostics>,
186    new_issue_url: Option<String>,
187    tray: Option<SystemTray>,
188}
189
190impl AppIdentity {
191    /// No name: what [`run`] and [`run_with_icon`] open as.
192    fn anonymous(icon: Option<ImageData>) -> Self {
193        Self { id: String::new(), icon, labels: None, diagnostics: None, new_issue_url: None, tray: None }
194    }
195
196    /// The app known as `id`, which is the name of its `.desktop` file on Linux.
197    pub fn new(id: impl Into<String>) -> Self {
198        Self { id: id.into(), icon: None, labels: None, diagnostics: None, new_issue_url: None, tray: None }
199    }
200
201    /// Sets the words of the whole UI, including the diagnostics panel.
202    pub fn labels(mut self, labels: Labels) -> Self {
203        self.labels = Some(labels);
204        self
205    }
206
207    /// Shares `diagnostics` with the app, for a button of its own that opens the
208    /// panel or starts a performance test.
209    pub fn diagnostics(mut self, diagnostics: Diagnostics) -> Self {
210        self.diagnostics = Some(diagnostics);
211        self
212    }
213
214    /// Sets the page for new issues, so the diagnostics panel can start a bug
215    /// report there. The person reviews it and sends it on that page.
216    pub fn new_issue_url(mut self, url: impl Into<String>) -> Self {
217        self.new_issue_url = Some(url.into());
218        self
219    }
220
221    /// An icon for the title bar and the task bar on Windows and on Linux with
222    /// X11. macOS takes an app's icon from its bundle instead.
223    pub fn icon(mut self, icon: ImageData) -> Self {
224        self.icon = Some(icon);
225        self
226    }
227
228    /// Closing the window keeps the app running behind an icon beside the clock, which
229    /// shows [`Self::icon`] and needs it. Opening the app again, or clicking its icon in the
230    /// Dock on macOS, brings the same window back. See [`SystemTray`] for where the system has no such icon.
231    pub fn close_to_tray(mut self, tray: SystemTray) -> Self {
232        self.tray = Some(tray);
233        self
234    }
235}
236
237/// Like [`run`], for an app the system should recognize as itself.
238pub fn run_as(
239    root: impl View + 'static,
240    options: WindowOptions,
241    identity: AppIdentity,
242) -> Result<(), PlatformError> {
243    run_window(root, options, identity)
244}
245
246/// Like [`run`], with an icon for the window's title bar and the task bar on
247/// Windows and Linux. macOS takes an app's icon from its bundle instead.
248pub fn run_with_icon(
249    root: impl View + 'static,
250    options: WindowOptions,
251    icon: ImageData,
252) -> Result<(), PlatformError> {
253    run_window(root, options, AppIdentity::anonymous(Some(icon)))
254}
255
256fn run_window(
257    root: impl View + 'static,
258    options: WindowOptions,
259    identity: AppIdentity,
260) -> Result<(), PlatformError> {
261    let AppIdentity { id, icon, labels, diagnostics, new_issue_url, tray } = identity;
262    let app_id = (!id.is_empty()).then_some(id);
263    let single_copy = app_id.as_deref().filter(|_| tray.is_some());
264    if single_copy.is_some_and(tray::ask_running_copy) {
265        tracing::info!("the app was already running, and opened its window");
266        return Ok(());
267    }
268    let event_loop = EventLoop::<UserEvent>::with_user_event()
269        .build()
270        .map_err(|e| PlatformError::EventLoop(e.to_string()))?;
271    event_loop.set_control_flow(ControlFlow::Wait);
272    let diagnostics = diagnostics.unwrap_or_default();
273    diagnostics.set_open(std::env::var_os(DIAGNOSTICS_VARIABLE).is_some());
274    if let Some(url) = new_issue_url {
275        diagnostics.set_bug_reporter(url, |url| open_url(url).map_err(|e| e.to_string()));
276    }
277    let root = DiagnosticsHost::new(root, diagnostics.clone());
278    let mut ui = match labels {
279        Some(labels) => Ui::new(Localized::new(labels, root), options.theme),
280        None => Ui::new(root, options.theme),
281    };
282    let wake = event_loop.create_proxy();
283    ui.set_remote_waker(move || {
284        // It only fails once the loop is gone, when there is nothing left to wake.
285        let _ = wake.send_event(UserEvent::Wake);
286    });
287    match arboard::Clipboard::new() {
288        Ok(clipboard) => ui.set_clipboard(SystemClipboard(std::cell::RefCell::new(clipboard))),
289        Err(e) => tracing::warn!(error = %e, "system clipboard unavailable; using an in-app clipboard"),
290    }
291    ui.set_sound(sound::SystemSound::default());
292    ui.set_file_picker(files::SystemFilePicker);
293    ui.set_links(SystemLinks);
294    let screen_window = ScreenWindow::default();
295    ui.set_full_screen(SystemFullScreen(screen_window.clone()));
296    let answer = event_loop.create_proxy();
297    let tray_notify: tray::Notify = Arc::new(move |action| {
298        // It only fails once the loop is gone, when there is nothing left to tell.
299        let _ = answer.send_event(UserEvent::Tray(action));
300    });
301    if let Some(id) = single_copy
302        && let Err(e) = tray::listen_for_other_copies(id, tray_notify.clone())
303    {
304        tracing::warn!(error = %e, "opening the app again will start a second copy");
305    }
306    let mut app = App {
307        display: event_loop.owned_display_handle(),
308        diagnostics,
309        proxy: event_loop.create_proxy(),
310        access: None,
311        ui,
312        options,
313        icon,
314        app_id,
315        window: None,
316        screen_window,
317        tray,
318        tray_notify,
319        in_tray: None,
320        away_in_tray: false,
321        place: None,
322        maximized: false,
323        pointer: None,
324        modifiers: Modifiers::default(),
325        last_click: None,
326        started: std::time::Instant::now(),
327        looking: false,
328        error: None,
329    };
330    event_loop.run_app(&mut app).map_err(|e| PlatformError::EventLoop(e.to_string()))?;
331    match app.error {
332        Some(e) => Err(e),
333        None => Ok(()),
334    }
335}
336
337struct WindowState {
338    // Field order matters: the surface must drop before the window.
339    surface: wgpu::Surface<'static>,
340    config: wgpu::SurfaceConfiguration,
341    gpu: GpuContext,
342    renderer: Renderer,
343    device_lost: Arc<AtomicBool>,
344    cursor: CursorIcon,
345    window: Arc<Window>,
346}
347
348/// The AccessKit adapter of the window and what it last sent.
349struct Access {
350    adapter: accesskit_winit::Adapter,
351    /// A screen reader asked for the tree; updates are sent while true.
352    active: bool,
353    last: Option<AccessTree>,
354}
355
356struct App {
357    /// Lets the GL backend reach the window system; without it, EGL has no
358    /// display to present on and only software rendering may be left on Wayland.
359    display: OwnedDisplayHandle,
360    diagnostics: Diagnostics,
361    ui: Ui,
362    proxy: EventLoopProxy<UserEvent>,
363    access: Option<Access>,
364    options: WindowOptions,
365    icon: Option<ImageData>,
366    app_id: Option<String>,
367    window: Option<WindowState>,
368    /// The window full screen applies to, once there is one.
369    screen_window: ScreenWindow,
370    tray: Option<SystemTray>,
371    tray_notify: tray::Notify,
372    /// The icon on screen, while the window is closed behind it.
373    in_tray: Option<tray::Shown>,
374    /// The app was seen in the background since it went to the icon, so coming to the
375    /// front now is someone asking for it, as with a click on the Dock.
376    away_in_tray: bool,
377    /// Where the window was when closed behind the icon, for opening it there.
378    place: Option<PhysicalPosition<i32>>,
379    maximized: bool,
380    /// Last pointer position in logical pixels.
381    pointer: Option<Point>,
382    modifiers: Modifiers,
383    /// Time and place of the last primary release, to detect double clicks.
384    last_click: Option<(std::time::Instant, Point)>,
385    started: std::time::Instant,
386    /// The diagnostics panel is open, so the frames are being looked at closely.
387    looking: bool,
388    error: Option<PlatformError>,
389}
390
391/// On macOS a closed window leaves the app in front, and then a click on the Dock would
392/// not change anything anyone could see.
393#[cfg(all(feature = "tray", target_os = "macos"))]
394fn leave_the_front(event_loop: &ActiveEventLoop) {
395    use winit::platform::macos::ActiveEventLoopExtMacOS as _;
396    event_loop.hide_application();
397}
398
399#[cfg(not(all(feature = "tray", target_os = "macos")))]
400fn leave_the_front(_event_loop: &ActiveEventLoop) {}
401
402/// The same setting serves Wayland's app id and X11's `WM_CLASS`.
403#[cfg(any(
404    target_os = "linux",
405    target_os = "dragonfly",
406    target_os = "freebsd",
407    target_os = "netbsd",
408    target_os = "openbsd"
409))]
410fn with_app_id(attributes: WindowAttributes, id: &str) -> WindowAttributes {
411    use winit::platform::wayland::WindowAttributesExtWayland as _;
412    attributes.with_name(id, id)
413}
414
415/// Other systems know the app by its bundle or its executable.
416#[cfg(not(any(
417    target_os = "linux",
418    target_os = "dragonfly",
419    target_os = "freebsd",
420    target_os = "netbsd",
421    target_os = "openbsd"
422)))]
423fn with_app_id(attributes: WindowAttributes, _id: &str) -> WindowAttributes {
424    attributes
425}
426
427impl App {
428    fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Result<WindowState, PlatformError> {
429        // Hidden until `show_window`, so nobody waits looking at an empty window.
430        // AccessKit also requires it until the adapter is attached.
431        let mut attributes = Window::default_attributes()
432            .with_visible(false)
433            .with_title(self.options.title.clone())
434            .with_inner_size(LogicalSize::new(self.options.size.width, self.options.size.height));
435        if let Some(place) = self.place {
436            attributes = attributes.with_position(place);
437        }
438        attributes = attributes.with_maximized(self.maximized);
439        if let Some(min) = self.options.min_size {
440            attributes = attributes.with_min_inner_size(LogicalSize::new(min.width, min.height));
441        }
442        if let Some(id) = &self.app_id {
443            attributes = with_app_id(attributes, id);
444        }
445        if let Some(image) = &self.icon {
446            match Icon::from_rgba(image.pixels().to_vec(), image.width(), image.height()) {
447                Ok(icon) => attributes = attributes.with_window_icon(Some(icon)),
448                Err(e) => tracing::warn!(error = %e, "could not use the window icon"),
449            }
450        }
451        let window =
452            Arc::new(event_loop.create_window(attributes).map_err(|e| PlatformError::Window(e.to_string()))?);
453        let adapter =
454            accesskit_winit::Adapter::with_event_loop_proxy(event_loop, &window, self.proxy.clone());
455        self.access = Some(Access { adapter, active: false, last: None });
456        *self.screen_window.borrow_mut() = Arc::downgrade(&window);
457        self.init_gpu(window)
458    }
459
460    /// Closes the window behind the icon, keeping the UI and its state. `false` if the
461    /// system shows no icon, and only closing the app is left.
462    ///
463    /// The window is only hidden, so that it comes back at once, with its last frame.
464    /// Wayland cannot hide one, so there it is destroyed, and that system shows a new
465    /// window only once it has something to show.
466    fn close_to_tray(&mut self, event_loop: &ActiveEventLoop) -> bool {
467        let (Some(tray), Some(icon), Some(state)) = (&self.tray, &self.icon, &self.window) else {
468            return false;
469        };
470        let id = self.app_id.as_deref().unwrap_or_default();
471        match tray::show(tray, id, icon, self.tray_notify.clone()) {
472            Ok(shown) => self.in_tray = Some(shown),
473            Err(e) => {
474                tracing::info!(error = %e, "no icon beside the clock, so closing the window closes the app");
475                return false;
476            }
477        }
478        let window = &state.window;
479        if !self.on_wayland() {
480            window.set_visible(false);
481            self.away_in_tray = false;
482            leave_the_front(event_loop);
483            return true;
484        }
485        let size = window.inner_size().to_logical::<f32>(window.scale_factor());
486        self.options.size = Size::new(size.width, size.height);
487        self.place = window.outer_position().ok();
488        self.maximized = window.is_maximized();
489        // These hold the window, which only closes when the last of them lets go.
490        state.gpu.device.set_device_lost_callback(|_, _| {});
491        self.ui.set_waker(|| {});
492        self.access = None;
493        self.pointer = None;
494        self.window = None;
495        true
496    }
497
498    /// Brings the window back, or to the front if it never left.
499    fn open_window(&mut self, event_loop: &ActiveEventLoop) {
500        self.in_tray = None;
501        if let Some(state) = &self.window {
502            state.window.set_visible(true);
503            state.window.set_minimized(false);
504            state.window.focus_window();
505            state.window.request_redraw();
506            return;
507        }
508        match self.create_window(event_loop) {
509            Ok(state) => self.show_window(state),
510            Err(e) => {
511                tracing::error!(error = %e, "could not open the window again");
512                self.error = Some(e);
513                event_loop.exit();
514            }
515        }
516    }
517
518    /// Puts a new window on screen. On macOS its first frame, the slow one with the
519    /// shaders and the glyphs, is drawn while hidden, so that nobody sees it black.
520    fn show_window(&mut self, state: WindowState) {
521        let window = state.window.clone();
522        self.window = Some(state);
523        if cfg!(target_os = "macos") {
524            self.redraw();
525        }
526        window.set_visible(true);
527        window.focus_window();
528        // What was drawn while hidden does not count for the system.
529        window.request_redraw();
530    }
531
532    /// Sends the accessibility tree to the screen reader if one is listening
533    /// and the tree changed.
534    fn update_access(&mut self) {
535        let Some(access) = &mut self.access else { return };
536        if !access.active {
537            return;
538        }
539        let tree = self.ui.access_tree();
540        if access.last.as_ref() == Some(&tree) {
541            return;
542        }
543        let scale = f64::from(self.ui.scale());
544        access.adapter.update_if_active(|| access::tree_update(&tree, scale));
545        access.last = Some(tree);
546    }
547
548    fn init_gpu(&mut self, window: Arc<Window>) -> Result<WindowState, PlatformError> {
549        let instance = diagnostics::new_instance(Some(&self.display), false);
550        let surface =
551            instance.create_surface(window.clone()).map_err(|e| GpuError::Surface(e.to_string()))?;
552        let gpu = pollster::block_on(GpuContext::new(&instance, Some(&surface)))?;
553        let caps = surface.get_capabilities(&gpu.adapter);
554        let format = preferred_surface_format(&caps.formats)
555            .ok_or_else(|| GpuError::Surface("the surface reports no supported formats".into()))?;
556        let size = window.inner_size();
557        let config = wgpu::SurfaceConfiguration {
558            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
559            format,
560            width: size.width.max(1),
561            height: size.height.max(1),
562            present_mode: wgpu::PresentMode::AutoVsync,
563            desired_maximum_frame_latency: 2,
564            alpha_mode: caps.alpha_modes.first().copied().unwrap_or(wgpu::CompositeAlphaMode::Auto),
565            view_formats: vec![],
566            color_space: wgpu::SurfaceColorSpace::Auto,
567        };
568        surface.configure(&gpu.device, &config);
569        let described = surface_report(&caps, format, config.present_mode);
570        let (display, adapter) = (self.display.clone(), gpu.adapter.clone());
571        self.diagnostics.set_report_source(move || {
572            diagnostics::gather(Some(&display), &adapter, Some(described.clone()))
573        });
574        let device_lost = Arc::new(AtomicBool::new(false));
575        let (lost, lost_window) = (device_lost.clone(), window.clone());
576        gpu.device.set_device_lost_callback(move |reason, message| {
577            tracing::error!(?reason, message, "GPU device lost; recreating it");
578            lost.store(true, Ordering::SeqCst);
579            lost_window.request_redraw();
580        });
581        let renderer = Renderer::new(&gpu.device, &gpu.queue, format);
582
583        let redraw = window.clone();
584        self.ui.set_waker(move || redraw.request_redraw());
585        self.update_viewport(&window, size);
586        window.request_redraw();
587        tracing::info!(?format, width = size.width, height = size.height, "window ready");
588        Ok(WindowState { surface, config, gpu, renderer, device_lost, cursor: CursorIcon::Default, window })
589    }
590
591    fn update_viewport(&mut self, window: &Window, size: PhysicalSize<u32>) {
592        let scale = window.scale_factor() as f32;
593        let logical = Size::new(size.width as f32 / scale, size.height as f32 / scale);
594        self.ui.set_viewport(logical, scale);
595    }
596
597    fn dispatch(&mut self, event: Event) {
598        self.ui.set_time(self.started.elapsed().as_secs_f64());
599        let redraw = self.ui.handle_event(&event);
600        if let Some(state) = &mut self.window {
601            let cursor = self.ui.cursor();
602            if cursor != state.cursor {
603                state.window.set_cursor(match cursor {
604                    CursorIcon::Default => winit::window::CursorIcon::Default,
605                    CursorIcon::Pointer => winit::window::CursorIcon::Pointer,
606                    CursorIcon::Text => winit::window::CursorIcon::Text,
607                    CursorIcon::ColResize => winit::window::CursorIcon::ColResize,
608                    CursorIcon::RowResize => winit::window::CursorIcon::RowResize,
609                    _ => winit::window::CursorIcon::Default,
610                });
611                state.cursor = cursor;
612            }
613            if redraw {
614                state.window.request_redraw();
615            }
616        }
617    }
618
619    fn redraw(&mut self) {
620        // Some systems still ask for frames of a hidden window. Coming back draws what changed.
621        if self.in_tray.is_some() {
622            return;
623        }
624        let lost = self.window.as_ref().is_some_and(|s| s.device_lost.load(Ordering::SeqCst));
625        if lost && let Some(old) = self.window.take() {
626            let window = old.window.clone();
627            drop(old);
628            match self.init_gpu(window) {
629                Ok(state) => self.window = Some(state),
630                Err(e) => tracing::error!(error = %e, "GPU recovery failed"),
631            }
632        }
633        self.ui.set_time(self.started.elapsed().as_secs_f64());
634        let Some(state) = &mut self.window else { return };
635        let scale = self.ui.scale();
636        // Looking costs a little, so it only happens while the panel is open.
637        let looking = self.diagnostics.is_open();
638        if looking != self.looking {
639            self.looking = looking;
640            let start = self.started;
641            let clock: Option<Rc<dyn Fn() -> f64>> =
642                looking.then(|| Rc::new(move || start.elapsed().as_secs_f64()) as Rc<dyn Fn() -> f64>);
643            self.ui.set_phase_clock(clock);
644            self.ui.set_view_profiling(looking);
645            state.renderer.set_gpu_timing(looking);
646        }
647        let started = std::time::Instant::now();
648        let scene = self.ui.update();
649        let updated = started.elapsed();
650        let frame = match state.surface.get_current_texture() {
651            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
652            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
653                state.surface.configure(&state.gpu.device, &state.config);
654                frame
655            }
656            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
657                state.surface.configure(&state.gpu.device, &state.config);
658                state.window.request_redraw();
659                return;
660            }
661            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return,
662            other => {
663                tracing::error!(?other, "could not acquire a frame");
664                return;
665            }
666        };
667        let acquired = started.elapsed();
668        let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
669        let stats = state.renderer.render(scene, &view, (state.config.width, state.config.height), scale);
670        let rendered = started.elapsed();
671        state.window.pre_present_notify();
672        state.gpu.queue.present(frame);
673        tracing::debug!(
674            update = ?updated,
675            acquire = ?acquired - updated,
676            render = ?rendered - acquired,
677            present = ?started.elapsed() - rendered,
678            ?stats,
679            "frame rendered"
680        );
681        let total = started.elapsed();
682        if looking {
683            self.diagnostics.set_view_costs(self.ui.view_costs());
684        }
685        self.diagnostics.record(FrameSample {
686            work: self.ui.take_frame_work(),
687            render_work: stats.work(),
688            update: updated,
689            acquire: acquired - updated,
690            render: rendered - acquired,
691            present: total - rendered,
692            quads: stats.quads,
693            glyphs: stats.glyphs,
694            draw_calls: stats.draw_calls,
695            size: (state.config.width, state.config.height),
696            scale,
697        });
698        if self.ui.needs_redraw() {
699            state.window.request_redraw();
700        }
701        self.update_access();
702    }
703}
704
705/// Ctrl+Shift+D, or Cmd+Shift+D on a Mac, opens the diagnostics panel.
706fn is_diagnostics_shortcut(key: &WinitKey, modifiers: Modifiers) -> bool {
707    (modifiers.ctrl || modifiers.meta)
708        && modifiers.shift
709        && matches!(key, WinitKey::Character(c) if c.eq_ignore_ascii_case("d"))
710}
711
712fn map_key(key: &WinitKey) -> Key {
713    match key {
714        WinitKey::Named(NamedKey::Enter) => Key::Enter,
715        WinitKey::Named(NamedKey::Space) => Key::Space,
716        WinitKey::Named(NamedKey::Tab) => Key::Tab,
717        WinitKey::Named(NamedKey::Escape) => Key::Escape,
718        WinitKey::Named(NamedKey::Backspace) => Key::Backspace,
719        WinitKey::Named(NamedKey::Delete) => Key::Delete,
720        WinitKey::Named(NamedKey::Home) => Key::Home,
721        WinitKey::Named(NamedKey::End) => Key::End,
722        WinitKey::Named(NamedKey::PageUp) => Key::PageUp,
723        WinitKey::Named(NamedKey::PageDown) => Key::PageDown,
724        WinitKey::Named(NamedKey::ArrowLeft) => Key::ArrowLeft,
725        WinitKey::Named(NamedKey::ArrowRight) => Key::ArrowRight,
726        WinitKey::Named(NamedKey::ArrowUp) => Key::ArrowUp,
727        WinitKey::Named(NamedKey::ArrowDown) => Key::ArrowDown,
728        WinitKey::Character(c) if c.as_str() == " " => Key::Space,
729        WinitKey::Character(c) => Key::Character(c.to_string()),
730        other => Key::Other(format!("{other:?}")),
731    }
732}
733
734fn map_button(button: MouseButton) -> PointerButton {
735    match button {
736        MouseButton::Left => PointerButton::Primary,
737        MouseButton::Right => PointerButton::Secondary,
738        MouseButton::Middle => PointerButton::Auxiliary,
739        MouseButton::Back => PointerButton::Other(3),
740        MouseButton::Forward => PointerButton::Other(4),
741        MouseButton::Other(n) => PointerButton::Other(n),
742    }
743}
744
745impl App {
746    /// On Wayland, winit does not deliver a touchpad's pinch, so it is heard apart.
747    fn on_wayland(&self) -> bool {
748        use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle};
749        self.display
750            .display_handle()
751            .is_ok_and(|handle| matches!(handle.as_raw(), RawDisplayHandle::Wayland(_)))
752    }
753
754    fn listen_for_wayland_pinches(&self) {
755        use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle};
756        let Ok(handle) = self.display.display_handle() else { return };
757        let RawDisplayHandle::Wayland(wayland) = handle.as_raw() else { return };
758        let proxy = self.proxy.clone();
759        let heard = ravenrs_wayland_gestures::listen(wayland.display, move |delta| {
760            let _ = proxy.send_event(UserEvent::Pinch(delta));
761        });
762        if let Err(e) = heard {
763            tracing::info!(error = %e, "no touchpad pinches on this Wayland");
764        }
765    }
766}
767
768impl ApplicationHandler<UserEvent> for App {
769    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
770        if let StartCause::ResumeTimeReached { .. } = cause
771            && let Some(state) = &self.window
772        {
773            state.window.request_redraw();
774        }
775    }
776
777    /// Sleeps until input arrives or until the UI's next timed wake-up.
778    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
779        if self.in_tray.is_some()
780            && let Some(active) = tray::app_is_active()
781        {
782            if !active {
783                self.away_in_tray = true;
784            } else if self.away_in_tray {
785                self.open_window(event_loop);
786            }
787        }
788        match self.ui.next_wake().filter(|_| self.in_tray.is_none()) {
789            Some(at) => {
790                let at = self.started + std::time::Duration::from_secs_f64(at.max(0.0));
791                event_loop.set_control_flow(ControlFlow::WaitUntil(at));
792            }
793            None => event_loop.set_control_flow(ControlFlow::Wait),
794        }
795    }
796
797    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
798        if self.window.is_some() {
799            return;
800        }
801        match self.create_window(event_loop) {
802            Ok(state) => {
803                self.show_window(state);
804                self.listen_for_wayland_pinches();
805            }
806            Err(e) => {
807                tracing::error!(error = %e, "could not start");
808                self.error = Some(e);
809                event_loop.exit();
810            }
811        }
812    }
813
814    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
815        use accesskit_winit::WindowEvent as A;
816        let event = match event {
817            UserEvent::Access(event) => event,
818            UserEvent::Tray(tray::TrayAction::Open) => return self.open_window(event_loop),
819            UserEvent::Tray(tray::TrayAction::Quit) => return event_loop.exit(),
820            UserEvent::Pinch(delta) => {
821                if let Some(position) = self.pointer {
822                    self.dispatch(Event::Pinch { position, delta });
823                }
824                return;
825            }
826            UserEvent::Wake => {
827                tracing::trace!("woken from another thread");
828                if let Some(state) = &self.window {
829                    state.window.request_redraw();
830                }
831                return;
832            }
833        };
834        match event.window_event {
835            A::InitialTreeRequested => {
836                if let Some(access) = &mut self.access {
837                    access.active = true;
838                    access.last = None;
839                }
840                self.ui.update();
841                self.update_access();
842            }
843            A::ActionRequested(request) => {
844                let Some(action) = access::action(request.action) else { return };
845                self.ui.set_time(self.started.elapsed().as_secs_f64());
846                if self.ui.perform_action(request.target_node.0, action)
847                    && let Some(state) = &self.window
848                {
849                    state.window.request_redraw();
850                }
851            }
852            A::AccessibilityDeactivated => {
853                if let Some(access) = &mut self.access {
854                    access.active = false;
855                    access.last = None;
856                }
857            }
858        }
859    }
860
861    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
862        if let (Some(access), Some(state)) = (&mut self.access, &self.window) {
863            access.adapter.process_event(&state.window, &event);
864        }
865        match event {
866            WindowEvent::CloseRequested => {
867                if !self.close_to_tray(event_loop) {
868                    event_loop.exit();
869                }
870            }
871            WindowEvent::Resized(size) => {
872                if let Some(state) = &mut self.window {
873                    state.config.width = size.width.max(1);
874                    state.config.height = size.height.max(1);
875                    state.surface.configure(&state.gpu.device, &state.config);
876                    let window = state.window.clone();
877                    self.update_viewport(&window, size);
878                    window.request_redraw();
879                }
880            }
881            WindowEvent::ScaleFactorChanged { .. } => {
882                if let Some(state) = &self.window {
883                    let window = state.window.clone();
884                    self.update_viewport(&window, window.inner_size());
885                    window.request_redraw();
886                }
887            }
888            WindowEvent::CursorMoved { position, .. } => {
889                let scale = self.ui.scale().max(f32::EPSILON) as f64;
890                let PhysicalPosition { x, y } = position;
891                let p = Point::new((x / scale) as f32, (y / scale) as f32);
892                self.pointer = Some(p);
893                self.dispatch(Event::PointerMove { position: p });
894            }
895            WindowEvent::HoveredFile(_) => self.ui.set_files_over(true),
896            WindowEvent::HoveredFileCancelled => self.ui.set_files_over(false),
897            WindowEvent::DroppedFile(path) => {
898                // Nothing here knows how large a place will take; this keeps a drop
899                // from filling the memory with something no place would keep.
900                if let Some(file) = files::read_picked(&path, Some(files::MOST_DROPPED_BYTES)) {
901                    self.ui.drop_files(self.pointer, vec![file]);
902                }
903            }
904            WindowEvent::CursorLeft { .. } => {
905                self.pointer = None;
906                self.dispatch(Event::PointerLeave);
907            }
908            WindowEvent::MouseInput { state, button, .. } => {
909                if let Some(position) = self.pointer {
910                    let button = map_button(button);
911                    let event = match state {
912                        ElementState::Pressed => Event::PointerDown { position, button },
913                        ElementState::Released => Event::PointerUp { position, button },
914                    };
915                    self.dispatch(event);
916                    if state == ElementState::Released && button == PointerButton::Primary {
917                        let now = std::time::Instant::now();
918                        let double = self.last_click.is_some_and(|(t, p)| {
919                            now.duration_since(t).as_millis() < 450
920                                && (p.x - position.x).hypot(p.y - position.y) < 5.0
921                        });
922                        if double {
923                            self.last_click = None;
924                            self.dispatch(Event::DoubleClick { position });
925                        } else {
926                            self.last_click = Some((now, position));
927                        }
928                    }
929                }
930            }
931            WindowEvent::MouseWheel { delta, .. } => {
932                if let Some(position) = self.pointer {
933                    let delta = match delta {
934                        // One notch scrolls three 16px lines, as browsers do.
935                        MouseScrollDelta::LineDelta(x, y) => Point::new(x * 48.0, y * 48.0),
936                        MouseScrollDelta::PixelDelta(p) => {
937                            let scale = self.ui.scale().max(f32::EPSILON) as f64;
938                            Point::new((p.x / scale) as f32, (p.y / scale) as f32)
939                        }
940                    };
941                    // Shift turns a wheel that only goes up and down sideways, as browsers do.
942                    let delta =
943                        if self.modifiers.shift && delta.x == 0.0 { Point::new(delta.y, 0.0) } else { delta };
944                    if self.modifiers.ctrl {
945                        // How touchpads on Windows send a pinch, and how a mouse asks to zoom.
946                        self.dispatch(Event::Pinch { position, delta: delta.y / WHEEL_PER_ZOOM });
947                    } else {
948                        self.dispatch(Event::Scroll { position, delta });
949                    }
950                }
951            }
952            WindowEvent::PinchGesture { delta, .. } => {
953                if let Some(position) = self.pointer {
954                    self.dispatch(Event::Pinch { position, delta: delta as f32 });
955                }
956            }
957            WindowEvent::ModifiersChanged(m) => {
958                let s: ModifiersState = m.state();
959                self.modifiers = Modifiers {
960                    shift: s.shift_key(),
961                    ctrl: s.control_key(),
962                    alt: s.alt_key(),
963                    meta: s.super_key(),
964                };
965            }
966            WindowEvent::KeyboardInput { event, .. } => {
967                if is_diagnostics_shortcut(&event.logical_key, self.modifiers) {
968                    if event.state == ElementState::Pressed && !event.repeat {
969                        self.diagnostics.toggle();
970                    }
971                    return;
972                }
973                let key = map_key(&event.logical_key);
974                let modifiers = self.modifiers;
975                self.dispatch(match event.state {
976                    ElementState::Pressed => Event::KeyDown { key, modifiers },
977                    ElementState::Released => Event::KeyUp { key, modifiers },
978                });
979            }
980            WindowEvent::Focused(false) => self.dispatch(Event::FocusLost),
981            WindowEvent::RedrawRequested => self.redraw(),
982            _ => {}
983        }
984    }
985}
986
987/// The window full screen applies to. Weak, so a window closed behind the icon still goes.
988type ScreenWindow = std::rc::Rc<std::cell::RefCell<std::sync::Weak<Window>>>;
989
990/// Full screen as a borderless window over the whole monitor it is on.
991struct SystemFullScreen(ScreenWindow);
992
993impl ravenrs_widgets::FullScreenProvider for SystemFullScreen {
994    fn is_on(&self) -> bool {
995        self.0.borrow().upgrade().is_some_and(|window| window.fullscreen().is_some())
996    }
997
998    fn set(&self, on: bool) {
999        if let Some(window) = self.0.borrow().upgrade() {
1000            window.set_fullscreen(on.then_some(winit::window::Fullscreen::Borderless(None)));
1001        }
1002    }
1003}
1004
1005/// Opens web addresses in the system's browser. Other addresses (relative paths, other
1006/// schemes) are refused there, so they are only logged.
1007struct SystemLinks;
1008
1009impl ravenrs_widgets::LinkOpener for SystemLinks {
1010    fn open(&self, url: &str) {
1011        if let Err(e) = files::open_url(url) {
1012            tracing::warn!(url, error = %e, "the address did not open");
1013        }
1014    }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020
1021    #[test]
1022    fn the_diagnostics_shortcut_needs_both_modifiers() {
1023        let key = WinitKey::Character("D".into());
1024        let held = |ctrl, shift, meta| Modifiers { ctrl, shift, meta, alt: false };
1025        assert!(is_diagnostics_shortcut(&key, held(true, true, false)));
1026        assert!(is_diagnostics_shortcut(&key, held(false, true, true)));
1027        assert!(!is_diagnostics_shortcut(&key, held(true, false, false)));
1028        assert!(!is_diagnostics_shortcut(&key, held(false, true, false)));
1029        assert!(!is_diagnostics_shortcut(&WinitKey::Character("e".into()), held(true, true, false)));
1030    }
1031
1032    #[test]
1033    fn named_keys_map_to_raven_keys() {
1034        let cases = [
1035            (NamedKey::Enter, Key::Enter),
1036            (NamedKey::Space, Key::Space),
1037            (NamedKey::Tab, Key::Tab),
1038            (NamedKey::Escape, Key::Escape),
1039            (NamedKey::Backspace, Key::Backspace),
1040            (NamedKey::Delete, Key::Delete),
1041            (NamedKey::Home, Key::Home),
1042            (NamedKey::End, Key::End),
1043            (NamedKey::PageUp, Key::PageUp),
1044            (NamedKey::PageDown, Key::PageDown),
1045            (NamedKey::ArrowLeft, Key::ArrowLeft),
1046            (NamedKey::ArrowRight, Key::ArrowRight),
1047            (NamedKey::ArrowUp, Key::ArrowUp),
1048            (NamedKey::ArrowDown, Key::ArrowDown),
1049        ];
1050        for (named, expected) in cases {
1051            assert_eq!(map_key(&WinitKey::Named(named)), expected, "{named:?}");
1052        }
1053    }
1054
1055    #[test]
1056    fn characters_and_unknown_keys() {
1057        assert_eq!(map_key(&WinitKey::Character(" ".into())), Key::Space, "a typed space is the Space key");
1058        assert_eq!(map_key(&WinitKey::Character("ç".into())), Key::Character("ç".into()));
1059        assert!(matches!(map_key(&WinitKey::Named(NamedKey::F5)), Key::Other(name) if name.contains("F5")));
1060    }
1061
1062    #[test]
1063    fn mouse_buttons_map_to_pointer_buttons() {
1064        assert_eq!(map_button(MouseButton::Left), PointerButton::Primary);
1065        assert_eq!(map_button(MouseButton::Right), PointerButton::Secondary);
1066        assert_eq!(map_button(MouseButton::Middle), PointerButton::Auxiliary);
1067        assert_eq!(map_button(MouseButton::Back), PointerButton::Other(3));
1068        assert_eq!(map_button(MouseButton::Forward), PointerButton::Other(4));
1069        assert_eq!(map_button(MouseButton::Other(9)), PointerButton::Other(9));
1070    }
1071}