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
11 changes: 0 additions & 11 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,6 @@ pub async fn main() {
#[cfg(target_os = "macos")]
hypr_intercept::setup_force_quit_handler();

#[cfg(target_os = "macos")]
{
let handle = app.handle().clone();
hypr_intercept::set_close_handler(move || {
for (_, window) in handle.webview_windows() {
let _ = window.close();
}
let _ = handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
});
}

#[allow(unused_variables)]
app.run(move |app, event| match event {
#[cfg(target_os = "macos")]
Expand Down
21 changes: 1 addition & 20 deletions crates/intercept/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#[cfg(target_os = "macos")]
use std::sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};

#[cfg(target_os = "macos")]
use swift_rs::swift;
Expand All @@ -22,9 +19,6 @@ static HANDLER_INITIALIZED: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "macos")]
static FORCE_QUIT: AtomicBool = AtomicBool::new(false);

#[cfg(target_os = "macos")]
static CLOSE_HANDLER: OnceLock<Box<dyn Fn() + Send + Sync>> = OnceLock::new();

#[cfg(target_os = "macos")]
pub fn setup_force_quit_handler() {
if !HANDLER_INITIALIZED.swap(true, Ordering::SeqCst) {
Expand All @@ -34,11 +28,6 @@ pub fn setup_force_quit_handler() {
}
}

#[cfg(target_os = "macos")]
pub fn set_close_handler(f: impl Fn() + Send + Sync + 'static) {
let _ = CLOSE_HANDLER.set(Box::new(f));
}

#[cfg(target_os = "macos")]
pub fn should_force_quit() -> bool {
FORCE_QUIT.load(Ordering::SeqCst)
Expand Down Expand Up @@ -68,11 +57,3 @@ pub fn demo_quit_progress() {
pub extern "C" fn rust_set_force_quit() {
FORCE_QUIT.store(true, Ordering::SeqCst);
}

#[unsafe(no_mangle)]
#[cfg(target_os = "macos")]
pub extern "C" fn rust_perform_close() {
if let Some(handler) = CLOSE_HANDLER.get() {
handler();
}
}
8 changes: 1 addition & 7 deletions crates/intercept/swift-lib/src/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import Cocoa
@_silgen_name("rust_set_force_quit")
func rustSetForceQuit()

@_silgen_name("rust_perform_close")
func rustPerformClose()

@_cdecl("_setup_force_quit_handler")
public func _setupForceQuitHandler() {
QuitInterceptor.shared.setup()
Expand All @@ -21,9 +18,6 @@ public func _showQuitOverlay() {
@_cdecl("_demo_quit_progress")
public func _demoQuitProgress() {
DispatchQueue.main.async {
let interceptor = QuitInterceptor.shared
interceptor.showOverlay()
interceptor.setHoldingAppearance()
interceptor.startProgressAnimation()
QuitInterceptor.shared.showOverlay()
}
}
75 changes: 3 additions & 72 deletions crates/intercept/swift-lib/src/QuitInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@ final class QuitInterceptor {
case idle
case firstPress
case awaiting
case holding
}

var keyMonitor: Any?
var panel: NSPanel?
var progressLayer: CALayer?
var pressLabel: NSTextField?
var holdLabel: NSTextField?
var messageLabel: NSTextField?
var state: State = .idle
var dismissTimer: DispatchWorkItem?
var holdThresholdTimer: DispatchWorkItem?
var quitTimer: DispatchWorkItem?

// MARK: - Setup

Expand All @@ -45,44 +40,28 @@ final class QuitInterceptor {
// MARK: - Actions

func performQuit() {
resetProgress()
setDefaultAppearance()
rustSetForceQuit()
hidePanel()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NSApplication.shared.terminate(nil)
}
}

func performClose() {
hidePanel()
rustPerformClose()
}

// MARK: - State Machine

func onCmdQPressed() {
switch state {
case .idle:
state = .firstPress
showOverlay()
scheduleTimer(&holdThresholdTimer, delay: QuitOverlay.holdThreshold) { [weak self] in
guard let self, self.state == .firstPress else { return }
self.state = .holding
self.setHoldingAppearance()
self.startProgressAnimation()
self.scheduleTimer(&self.quitTimer, delay: QuitOverlay.holdDuration) { [weak self] in
self?.performQuit()
}
}

case .firstPress, .holding:
case .firstPress:
break

case .awaiting:
state = .idle
cancelTimer(&dismissTimer)
performClose()
performQuit()
}
}

Expand All @@ -93,18 +72,6 @@ final class QuitInterceptor {

case .firstPress:
state = .awaiting
cancelTimer(&holdThresholdTimer)
scheduleTimer(&dismissTimer, delay: QuitOverlay.overlayDuration) { [weak self] in
guard let self, self.state == .awaiting else { return }
self.state = .idle
self.hidePanel()
}

case .holding:
state = .awaiting
cancelTimer(&quitTimer)
resetProgress()
setDefaultAppearance()
scheduleTimer(&dismissTimer, delay: QuitOverlay.overlayDuration) { [weak self] in
guard let self, self.state == .awaiting else { return }
self.state = .idle
Expand All @@ -113,42 +80,6 @@ final class QuitInterceptor {
}
}

// MARK: - Label Emphasis

func setHoldingAppearance() {
pressLabel?.textColor = QuitOverlay.secondaryTextColor
holdLabel?.textColor = QuitOverlay.primaryTextColor
}

func setDefaultAppearance() {
pressLabel?.textColor = QuitOverlay.primaryTextColor
holdLabel?.textColor = QuitOverlay.secondaryTextColor
}

// MARK: - Progress Bar

func startProgressAnimation() {
guard let progressLayer else { return }

progressLayer.removeAllAnimations()

let animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fromValue = 0
animation.toValue = QuitOverlay.size.width
animation.duration = QuitOverlay.holdDuration
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false

progressLayer.add(animation, forKey: "progress")
}

func resetProgress() {
guard let progressLayer else { return }
progressLayer.removeAllAnimations()
progressLayer.frame.size.width = 0
}

// MARK: - Timer Helpers

func scheduleTimer(
Expand Down
11 changes: 2 additions & 9 deletions crates/intercept/swift-lib/src/QuitOverlayConfig.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import Cocoa

enum QuitOverlay {
static let size = NSSize(width: 340, height: 96)
static let size = NSSize(width: 360, height: 84)
static let cornerRadius: CGFloat = 12
static let verticalOffsetRatio: CGFloat = 0.15
static let backgroundColor = NSColor(white: 0.12, alpha: 0.88)

static let pressText = "Press ⌘Q to Close"
static let holdText = "Hold ⌘Q to Quit"
static let messageText = "Press ⌘Q again to Quit"
static let font = NSFont.systemFont(ofSize: 22, weight: .medium)
static let primaryTextColor = NSColor.white
static let secondaryTextColor = NSColor(white: 1.0, alpha: 0.5)

static let animationDuration: TimeInterval = 0.15
static let holdThreshold: TimeInterval = 0.2
static let holdDuration: TimeInterval = 1.0
static let overlayDuration: TimeInterval = 1.5

static let progressBarHeight: CGFloat = 4
static let progressBarColor = NSColor(white: 1.0, alpha: 0.6)
}
52 changes: 9 additions & 43 deletions crates/intercept/swift-lib/src/QuitOverlayPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,51 +43,17 @@ extension QuitInterceptor {
container.layer?.cornerRadius = QuitOverlay.cornerRadius
container.layer?.masksToBounds = true

let pressLabel = makeLabel(QuitOverlay.pressText, color: QuitOverlay.primaryTextColor)
let holdLabel = makeLabel(QuitOverlay.holdText, color: QuitOverlay.secondaryTextColor)
self.pressLabel = pressLabel
self.holdLabel = holdLabel

let prefixDelta =
NSAttributedString(
string: "Press ", attributes: [.font: QuitOverlay.font]
).size().width
- NSAttributedString(
string: "Hold ", attributes: [.font: QuitOverlay.font]
).size().width

let spacing: CGFloat = 10
let totalHeight = pressLabel.frame.height + spacing + holdLabel.frame.height
let topY = (size.height + totalHeight) / 2 - pressLabel.frame.height
let pressX = (size.width - pressLabel.frame.width) / 2

pressLabel.frame = NSRect(
x: pressX,
y: topY,
width: pressLabel.frame.width,
height: pressLabel.frame.height
let messageLabel = makeLabel(QuitOverlay.messageText, color: QuitOverlay.primaryTextColor)
self.messageLabel = messageLabel

messageLabel.frame = NSRect(
x: (size.width - messageLabel.frame.width) / 2,
y: (size.height - messageLabel.frame.height) / 2,
width: messageLabel.frame.width,
height: messageLabel.frame.height
)
holdLabel.frame = NSRect(
x: pressX + prefixDelta,
y: topY - spacing - holdLabel.frame.height,
width: holdLabel.frame.width,
height: holdLabel.frame.height
)

container.addSubview(pressLabel)
container.addSubview(holdLabel)

let progress = CALayer()
progress.anchorPoint = CGPoint(x: 0, y: 0)
progress.frame = NSRect(
x: 0,
y: 0,
width: 0,
height: QuitOverlay.progressBarHeight
)
progress.backgroundColor = QuitOverlay.progressBarColor.cgColor
container.layer?.addSublayer(progress)
progressLayer = progress
container.addSubview(messageLabel)

return container
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/tray/src/menu_items/tray_quit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl MenuItemHandler for TrayQuit {
const ID: &'static str = "hypr_tray_quit";

fn build(app: &AppHandle<tauri::Wry>) -> Result<MenuItemKind<tauri::Wry>> {
let item = MenuItem::with_id(app, Self::ID, "Quit Completely", true, Some("cmd+shift+q"))?;
let item = MenuItem::with_id(app, Self::ID, "Quit", true, Some("cmd+q"))?;
Ok(MenuItemKind::MenuItem(item))
}

Expand Down
Loading