diff --git a/packages/rsnap-overlay/src/overlay.rs b/packages/rsnap-overlay/src/overlay.rs index 0229273a..fb2df6ba 100644 --- a/packages/rsnap-overlay/src/overlay.rs +++ b/packages/rsnap-overlay/src/overlay.rs @@ -5389,25 +5389,49 @@ impl OverlaySession { window_id: WindowId, position: PhysicalPosition, ) -> OverlayControl { - let should_trace_frozen_selection_drag_timing = - self.should_trace_frozen_selection_drag_timing(); - let cursor_move_started_at = should_trace_frozen_selection_drag_timing.then(Instant::now); let old_monitor = self.active_cursor_monitor(); - let now = Instant::now(); let Some(overlay_window) = self.windows.get(&window_id) else { return self.handle_cursor_moved_without_overlay_window(window_id, old_monitor); }; let window_monitor = overlay_window.monitor; let scale_factor = overlay_window.window.scale_factor(); let window_size = overlay_window.window.inner_size(); - // Clamp to overlay window bounds and map to monitor coordinates. - let max_local_x = ((window_size.width as f64) / scale_factor).max(1.0) as i32 - 1; - let max_local_y = ((window_size.height as f64) / scale_factor).max(1.0) as i32 - 1; - let local_x = (position.x / scale_factor).round() as i32; - let local_y = (position.y / scale_factor).round() as i32; - let event_global = GlobalPoint::new( - window_monitor.origin.x + local_x.clamp(0, max_local_x), - window_monitor.origin.y + local_y.clamp(0, max_local_y), + + self.handle_cursor_moved_with_overlay_window( + window_id, + position, + old_monitor, + window_monitor, + scale_factor, + window_size, + ) + } + + fn handle_cursor_moved_with_overlay_window( + &mut self, + window_id: WindowId, + position: PhysicalPosition, + old_monitor: Option, + window_monitor: MonitorRect, + scale_factor: f64, + window_size: PhysicalSize, + ) -> OverlayControl { + let should_trace_frozen_selection_drag_timing = + self.should_trace_frozen_selection_drag_timing(); + let cursor_move_started_at = should_trace_frozen_selection_drag_timing.then(Instant::now); + let now = Instant::now(); + let event_global = Self::overlay_window_event_global_position( + window_monitor, + scale_factor, + window_size, + position, + ); + let frozen_selection_drag_global = self.frozen_selection_drag_cursor_move_global( + window_monitor, + scale_factor, + window_size, + position, + event_global, ); let monitor = window_monitor; let global = event_global; @@ -5435,9 +5459,9 @@ impl OverlaySession { should_trace_frozen_selection_drag_timing, cursor_move_started_at, old_monitor, - old_cursor, monitor, global, + frozen_selection_drag_global, ); if should_trace_frozen_selection_drag_timing { @@ -5450,6 +5474,65 @@ impl OverlaySession { OverlayControl::Continue } + fn overlay_window_event_global_position( + window_monitor: MonitorRect, + scale_factor: f64, + window_size: PhysicalSize, + position: PhysicalPosition, + ) -> GlobalPoint { + let scale_factor = scale_factor.max(f64::MIN_POSITIVE); + let logical_width = ((window_size.width as f64) / scale_factor).max(1.0); + let logical_height = ((window_size.height as f64) / scale_factor).max(1.0); + let max_local_x = logical_width as i32 - 1; + let max_local_y = logical_height as i32 - 1; + let local_x = (position.x / scale_factor).round() as i32; + let local_y = (position.y / scale_factor).round() as i32; + + GlobalPoint::new( + window_monitor.origin.x + local_x.clamp(0, max_local_x), + window_monitor.origin.y + local_y.clamp(0, max_local_y), + ) + } + + fn frozen_selection_drag_cursor_move_global( + &self, + window_monitor: MonitorRect, + scale_factor: f64, + window_size: PhysicalSize, + position: PhysicalPosition, + default_global: GlobalPoint, + ) -> GlobalPoint { + if !matches!(self.state.mode, OverlayMode::Frozen) || !self.frozen_selection_drag.active { + return default_global; + } + + Self::overlay_window_frozen_selection_drag_global_position( + window_monitor, + scale_factor, + window_size, + position, + ) + } + + fn overlay_window_frozen_selection_drag_global_position( + window_monitor: MonitorRect, + scale_factor: f64, + window_size: PhysicalSize, + position: PhysicalPosition, + ) -> GlobalPoint { + let scale_factor = scale_factor.max(f64::MIN_POSITIVE); + let logical_width = ((window_size.width as f64) / scale_factor).max(1.0); + let logical_height = ((window_size.height as f64) / scale_factor).max(1.0); + let max_local_x = logical_width.ceil() as i32 - 1; + let max_local_y = logical_height.ceil() as i32 - 1; + // Frozen selection dragging should treat the event as covering the current logical cell so + // the final fractional trackpad move cannot nudge the rect lower-right on release. + let local_x = ((position.x / scale_factor).floor() as i32).clamp(0, max_local_x); + let local_y = ((position.y / scale_factor).floor() as i32).clamp(0, max_local_y); + + GlobalPoint::new(window_monitor.origin.x + local_x, window_monitor.origin.y + local_y) + } + fn handle_cursor_moved_without_overlay_window( &mut self, window_id: WindowId, @@ -5489,9 +5572,9 @@ impl OverlaySession { should_trace_frozen_selection_drag_timing, cursor_move_started_at, old_monitor, - old_cursor, monitor, global, + global, ); if should_trace_frozen_selection_drag_timing { @@ -5509,10 +5592,11 @@ impl OverlaySession { should_trace_frozen_selection_drag_timing: bool, cursor_move_started_at: Option, old_monitor: Option, - old_cursor: Option, monitor: MonitorRect, global: GlobalPoint, + frozen_selection_drag_global: GlobalPoint, ) -> FrozenSelectionDragCursorMoveTiming { + let old_cursor = self.state.cursor; let cursor_update_elapsed = Self::measure_duration_if(should_trace_frozen_selection_drag_timing, || { self.update_cursor_for_live_move(old_monitor, old_cursor, monitor, global) @@ -5525,14 +5609,16 @@ impl OverlaySession { let (frozen_rect_changed, frozen_drag_update_elapsed) = if should_trace_frozen_selection_drag_timing { let frozen_drag_update_started_at = Instant::now(); - let frozen_rect_changed = self.update_frozen_selection_drag_rect(global); + let frozen_rect_changed = + self.update_frozen_selection_drag_rect(frozen_selection_drag_global); self.update_frozen_mosaic_drag_rect(global); self.update_frozen_text_edit_drag_anchor(global); (frozen_rect_changed, Some(frozen_drag_update_started_at.elapsed())) } else { - let frozen_rect_changed = self.update_frozen_selection_drag_rect(global); + let frozen_rect_changed = + self.update_frozen_selection_drag_rect(frozen_selection_drag_global); self.update_frozen_mosaic_drag_rect(global); self.update_frozen_text_edit_drag_anchor(global); diff --git a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs index 07908dff..af4b5efd 100644 --- a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs @@ -1,9 +1,15 @@ use image::RgbaImage; +#[cfg(target_os = "macos")] +use winit::dpi::{PhysicalPosition, PhysicalSize}; +#[cfg(target_os = "macos")] +use winit::event::ElementState; #[cfg(target_os = "macos")] use crate::live_frame_stream_macos::MacLiveFrameStream; #[cfg(target_os = "macos")] use crate::overlay::DeviceCursorPointSource; +#[cfg(target_os = "macos")] +use crate::overlay::FrozenCaptureSource; use crate::overlay::OverlayControl; #[allow(unused_imports)] use crate::overlay::tests::{ @@ -673,6 +679,86 @@ fn resolve_device_cursor_point_keeps_direct_points_when_they_match_recent_cursor ); } +#[cfg(target_os = "macos")] +#[test] +fn overlay_window_event_global_position_rounds_fractional_scaled_positions() { + let monitor = tests::test_monitor_with_scale(1_000, 800, 2_000); + let window_size = PhysicalSize::new(2_000, 1_600); + + assert_eq!( + OverlaySession::overlay_window_event_global_position( + monitor, + 2.0, + window_size, + PhysicalPosition::new(301.9, 361.9), + ), + GlobalPoint::new(151, 181) + ); + assert_eq!( + OverlaySession::overlay_window_event_global_position( + monitor, + 2.0, + window_size, + PhysicalPosition::new(1_999.9, 1_599.9), + ), + GlobalPoint::new(999, 799) + ); +} + +#[cfg(target_os = "macos")] +#[test] +fn frozen_selection_drag_uses_non_rounding_cursor_move_updates_without_shifting_cursor_context() { + let monitor = tests::test_monitor_with_scale(1_000, 800, 2_000); + let window_id = WindowId::from(1); + let position = PhysicalPosition::new(601.9, 721.9); + let window_size = PhysicalSize::new(2_000, 1_600); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let event_global = + OverlaySession::overlay_window_event_global_position(monitor, 2.0, window_size, position); + let frozen_drag_global = OverlaySession::overlay_window_frozen_selection_drag_global_position( + monitor, + 2.0, + window_size, + position, + ); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.monitor = Some(monitor); + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + + assert!(session.begin_frozen_selection_drag(GlobalPoint::new(150, 180))); + assert_eq!(event_global, GlobalPoint::new(301, 361)); + assert_eq!(frozen_drag_global, GlobalPoint::new(300, 360)); + assert!(matches!( + session.handle_cursor_moved_with_overlay_window( + window_id, + position, + Some(monitor), + monitor, + 2.0, + window_size, + ), + OverlayControl::Continue + )); + assert_eq!(session.last_event_cursor, Some((monitor, event_global))); + assert!(session.last_event_cursor_at.is_some()); + assert_eq!(session.cursor_monitor, Some(monitor)); + assert_eq!(session.state.cursor, Some(event_global)); + assert_eq!(session.state.frozen_capture_rect, Some(RectPoints::new(250, 300, 200, 240))); + assert!(matches!( + session.handle_frozen_left_mouse_input(monitor, ElementState::Released), + OverlayControl::Continue + )); + assert_eq!(session.last_event_cursor, Some((monitor, event_global))); + assert_eq!(session.state.cursor, Some(event_global)); + assert_eq!(session.state.frozen_capture_rect, Some(RectPoints::new(250, 300, 200, 240))); + assert_eq!(session.frozen_selection_drag, crate::overlay::FrozenSelectionDragState::default()); +} + #[cfg(target_os = "macos")] #[test] fn startup_live_rgb_plan_keeps_focus_independent_from_seed_monitor() {