diff --git a/apps/rsnap/src/app.rs b/apps/rsnap/src/app.rs index 912227d1..21e4759c 100644 --- a/apps/rsnap/src/app.rs +++ b/apps/rsnap/src/app.rs @@ -35,6 +35,8 @@ use winit::event_loop::ActiveEventLoop; #[cfg(target_os = "macos")] use winit::event_loop::EventLoopProxy; +#[cfg(target_os = "macos")] +use self::capture_host_macos::OverlayNativeCaptureInputBuffer; #[cfg(target_os = "macos")] use self::scroll_input_macos::ScrollInputObserverLifecycle; #[cfg(target_os = "macos")] @@ -45,7 +47,7 @@ use crate::settings::AppSettings; use crate::settings_window::{SettingsWindow, SettingsWindowEntry}; use rsnap_overlay::OverlaySession; #[cfg(target_os = "macos")] -use rsnap_overlay::{FrozenGlobalHotkey, MacOSCaptureHost, MacOSNativeCaptureInputEvent}; +use rsnap_overlay::{FrozenGlobalHotkey, MacOSCaptureHost}; pub(crate) enum UserEvent { TrayIcon, @@ -60,7 +62,7 @@ pub(crate) enum UserEvent { #[cfg(target_os = "macos")] OverlayWorkerResponse, #[cfg(target_os = "macos")] - OverlayNativeCaptureInput(u64, MacOSNativeCaptureInputEvent), + OverlayNativeCaptureInput, } #[cfg(target_os = "macos")] @@ -160,6 +162,8 @@ struct App { #[cfg(target_os = "macos")] overlay_stream_event_pending: Arc, #[cfg(target_os = "macos")] + overlay_native_capture_input_buffer: OverlayNativeCaptureInputBuffer, + #[cfg(target_os = "macos")] latest_deferred_ocr_generation: Arc, #[cfg(target_os = "macos")] pending_deferred_ocr_generation: Arc, @@ -280,6 +284,8 @@ impl App { #[cfg(target_os = "macos")] overlay_stream_event_pending: Arc::new(AtomicBool::new(false)), #[cfg(target_os = "macos")] + overlay_native_capture_input_buffer: OverlayNativeCaptureInputBuffer::new(), + #[cfg(target_os = "macos")] latest_deferred_ocr_generation: Arc::new(AtomicU64::new(0)), #[cfg(target_os = "macos")] pending_deferred_ocr_generation: Arc::new(AtomicU64::new(0)), diff --git a/apps/rsnap/src/app/capture.rs b/apps/rsnap/src/app/capture.rs index 68dfd89f..2bb68336 100644 --- a/apps/rsnap/src/app/capture.rs +++ b/apps/rsnap/src/app/capture.rs @@ -518,6 +518,7 @@ impl App { #[cfg(target_os = "macos")] fn reset_capture_start_after_failure(&mut self) { + self.reset_overlay_native_capture_input_dispatch(); self.pending_deferred_ocr_generation.store(0, Ordering::Release); self.scroll_input_shared_state.set_enabled(false); self.scroll_input_shared_state.set_event_waker(None); @@ -635,6 +636,7 @@ impl App { let reset_started_at = Instant::now(); self.finish_coalesced_overlay_stream_frame_send(); + self.reset_overlay_native_capture_input_dispatch(); self.scroll_input_shared_state.clear(); reset_started_at.elapsed().as_millis() @@ -736,6 +738,7 @@ impl App { #[cfg(target_os = "macos")] { self.teardown_overlay_capture_host(); + self.reset_overlay_native_capture_input_dispatch(); self.unregister_overlay_cancel_hotkey(); self.unregister_overlay_loupe_hotkey(); self.unregister_overlay_frozen_hotkeys(); diff --git a/apps/rsnap/src/app/capture_host_macos.rs b/apps/rsnap/src/app/capture_host_macos.rs index 9749d69a..999d4e14 100644 --- a/apps/rsnap/src/app/capture_host_macos.rs +++ b/apps/rsnap/src/app/capture_host_macos.rs @@ -1,19 +1,130 @@ -use std::sync::Arc; +use std::collections::VecDeque; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; use crate::app::{App, UserEvent}; -use rsnap_overlay::{MacOSCaptureHost, OverlayExit}; +use rsnap_overlay::{MacOSCaptureHost, MacOSNativeCaptureInputEvent, OverlayControl, OverlayExit}; + +#[derive(Clone)] +pub(super) struct OverlayNativeCaptureInputBuffer { + queue: Arc>>, + event_pending: Arc, +} +impl OverlayNativeCaptureInputBuffer { + pub(super) fn new() -> Self { + Self { + queue: Arc::new(Mutex::new(VecDeque::new())), + event_pending: Arc::new(AtomicBool::new(false)), + } + } + + fn enqueue(&self, generation: u64, event: MacOSNativeCaptureInputEvent) -> bool { + match self.queue.lock() { + Ok(mut queue) => queue.push_back((generation, event)), + Err(poisoned) => { + tracing::warn!( + op = "capture.native_input_queue_poisoned", + "Dropping native capture input event because the queue lock was poisoned." + ); + + poisoned.into_inner().clear(); + + return false; + }, + } + + self.event_pending + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + } + + fn finish_send(&self) { + self.event_pending.store(false, Ordering::Release); + } + + fn drain(&self) -> Vec<(u64, MacOSNativeCaptureInputEvent)> { + match self.queue.lock() { + Ok(mut queue) => queue.drain(..).collect(), + Err(poisoned) => { + tracing::warn!( + op = "capture.native_input_queue_poisoned", + "Draining native capture input from a poisoned queue." + ); + + poisoned.into_inner().drain(..).collect() + }, + } + } + + fn reset(&self) { + self.finish_send(); + + match self.queue.lock() { + Ok(mut queue) => queue.clear(), + Err(poisoned) => { + tracing::warn!( + op = "capture.native_input_queue_poisoned", + "Resetting native capture input from a poisoned queue." + ); + + poisoned.into_inner().clear(); + }, + } + } +} impl App { pub(super) fn build_overlay_capture_host(&self) -> MacOSCaptureHost { let overlay_proxy = self.overlay_proxy.clone(); + let native_input_buffer = self.overlay_native_capture_input_buffer.clone(); let generation = self.overlay_session_generation; MacOSCaptureHost::new(Arc::new(move |event| { - let _ = - overlay_proxy.send_event(UserEvent::OverlayNativeCaptureInput(generation, event)); + if native_input_buffer.enqueue(generation, event) + && overlay_proxy.send_event(UserEvent::OverlayNativeCaptureInput).is_err() + { + native_input_buffer.finish_send(); + } })) } + pub(super) fn finish_coalesced_overlay_native_capture_input_send(&self) { + self.overlay_native_capture_input_buffer.finish_send(); + } + + pub(super) fn drain_overlay_native_capture_input_events( + &self, + ) -> Vec<(u64, MacOSNativeCaptureInputEvent)> { + self.overlay_native_capture_input_buffer.drain() + } + + pub(super) fn handle_overlay_native_capture_input_ready(&mut self) -> OverlayControl { + self.finish_coalesced_overlay_native_capture_input_send(); + + for (generation, event) in self.drain_overlay_native_capture_input_events() { + if generation != self.overlay_session_generation { + continue; + } + + let Some(session) = self.overlay_session.as_mut() else { + break; + }; + let control = session.handle_native_capture_input_event(event); + + if !matches!(control, OverlayControl::Continue) { + return control; + } + } + + OverlayControl::Continue + } + + pub(super) fn reset_overlay_native_capture_input_dispatch(&self) { + self.overlay_native_capture_input_buffer.reset(); + } + pub(super) fn sync_overlay_capture_host(&mut self) { let sync_result = match (self.overlay_session.as_mut(), self.overlay_capture_host.as_mut()) { @@ -36,3 +147,39 @@ impl App { self.overlay_capture_host = None; } } + +#[cfg(test)] +mod tests { + use crate::app::capture_host_macos::OverlayNativeCaptureInputBuffer; + use rsnap_overlay::MacOSNativeCaptureInputEvent; + + #[test] + fn native_capture_input_buffer_coalesces_multiple_events_behind_one_wakeup() { + let buffer = OverlayNativeCaptureInputBuffer::new(); + + assert!(buffer.enqueue(7, MacOSNativeCaptureInputEvent::ToolbarPointerLeft)); + assert!(!buffer.enqueue(7, MacOSNativeCaptureInputEvent::ToolbarPointerLeft)); + + buffer.finish_send(); + + assert_eq!( + buffer.drain(), + vec![ + (7, MacOSNativeCaptureInputEvent::ToolbarPointerLeft), + (7, MacOSNativeCaptureInputEvent::ToolbarPointerLeft), + ] + ); + } + + #[test] + fn native_capture_input_buffer_reset_clears_pending_and_buffered_events() { + let buffer = OverlayNativeCaptureInputBuffer::new(); + + assert!(buffer.enqueue(5, MacOSNativeCaptureInputEvent::ToolbarPointerLeft)); + + buffer.reset(); + + assert!(buffer.drain().is_empty()); + assert!(buffer.enqueue(6, MacOSNativeCaptureInputEvent::ToolbarPointerLeft)); + } +} diff --git a/apps/rsnap/src/app/runtime.rs b/apps/rsnap/src/app/runtime.rs index 9e36274d..c3d93c41 100644 --- a/apps/rsnap/src/app/runtime.rs +++ b/apps/rsnap/src/app/runtime.rs @@ -84,16 +84,10 @@ impl ApplicationHandler for App { } }, #[cfg(target_os = "macos")] - UserEvent::OverlayNativeCaptureInput(generation, event) => { - if generation != self.overlay_session_generation { - return; - } + UserEvent::OverlayNativeCaptureInput => { + let control = self.handle_overlay_native_capture_input_ready(); - if let Some(session) = self.overlay_session.as_mut() { - let control = session.handle_native_capture_input_event(event); - - self.handle_overlay_control(control); - } + self.handle_overlay_control(control); }, } }