Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion apps/rsnap/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use color_eyre::eyre::Result;
#[cfg(target_os = "macos")]
use global_hotkey::Error;
#[cfg(target_os = "macos")]
use global_hotkey::hotkey::Code;
use global_hotkey::hotkey::{Code, Modifiers};
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, hotkey::HotKey};
#[cfg(target_os = "macos")]
use objc2::rc::Retained;
Expand All @@ -41,6 +41,8 @@ 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;

pub(crate) enum UserEvent {
Expand All @@ -55,6 +57,8 @@ pub(crate) enum UserEvent {
OverlayScrollInput,
#[cfg(target_os = "macos")]
OverlayWorkerResponse,
#[cfg(target_os = "macos")]
OverlayNativeCaptureInput,
}

#[cfg(target_os = "macos")]
Expand All @@ -78,6 +82,28 @@ impl OverlayHotkeyRegistrationState {
}
}

#[cfg(target_os = "macos")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct OverlayFrozenHotkeyBinding {
action: FrozenGlobalHotkey,
hotkey: HotKey,
hotkey_id: u32,
registration_state: OverlayHotkeyRegistrationState,
label: &'static str,
}
#[cfg(target_os = "macos")]
impl OverlayFrozenHotkeyBinding {
fn new(action: FrozenGlobalHotkey, hotkey: HotKey, label: &'static str) -> Self {
Self {
action,
hotkey_id: hotkey.id(),
hotkey,
registration_state: OverlayHotkeyRegistrationState::Unregistered,
label,
}
}
}

struct App {
capture_hotkey: HotKey,
capture_hotkey_id: u32,
Expand All @@ -96,6 +122,8 @@ struct App {
overlay_loupe_hotkey_id: u32,
#[cfg(target_os = "macos")]
overlay_loupe_hotkey_registration_state: OverlayHotkeyRegistrationState,
#[cfg(target_os = "macos")]
overlay_frozen_hotkeys: Vec<OverlayFrozenHotkeyBinding>,
capture_hotkey_recording_suspended: bool,
tray_icon: Option<TrayIcon>,
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -128,6 +156,8 @@ struct App {
#[cfg(target_os = "macos")]
overlay_stream_event_pending: Arc<AtomicBool>,
#[cfg(target_os = "macos")]
overlay_native_capture_input_event_pending: Arc<AtomicBool>,
#[cfg(target_os = "macos")]
latest_deferred_ocr_generation: Arc<AtomicU64>,
#[cfg(target_os = "macos")]
pending_deferred_ocr_generation: Arc<AtomicU64>,
Expand All @@ -151,6 +181,37 @@ impl App {
HotKey::new(None, Code::Tab)
}

#[cfg(target_os = "macos")]
fn overlay_frozen_hotkeys() -> Vec<OverlayFrozenHotkeyBinding> {
vec![
OverlayFrozenHotkeyBinding::new(
FrozenGlobalHotkey::Copy,
HotKey::new(None, Code::Space),
"Space",
),
OverlayFrozenHotkeyBinding::new(
FrozenGlobalHotkey::AutoCenter,
HotKey::new(None, Code::KeyC),
"C",
),
OverlayFrozenHotkeyBinding::new(
FrozenGlobalHotkey::ToggleToolbar,
HotKey::new(None, Code::KeyH),
"H",
),
OverlayFrozenHotkeyBinding::new(
FrozenGlobalHotkey::StartScrollCapture,
HotKey::new(None, Code::KeyS),
"S",
),
OverlayFrozenHotkeyBinding::new(
FrozenGlobalHotkey::Save,
HotKey::new(Some(Modifiers::SUPER), Code::KeyS),
"Cmd+S",
),
]
}

#[allow(clippy::too_many_arguments)]
fn new(
capture_hotkey: HotKey,
Expand Down Expand Up @@ -180,6 +241,8 @@ impl App {
overlay_loupe_hotkey_id: Self::overlay_loupe_hotkey().id(),
#[cfg(target_os = "macos")]
overlay_loupe_hotkey_registration_state: OverlayHotkeyRegistrationState::Unregistered,
#[cfg(target_os = "macos")]
overlay_frozen_hotkeys: Self::overlay_frozen_hotkeys(),
capture_hotkey_recording_suspended: false,
_hotkey_manager: hotkey_manager,
tray_icon: None,
Expand Down Expand Up @@ -213,6 +276,8 @@ 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)),
Expand Down Expand Up @@ -312,6 +377,8 @@ mod tests {
#[cfg(target_os = "macos")]
use crate::app::OverlayHotkeyRegistrationState;
use crate::app::{self, SettingsWindowEntry};
#[cfg(target_os = "macos")]
use rsnap_overlay::FrozenGlobalHotkey;

#[test]
fn startup_permission_check_uses_permissions_entry() {
Expand Down Expand Up @@ -352,6 +419,24 @@ mod tests {
assert_eq!(hotkey.mods, Modifiers::empty());
}

#[cfg(target_os = "macos")]
#[test]
fn overlay_frozen_hotkeys_cover_copy_center_toolbar_scroll_and_save() {
let bindings = app::App::overlay_frozen_hotkeys();

assert_eq!(bindings.len(), 5);
assert!(bindings.iter().any(|binding| {
binding.action == FrozenGlobalHotkey::Copy
&& binding.hotkey.key == Code::Space
&& binding.hotkey.mods == Modifiers::empty()
}));
assert!(bindings.iter().any(|binding| {
binding.action == FrozenGlobalHotkey::Save
&& binding.hotkey.key == Code::KeyS
&& binding.hotkey.mods == Modifiers::SUPER
}));
}

#[cfg(target_os = "macos")]
#[test]
fn blocked_overlay_hotkey_registration_skips_retries_until_reset() {
Expand Down
66 changes: 66 additions & 0 deletions apps/rsnap/src/app/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use winit::event_loop::ActiveEventLoop;

use crate::app::App;
#[cfg(target_os = "macos")]
use crate::app::OverlayFrozenHotkeyBinding;
#[cfg(target_os = "macos")]
use crate::app::OverlayHotkeyRegistrationState;
#[cfg(target_os = "macos")]
use crate::app::UserEvent;
Expand Down Expand Up @@ -195,6 +197,20 @@ impl App {
}
}

#[cfg(target_os = "macos")]
fn overlay_frozen_hotkey_spec(binding: &OverlayFrozenHotkeyBinding) -> OverlayHotkeySpec {
OverlayHotkeySpec {
hotkey: binding.hotkey,
hotkey_id: binding.hotkey_id,
hotkey_label: binding.label,
missing_manager_message: "Frozen overlay hotkeys are unavailable because the global hotkey manager is missing.",
register_failure_message: "Failed to register a frozen overlay hotkey.",
register_success_message: "Registered a frozen overlay hotkey.",
unregister_failure_message: "Failed to unregister a frozen overlay hotkey.",
unregister_success_message: "Unregistered a frozen overlay hotkey.",
}
}

#[cfg(target_os = "macos")]
fn register_overlay_cancel_hotkey(&mut self) {
let spec = self.overlay_cancel_hotkey_spec();
Expand Down Expand Up @@ -227,12 +243,37 @@ impl App {
self.unregister_overlay_hotkey(spec, self.overlay_loupe_hotkey_registration_state);
}

#[cfg(target_os = "macos")]
fn register_overlay_frozen_hotkeys(&mut self) {
for index in 0..self.overlay_frozen_hotkeys.len() {
let binding = self.overlay_frozen_hotkeys[index];
let spec = Self::overlay_frozen_hotkey_spec(&binding);
let registration_state = self.register_overlay_hotkey(spec, binding.registration_state);

self.overlay_frozen_hotkeys[index].registration_state = registration_state;
}
}

#[cfg(target_os = "macos")]
fn unregister_overlay_frozen_hotkeys(&mut self) {
for index in 0..self.overlay_frozen_hotkeys.len() {
let binding = self.overlay_frozen_hotkeys[index];
let spec = Self::overlay_frozen_hotkey_spec(&binding);
let registration_state =
self.unregister_overlay_hotkey(spec, binding.registration_state);

self.overlay_frozen_hotkeys[index].registration_state = registration_state;
}
}

#[cfg(target_os = "macos")]
fn sync_overlay_hotkey_registrations(&mut self) {
let should_register_cancel =
self.overlay_session.as_ref().is_some_and(OverlaySession::wants_global_cancel_hotkey);
let should_register_loupe =
self.overlay_session.as_ref().is_some_and(OverlaySession::wants_global_loupe_hotkey);
let should_register_frozen =
self.overlay_session.as_ref().is_some_and(OverlaySession::wants_global_frozen_hotkeys);

if should_register_cancel {
self.register_overlay_cancel_hotkey();
Expand All @@ -244,6 +285,11 @@ impl App {
} else {
self.unregister_overlay_loupe_hotkey();
}
if should_register_frozen {
self.register_overlay_frozen_hotkeys();
} else {
self.unregister_overlay_frozen_hotkeys();
}
}

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -366,6 +412,7 @@ 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);
}
Expand Down Expand Up @@ -603,6 +650,22 @@ 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);

Expand Down Expand Up @@ -650,10 +713,13 @@ impl App {
return;
};

#[cfg(target_os = "macos")]
self.overlay_native_capture_input_event_pending.store(false, Ordering::Release);
#[cfg(target_os = "macos")]
{
self.unregister_overlay_cancel_hotkey();
Comment thread
yvette-carlisle marked this conversation as resolved.
self.unregister_overlay_loupe_hotkey();
self.unregister_overlay_frozen_hotkeys();
}

#[cfg(target_os = "macos")]
Expand Down
12 changes: 11 additions & 1 deletion apps/rsnap/src/app/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::VecDeque;
#[cfg(target_os = "macos")]
use std::sync::Arc;
use std::sync::{Arc, atomic::Ordering};
use std::time::{Duration, Instant};

use color_eyre::eyre;
Expand Down Expand Up @@ -78,6 +78,16 @@ impl ApplicationHandler<UserEvent> for App {
if let Some(session) = self.overlay_session.as_mut() {
let control = session.handle_worker_response_ready();

self.handle_overlay_control(control);
}
},
#[cfg(target_os = "macos")]
UserEvent::OverlayNativeCaptureInput => {
self.overlay_native_capture_input_event_pending.store(false, Ordering::Release);

if let Some(session) = self.overlay_session.as_mut() {
let control = session.handle_native_capture_input_ready();

self.handle_overlay_control(control);
}
},
Expand Down
19 changes: 19 additions & 0 deletions apps/rsnap/src/app/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,25 @@ impl App {

return;
}
#[cfg(target_os = "macos")]
if self.overlay_session.is_some()
&& let Some(binding) =
self.overlay_frozen_hotkeys.iter().find(|binding| binding.hotkey_id == event.id())
{
tracing::info!(
hotkey = binding.label,
action = ?binding.action,
"Capture frozen action requested from hotkey."
);

if let Some(session) = self.overlay_session.as_mut() {
let control = session.handle_global_frozen_hotkey(binding.action);

self.handle_overlay_control(control);
}

return;
}
if event.id() == self.capture_hotkey_id {
tracing::info!(
hotkey = %self.capture_key_label(),
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ Then keep the body descriptive:
directories
- `docs/reference/live-sampling.md` for the stream-first live RGB and loupe sampling path
- `docs/reference/window-hit-testing.md` for live-mode hovered-window targeting strategy
- `docs/reference/macos-native-capture-window-layer.md` for the passive AppKit shell and
explicit key-focus shell boundary on macOS
Loading
Loading