diff --git a/apps/rsnap/src/app.rs b/apps/rsnap/src/app.rs index c1b09ce1..912227d1 100644 --- a/apps/rsnap/src/app.rs +++ b/apps/rsnap/src/app.rs @@ -1,4 +1,6 @@ mod capture; +#[cfg(target_os = "macos")] +mod capture_host_macos; mod hotkeys; mod runtime; #[cfg(target_os = "macos")] @@ -41,9 +43,9 @@ use self::scroll_input_macos::SharedScrollInputState; use crate::permissions_macos; use crate::settings::AppSettings; use crate::settings_window::{SettingsWindow, SettingsWindowEntry}; -#[cfg(target_os = "macos")] -use rsnap_overlay::FrozenGlobalHotkey; use rsnap_overlay::OverlaySession; +#[cfg(target_os = "macos")] +use rsnap_overlay::{FrozenGlobalHotkey, MacOSCaptureHost, MacOSNativeCaptureInputEvent}; pub(crate) enum UserEvent { TrayIcon, @@ -58,7 +60,7 @@ pub(crate) enum UserEvent { #[cfg(target_os = "macos")] OverlayWorkerResponse, #[cfg(target_os = "macos")] - OverlayNativeCaptureInput, + OverlayNativeCaptureInput(u64, MacOSNativeCaptureInputEvent), } #[cfg(target_os = "macos")] @@ -141,6 +143,8 @@ struct App { menubar_quit_menu_id: Option, overlay_session: Option, #[cfg(target_os = "macos")] + overlay_capture_host: Option, + #[cfg(target_os = "macos")] prewarmed_overlay_session: Option, settings_window: Option, settings_window_capture_window_id: Option, @@ -156,8 +160,6 @@ struct App { #[cfg(target_os = "macos")] overlay_stream_event_pending: Arc, #[cfg(target_os = "macos")] - overlay_native_capture_input_event_pending: Arc, - #[cfg(target_os = "macos")] latest_deferred_ocr_generation: Arc, #[cfg(target_os = "macos")] pending_deferred_ocr_generation: Arc, @@ -261,6 +263,8 @@ impl App { menubar_quit_menu_id: None, overlay_session: None, #[cfg(target_os = "macos")] + overlay_capture_host: None, + #[cfg(target_os = "macos")] prewarmed_overlay_session: None, settings_window: None, settings_window_capture_window_id: None, @@ -276,8 +280,6 @@ impl App { #[cfg(target_os = "macos")] overlay_stream_event_pending: Arc::new(AtomicBool::new(false)), #[cfg(target_os = "macos")] - overlay_native_capture_input_event_pending: Arc::new(AtomicBool::new(false)), - #[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 15fe1246..68dfd89f 100644 --- a/apps/rsnap/src/app/capture.rs +++ b/apps/rsnap/src/app/capture.rs @@ -40,7 +40,7 @@ use crate::app::scroll_input_macos::{ #[cfg(target_os = "macos")] use crate::permissions_macos; #[cfg(target_os = "macos")] -use rsnap_overlay::DeferredTextRecognitionRequest; +use rsnap_overlay::{DeferredTextRecognitionRequest, MacOSCaptureHost}; use rsnap_overlay::{HudAnchor, OverlayConfig, OverlayControl, OverlayExit, OverlaySession}; #[cfg(target_os = "macos")] @@ -412,7 +412,6 @@ impl App { self.overlay_session_prewarm_retry_not_before = None; self.overlay_session_generation = self.overlay_session_generation.wrapping_add(1); - self.overlay_native_capture_input_event_pending.store(false, Ordering::Release); self.pending_deferred_ocr_generation .store(self.overlay_session_generation, Ordering::Release); } @@ -424,6 +423,8 @@ impl App { let hook_wiring_ms = hook_wiring_started_at.elapsed().as_millis(); let overlay_start_started_at = Instant::now(); + #[cfg(target_os = "macos")] + let mut overlay_capture_host = self.begin_overlay_capture_host_session(); match overlay_session.start(event_loop) { Ok(()) => { @@ -458,6 +459,11 @@ impl App { self.overlay_session = Some(overlay_session); + #[cfg(target_os = "macos")] + if !self.attach_overlay_capture_host_after_start(overlay_capture_host) { + return; + } + #[cfg(target_os = "macos")] self.sync_overlay_hotkey_registrations(); }, @@ -465,13 +471,9 @@ impl App { let overlay_start_ms = overlay_start_started_at.elapsed().as_millis(); #[cfg(target_os = "macos")] - self.pending_deferred_ocr_generation.store(0, Ordering::Release); + overlay_capture_host.cancel_session_start(); #[cfg(target_os = "macos")] - { - self.scroll_input_shared_state.set_enabled(false); - self.scroll_input_shared_state.set_event_waker(None); - self.scroll_input_shared_state.clear(); - } + self.reset_capture_start_after_failure(); tracing::warn!( op = "capture.start_phase_timing", @@ -488,14 +490,48 @@ impl App { "Failed to start overlay session." ); - #[cfg(target_os = "macos")] - { - self.overlay_session_prewarm_requested = true; - } + self.note_capture_start_failure_for_prewarm(); }, } } + #[cfg(target_os = "macos")] + fn begin_overlay_capture_host_session(&self) -> MacOSCaptureHost { + let mut overlay_capture_host = self.build_overlay_capture_host(); + + overlay_capture_host.begin_session(); + + overlay_capture_host + } + + #[cfg(target_os = "macos")] + fn attach_overlay_capture_host_after_start( + &mut self, + overlay_capture_host: MacOSCaptureHost, + ) -> bool { + self.overlay_capture_host = Some(overlay_capture_host); + + self.sync_overlay_capture_host(); + + self.overlay_session.is_some() + } + + #[cfg(target_os = "macos")] + fn reset_capture_start_after_failure(&mut self) { + 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); + self.scroll_input_shared_state.clear(); + } + + #[cfg(target_os = "macos")] + fn note_capture_start_failure_for_prewarm(&mut self) { + self.overlay_session_prewarm_requested = true; + } + + #[cfg(not(target_os = "macos"))] + fn note_capture_start_failure_for_prewarm(&mut self) {} + #[cfg(target_os = "macos")] fn take_overlay_session_for_capture_start(&mut self) -> (&'static str, OverlaySession) { if let Some(overlay_session) = self.prewarmed_overlay_session.take() { @@ -650,22 +686,6 @@ impl App { let _ = overlay_proxy.send_event(UserEvent::OverlayWorkerResponse); } })); - overlay_session.set_native_capture_input_waker(Arc::new({ - let overlay_proxy = self.overlay_proxy.clone(); - let native_input_pending = - Arc::clone(&self.overlay_native_capture_input_event_pending); - - move || { - if native_input_pending - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_ok() && overlay_proxy - .send_event(UserEvent::OverlayNativeCaptureInput) - .is_err() - { - native_input_pending.store(false, Ordering::Release); - } - } - })); overlay_session.set_external_scroll_input_drain_reader(Arc::new({ let shared_state = Arc::clone(&self.scroll_input_shared_state); @@ -709,19 +729,22 @@ impl App { } pub(super) fn end_overlay_session(&mut self, exit: OverlayExit) { - let Some(_session) = self.overlay_session.take() else { + if self.overlay_session.is_none() { return; - }; + } - #[cfg(target_os = "macos")] - self.overlay_native_capture_input_event_pending.store(false, Ordering::Release); #[cfg(target_os = "macos")] { + self.teardown_overlay_capture_host(); self.unregister_overlay_cancel_hotkey(); self.unregister_overlay_loupe_hotkey(); self.unregister_overlay_frozen_hotkeys(); } + let Some(_session) = self.overlay_session.take() else { + return; + }; + #[cfg(target_os = "macos")] { self.prewarmed_overlay_session = None; @@ -936,6 +959,8 @@ impl App { #[cfg(target_os = "macos")] self.sync_overlay_hotkey_registrations(); + #[cfg(target_os = "macos")] + self.sync_overlay_capture_host(); } } diff --git a/apps/rsnap/src/app/capture_host_macos.rs b/apps/rsnap/src/app/capture_host_macos.rs new file mode 100644 index 00000000..9749d69a --- /dev/null +++ b/apps/rsnap/src/app/capture_host_macos.rs @@ -0,0 +1,38 @@ +use std::sync::Arc; + +use crate::app::{App, UserEvent}; +use rsnap_overlay::{MacOSCaptureHost, OverlayExit}; + +impl App { + pub(super) fn build_overlay_capture_host(&self) -> MacOSCaptureHost { + let overlay_proxy = self.overlay_proxy.clone(); + let generation = self.overlay_session_generation; + + MacOSCaptureHost::new(Arc::new(move |event| { + let _ = + overlay_proxy.send_event(UserEvent::OverlayNativeCaptureInput(generation, event)); + })) + } + + pub(super) fn sync_overlay_capture_host(&mut self) { + let sync_result = match (self.overlay_session.as_mut(), self.overlay_capture_host.as_mut()) + { + (Some(session), Some(host)) => session.sync_macos_capture_host(host), + _ => return, + }; + + if let Err(err) = sync_result { + self.end_overlay_session(OverlayExit::Error(err)); + } + } + + pub(super) fn teardown_overlay_capture_host(&mut self) { + if let (Some(session), Some(host)) = + (self.overlay_session.as_mut(), self.overlay_capture_host.as_mut()) + { + session.teardown_macos_capture_host(host); + } + + self.overlay_capture_host = None; + } +} diff --git a/apps/rsnap/src/app/runtime.rs b/apps/rsnap/src/app/runtime.rs index 7ba10855..9e36274d 100644 --- a/apps/rsnap/src/app/runtime.rs +++ b/apps/rsnap/src/app/runtime.rs @@ -1,6 +1,6 @@ use std::collections::VecDeque; #[cfg(target_os = "macos")] -use std::sync::{Arc, atomic::Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; use color_eyre::eyre; @@ -53,6 +53,8 @@ impl ApplicationHandler for App { && let Err(err) = session.finish_startup_aux_window_creation(event_loop) { self.end_overlay_session(OverlayExit::Error(err)); + } else { + self.sync_overlay_capture_host(); } }, #[cfg(target_os = "macos")] @@ -82,11 +84,13 @@ impl ApplicationHandler for App { } }, #[cfg(target_os = "macos")] - UserEvent::OverlayNativeCaptureInput => { - self.overlay_native_capture_input_event_pending.store(false, Ordering::Release); + UserEvent::OverlayNativeCaptureInput(generation, event) => { + if generation != self.overlay_session_generation { + return; + } if let Some(session) = self.overlay_session.as_mut() { - let control = session.handle_native_capture_input_ready(); + let control = session.handle_native_capture_input_event(event); self.handle_overlay_control(control); } diff --git a/apps/rsnap/src/settings_window.rs b/apps/rsnap/src/settings_window.rs index d920aaa7..1fda4b35 100644 --- a/apps/rsnap/src/settings_window.rs +++ b/apps/rsnap/src/settings_window.rs @@ -19,8 +19,7 @@ use egui_wgpu::Renderer; use global_hotkey::hotkey::HotKey; use wgpu::Surface; use wgpu::SurfaceConfiguration; -use winit::event::ElementState; -use winit::event::WindowEvent; +use winit::event::{ElementState, KeyEvent, WindowEvent}; use winit::event_loop::ActiveEventLoop; use winit::keyboard::ModifiersState; use winit::window::Theme; @@ -228,19 +227,15 @@ impl SettingsWindow { self.window.request_redraw(); }, - WindowEvent::KeyboardInput { event, .. } if self.capture_hotkey_recording => { - if event.state == ElementState::Pressed { - self.handle_capture_hotkey_recording_input(event); - } + WindowEvent::KeyboardInput { event, .. } if self.should_record_hotkey(event) => { + self.handle_capture_hotkey_recording_input(event); }, WindowEvent::ThemeChanged(_) => { // Follow system theme changes when ThemeMode::System is active. self.window.request_redraw(); }, - WindowEvent::KeyboardInput { event, .. } => { - if platform::should_close_from_keyboard(self.modifiers, event) { - return SettingsControl::CloseRequested; - } + WindowEvent::KeyboardInput { event, .. } if self.should_close_from_keyboard(event) => { + return SettingsControl::CloseRequested; }, WindowEvent::Resized(size) => self.resize(*size), WindowEvent::ScaleFactorChanged { .. } => self.resize(self.window.inner_size()), @@ -254,6 +249,14 @@ impl SettingsWindow { SettingsControl::Continue } + fn should_record_hotkey(&self, event: &KeyEvent) -> bool { + self.capture_hotkey_recording && event.state == ElementState::Pressed + } + + fn should_close_from_keyboard(&self, event: &KeyEvent) -> bool { + platform::should_close_from_keyboard(self.modifiers, event) + } + pub fn drain_actions(&mut self) -> VecDeque { mem::take(&mut self.action_queue) } diff --git a/packages/rsnap-overlay/src/lib.rs b/packages/rsnap-overlay/src/lib.rs index 5705701f..4cd93413 100644 --- a/packages/rsnap-overlay/src/lib.rs +++ b/packages/rsnap-overlay/src/lib.rs @@ -48,7 +48,12 @@ pub use crate::deferred_text_recognition::{ }; pub use crate::overlay::{ AltActivationMode, FrozenGlobalHotkey, HudAnchor, OutputNaming, OverlayConfig, OverlayControl, - OverlayExit, OverlaySession, ThemeMode, ToolbarPlacement, WindowCaptureAlphaMode, + OverlayExit, OverlayKeyboardInputEvent, OverlaySession, ThemeMode, ToolbarPlacement, + WindowCaptureAlphaMode, +}; +#[cfg(target_os = "macos")] +pub use crate::overlay::{ + MacOSCaptureHost, MacOSNativeCaptureInputEvent, MacOSNativeCaptureScrollDelta, }; pub use crate::state::{ GlobalPoint, LiveCursorSample, MonitorImageSnapshot, MonitorRect, RectPoints, Rgb, WindowHit, diff --git a/packages/rsnap-overlay/src/overlay.rs b/packages/rsnap-overlay/src/overlay.rs index 480d432f..0d40a726 100644 --- a/packages/rsnap-overlay/src/overlay.rs +++ b/packages/rsnap-overlay/src/overlay.rs @@ -33,7 +33,8 @@ mod window_runtime; mod worker_runtime; #[cfg(target_os = "macos")] -use std::collections::VecDeque; +pub use self::macos_native_capture_shell_runtime::MacOSCaptureHost; + #[cfg(not(target_os = "macos"))] use std::env; #[cfg(target_os = "macos")] @@ -154,8 +155,6 @@ use winit::{ use self::frozen_text_runtime::{FrozenTextInputSource, FrozenTextRecentInput}; #[cfg(target_os = "macos")] -use self::macos_native_capture_shell_runtime::MacOSNativeCaptureShells; -#[cfg(target_os = "macos")] use self::rendering::StartupLiveRgbPlan; use self::rendering::{ GpuContext, HudOverlayWindow, HudPillGeometry, HudRedrawSummary, OverlayWindow, @@ -536,8 +535,6 @@ pub struct OverlaySession { cursor_monitor: Option, egui_repaint_deadline: Arc>>, windows: HashMap, - #[cfg(target_os = "macos")] - native_capture_shells: Option, focused_window_ids: HashSet, pending_focus_loss_cleanup: bool, hud_window: Option, @@ -671,17 +668,11 @@ pub struct OverlaySession { #[cfg(target_os = "macos")] startup_aux_window_waker: Option>, #[cfg(target_os = "macos")] - native_capture_input_waker: Option>, - #[cfg(target_os = "macos")] - native_capture_input_queue: Arc>>, - #[cfg(target_os = "macos")] startup_aux_window_creation_pending: bool, #[cfg(target_os = "macos")] startup_aux_window_creation_scheduled: bool, #[cfg(target_os = "macos")] pending_startup_aux_live_stream_filter_upgrade: bool, - #[cfg(target_os = "macos")] - frontmost_application_before_start: Option, response_waker: Option>, } impl OverlaySession { @@ -795,7 +786,7 @@ impl OverlaySession { state: OverlayState::new(), session_active: false, cursor_monitor: None, - windows: HashMap::new(), #[cfg(target_os = "macos")] native_capture_shells: None, + windows: HashMap::new(), focused_window_ids: HashSet::new(), pending_focus_loss_cleanup: false, hud_window: None, loupe_window: None, toolbar_window: None, scroll_preview_window: None, @@ -888,12 +879,9 @@ impl OverlaySession { #[cfg(target_os = "macos")] scroll_capture_started_hook: None, #[cfg(target_os = "macos")] startup_aux_window_waker: None, - #[cfg(target_os = "macos")] native_capture_input_waker: None, - #[cfg(target_os = "macos")] native_capture_input_queue: Arc::new(Mutex::new(VecDeque::new())), #[cfg(target_os = "macos")] startup_aux_window_creation_pending: false, #[cfg(target_os = "macos")] startup_aux_window_creation_scheduled: false, #[cfg(target_os = "macos")] pending_startup_aux_live_stream_filter_upgrade: false, - #[cfg(target_os = "macos")] frontmost_application_before_start: None, response_waker: None, } } @@ -936,55 +924,6 @@ impl OverlaySession { self.maybe_schedule_startup_aux_window_creation(); } - #[cfg(target_os = "macos")] - fn capture_frontmost_application_for_exit_restore(&mut self) { - self.frontmost_application_before_start = macos_frontmost_application(); - - tracing::info!( - op = "overlay.frontmost_app_captured", - target_process_id = - self.frontmost_application_before_start.map(|target| target.process_id), - "Captured the pre-capture frontmost application for later restore." - ); - } - - #[cfg(target_os = "macos")] - fn restore_frontmost_application_after_exit(&self, target: Option) { - let Some(target) = target else { - tracing::info!( - op = "overlay.frontmost_app_restore_attempted", - target = "none", - "Skipped restoring the pre-capture frontmost application because none was recorded." - ); - - return; - }; - let restored = macos_restore_frontmost_application(target); - - tracing::info!( - op = "overlay.frontmost_app_restore_attempted", - target_process_id = target.process_id, - restored, - "Attempted to restore the pre-capture frontmost application." - ); - } - - #[cfg(target_os = "macos")] - fn restore_recorded_frontmost_application_for_focus_preservation(&self, reason: &'static str) { - let Some(target) = self.frontmost_application_before_start else { - return; - }; - let restored = macos_restore_frontmost_application(target); - - tracing::info!( - op = "overlay.frontmost_app_focus_preservation_attempted", - target_process_id = target.process_id, - reason, - restored, - "Attempted to preserve the pre-capture frontmost application during overlay interaction." - ); - } - #[cfg(target_os = "macos")] /// Registers a wake callback for macOS live-stream frame notifications. pub fn set_scroll_frame_waker(&mut self, waker: Arc) { @@ -1024,12 +963,6 @@ impl OverlaySession { self.startup_aux_window_waker = Some(waker); } - #[cfg(target_os = "macos")] - /// Registers a wake callback for AppKit-native passive capture input. - pub fn set_native_capture_input_waker(&mut self, waker: Arc) { - self.native_capture_input_waker = Some(waker); - } - #[cfg(target_os = "macos")] /// Supplies a host-owned guard that must approve scroll capture before it can start. /// Return `Ok(false)` to reject the attempt without surfacing a HUD error. @@ -1056,32 +989,11 @@ impl OverlaySession { } #[cfg(target_os = "macos")] - fn native_capture_input_dispatch(&self) -> Option { - self.native_capture_input_waker.as_ref().cloned().map(|waker| { - MacOSNativeCaptureInputDispatch { - queue: Arc::clone(&self.native_capture_input_queue), - waker, - } - }) - } - - #[cfg(target_os = "macos")] - fn drain_native_capture_input_events(&self) -> Vec { - let Ok(mut queue) = self.native_capture_input_queue.lock() else { - tracing::warn!( - op = "overlay.native_capture_input_queue_poisoned", - "Draining native capture input from a poisoned queue." - ); - - return Vec::new(); - }; - - queue.drain(..).collect() - } - - #[cfg(target_os = "macos")] - /// Drains and applies any queued passive AppKit capture input events. - pub fn handle_native_capture_input_ready(&mut self) -> OverlayControl { + /// Applies one host-routed passive AppKit capture input event. + pub fn handle_native_capture_input_event( + &mut self, + event: MacOSNativeCaptureInputEvent, + ) -> OverlayControl { let now = Instant::now(); self.maybe_log_event_loop_stall(now); @@ -1090,83 +1002,68 @@ impl OverlaySession { Some("native_capture_input"), ); - for event in self.drain_native_capture_input_events() { - let control = match event { - MacOSNativeCaptureInputEvent::OverlayPointerMoved { monitor, global } => { - self.handle_native_overlay_pointer_moved(monitor, global) - }, - MacOSNativeCaptureInputEvent::OverlayMouseInput { - monitor, - global, - button, - state, - } => { - self.maybe_stop_frozen_selection_drag_for_mouse_input(state, button); - - match (state, button) { - (ElementState::Pressed, MouseButton::Right) => { - self.cancel_overlay("native_capture_right_click") - }, - (_, MouseButton::Left) => { - self.handle_live_overlay_left_mouse_input(monitor, global, state) - }, - _ => OverlayControl::Continue, - } - }, - MacOSNativeCaptureInputEvent::ToolbarPointerMoved { - monitor, - local, - global, - outer_position, - } => self.handle_native_toolbar_pointer_moved( - monitor, - local, - global, - Some(outer_position), - ), - MacOSNativeCaptureInputEvent::ToolbarPointerLeft => { - self.handle_toolbar_cursor_left() - }, - MacOSNativeCaptureInputEvent::ToolbarMouseInput { button, state } => { - self.maybe_stop_frozen_selection_drag_for_mouse_input(state, button); - - match (state, button) { - (ElementState::Pressed, MouseButton::Right) => { - self.cancel_overlay("native_toolbar_right_click") - }, - (_, MouseButton::Left) => self.handle_toolbar_mouse_input(state), - _ => OverlayControl::Continue, - } - }, - MacOSNativeCaptureInputEvent::ToolbarScrollWheel { delta } => { - let delta = match delta { - MacOSNativeCaptureScrollDelta::Line { x, y } => { - MouseScrollDelta::LineDelta(x, y) - }, - MacOSNativeCaptureScrollDelta::Pixel { x, y } => { - MouseScrollDelta::PixelDelta(PhysicalPosition::new(x, y)) - }, - }; + match event { + MacOSNativeCaptureInputEvent::OverlayPointerMoved { monitor, global } => { + self.handle_native_overlay_pointer_moved(monitor, global) + }, + MacOSNativeCaptureInputEvent::OverlayMouseInput { monitor, global, button, state } => { + self.maybe_stop_frozen_selection_drag_for_mouse_input(state, button); - self.handle_toolbar_mouse_wheel(&delta) - }, - MacOSNativeCaptureInputEvent::KeyboardInput { monitor: _, event } => { - self.handle_overlay_keyboard_input_event(&event) - }, - MacOSNativeCaptureInputEvent::Ime { monitor, event } => { - self.handle_overlay_ime_event(monitor, &event) - }, - MacOSNativeCaptureInputEvent::ModifiersChanged { state } => { - self.handle_modifiers_state_changed(state) - }, - }; + match (state, button) { + (ElementState::Pressed, MouseButton::Right) => { + self.cancel_overlay("native_capture_right_click") + }, + (_, MouseButton::Left) => { + self.handle_live_overlay_left_mouse_input(monitor, global, state) + }, + _ => OverlayControl::Continue, + } + }, + MacOSNativeCaptureInputEvent::ToolbarPointerMoved { + monitor, + local, + global, + outer_position, + } => self.handle_native_toolbar_pointer_moved( + monitor, + local, + global, + Some(outer_position), + ), + MacOSNativeCaptureInputEvent::ToolbarPointerLeft => self.handle_toolbar_cursor_left(), + MacOSNativeCaptureInputEvent::ToolbarMouseInput { button, state } => { + self.maybe_stop_frozen_selection_drag_for_mouse_input(state, button); + + match (state, button) { + (ElementState::Pressed, MouseButton::Right) => { + self.cancel_overlay("native_toolbar_right_click") + }, + (_, MouseButton::Left) => self.handle_toolbar_mouse_input(state), + _ => OverlayControl::Continue, + } + }, + MacOSNativeCaptureInputEvent::ToolbarScrollWheel { delta } => { + let delta = match delta { + MacOSNativeCaptureScrollDelta::Line { x, y } => { + MouseScrollDelta::LineDelta(x, y) + }, + MacOSNativeCaptureScrollDelta::Pixel { x, y } => { + MouseScrollDelta::PixelDelta(PhysicalPosition::new(x, y)) + }, + }; - if !matches!(control, OverlayControl::Continue) { - return control; - } + self.handle_toolbar_mouse_wheel(&delta) + }, + MacOSNativeCaptureInputEvent::KeyboardInput { monitor: _, event } => { + self.handle_overlay_keyboard_input_event(&event) + }, + MacOSNativeCaptureInputEvent::Ime { monitor, event } => { + self.handle_overlay_ime_event(monitor, &event) + }, + MacOSNativeCaptureInputEvent::ModifiersChanged { state } => { + self.handle_modifiers_state_changed(state) + }, } - - OverlayControl::Continue } #[cfg(target_os = "macos")] @@ -1411,9 +1308,6 @@ impl OverlaySession { self.state.begin_freeze(monitor); - #[cfg(target_os = "macos")] - let _ = self.sync_native_capture_shells(); - self.state.drag_rect = None; self.state.hovered_window_rect = None; } @@ -2211,8 +2105,6 @@ impl OverlaySession { self.prepare_toolbar_for_frozen_capture_transition(monitor, capture_rect); self.prepare_frozen_capture_handoff_state(monitor, window_target); - #[cfg(target_os = "macos")] - self.restore_recorded_frontmost_application_for_focus_preservation("begin_frozen_capture"); #[cfg(target_os = "macos")] if self.begin_frozen_capture_with_rect_macos(monitor, window_target, cursor) { @@ -3631,13 +3523,7 @@ impl OverlaySession { self.log_exit_begin(&exit_metadata); self.finalize_scroll_capture_for_exit(); - - #[cfg(target_os = "macos")] - let frontmost_application_before_start = self.frontmost_application_before_start.take(); - self.reset_runtime_for_exit(); - #[cfg(target_os = "macos")] - self.restore_frontmost_application_after_exit(frontmost_application_before_start); self.log_exit_end(&exit_metadata); OverlayControl::Exit(exit) @@ -3698,10 +3584,6 @@ impl OverlaySession { self.session_active = false; - #[cfg(target_os = "macos")] - { - self.destroy_native_capture_shells(); - } self.windows.clear(); self.hud_window = None; @@ -3734,10 +3616,6 @@ impl OverlaySession { self.loupe_window_visible = false; self.loupe_window_warmup_redraws_remaining = 0; self.scroll_capture = ScrollCaptureState::default(); - #[cfg(target_os = "macos")] - { - self.frontmost_application_before_start = None; - } self.frozen_capture_source = FrozenCaptureSource::None; self.cursor_monitor = None; self.gpu = None; @@ -3816,6 +3694,25 @@ impl Default for OverlaySession { } } +#[derive(Clone, Debug, PartialEq)] +/// Opaque keyboard event payload forwarded from the native passive capture host. +pub struct OverlayKeyboardInputEvent { + logical_key: Key, + text: Option, + state: ElementState, + repeat: bool, +} +impl OverlayKeyboardInputEvent { + fn from_winit(event: &KeyEvent) -> Self { + Self { + logical_key: event.logical_key.clone(), + text: event.text.as_deref().map(ToOwned::to_owned), + state: event.state, + repeat: event.repeat, + } + } +} + struct FrozenTransitionTimingInfo { op: &'static str, monitor: Option, @@ -3999,6 +3896,18 @@ struct FrozenArrowGeometry { head_right: Pos2, } +#[cfg(target_os = "macos")] +#[derive(Clone)] +struct MacOSNativeCaptureInputDispatch { + sink: Arc, +} +#[cfg(target_os = "macos")] +impl MacOSNativeCaptureInputDispatch { + fn enqueue(&self, event: MacOSNativeCaptureInputEvent) { + (self.sink)(event); + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] /// Selects how the live HUD should be positioned. pub enum HudAnchor { @@ -4109,93 +4018,92 @@ pub enum WindowCaptureAlphaMode { #[cfg(target_os = "macos")] #[derive(Clone, Copy, Debug, PartialEq)] -enum MacOSNativeCaptureScrollDelta { - Line { x: f32, y: f32 }, - Pixel { x: f64, y: f64 }, -} - -#[derive(Clone, Debug, PartialEq)] -struct OverlayKeyboardInputEvent { - logical_key: Key, - text: Option, - state: ElementState, - repeat: bool, -} -impl OverlayKeyboardInputEvent { - fn from_winit(event: &KeyEvent) -> Self { - Self { - logical_key: event.logical_key.clone(), - text: event.text.as_deref().map(ToOwned::to_owned), - state: event.state, - repeat: event.repeat, - } - } +/// Scroll-wheel delta routed from the native passive capture host. +pub enum MacOSNativeCaptureScrollDelta { + /// A line-based scroll delta measured in lines along the x/y axes. + Line { + /// Horizontal line delta. + x: f32, + /// Vertical line delta. + y: f32, + }, + /// A pixel-based scroll delta measured in pixels along the x/y axes. + Pixel { + /// Horizontal pixel delta. + x: f64, + /// Vertical pixel delta. + y: f64, + }, } #[cfg(target_os = "macos")] #[derive(Clone, Debug, PartialEq)] -enum MacOSNativeCaptureInputEvent { +/// Input event routed from the app-owned macOS passive capture host into the overlay core. +pub enum MacOSNativeCaptureInputEvent { + /// Pointer movement over a passive overlay shell. OverlayPointerMoved { + /// Monitor whose passive shell observed the pointer movement. monitor: MonitorRect, + /// Pointer location in global desktop coordinates. global: GlobalPoint, }, + /// Mouse-button activity observed by a passive overlay shell. OverlayMouseInput { + /// Monitor whose passive shell observed the mouse input. monitor: MonitorRect, + /// Pointer location in global desktop coordinates. global: GlobalPoint, + /// Mouse button that changed state. button: MouseButton, + /// New state for the button. state: ElementState, }, + /// Pointer movement over a passive toolbar shell. ToolbarPointerMoved { + /// Monitor that currently anchors the toolbar shell. monitor: MonitorRect, + /// Pointer location in toolbar-local coordinates. local: Pos2, + /// Pointer location in global desktop coordinates. global: GlobalPoint, + /// Toolbar shell origin in global desktop coordinates. outer_position: GlobalPoint, }, + /// Pointer exit from the passive toolbar shell. ToolbarPointerLeft, + /// Mouse-button activity observed by the passive toolbar shell. ToolbarMouseInput { + /// Mouse button that changed state. button: MouseButton, + /// New state for the button. state: ElementState, }, + /// Scroll-wheel input observed by the passive toolbar shell. ToolbarScrollWheel { + /// Scroll delta reported by the native host. delta: MacOSNativeCaptureScrollDelta, }, + /// Keyboard input forwarded from the passive key-focus shell. KeyboardInput { + /// Monitor associated with the active key-focus shell, if any. monitor: Option, + /// Opaque keyboard event payload translated from winit/native state. event: OverlayKeyboardInputEvent, }, + /// IME input forwarded from the passive key-focus shell. Ime { + /// Monitor associated with the active key-focus shell, if any. monitor: Option, + /// IME payload to apply inside the overlay session. event: Ime, }, + /// Modifier-key state update forwarded from the native host. ModifiersChanged { + /// Current modifier-key state. state: ModifiersState, }, } -#[cfg(target_os = "macos")] -#[derive(Clone)] -struct MacOSNativeCaptureInputDispatch { - queue: Arc>>, - waker: Arc, -} -#[cfg(target_os = "macos")] -impl MacOSNativeCaptureInputDispatch { - fn enqueue(&self, event: MacOSNativeCaptureInputEvent) { - if let Ok(mut queue) = self.queue.lock() { - queue.push_back(event); - } else { - tracing::warn!( - op = "overlay.native_capture_input_queue_poisoned", - "Dropping native capture input event because the queue lock was poisoned." - ); - - return; - } - - (self.waker)(); - } -} - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] /// Single source of truth for live capture entry. /// diff --git a/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs b/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs index ca71e9fe..178b78a5 100644 --- a/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs @@ -24,7 +24,6 @@ impl OverlaySession { pub(super) fn hide_capture_windows(&mut self) { self.capture_windows_hidden = true; - let _ = self.sync_native_capture_shells(); let hide_overlay_windows = self.should_hide_overlay_windows_during_capture(); if hide_overlay_windows { @@ -89,10 +88,6 @@ impl OverlaySession { } self.capture_windows_hidden = false; - - #[cfg(target_os = "macos")] - let _ = self.sync_native_capture_shells(); - #[cfg(target_os = "macos")] { for overlay_window in self.windows.values() { diff --git a/packages/rsnap-overlay/src/overlay/frozen_text_runtime.rs b/packages/rsnap-overlay/src/overlay/frozen_text_runtime.rs index 3780eb0a..6ff5617c 100644 --- a/packages/rsnap-overlay/src/overlay/frozen_text_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/frozen_text_runtime.rs @@ -38,7 +38,6 @@ impl OverlaySession { pub(super) fn sync_text_input_ime_state(&mut self) { #[cfg(target_os = "macos")] { - let _ = self.sync_native_capture_shells(); let ime_allowed = self.frozen_text_tool_active() && self.frozen_text_edit.is_some(); for overlay_window in self.windows.values() { @@ -95,8 +94,6 @@ impl OverlaySession { ), ), ); - - let _ = self.sync_native_capture_shells(); } #[cfg(not(target_os = "macos"))] @@ -498,7 +495,5 @@ impl OverlaySession { monitor_id = ?monitor.map(|target| target.id), "Requested frozen text input focus." ); - - let _ = self.sync_native_capture_shells(); } } diff --git a/packages/rsnap-overlay/src/overlay/macos_native_capture_shell_runtime.rs b/packages/rsnap-overlay/src/overlay/macos_native_capture_shell_runtime.rs index e5719fc4..6f41469b 100644 --- a/packages/rsnap-overlay/src/overlay/macos_native_capture_shell_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/macos_native_capture_shell_runtime.rs @@ -14,6 +14,7 @@ use winit::event::{ElementState, Ime, MouseButton}; use winit::keyboard::{Key, ModifiersState, NamedKey, NativeKey}; use winit::window::WindowId; +use crate::overlay::MacOSFrontmostApplication; use crate::overlay::OverlayWindow; use crate::overlay::{ CursorIcon, GlobalPoint, MacOSNativeCaptureInputDispatch, MacOSNativeCaptureInputEvent, @@ -162,6 +163,227 @@ impl MacOSNativeCaptureShells { } } +/// App-owned macOS capture host adapter that owns native shell lifecycle, +/// host-routed capture input dispatch, and focus restoration side effects. +pub struct MacOSCaptureHost { + native_capture_shells: Option, + native_capture_input_dispatch: MacOSNativeCaptureInputDispatch, + frontmost_application_before_start: Option, + last_synced_mode: Option, +} +impl MacOSCaptureHost { + /// Creates a new host adapter that forwards native capture events to the caller. + pub fn new(event_sink: Arc) -> Self { + Self { + native_capture_shells: None, + native_capture_input_dispatch: MacOSNativeCaptureInputDispatch { sink: event_sink }, + frontmost_application_before_start: None, + last_synced_mode: None, + } + } + + /// Captures the currently frontmost application before starting a capture session. + pub fn begin_session(&mut self) { + self.frontmost_application_before_start = super::macos_frontmost_application(); + self.last_synced_mode = None; + + tracing::info!( + op = "overlay.frontmost_app_captured", + target_process_id = + self.frontmost_application_before_start.map(|target| target.process_id), + "Captured the pre-capture frontmost application for later restore." + ); + } + + /// Cancels a session start before any host-owned shells were synchronized. + pub fn cancel_session_start(&mut self) { + let target = self.frontmost_application_before_start.take(); + + self.restore_frontmost_application_after_exit(target); + } + + fn ensure_native_capture_shells(&mut self, session: &mut OverlaySession) -> Result<(), String> { + if self.native_capture_shells.is_some() { + self.sync_native_capture_shells(session)?; + + return Ok(()); + } + + let mut overlay_shells = HashMap::with_capacity(session.windows.len()); + + for overlay_window in session.windows.values() { + let shell = macos_create_passive_overlay_shell_window( + overlay_window.window.as_ref(), + overlay_window.monitor, + self.native_capture_input_dispatch.clone(), + )?; + + overlay_shells.insert(overlay_window.monitor.id, shell); + } + + self.native_capture_shells = Some(MacOSNativeCaptureShells::new( + overlay_shells, + self.native_capture_input_dispatch.clone(), + )); + + self.sync_native_capture_shells(session)?; + + Ok(()) + } + + fn sync_native_capture_shells(&mut self, session: &mut OverlaySession) -> Result<(), String> { + let live_shell_visible = session.should_host_live_pointer_input_in_native_shell(); + let toolbar_shell_visible = session.should_host_toolbar_pointer_input_in_native_shell(); + let toolbar_context = session.toolbar_window.as_ref().map(|toolbar_window| { + let monitor = session.state.monitor.or_else(|| session.active_cursor_monitor()); + let outer_position = OverlaySession::toolbar_window_outer_position(toolbar_window) + .or(session.pending_toolbar_outer_pos) + .or(session.toolbar_outer_pos) + .or_else(|| { + monitor.and_then(|monitor| { + session.toolbar_state.floating_position.map(|floating_position| { + session.toolbar_outer_position_from_primary_anchor( + monitor, + floating_position, + ) + }) + }) + }); + + (Arc::clone(&toolbar_window.window), monitor, outer_position) + }); + let key_focus_context = session.native_key_focus_shell_context(); + + for overlay_window in session.windows.values() { + super::macos_set_capture_window_mouse_passthrough( + overlay_window.window.as_ref(), + live_shell_visible, + ); + } + + let Some(shells) = self.native_capture_shells.as_mut() else { + if let Some((toolbar_window, _, _)) = toolbar_context.as_ref() { + super::macos_set_capture_window_mouse_passthrough(toolbar_window.as_ref(), false); + } + + return Ok(()); + }; + + shells.sync_overlay_shells(&session.windows, live_shell_visible); + + if let Some((toolbar_window, monitor, outer_position)) = toolbar_context { + if let (Some(monitor), Some(outer_position)) = (monitor, outer_position) { + shells.sync_toolbar_shell( + toolbar_window.as_ref(), + monitor, + outer_position, + toolbar_shell_visible, + )?; + + super::macos_set_capture_window_mouse_passthrough( + toolbar_window.as_ref(), + toolbar_shell_visible, + ); + } else { + super::macos_set_capture_window_mouse_passthrough(toolbar_window.as_ref(), false); + + shells.clear_toolbar_shell(); + } + } else { + shells.clear_toolbar_shell(); + } + if let Some((render_window, target)) = key_focus_context { + shells.sync_key_focus_shell(render_window.as_ref(), target, true)?; + } else { + shells.clear_key_focus_shell(); + } + + self.maybe_preserve_frontmost_application(session); + + self.last_synced_mode = Some(session.state.mode); + + Ok(()) + } + + fn destroy_native_capture_shells(&mut self, session: &OverlaySession) { + macos_set_system_cursor_hidden(false); + + for overlay_window in session.windows.values() { + super::macos_set_capture_window_mouse_passthrough( + overlay_window.window.as_ref(), + false, + ); + } + + if let Some(toolbar_window) = session.toolbar_window.as_ref() { + super::macos_set_capture_window_mouse_passthrough( + toolbar_window.window.as_ref(), + false, + ); + } + + self.native_capture_shells = None; + self.last_synced_mode = None; + + let target = self.frontmost_application_before_start.take(); + + self.restore_frontmost_application_after_exit(target); + } + + fn maybe_preserve_frontmost_application(&mut self, session: &mut OverlaySession) { + let transitioned_into_frozen = matches!(session.state.mode, OverlayMode::Frozen) + && !matches!(self.last_synced_mode, Some(OverlayMode::Frozen)); + + if transitioned_into_frozen { + self.restore_recorded_frontmost_application_for_focus_preservation( + "begin_frozen_capture", + ); + } + if session.toolbar_window_visible && session.preserve_frontmost_on_next_toolbar_show { + session.preserve_frontmost_on_next_toolbar_show = false; + + self.restore_recorded_frontmost_application_for_focus_preservation( + "toolbar_first_show", + ); + } + } + + fn restore_frontmost_application_after_exit(&self, target: Option) { + let Some(target) = target else { + tracing::info!( + op = "overlay.frontmost_app_restore_attempted", + target = "none", + "Skipped restoring the pre-capture frontmost application because none was recorded." + ); + + return; + }; + let restored = super::macos_restore_frontmost_application(target); + + tracing::info!( + op = "overlay.frontmost_app_restore_attempted", + target_process_id = target.process_id, + restored, + "Attempted to restore the pre-capture frontmost application." + ); + } + + fn restore_recorded_frontmost_application_for_focus_preservation(&self, reason: &'static str) { + let Some(target) = self.frontmost_application_before_start else { + return; + }; + let restored = super::macos_restore_frontmost_application(target); + + tracing::info!( + op = "overlay.frontmost_app_focus_preservation_attempted", + target_process_id = target.process_id, + reason, + restored, + "Attempted to preserve the pre-capture frontmost application during overlay interaction." + ); + } +} + #[repr(C)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct MacOSRange { @@ -505,124 +727,14 @@ impl From for NSRect { } impl OverlaySession { - pub(super) fn ensure_native_capture_shells(&mut self) -> Result<(), String> { - let Some(dispatch) = self.native_capture_input_dispatch() else { - return Ok(()); - }; - - if self.native_capture_shells.is_some() { - self.sync_native_capture_shells()?; - - return Ok(()); - } - - let mut overlay_shells = HashMap::with_capacity(self.windows.len()); - - for overlay_window in self.windows.values() { - let shell = macos_create_passive_overlay_shell_window( - overlay_window.window.as_ref(), - overlay_window.monitor, - dispatch.clone(), - )?; - - overlay_shells.insert(overlay_window.monitor.id, shell); - } - - self.native_capture_shells = Some(MacOSNativeCaptureShells::new(overlay_shells, dispatch)); - - self.sync_native_capture_shells()?; - - Ok(()) - } - - pub(super) fn sync_native_capture_shells(&mut self) -> Result<(), String> { - let live_shell_visible = self.should_host_live_pointer_input_in_native_shell(); - let toolbar_shell_visible = self.should_host_toolbar_pointer_input_in_native_shell(); - let toolbar_context = self.toolbar_window.as_ref().map(|toolbar_window| { - let monitor = self.state.monitor.or_else(|| self.active_cursor_monitor()); - let outer_position = Self::toolbar_window_outer_position(toolbar_window) - .or(self.pending_toolbar_outer_pos) - .or(self.toolbar_outer_pos) - .or_else(|| { - monitor.and_then(|monitor| { - self.toolbar_state.floating_position.map(|floating_position| { - self.toolbar_outer_position_from_primary_anchor( - monitor, - floating_position, - ) - }) - }) - }); - - (Arc::clone(&toolbar_window.window), monitor, outer_position) - }); - let key_focus_context = self.native_key_focus_shell_context(); - - for overlay_window in self.windows.values() { - super::macos_set_capture_window_mouse_passthrough( - overlay_window.window.as_ref(), - live_shell_visible, - ); - } - - let Some(shells) = self.native_capture_shells.as_mut() else { - if let Some((toolbar_window, _, _)) = toolbar_context.as_ref() { - super::macos_set_capture_window_mouse_passthrough(toolbar_window.as_ref(), false); - } - - return Ok(()); - }; - - shells.sync_overlay_shells(&self.windows, live_shell_visible); - - if let Some((toolbar_window, monitor, outer_position)) = toolbar_context { - if let (Some(monitor), Some(outer_position)) = (monitor, outer_position) { - shells.sync_toolbar_shell( - toolbar_window.as_ref(), - monitor, - outer_position, - toolbar_shell_visible, - )?; - - super::macos_set_capture_window_mouse_passthrough( - toolbar_window.as_ref(), - toolbar_shell_visible, - ); - } else { - super::macos_set_capture_window_mouse_passthrough(toolbar_window.as_ref(), false); - - shells.clear_toolbar_shell(); - } - } else { - shells.clear_toolbar_shell(); - } - if let Some((render_window, target)) = key_focus_context { - shells.sync_key_focus_shell(render_window.as_ref(), target, true)?; - } else { - shells.clear_key_focus_shell(); - } - - Ok(()) + /// Synchronizes the app-owned macOS capture host with the current overlay session state. + pub fn sync_macos_capture_host(&mut self, host: &mut MacOSCaptureHost) -> Result<(), String> { + host.ensure_native_capture_shells(self) } - pub(super) fn destroy_native_capture_shells(&mut self) { - macos_set_system_cursor_hidden(false); - - for overlay_window in self.windows.values() { - super::macos_set_capture_window_mouse_passthrough( - overlay_window.window.as_ref(), - false, - ); - } - - if let Some(toolbar_window) = self.toolbar_window.as_ref() { - super::macos_set_capture_window_mouse_passthrough( - toolbar_window.window.as_ref(), - false, - ); - } - - self.native_capture_shells = None; + /// Tears down the app-owned macOS capture host for the current session. + pub fn teardown_macos_capture_host(&mut self, host: &mut MacOSCaptureHost) { + host.destroy_native_capture_shells(self); } fn native_key_focus_shell_context(&self) -> Option<(Arc, MacOSKeyFocusShellTarget)> { diff --git a/packages/rsnap-overlay/src/overlay/scroll_capture_runtime.rs b/packages/rsnap-overlay/src/overlay/scroll_capture_runtime.rs index a2ca9749..89800789 100644 --- a/packages/rsnap-overlay/src/overlay/scroll_capture_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/scroll_capture_runtime.rs @@ -559,8 +559,6 @@ impl OverlaySession { #[cfg(target_os = "macos")] fn focus_scroll_keyboard_window(&mut self) { super::macos_activate_app(); - - let _ = self.sync_native_capture_shells(); } pub(super) fn update_scroll_toolbar_default_position(&mut self, monitor: MonitorRect) { diff --git a/packages/rsnap-overlay/src/overlay/tests.rs b/packages/rsnap-overlay/src/overlay/tests.rs index 507a640d..b3ebad8c 100644 --- a/packages/rsnap-overlay/src/overlay/tests.rs +++ b/packages/rsnap-overlay/src/overlay/tests.rs @@ -655,12 +655,26 @@ fn configured_session_with_macos_worker() -> (OverlaySession, u64) { (session, worker_debug_id) } +#[cfg(target_os = "macos")] +fn fresh_live_stream_snapshot_captured_at() -> Instant { + // Bias "fresh snapshot" fixtures away from nextest scheduling jitter so these tests assert + // display-first semantics, not whether the process lost >90ms of CPU between two lines. + Instant::now() + Duration::from_secs(1) +} + +#[cfg(target_os = "macos")] +fn stale_live_stream_snapshot_captured_at() -> Instant { + Instant::now() + - crate::live_frame_stream_macos::STREAM_REGION_FRAME_MAX_AGE + - Duration::from_millis(1) +} + #[cfg(target_os = "macos")] #[test] fn sync_live_surface_bg_from_stream_promotes_clean_live_snapshot() { let monitor = test_monitor(); let stream = MacLiveFrameStream::new(); - let captured_at = Instant::now(); + let captured_at = fresh_live_stream_snapshot_captured_at(); let mut session = OverlaySession::new(); stream.debug_set_active_stream_generation(monitor.id, 1); diff --git a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs index 0f847812..fce87f38 100644 --- a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs @@ -177,13 +177,10 @@ fn native_capture_input_ready_routes_toolbar_pointer_left_without_window_id() { session.toolbar_state.visible = true; session.toolbar_pointer_local = Some(Pos2::new(12.0, 10.0)); - session - .native_capture_input_queue - .lock() - .expect("native capture input queue") - .push_back(MacOSNativeCaptureInputEvent::ToolbarPointerLeft); - - assert!(matches!(session.handle_native_capture_input_ready(), OverlayControl::Continue)); + assert!(matches!( + session.handle_native_capture_input_event(MacOSNativeCaptureInputEvent::ToolbarPointerLeft), + OverlayControl::Continue + )); assert_eq!(session.toolbar_pointer_local, None); } @@ -201,9 +198,8 @@ fn native_capture_input_ready_routes_keyboard_input_to_frozen_text_edit() { session.toolbar_state.selected_tool = FrozenToolbarTool::Text; assert!(session.begin_frozen_text_edit_at(monitor, GlobalPoint::new(140, 160))); - - session.native_capture_input_queue.lock().expect("native capture input queue").push_back( - MacOSNativeCaptureInputEvent::KeyboardInput { + assert!(matches!( + session.handle_native_capture_input_event(MacOSNativeCaptureInputEvent::KeyboardInput { monitor: Some(monitor), event: OverlayKeyboardInputEvent { logical_key: Key::Character(String::from("A").into()), @@ -211,10 +207,9 @@ fn native_capture_input_ready_routes_keyboard_input_to_frozen_text_edit() { state: ElementState::Pressed, repeat: false, }, - }, - ); - - assert!(matches!(session.handle_native_capture_input_ready(), OverlayControl::Continue)); + }), + OverlayControl::Continue + )); assert_eq!(session.frozen_text_edit.as_ref().map(|edit| edit.text.as_str()), Some("A")); } @@ -232,15 +227,13 @@ fn native_capture_input_ready_routes_ime_preedit_to_frozen_text_edit() { session.toolbar_state.selected_tool = FrozenToolbarTool::Text; assert!(session.begin_frozen_text_edit_at(monitor, GlobalPoint::new(140, 160))); - - session.native_capture_input_queue.lock().expect("native capture input queue").push_back( - MacOSNativeCaptureInputEvent::Ime { + assert!(matches!( + session.handle_native_capture_input_event(MacOSNativeCaptureInputEvent::Ime { monitor: Some(monitor), event: Ime::Preedit(String::from("汉"), Some((0, 0))), - }, - ); - - assert!(matches!(session.handle_native_capture_input_ready(), OverlayControl::Continue)); + }), + OverlayControl::Continue + )); assert_eq!( session.frozen_text_edit.as_ref().and_then(|edit| edit.ime_preedit.as_deref()), Some("汉") @@ -259,8 +252,8 @@ fn native_capture_input_ready_routes_scroll_capture_escape_without_winit_window_ session.scroll_capture.active = true; - session.native_capture_input_queue.lock().expect("native capture input queue").push_back( - MacOSNativeCaptureInputEvent::KeyboardInput { + assert!(matches!( + session.handle_native_capture_input_event(MacOSNativeCaptureInputEvent::KeyboardInput { monitor: Some(monitor), event: OverlayKeyboardInputEvent { logical_key: Key::Named(NamedKey::Escape), @@ -268,11 +261,7 @@ fn native_capture_input_ready_routes_scroll_capture_escape_without_winit_window_ state: ElementState::Pressed, repeat: false, }, - }, - ); - - assert!(matches!( - session.handle_native_capture_input_ready(), + }), OverlayControl::Exit(OverlayExit::Cancelled) )); } @@ -1216,7 +1205,7 @@ fn begin_live_capture_press_force_refreshes_existing_live_snapshot() { let stream = MacLiveFrameStream::new(); let mut session = OverlaySession::new(); - stream.debug_store_test_snapshot(monitor, Instant::now()); + stream.debug_store_test_snapshot(monitor, tests::fresh_live_stream_snapshot_captured_at()); session.live_sample_stream = Some(stream); diff --git a/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs b/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs index cb49d1e8..b76b07c2 100644 --- a/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs +++ b/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs @@ -1,8 +1,6 @@ use std::slice; #[cfg(target_os = "macos")] use std::sync::Arc; -#[cfg(target_os = "macos")] -use std::time::Instant; use egui::Id; use egui::LayerId; @@ -106,7 +104,7 @@ fn snapshot_background_capture_finishes_frozen_transition_immediately() { let capture_rect = RectPoints::new(120, 160, 320, 240); let frozen_image = tests::test_frozen_image(); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now(), + captured_at: tests::fresh_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(frozen_image.clone()), @@ -147,7 +145,7 @@ fn snapshot_matte_window_capture_keeps_authoritative_handoff_pending() { let monitor = tests::test_monitor(); let capture_rect = RectPoints::new(120, 160, 320, 240); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now(), + captured_at: tests::fresh_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(tests::test_frozen_image()), @@ -184,9 +182,7 @@ fn stale_snapshot_does_not_finish_frozen_transition_immediately() { let monitor = tests::test_monitor(); let capture_rect = RectPoints::new(120, 160, 320, 240); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now() - - crate::live_frame_stream_macos::STREAM_REGION_FRAME_MAX_AGE - - Duration::from_millis(1), + captured_at: tests::stale_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(tests::test_frozen_image()), @@ -222,7 +218,7 @@ fn snapshot_seeded_preview_keeps_authoritative_handoff_pending() { let capture_rect = RectPoints::new(120, 160, 320, 240); let frozen_image = tests::test_frozen_image(); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now(), + captured_at: tests::fresh_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(frozen_image.clone()), @@ -253,7 +249,7 @@ fn snapshot_seeded_preview_makes_toolbar_eligible_before_final_capture_ready() { let monitor = tests::test_monitor(); let capture_rect = RectPoints::new(120, 160, 320, 240); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now(), + captured_at: tests::fresh_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(tests::test_frozen_image()), @@ -285,9 +281,7 @@ fn stale_snapshot_does_not_seed_frozen_preview() { let monitor = tests::test_monitor(); let capture_rect = RectPoints::new(120, 160, 320, 240); let snapshot = Arc::new(MonitorImageSnapshot { - captured_at: Instant::now() - - crate::live_frame_stream_macos::STREAM_REGION_FRAME_MAX_AGE - - Duration::from_millis(1), + captured_at: tests::stale_live_stream_snapshot_captured_at(), stream_generation: 1, monitor, image: Arc::new(tests::test_frozen_image()), diff --git a/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs index 0fecc98f..59f4b8c0 100644 --- a/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs @@ -270,7 +270,7 @@ fn pending_frozen_handoff_keeps_live_mode_and_hover_until_first_display_image_ar monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); let _ = session.about_to_wait(); @@ -297,7 +297,7 @@ fn begin_frozen_capture_background_snapshot_finishes_without_hiding_overlay_wind monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -337,7 +337,7 @@ fn live_snapshot_followup_can_finish_background_capture_before_timeout() { monitor, 51, 2, - Instant::now() + Duration::from_millis(1), + tests::fresh_live_stream_snapshot_captured_at(), ); let _ = session.about_to_wait(); @@ -370,7 +370,7 @@ fn background_capture_prefers_live_surface_bg_for_initial_display_handoff() { monitor, 2, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -415,7 +415,7 @@ fn background_capture_followup_export_does_not_overwrite_live_surface_preview() monitor, 4, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); let _ = session.about_to_wait(); @@ -443,7 +443,7 @@ fn window_matte_capture_seeds_preview_and_arms_worker_without_hiding_overlay_win monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -482,7 +482,7 @@ fn window_matte_capture_dispatches_worker_without_hiding_overlay_windows() { monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -520,7 +520,7 @@ fn window_matte_capture_miss_keeps_preview_and_surfaces_export_error() { monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -578,7 +578,7 @@ fn stale_window_matte_response_after_export_failure_is_ignored() { monitor, 1, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); session.begin_frozen_capture_with_rect( monitor, @@ -698,7 +698,7 @@ fn background_capture_without_initial_snapshot_waits_for_live_stream_followup_wi monitor, 2, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); let _ = session.about_to_wait(); @@ -727,7 +727,7 @@ fn incomplete_self_capture_filter_blocks_display_first_snapshot_handoff() { monitor, 2, 1, - Instant::now(), + tests::fresh_live_stream_snapshot_captured_at(), ); let _ = session.about_to_wait(); diff --git a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs index e6596585..b3fd6914 100644 --- a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs @@ -45,10 +45,6 @@ impl OverlaySession { } else { self.last_toolbar_window_move_at = Instant::now(); } - - #[cfg(target_os = "macos")] - let _ = self.sync_native_capture_shells(); - if changed { self.request_redraw_toolbar_window(); } @@ -299,9 +295,6 @@ impl OverlaySession { let _ = self.update_toolbar_outer_position(drag_monitor, desired_local); self.force_apply_pending_toolbar_window_move(); - - #[cfg(target_os = "macos")] - let _ = self.sync_native_capture_shells(); } #[cfg(target_os = "macos")] if !manual_toolbar_drag && self.toolbar_state.dragging { @@ -511,9 +504,6 @@ impl OverlaySession { } self.toolbar_window_warmup_redraws_remaining = 0; self.last_present_at = Instant::now(); - - #[cfg(target_os = "macos")] - let _ = self.sync_native_capture_shells(); } pub(super) fn draw_toolbar_window_frame( @@ -638,16 +628,6 @@ impl OverlaySession { self.toolbar_window_warmup_redraws_remaining = TOOLBAR_WINDOW_WARMUP_REDRAWS; toolbar_became_visible = true; } - #[cfg(target_os = "macos")] - if toolbar_became_visible && self.preserve_frontmost_on_next_toolbar_show { - self.preserve_frontmost_on_next_toolbar_show = false; - - self.restore_recorded_frontmost_application_for_focus_preservation( - "toolbar_first_show", - ); - } - - let _ = self.sync_native_capture_shells(); Some(toolbar_became_visible) } diff --git a/packages/rsnap-overlay/src/overlay/window_runtime.rs b/packages/rsnap-overlay/src/overlay/window_runtime.rs index 9a6ecafe..20440b5f 100644 --- a/packages/rsnap-overlay/src/overlay/window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/window_runtime.rs @@ -34,8 +34,6 @@ impl OverlaySession { let reset_started_at = Instant::now(); self.reset_for_start(); - #[cfg(target_os = "macos")] - self.capture_frontmost_application_for_exit_restore(); let reset_ms = reset_started_at.elapsed().as_millis(); let worker_setup_ms = self.setup_startup_worker(); @@ -89,8 +87,6 @@ impl OverlaySession { self.session_active = true; - #[cfg(target_os = "macos")] - self.ensure_native_capture_shells()?; self.request_redraw_all(); let request_redraw_ms = request_redraw_started_at.elapsed().as_millis(); @@ -508,8 +504,6 @@ impl OverlaySession { #[cfg(target_os = "macos")] let startup_aux_window_waker = self.startup_aux_window_waker.clone(); #[cfg(target_os = "macos")] - let native_capture_input_waker = self.native_capture_input_waker.clone(); - #[cfg(target_os = "macos")] let external_scroll_input_drain_reader = self.scroll_capture.external_scroll_input_drain_reader.clone(); @@ -525,7 +519,6 @@ impl OverlaySession { self.scroll_capture_starting_hook = scroll_capture_starting_hook; self.scroll_capture_started_hook = scroll_capture_started_hook; self.startup_aux_window_waker = startup_aux_window_waker; - self.native_capture_input_waker = native_capture_input_waker; self.pending_startup_aux_live_stream_filter_upgrade = false; self.scroll_capture.external_scroll_input_drain_reader = external_scroll_input_drain_reader; diff --git a/packages/rsnap-overlay/src/scroll_capture/support.rs b/packages/rsnap-overlay/src/scroll_capture/support.rs index 7d45b7a8..2d3e59f7 100644 --- a/packages/rsnap-overlay/src/scroll_capture/support.rs +++ b/packages/rsnap-overlay/src/scroll_capture/support.rs @@ -65,7 +65,7 @@ pub(crate) fn scroll_capture_fingerprint_delta(left: &[u8], right: &[u8]) -> u32 comparisons = comparisons.saturating_add(4); } - if comparisons == 0 { u32::MAX } else { (total_abs_diff / comparisons) as u32 } + total_abs_diff.checked_div(comparisons).map_or(u32::MAX, |mean_abs_diff| mean_abs_diff as u32) } #[cfg(test)]