From 7271e9ea876e95782f69126e7c90aad6a352fe9b Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 29 Mar 2024 13:26:35 +0700 Subject: [PATCH] feat: add custom traffic light inset for macos --- src-tauri/Cargo.lock | 3 + src-tauri/Cargo.toml | 5 + src-tauri/src/main.rs | 12 ++ src-tauri/src/traffic_light.rs | 342 +++++++++++++++++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 src-tauri/src/traffic_light.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 68616b44..f3c961ee 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2970,8 +2970,11 @@ dependencies = [ name = "lume" version = "4.0.0" dependencies = [ + "cocoa", "keyring", "nostr-sdk", + "objc", + "rand 0.8.5", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index accc2b92..c05857a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,11 @@ tauri-plugin-window-state = "2.0.0-beta" webpage = { version = "2.0", features = ["serde"] } keyring = "2" +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.25.0" +objc = "0.2.7" +rand = "0.8.5" + [profile.release] codegen-units = 1 lto = true diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 16188242..07add541 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,12 +5,21 @@ pub mod commands; pub mod nostr; +pub mod traffic_light; pub mod tray; +#[cfg(target_os = "macos")] +extern crate cocoa; + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + use nostr_sdk::prelude::*; use std::fs; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; +use traffic_light::setup_traffic_light_positioner; pub struct Nostr { client: Client, @@ -22,6 +31,9 @@ fn main() { #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Regular); + #[cfg(target_os = "macos")] + setup_traffic_light_positioner(app.get_window("main").unwrap()); + let _tray = tray::create_tray(app.handle()).unwrap(); let handle = app.handle().clone(); let home_dir = handle.path().home_dir().unwrap(); diff --git a/src-tauri/src/traffic_light.rs b/src-tauri/src/traffic_light.rs new file mode 100644 index 00000000..d2dc4750 --- /dev/null +++ b/src-tauri/src/traffic_light.rs @@ -0,0 +1,342 @@ +use objc::{msg_send, sel, sel_impl}; +use rand::{distributions::Alphanumeric, Rng}; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, Window, +}; // 0.8 + +const WINDOW_CONTROL_PAD_X: f64 = 8.0; +const WINDOW_CONTROL_PAD_Y: f64 = 16.0; + +struct UnsafeWindowHandle(*mut std::ffi::c_void); +unsafe impl Send for UnsafeWindowHandle {} +unsafe impl Sync for UnsafeWindowHandle {} + +pub fn init() -> TauriPlugin { + Builder::new("traffic_light_positioner") + .on_window_ready(|window| { + #[cfg(target_os = "macos")] + setup_traffic_light_positioner(window); + return; + }) + .build() +} + +#[cfg(target_os = "macos")] +fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) { + use cocoa::appkit::{NSView, NSWindow, NSWindowButton}; + use cocoa::foundation::NSRect; + let ns_window = ns_window_handle.0 as cocoa::base::id; + unsafe { + let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); + let miniaturize = ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); + let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); + + let title_bar_container_view = close.superview().superview(); + + let close_rect: NSRect = msg_send![close, frame]; + let button_height = close_rect.size.height; + + let title_bar_frame_height = button_height + y; + let mut title_bar_rect = NSView::frame(title_bar_container_view); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; + let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect]; + + let window_buttons = vec![close, miniaturize, zoom]; + let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x; + + for (i, button) in window_buttons.into_iter().enumerate() { + let mut rect: NSRect = NSView::frame(button); + rect.origin.x = x + (i as f64 * space_between); + button.setFrameOrigin(rect.origin); + } + } +} + +#[cfg(target_os = "macos")] +#[derive(Debug)] +struct WindowState { + window: Window, +} + +#[cfg(target_os = "macos")] +pub fn setup_traffic_light_positioner(window: Window) { + use cocoa::appkit::NSWindow; + use cocoa::base::{id, BOOL}; + use cocoa::foundation::NSUInteger; + use objc::runtime::{Object, Sel}; + use std::ffi::c_void; + + // Do the initial positioning + position_traffic_lights( + UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + + // Ensure they stay in place while resizing the window. + fn with_window_state) -> T, T>(this: &Object, func: F) { + let ptr = unsafe { + let x: *mut c_void = *this.get_ivar("app_box"); + &mut *(x as *mut WindowState) + }; + func(ptr); + } + + unsafe { + let ns_win = window + .ns_window() + .expect("NS Window should exist to mount traffic light delegate.") as id; + + let current_delegate: id = ns_win.delegate(); + + extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, windowShouldClose: sender] + } + } + extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillClose: notification]; + } + } + extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_window_state(&*this, |state: &mut WindowState| { + let id = state + .window + .ns_window() + .expect("NS window should exist on state to handle resize") as id; + + #[cfg(target_os = "macos")] + position_traffic_lights( + UnsafeWindowHandle(id as *mut std::ffi::c_void), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidResize: notification]; + } + } + extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidMove: notification]; + } + } + extern "C" fn on_window_did_change_backing_properties( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification]; + } + } + extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidBecomeKey: notification]; + } + } + extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidResignKey: notification]; + } + } + extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, draggingEntered: notification] + } + } + extern "C" fn on_prepare_for_drag_operation( + this: &Object, + _cmd: Sel, + notification: id, + ) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, prepareForDragOperation: notification] + } + } + extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, performDragOperation: sender] + } + } + extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, concludeDragOperation: notification]; + } + } + extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, draggingExited: notification]; + } + } + extern "C" fn on_window_will_use_full_screen_presentation_options( + this: &Object, + _cmd: Sel, + window: id, + proposed_options: NSUInteger, + ) -> NSUInteger { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options] + } + } + extern "C" fn on_window_did_enter_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("did-enter-fullscreen", ()) + .expect("Failed to emit event"); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidEnterFullScreen: notification]; + } + } + extern "C" fn on_window_will_enter_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("will-enter-fullscreen", ()) + .expect("Failed to emit event"); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillEnterFullScreen: notification]; + } + } + extern "C" fn on_window_did_exit_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("did-exit-fullscreen", ()) + .expect("Failed to emit event"); + + let id = state.window.ns_window().expect("Failed to emit event") as id; + position_traffic_lights( + UnsafeWindowHandle(id as *mut std::ffi::c_void), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidExitFullScreen: notification]; + } + } + extern "C" fn on_window_will_exit_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("will-exit-fullscreen", ()) + .expect("Failed to emit event"); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillExitFullScreen: notification]; + } + } + extern "C" fn on_window_did_fail_to_enter_full_screen(this: &Object, _cmd: Sel, window: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window]; + } + } + extern "C" fn on_effective_appearance_did_change(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification]; + } + } + extern "C" fn on_effective_appearance_did_changed_on_main_thread( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![ + super_del, + effectiveAppearanceDidChangedOnMainThread: notification + ]; + } + } + + // Are we deallocing this properly ? (I miss safe Rust :( ) + let window_label = window.label().to_string(); + + let app_state = WindowState { window }; + let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; + let random_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(20) + .map(char::from) + .collect(); + + // We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate + // delegate with the same name. + let delegate_name = format!("windowDelegate_{}_{}", window_label, random_str); + + ns_win.setDelegate_(cocoa::delegate!(&delegate_name, { + window: id = ns_win, + app_box: *mut c_void = app_box, + toolbar: id = cocoa::base::nil, + super_delegate: id = current_delegate, + (windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL, + (windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id), + (windowDidResize:) => on_window_did_resize:: as extern fn(&Object, Sel, id), + (windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id), + (windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id), + (windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id), + (windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id), + (draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL, + (prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + (performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + (concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id), + (draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id), + (window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + (windowDidEnterFullScreen:) => on_window_did_enter_full_screen:: as extern fn(&Object, Sel, id), + (windowWillEnterFullScreen:) => on_window_will_enter_full_screen:: as extern fn(&Object, Sel, id), + (windowDidExitFullScreen:) => on_window_did_exit_full_screen:: as extern fn(&Object, Sel, id), + (windowWillExitFullScreen:) => on_window_will_exit_full_screen:: as extern fn(&Object, Sel, id), + (windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id), + (effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id), + (effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id) + })) + } +}