1mod access;
11mod diagnostics;
12mod files;
13mod notice;
14mod sound;
15mod tray;
16
17struct 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
58const WHEEL_PER_ZOOM: f32 = 400.0;
60const DIAGNOSTICS_VARIABLE: &str = "RAVENRS_DIAG";
62
63#[derive(Debug)]
65enum UserEvent {
66 Access(accesskit_winit::Event),
68 Wake,
70 Pinch(f32),
72 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#[derive(Clone, Debug)]
84pub struct WindowOptions {
85 pub title: String,
87 pub size: Size,
89 pub min_size: Option<Size>,
91 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 pub fn new(title: impl Into<String>) -> Self {
117 Self { title: title.into(), ..Self::default() }
118 }
119
120 pub fn size(mut self, width: f32, height: f32) -> Self {
122 self.size = Size::new(width, height);
123 self
124 }
125
126 pub fn theme(mut self, theme: Theme) -> Self {
128 self.theme = theme;
129 self
130 }
131}
132
133#[derive(Debug)]
135pub enum PlatformError {
136 EventLoop(String),
138 Window(String),
140 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
162pub fn run(root: impl View + 'static, options: WindowOptions) -> Result<(), PlatformError> {
164 run_window(root, options, AppIdentity::anonymous(None))
165}
166
167#[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 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 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 pub fn labels(mut self, labels: Labels) -> Self {
203 self.labels = Some(labels);
204 self
205 }
206
207 pub fn diagnostics(mut self, diagnostics: Diagnostics) -> Self {
210 self.diagnostics = Some(diagnostics);
211 self
212 }
213
214 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 pub fn icon(mut self, icon: ImageData) -> Self {
224 self.icon = Some(icon);
225 self
226 }
227
228 pub fn close_to_tray(mut self, tray: SystemTray) -> Self {
232 self.tray = Some(tray);
233 self
234 }
235}
236
237pub 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
246pub 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 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 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 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
348struct Access {
350 adapter: accesskit_winit::Adapter,
351 active: bool,
353 last: Option<AccessTree>,
354}
355
356struct App {
357 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 screen_window: ScreenWindow,
370 tray: Option<SystemTray>,
371 tray_notify: tray::Notify,
372 in_tray: Option<tray::Shown>,
374 away_in_tray: bool,
377 place: Option<PhysicalPosition<i32>>,
379 maximized: bool,
380 pointer: Option<Point>,
382 modifiers: Modifiers,
383 last_click: Option<(std::time::Instant, Point)>,
385 started: std::time::Instant,
386 looking: bool,
388 error: Option<PlatformError>,
389}
390
391#[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#[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#[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 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 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 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 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 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 window.request_redraw();
530 }
531
532 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 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 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
705fn 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 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 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 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 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 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 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
987type ScreenWindow = std::rc::Rc<std::cell::RefCell<std::sync::Weak<Window>>>;
989
990struct 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
1005struct 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}