diff --git a/packages/rsnap-overlay/src/overlay.rs b/packages/rsnap-overlay/src/overlay.rs index b37dd990..8d81d797 100644 --- a/packages/rsnap-overlay/src/overlay.rs +++ b/packages/rsnap-overlay/src/overlay.rs @@ -147,9 +147,9 @@ use self::rendering::{ #[cfg(all(target_os = "macos", test))] use self::session_state::InflightScrollCaptureObservation; use self::session_state::{ - CursorMoveTrace, FrozenSelectionDragState, FrozenToolbarPointerState, FrozenToolbarState, - HudDrawConfig, LiveSampleApplyResult, ScrollCaptureState, SlowOperationLogger, - WindowFreezeCaptureTarget, + CursorMoveTrace, FrozenSelectionDragCursorMoveTiming, FrozenSelectionDragState, + FrozenToolbarPointerState, FrozenToolbarState, HudDrawConfig, LiveSampleApplyResult, + ScrollCaptureState, SlowOperationLogger, WindowFreezeCaptureTarget, }; #[cfg(target_os = "macos")] use self::session_state::{ @@ -754,10 +754,12 @@ pub struct OverlaySession { pending_loupe_outer_pos: Option, loupe_inner_size_points: Option<(u32, u32)>, toolbar_outer_pos: Option, + pending_toolbar_outer_pos: Option, toolbar_inner_size_points: Option<(u32, u32)>, gpu: Option, last_hud_window_move_at: Instant, last_loupe_window_move_at: Instant, + last_toolbar_window_move_at: Instant, last_present_at: Instant, last_live_cursor_poll_at: Instant, last_frozen_cursor_poll_at: Instant, @@ -824,6 +826,7 @@ pub struct OverlaySession { frozen_selection_drag: FrozenSelectionDragState, hud_window_visible: bool, toolbar_window_visible: bool, + skip_toolbar_focus_on_next_show: bool, toolbar_window_warmup_redraws_remaining: u8, loupe_window_visible: bool, loupe_window_warmup_redraws_remaining: u8, @@ -963,11 +966,12 @@ impl OverlaySession { macos_hud_window_config_cache: HashMap::new(), hud_outer_pos: None, pending_hud_outer_pos: None, hud_inner_size_points: None, loupe_outer_pos: None, pending_loupe_outer_pos: None, loupe_inner_size_points: None, - toolbar_outer_pos: None, + toolbar_outer_pos: None, pending_toolbar_outer_pos: None, toolbar_inner_size_points: None, gpu: None, last_hud_window_move_at: Instant::now(), last_loupe_window_move_at: Instant::now(), + last_toolbar_window_move_at: Instant::now(), last_present_at: Instant::now(), last_live_cursor_poll_at: Instant::now(), last_frozen_cursor_poll_at: Instant::now(), @@ -1022,7 +1026,8 @@ impl OverlaySession { toolbar_pointer_local: None, left_mouse_button_down: false, left_mouse_button_down_monitor: None, left_mouse_button_down_global: None, frozen_selection_drag: FrozenSelectionDragState::default(), - hud_window_visible: false, toolbar_window_visible: false, toolbar_window_warmup_redraws_remaining: 0, + hud_window_visible: false, toolbar_window_visible: false, + skip_toolbar_focus_on_next_show: false, toolbar_window_warmup_redraws_remaining: 0, loupe_window_visible: false, loupe_window_warmup_redraws_remaining: 0, scroll_capture: ScrollCaptureState::default(), @@ -1050,6 +1055,7 @@ impl OverlaySession { self.state = runtime.state; self.last_hud_window_move_at = runtime.now; self.last_loupe_window_move_at = runtime.now; + self.last_toolbar_window_move_at = runtime.now; self.last_present_at = runtime.now; self.last_live_cursor_poll_at = runtime.now - CURSOR_POLL_INTERVAL_MIN; self.last_frozen_cursor_poll_at = runtime.now - CURSOR_POLL_INTERVAL_MIN; @@ -1676,6 +1682,8 @@ impl OverlaySession { press_cursor_y: cursor_y, }; + self.hide_auxiliary_windows_for_frozen_selection_drag(); + true } @@ -1716,7 +1724,7 @@ impl OverlaySession { FrozenSelectionInteractionKind::Resize(corner) => { Self::frozen_selection_resize_cursor_icon(corner) }, - FrozenSelectionInteractionKind::Move => CursorIcon::Default, + FrozenSelectionInteractionKind::Move => CursorIcon::Grabbing, }; } @@ -1731,7 +1739,8 @@ impl OverlaySession { Some(FrozenSelectionInteractionKind::Resize(corner)) => { Self::frozen_selection_resize_cursor_icon(corner) }, - _ => CursorIcon::Default, + Some(FrozenSelectionInteractionKind::Move) => CursorIcon::Grab, + None => CursorIcon::Default, } } @@ -1750,8 +1759,48 @@ impl OverlaySession { } } + pub(super) fn frozen_selection_drag_hides_auxiliary_windows(&self) -> bool { + matches!(self.state.mode, OverlayMode::Frozen) && self.frozen_selection_drag.active + } + + fn hide_auxiliary_windows_for_frozen_selection_drag(&mut self) { + if let Some(hud_window) = self.hud_window.as_ref() { + hud_window.window.set_visible(false); + } + + self.hud_window_visible = false; + + if let Some(loupe_window) = self.loupe_window.as_ref() { + loupe_window.window.set_visible(false); + } + + self.loupe_window_visible = false; + + self.reset_loupe_window_warmup_redraws(); + + if let Some(toolbar_window) = self.toolbar_window.as_ref() { + toolbar_window.window.set_visible(false); + } + + self.skip_toolbar_focus_on_next_show = true; + self.toolbar_window_visible = false; + self.toolbar_window_warmup_redraws_remaining = 0; + + if let Some(preview_window) = self.scroll_preview_window.as_ref() { + preview_window.window.set_visible(false); + } + + self.last_present_at = Instant::now(); + } + fn stop_frozen_selection_drag(&mut self) { + let was_active = self.frozen_selection_drag.active; + self.frozen_selection_drag = FrozenSelectionDragState::default(); + + if was_active && let Some(monitor) = self.state.monitor { + self.request_redraw_for_monitor(monitor); + } } fn update_frozen_selection_drag_rect(&mut self, global: GlobalPoint) -> bool { @@ -1928,11 +1977,57 @@ impl OverlaySession { self.toolbar_state.default_slot_position = Some(toolbar_default_pos); self.toolbar_state.floating_position = Some(toolbar_pos); - let _ = self.update_toolbar_outer_position(monitor, toolbar_pos); + let should_trace_frozen_selection_drag_timing = + self.should_trace_frozen_selection_drag_timing(); + let toolbar_position_elapsed = if should_trace_frozen_selection_drag_timing { + let toolbar_position_started_at = Instant::now(); + let _ = self.update_toolbar_outer_position(monitor, toolbar_pos); - self.request_redraw_for_monitor(monitor); - self.request_redraw_toolbar_window(); - self.request_redraw_scroll_preview_window(); + Some(toolbar_position_started_at.elapsed()) + } else { + let _ = self.update_toolbar_outer_position(monitor, toolbar_pos); + + None + }; + let redraw_request_elapsed = if should_trace_frozen_selection_drag_timing { + let redraw_request_started_at = Instant::now(); + + self.request_redraw_for_monitor(monitor); + self.request_redraw_toolbar_window(); + + if self.scroll_capture.active { + self.request_redraw_scroll_preview_window(); + } + + Some(redraw_request_started_at.elapsed()) + } else { + self.request_redraw_for_monitor(monitor); + self.request_redraw_toolbar_window(); + + if self.scroll_capture.active { + self.request_redraw_scroll_preview_window(); + } + + None + }; + + if should_trace_frozen_selection_drag_timing { + tracing::trace!( + op = "overlay.frozen_selection_drag.rect_update", + monitor_id = monitor.id, + rect_x = next_rect.x, + rect_y = next_rect.y, + rect_width = next_rect.width, + rect_height = next_rect.height, + toolbar_position_us = + toolbar_position_elapsed.map_or(0, |elapsed| elapsed.as_micros()), + redraw_request_us = redraw_request_elapsed.map_or(0, |elapsed| elapsed.as_micros()), + scroll_capture_active = self.scroll_capture.active, + toolbar_outer_pos = ?self.toolbar_outer_pos, + toolbar_floating_position = ?self.toolbar_state.floating_position, + "Applied frozen selection rect update." + ); + } true } @@ -3004,6 +3099,9 @@ 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 { @@ -3042,28 +3140,18 @@ impl OverlaySession { }; self.trace_cursor_moved_with_mapping(trace); - self.update_cursor_for_live_move(old_monitor, old_cursor, monitor, global); - - let previous_drag_rect = self.state.drag_rect; - - self.update_live_drag_rect(monitor, global); - self.update_frozen_selection_drag_rect(global); - self.sync_overlay_cursor_icons(); - self.request_cursor_move_samples(monitor, global); - if let Some(old_monitor) = old_monitor - && old_monitor != monitor - { - self.request_redraw_for_monitor(old_monitor); - } - - if Self::live_overlay_redraw_needed_for_cursor_update( + let timing = self.run_cursor_move_updates( + should_trace_frozen_selection_drag_timing, + cursor_move_started_at, old_monitor, + old_cursor, monitor, - previous_drag_rect, - self.state.drag_rect, - ) { - self.request_redraw_for_monitor(monitor); + global, + ); + + if should_trace_frozen_selection_drag_timing { + self.trace_frozen_selection_drag_cursor_move(monitor, old_monitor, old_cursor, timing); } OverlayControl::Continue @@ -3074,6 +3162,10 @@ impl OverlaySession { window_id: WindowId, old_monitor: Option, ) -> 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); + if self.should_ignore_live_auxiliary_cursor_event(window_id) { return OverlayControl::Continue; } @@ -3100,14 +3192,57 @@ impl OverlaySession { ); } - self.update_cursor_for_live_move(old_monitor, old_cursor, monitor, global); + let timing = self.run_cursor_move_updates( + should_trace_frozen_selection_drag_timing, + cursor_move_started_at, + old_monitor, + old_cursor, + monitor, + global, + ); - let previous_drag_rect = self.state.drag_rect; + if should_trace_frozen_selection_drag_timing { + self.trace_frozen_selection_drag_cursor_move(monitor, old_monitor, old_cursor, timing); + } - self.update_live_drag_rect(monitor, global); - self.update_frozen_selection_drag_rect(global); - self.sync_overlay_cursor_icons(); - self.request_cursor_move_samples(monitor, global); + OverlayControl::Continue + } + + fn run_cursor_move_updates( + &mut self, + should_trace_frozen_selection_drag_timing: bool, + cursor_move_started_at: Option, + old_monitor: Option, + old_cursor: Option, + monitor: MonitorRect, + global: GlobalPoint, + ) -> FrozenSelectionDragCursorMoveTiming { + 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) + }); + let previous_drag_rect = self.state.drag_rect; + let live_drag_update_elapsed = + Self::measure_duration_if(should_trace_frozen_selection_drag_timing, || { + self.update_live_drag_rect(monitor, global); + }); + 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); + + (frozen_rect_changed, Some(frozen_drag_update_started_at.elapsed())) + } else { + (self.update_frozen_selection_drag_rect(global), None) + }; + let sync_cursor_icons_elapsed = + Self::measure_duration_if(should_trace_frozen_selection_drag_timing, || { + self.sync_overlay_cursor_icons(); + }); + let request_samples_elapsed = + Self::measure_duration_if(should_trace_frozen_selection_drag_timing, || { + self.request_cursor_move_samples(monitor, global); + }); if let Some(old_monitor) = old_monitor && old_monitor != monitor @@ -3124,7 +3259,30 @@ impl OverlaySession { self.request_redraw_for_monitor(monitor); } - OverlayControl::Continue + FrozenSelectionDragCursorMoveTiming { + cursor_update_elapsed: cursor_update_elapsed.unwrap_or_default(), + live_drag_update_elapsed: live_drag_update_elapsed.unwrap_or_default(), + frozen_drag_update_elapsed: frozen_drag_update_elapsed.unwrap_or_default(), + frozen_rect_changed, + sync_cursor_icons_elapsed: sync_cursor_icons_elapsed.unwrap_or_default(), + request_samples_elapsed: request_samples_elapsed.unwrap_or_default(), + total_elapsed: cursor_move_started_at + .map_or(Duration::ZERO, |started_at| started_at.elapsed()), + } + } + + fn measure_duration_if(enabled: bool, operation: impl FnOnce()) -> Option { + if enabled { + let started_at = Instant::now(); + + operation(); + + Some(started_at.elapsed()) + } else { + operation(); + + None + } } fn should_ignore_live_auxiliary_cursor_event(&self, window_id: WindowId) -> bool { @@ -3181,6 +3339,42 @@ impl OverlaySession { ); } + fn trace_frozen_selection_drag_cursor_move( + &self, + monitor: MonitorRect, + old_monitor: Option, + old_cursor: Option, + timing: FrozenSelectionDragCursorMoveTiming, + ) { + if !self.should_trace_frozen_selection_drag_timing() { + return; + } + + tracing::trace!( + op = "overlay.frozen_selection_drag.cursor_move_timing", + monitor_id = monitor.id, + old_monitor_id = old_monitor.map(|target| target.id), + old_cursor = ?old_cursor, + cursor = ?self.state.cursor, + interaction = ?self.frozen_selection_drag.interaction, + frozen_rect_changed = timing.frozen_rect_changed, + cursor_update_us = timing.cursor_update_elapsed.as_micros(), + live_drag_update_us = timing.live_drag_update_elapsed.as_micros(), + frozen_drag_update_us = timing.frozen_drag_update_elapsed.as_micros(), + sync_cursor_icons_us = timing.sync_cursor_icons_elapsed.as_micros(), + request_samples_us = timing.request_samples_elapsed.as_micros(), + total_us = timing.total_elapsed.as_micros(), + overlay_window_count = self.windows.len(), + "Frozen selection drag cursor move timing." + ); + } + + fn should_trace_frozen_selection_drag_timing(&self) -> bool { + tracing::enabled!(tracing::Level::TRACE) + && matches!(self.state.mode, OverlayMode::Frozen) + && self.frozen_selection_drag.active + } + fn update_cursor_for_live_move( &mut self, old_monitor: Option, @@ -5341,8 +5535,10 @@ impl OverlaySession { self.scroll_preview_window = None; self.toolbar_inner_size_points = None; self.toolbar_outer_pos = None; + self.pending_toolbar_outer_pos = None; self.hud_window_visible = false; self.toolbar_window_visible = false; + self.skip_toolbar_focus_on_next_show = false; self.toolbar_window_warmup_redraws_remaining = 0; self.loupe_window_visible = false; self.loupe_window_warmup_redraws_remaining = 0; diff --git a/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs b/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs index 73757c00..1c1b886b 100644 --- a/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs @@ -302,6 +302,7 @@ impl OverlaySession { self.maybe_apply_pending_hud_window_move(now); self.maybe_apply_pending_loupe_window_move(now); + self.maybe_apply_pending_toolbar_window_move(now); } pub(super) fn maybe_apply_pending_hud_window_move(&mut self, now: Instant) { @@ -354,6 +355,7 @@ impl OverlaySession { pub(super) fn force_apply_pending_hud_and_loupe_moves(&mut self) { self.force_apply_pending_hud_window_move(); self.force_apply_pending_loupe_window_move(); + self.force_apply_pending_toolbar_window_move(); } pub(super) fn maybe_apply_pending_loupe_window_move(&mut self, now: Instant) { @@ -410,6 +412,65 @@ impl OverlaySession { self.last_loupe_window_move_at = now; } + pub(super) fn maybe_apply_pending_toolbar_window_move(&mut self, now: Instant) { + self.apply_pending_toolbar_window_move(now, false); + } + + pub(super) fn force_apply_pending_toolbar_window_move(&mut self) { + self.apply_pending_toolbar_window_move(Instant::now(), true); + } + + pub(super) fn apply_pending_toolbar_window_move(&mut self, now: Instant, force: bool) { + let Some(desired) = self.pending_toolbar_outer_pos else { + return; + }; + + if self.frozen_selection_drag_hides_auxiliary_windows() { + return; + } + + let elapsed = now.duration_since(self.last_toolbar_window_move_at); + let interval = self + .repaint_interval_for_monitor(self.state.monitor.or(self.active_cursor_monitor())) + .max(HUD_LOUPE_MOVE_INTERVAL_MIN); + + if !force && elapsed < interval { + let delay = interval.saturating_sub(elapsed); + + self.schedule_egui_repaint_after(delay); + + return; + } + + let Some(toolbar_window) = self.toolbar_window.as_ref() else { + return; + }; + let started_at = Instant::now(); + + toolbar_window + .window + .set_outer_position(LogicalPosition::new(desired.x as f64, desired.y as f64)); + + let elapsed = started_at.elapsed(); + + self.slow_op_logger.warn_if_slow( + "overlay.toolbar_window_set_outer_position", + elapsed, + SLOW_OP_WARN_OUTER_POSITION, + || { + format!( + "window_id={:?} pos=({}, {})", + toolbar_window.window.id(), + desired.x, + desired.y + ) + }, + ); + + self.pending_toolbar_outer_pos = None; + self.last_toolbar_window_move_at = now; + } + pub(super) fn schedule_egui_repaint_after(&self, delay: Duration) { let deadline = Instant::now() + delay; let mut next_repaint = diff --git a/packages/rsnap-overlay/src/overlay/hud_runtime.rs b/packages/rsnap-overlay/src/overlay/hud_runtime.rs index 3d79e921..7795ea17 100644 --- a/packages/rsnap-overlay/src/overlay/hud_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/hud_runtime.rs @@ -33,6 +33,18 @@ impl OverlaySession { } pub(super) fn maybe_skip_hud_redraw(&mut self) -> Option { + if self.frozen_selection_drag_hides_auxiliary_windows() { + if let Some(hud_window) = self.hud_window.as_ref() + && self.hud_window_visible + { + hud_window.window.set_visible(false); + } + + self.hud_window_visible = false; + self.last_present_at = Instant::now(); + + return Some(OverlayControl::Continue); + } if self.scroll_capture.active { if let Some(hud_window) = self.hud_window.as_ref() && self.hud_window_visible @@ -327,7 +339,8 @@ impl OverlaySession { } pub(super) fn should_skip_loupe_redraw(&self) -> bool { - self.scroll_capture.active + self.frozen_selection_drag_hides_auxiliary_windows() + || self.scroll_capture.active || self.capture_windows_hidden || !self.state.alt_held || (matches!(self.state.mode, OverlayMode::Live) && self.live_loupe_uses_hud_window()) diff --git a/packages/rsnap-overlay/src/overlay/scroll_preview_runtime.rs b/packages/rsnap-overlay/src/overlay/scroll_preview_runtime.rs index d1cfcecc..79e3bb27 100644 --- a/packages/rsnap-overlay/src/overlay/scroll_preview_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/scroll_preview_runtime.rs @@ -142,16 +142,19 @@ impl OverlaySession { } pub(super) fn handle_scroll_preview_redraw_requested(&mut self) -> OverlayControl { + let should_hide_preview = self.should_hide_scroll_preview_window(); let Some(preview_window) = self.scroll_preview_window.as_mut() else { return OverlayControl::Continue; }; - if !self.scroll_capture.active { + if should_hide_preview { preview_window.window.set_visible(false); return OverlayControl::Continue; } + preview_window.window.set_visible(true); + let theme = hud_helpers::effective_hud_theme(self.config.theme_mode, preview_window.window.theme()); let view = ScrollPreviewView { paused: self.scroll_capture.paused, theme }; @@ -165,6 +168,10 @@ impl OverlaySession { } } + pub(super) fn should_hide_scroll_preview_window(&self) -> bool { + self.frozen_selection_drag_hides_auxiliary_windows() || !self.scroll_capture.active + } + #[cfg(target_os = "macos")] pub(super) fn position_scroll_preview_window(&self, monitor: MonitorRect) { let Some(preview_window) = self.scroll_preview_window.as_ref() else { diff --git a/packages/rsnap-overlay/src/overlay/session_state.rs b/packages/rsnap-overlay/src/overlay/session_state.rs index 7fba85a1..33be83f4 100644 --- a/packages/rsnap-overlay/src/overlay/session_state.rs +++ b/packages/rsnap-overlay/src/overlay/session_state.rs @@ -118,6 +118,17 @@ pub(super) struct CursorMoveTrace { pub(super) source: DeviceCursorPointSource, } +#[derive(Clone, Copy)] +pub(super) struct FrozenSelectionDragCursorMoveTiming { + pub(super) cursor_update_elapsed: Duration, + pub(super) live_drag_update_elapsed: Duration, + pub(super) frozen_drag_update_elapsed: Duration, + pub(super) frozen_rect_changed: bool, + pub(super) sync_cursor_icons_elapsed: Duration, + pub(super) request_samples_elapsed: Duration, + pub(super) total_elapsed: Duration, +} + #[derive(Clone, Copy, Debug)] pub(super) struct HudDrawConfig { pub(super) can_draw_hud: bool, diff --git a/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs b/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs index 7b800d52..5ae47814 100644 --- a/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs +++ b/packages/rsnap-overlay/src/overlay/tests/rendering_behaviors.rs @@ -8,10 +8,10 @@ use winit::window::CursorIcon; use crate::OverlayControl; #[allow(unused_imports)] use crate::overlay::tests::{ - self, ElementState, FrozenCaptureSource, FrozenSelectionDragState, FrozenToolbarState, - FrozenToolbarTool, GlobalPoint, HUD_LOUPE_STRIP_GAP_POINTS, HudTheme, MonitorRect, - MonitorRectPoints, MouseButton, OverlayMode, OverlaySession, OverlayState, PngAction, Pos2, - RawInput, Rect, RectPoints, Rgba, SELECTION_DASHED_BORDER_DASH_LENGTH_PX, + self, Duration, ElementState, FrozenCaptureSource, FrozenSelectionDragState, + FrozenToolbarState, FrozenToolbarTool, GlobalPoint, HUD_LOUPE_STRIP_GAP_POINTS, HudTheme, + MonitorRect, MonitorRectPoints, MouseButton, OverlayMode, OverlaySession, OverlayState, + PngAction, Pos2, RawInput, Rect, RectPoints, Rgba, SELECTION_DASHED_BORDER_DASH_LENGTH_PX, SELECTION_DASHED_BORDER_GAP_LENGTH_PX, SELECTION_DASHED_BORDER_WIDTH_PX, SELECTION_SIZE_BADGE_GAP_PX, SELECTION_SIZE_BADGE_INSIDE_MARGIN_PX, SELECTION_SIZE_BADGE_SCREEN_MARGIN_PX, ScrollSession, SelectionDashedBorderCache, @@ -221,6 +221,114 @@ fn frozen_selection_drag_updates_capture_rect_and_toolbar_position() { assert_eq!(session.toolbar_state.floating_position, Some(expected_toolbar_pos)); } +#[test] +fn frozen_selection_drag_hides_auxiliary_windows_while_active() { + let monitor = tests::test_monitor(); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + session.toolbar_state.visible = true; + session.hud_window_visible = true; + session.loupe_window_visible = true; + session.toolbar_window_visible = true; + + assert!(!session.frozen_selection_drag_hides_auxiliary_windows()); + assert!(!session.should_hide_toolbar_window(monitor)); + assert!(session.should_hide_scroll_preview_window()); + assert!(session.begin_frozen_selection_drag(GlobalPoint::new(150, 180))); + assert!(session.frozen_selection_drag_hides_auxiliary_windows()); + assert!(!session.hud_window_visible); + assert!(!session.loupe_window_visible); + assert!(!session.toolbar_window_visible); + assert!(session.skip_toolbar_focus_on_next_show); + assert!(session.should_hide_toolbar_window(monitor)); + assert!(!session.should_focus_frozen_toolbar_window_on_show()); + + session.stop_frozen_selection_drag(); + + assert!(!session.frozen_selection_drag_hides_auxiliary_windows()); + assert!(!session.should_hide_toolbar_window(monitor)); + assert!(!session.should_focus_frozen_toolbar_window_on_show()); +} + +#[test] +fn frozen_selection_drag_releases_scroll_preview_hide_after_drag_stops() { + let monitor = tests::test_monitor(); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + + assert!(session.begin_frozen_selection_drag(GlobalPoint::new(150, 180))); + + session.scroll_capture.active = true; + + assert!(session.should_hide_scroll_preview_window()); + + session.stop_frozen_selection_drag(); + + assert!(!session.should_hide_scroll_preview_window()); +} + +#[test] +fn frozen_selection_drag_defers_pending_toolbar_window_move() { + let monitor = tests::test_monitor(); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + + assert!(session.begin_frozen_selection_drag(GlobalPoint::new(150, 180))); + + let last_move_at = session.last_toolbar_window_move_at; + + session.pending_toolbar_outer_pos = Some(GlobalPoint::new(220, 260)); + + session.maybe_apply_pending_toolbar_window_move(last_move_at + Duration::from_millis(32)); + + assert_eq!(session.pending_toolbar_outer_pos, Some(GlobalPoint::new(220, 260))); + assert_eq!(session.last_toolbar_window_move_at, last_move_at); +} + +#[test] +fn frozen_selection_drag_skips_toolbar_focus_even_before_first_show() { + let monitor = tests::test_monitor(); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + session.toolbar_state.visible = true; + + assert!(!session.toolbar_window_visible); + assert!(!session.skip_toolbar_focus_on_next_show); + assert!(session.should_focus_frozen_toolbar_window_on_show()); + assert!(session.begin_frozen_selection_drag(GlobalPoint::new(150, 180))); + assert!(session.skip_toolbar_focus_on_next_show); + assert!(!session.should_focus_frozen_toolbar_window_on_show()); + + session.stop_frozen_selection_drag(); + + assert!(session.skip_toolbar_focus_on_next_show); + assert!(!session.should_focus_frozen_toolbar_window_on_show()); +} + #[test] fn frozen_selection_resize_updates_capture_rect_and_toolbar_position() { let monitor = tests::test_monitor(); @@ -246,6 +354,19 @@ fn frozen_selection_resize_updates_capture_rect_and_toolbar_position() { assert_eq!(session.toolbar_state.floating_position, Some(expected_toolbar_pos)); } +#[test] +fn toolbar_position_update_queues_pending_move_without_window() { + let monitor = tests::test_monitor(); + let mut session = OverlaySession::new(); + + session.toolbar_inner_size_points = Some((460, 54)); + + assert!(session.update_toolbar_outer_position(monitor, Pos2::new(120.0, 160.0))); + assert_eq!(session.toolbar_state.floating_position, Some(Pos2::new(120.0, 160.0))); + assert_eq!(session.toolbar_outer_pos, Some(GlobalPoint::new(120, 160))); + assert_eq!(session.pending_toolbar_outer_pos, Some(GlobalPoint::new(120, 160))); +} + #[test] fn frozen_selection_resize_preserves_handle_press_offset() { let monitor = tests::test_monitor(); @@ -438,7 +559,7 @@ fn frozen_selection_cursor_icon_uses_corner_resize_hover() { session.state.cursor = Some(GlobalPoint::new(150, 180)); - assert_eq!(session.frozen_selection_cursor_icon_for_monitor(monitor), CursorIcon::Default); + assert_eq!(session.frozen_selection_cursor_icon_for_monitor(monitor), CursorIcon::Grab); } #[test] @@ -465,6 +586,30 @@ fn frozen_selection_cursor_icon_tracks_active_resize_drag() { assert_eq!(session.frozen_selection_cursor_icon_for_monitor(monitor), CursorIcon::SeResize); } +#[test] +fn frozen_selection_cursor_icon_tracks_active_move_drag() { + let monitor = tests::test_monitor(); + let capture_rect = RectPoints::new(100, 120, 200, 240); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + session.state.finish_freeze(monitor, tests::test_frozen_image()); + + session.state.frozen_capture_rect = Some(capture_rect); + session.frozen_capture_source = FrozenCaptureSource::DragRegion; + session.frozen_selection_drag = FrozenSelectionDragState { + active: true, + interaction: FrozenSelectionInteractionKind::Move, + anchor_rect: capture_rect, + pointer_offset_x: 50, + pointer_offset_y: 60, + press_cursor_x: 150, + press_cursor_y: 180, + }; + + assert_eq!(session.frozen_selection_cursor_icon_for_monitor(monitor), CursorIcon::Grabbing); +} + #[test] fn frozen_toolbar_default_position_centers_on_capture_rect_midpoint() { let monitor = tests::test_monitor_with_scale(400, 300, 2_000); diff --git a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs index d2aa57bf..a5c4a22e 100644 --- a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs @@ -160,12 +160,21 @@ impl OverlaySession { } pub(super) fn should_hide_toolbar_window(&self, monitor: MonitorRect) -> bool { - !matches!(self.state.mode, OverlayMode::Frozen) + self.frozen_selection_drag_hides_auxiliary_windows() + || !matches!(self.state.mode, OverlayMode::Frozen) || !self.toolbar_state.visible || self.state.frozen_image.is_none() || self.state.monitor != Some(monitor) } + #[cfg(any(target_os = "macos", test))] + pub(super) fn should_focus_frozen_toolbar_window_on_show(&self) -> bool { + !self.toolbar_window_visible + && !self.skip_toolbar_focus_on_next_show + && matches!(self.state.mode, OverlayMode::Frozen) + && !self.scroll_capture.active + } + pub(super) fn set_toolbar_window_hidden(&mut self) { if let Some(toolbar_window) = self.toolbar_window.as_ref() { toolbar_window.window.set_visible(false); @@ -202,9 +211,7 @@ impl OverlaySession { } #[cfg(target_os = "macos")] { - let should_focus_frozen_keyboard = !self.toolbar_window_visible - && matches!(self.state.mode, OverlayMode::Frozen) - && !self.scroll_capture.active; + let should_focus_frozen_keyboard = self.should_focus_frozen_toolbar_window_on_show(); if !self.toolbar_window_visible { self.maybe_apply_pending_startup_aux_live_stream_filter_upgrade(monitor); @@ -221,6 +228,7 @@ impl OverlaySession { if !self.toolbar_window_visible { self.toolbar_window_visible = true; + self.skip_toolbar_focus_on_next_show = false; self.toolbar_window_warmup_redraws_remaining = TOOLBAR_WINDOW_WARMUP_REDRAWS; } if should_focus_frozen_keyboard { @@ -291,6 +299,8 @@ impl OverlaySession { } pub(super) fn handle_toolbar_window_redraw_requested(&mut self) -> OverlayControl { + let redraw_started_at = Instant::now(); + self.event_loop_last_progress_window_id = self.toolbar_window.as_ref().map(|toolbar_window| toolbar_window.window.id()); self.event_loop_last_progress_monitor_id = self.state.monitor.map(|monitor| monitor.id); @@ -303,6 +313,7 @@ impl OverlaySession { }; let toolbar_input = self.toolbar_pointer_state(monitor, self.toolbar_pointer_local); let should_hide_toolbar_window = self.should_hide_toolbar_window(monitor); + let mut position_update_elapsed = None; if should_hide_toolbar_window { self.set_toolbar_window_hidden(); @@ -310,14 +321,23 @@ impl OverlaySession { return OverlayControl::Continue; } + let draw_frame_started_at = Instant::now(); + if let Err(err) = self.draw_toolbar_window_frame(monitor, toolbar_input) { return self.exit(OverlayExit::Error(format!("{err:#}"))); } + let draw_frame_elapsed = draw_frame_started_at.elapsed(); + self.update_scroll_toolbar_default_position(monitor); if let Some(toolbar_pos) = self.toolbar_state.floating_position { + let position_update_started_at = Instant::now(); let _ = self.update_toolbar_outer_position(monitor, toolbar_pos); + + self.force_apply_pending_toolbar_window_move(); + + position_update_elapsed = Some(position_update_started_at.elapsed()); } if let Some(action) = self.toolbar_state.pending_action.take() { let control = self.handle_toolbar_action(action); @@ -334,6 +354,21 @@ impl OverlaySession { self.request_redraw_toolbar_window(); } + if tracing::enabled!(tracing::Level::TRACE) + && matches!(self.state.mode, OverlayMode::Frozen) + { + tracing::trace!( + op = "overlay.toolbar_redraw_phase_timing", + monitor_id = monitor.id, + total_us = redraw_started_at.elapsed().as_micros(), + draw_frame_us = draw_frame_elapsed.as_micros(), + position_update_us = + position_update_elapsed.map_or(0, |elapsed| elapsed.as_micros()), + hidden = should_hide_toolbar_window, + frozen_selection_drag_active = self.frozen_selection_drag.active, + "Toolbar redraw phase timing." + ); + } OverlayControl::Continue } diff --git a/packages/rsnap-overlay/src/overlay/window_position_runtime.rs b/packages/rsnap-overlay/src/overlay/window_position_runtime.rs index 506ecd2c..3b47675e 100644 --- a/packages/rsnap-overlay/src/overlay/window_position_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/window_position_runtime.rs @@ -1,8 +1,7 @@ #[allow(unused_imports)] use crate::overlay::{ - GlobalPoint, HUD_LOUPE_STRIP_GAP_POINTS, Instant, LogicalPosition, MonitorRect, OverlayMode, - OverlaySession, Pos2, Rect, SLOW_OP_WARN_OUTER_POSITION, TOOLBAR_SCREEN_MARGIN_PX, Vec2, - WindowRenderer, + GlobalPoint, HUD_LOUPE_STRIP_GAP_POINTS, MonitorRect, OverlayMode, OverlaySession, Pos2, Rect, + TOOLBAR_SCREEN_MARGIN_PX, Vec2, WindowRenderer, }; impl OverlaySession { @@ -196,18 +195,17 @@ impl OverlaySession { monitor: MonitorRect, local_pos: Pos2, ) -> bool { - let Some(toolbar_window) = self.toolbar_window.as_ref() else { - return false; - }; - let toolbar_scale = toolbar_window.window.scale_factor().max(1.0); let toolbar_size = if let Some((width, height)) = self.toolbar_inner_size_points { Vec2::new(width as f32, height as f32) - } else { + } else if let Some(toolbar_window) = self.toolbar_window.as_ref() { + let toolbar_scale = toolbar_window.window.scale_factor().max(1.0); let size = toolbar_window.window.inner_size(); let toolbar_w = ((size.width as f64) / toolbar_scale).ceil().max(1.0) as f32; let toolbar_h = ((size.height as f64) / toolbar_scale).ceil().max(1.0) as f32; Vec2::new(toolbar_w, toolbar_h) + } else { + WindowRenderer::frozen_toolbar_size(&self.toolbar_state) }; let screen_rect = Rect::from_min_size(Pos2::ZERO, Vec2::new(monitor.width as f32, monitor.height as f32)); @@ -228,28 +226,9 @@ impl OverlaySession { } self.toolbar_outer_pos = Some(desired); + self.pending_toolbar_outer_pos = Some(desired); self.toolbar_state.floating_position = Some(clamped_local_pos); - let started_at = Instant::now(); - - toolbar_window - .window - .set_outer_position(LogicalPosition::new(desired.x as f64, desired.y as f64)); - self.slow_op_logger.warn_if_slow( - "overlay.toolbar_window_set_outer_position", - started_at.elapsed(), - SLOW_OP_WARN_OUTER_POSITION, - || { - format!( - "window_id={:?} pos=({}, {})", - toolbar_window.window.id(), - desired.x, - desired.y - ) - }, - ); - toolbar_window.window.request_redraw(); - true } } diff --git a/packages/rsnap-overlay/src/overlay/window_runtime.rs b/packages/rsnap-overlay/src/overlay/window_runtime.rs index 3da37d76..46d24b50 100644 --- a/packages/rsnap-overlay/src/overlay/window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/window_runtime.rs @@ -823,48 +823,96 @@ impl OverlaySession { } } - if let Some(hud) = self.hud_window.as_ref() { + let hide_auxiliary_windows = self.frozen_selection_drag_hides_auxiliary_windows(); + let request_hud_window = !hide_auxiliary_windows && self.hud_window.is_some(); + let request_loupe_window = !hide_auxiliary_windows && self.loupe_window.is_some(); + let request_toolbar_window = !hide_auxiliary_windows + && cfg!(target_os = "macos") + && matches!(self.state.mode, OverlayMode::Frozen) + && self.toolbar_state.visible + && self.state.monitor == Some(monitor) + && self.state.frozen_image.is_some(); + let request_scroll_preview_window = + !hide_auxiliary_windows && self.scroll_preview_window.is_some(); + + if tracing::enabled!(tracing::Level::TRACE) + && matches!(self.state.mode, OverlayMode::Frozen) + && self.frozen_selection_drag.active + && self.state.monitor == Some(monitor) + { + let overlay_windows = + self.windows.values().filter(|window| window.monitor == monitor).count(); + + tracing::trace!( + op = "overlay.frozen_selection_drag.redraw_fanout", + monitor_id = monitor.id, + overlay_window_count = overlay_windows, + request_hud_window, + request_loupe_window, + request_toolbar_window, + request_scroll_preview_window, + hide_auxiliary_windows, + scroll_capture_active = self.scroll_capture.active, + alt_held = self.state.alt_held, + "Requested redraw fan-out for frozen selection drag." + ); + } + if hide_auxiliary_windows { + return; + } + if request_hud_window && let Some(hud) = self.hud_window.as_ref() { hud.window.request_redraw(); } - if let Some(loupe) = self.loupe_window.as_ref() { + if request_loupe_window && let Some(loupe) = self.loupe_window.as_ref() { loupe.window.request_redraw(); } - // macOS uses a native toolbar popup window with compositor blur; keep shader-viewport // toolbar redraw on the fullscreen overlay path disabled for this platform. // Future direction: if toolbar styling moves off native blur, add a dedicated capture // pass feeding a toolbar-local shader-blur texture. - if cfg!(target_os = "macos") - && matches!(self.state.mode, OverlayMode::Frozen) - && self.toolbar_state.visible - && self.state.monitor == Some(monitor) - && self.state.frozen_image.is_some() - { + if request_toolbar_window { self.request_redraw_toolbar_window(); } - - self.request_redraw_scroll_preview_window(); + if request_scroll_preview_window { + self.request_redraw_scroll_preview_window(); + } } pub(super) fn request_redraw_hud_window(&self) { + if self.frozen_selection_drag_hides_auxiliary_windows() { + return; + } + if let Some(hud) = self.hud_window.as_ref() { hud.window.request_redraw(); } } pub(super) fn request_redraw_toolbar_window(&self) { + if self.frozen_selection_drag_hides_auxiliary_windows() { + return; + } + if let Some(toolbar) = self.toolbar_window.as_ref() { toolbar.window.request_redraw(); } } pub(super) fn request_redraw_loupe_window(&self) { + if self.frozen_selection_drag_hides_auxiliary_windows() { + return; + } + if let Some(loupe) = self.loupe_window.as_ref() { loupe.window.request_redraw(); } } pub(super) fn request_redraw_scroll_preview_window(&self) { + if self.frozen_selection_drag_hides_auxiliary_windows() { + return; + } + if let Some(preview) = self.scroll_preview_window.as_ref() { preview.window.request_redraw(); }