mirror of
https://github.com/ParthJadhav/Verve.git
synced 2026-06-19 07:37:17 +00:00
feat: add ns-panel support (#44)
This commit is contained in:
Generated
+41
@@ -1771,6 +1771,17 @@ dependencies = [
|
||||
"objc_exception",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_exception"
|
||||
version = "0.1.2"
|
||||
@@ -2660,6 +2671,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "5.0.0"
|
||||
@@ -3247,15 +3271,22 @@ dependencies = [
|
||||
"auto-launch",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"cocoa",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"directories",
|
||||
"localzone",
|
||||
"num-format",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"plist",
|
||||
"rust_search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smartcalc",
|
||||
"strsim",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"window-vibrancy",
|
||||
@@ -3344,6 +3375,16 @@ version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "0.18.2"
|
||||
|
||||
+11
-2
@@ -27,6 +27,15 @@ smartcalc = { git = "https://github.com/ParthJadhav/smartcalc", branch = "stable
|
||||
chrono-tz = { version = "0.6.1", default-features = false }
|
||||
num-format = { version = "0.4", features = ["with-system-locale"] }
|
||||
localzone = "0.2.0"
|
||||
sys-locale = "0.2.3"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
core-graphics = {version = "0.22.3"}
|
||||
core-foundation = { version = "0.9.3" }
|
||||
cocoa = { version = "0.24.1" }
|
||||
objc = { version = "0.2.7" }
|
||||
objc_id = {version = "0.1.1" }
|
||||
objc-foundation = { version = "0.1.1" }
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
@@ -34,7 +43,7 @@ version = "0.4"
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = [ "custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::nursery, clippy::pedantic)]
|
||||
|
||||
mod util;
|
||||
mod ns_panel;
|
||||
|
||||
use tauri::{
|
||||
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
@@ -36,7 +37,10 @@ fn main() {
|
||||
open_command,
|
||||
get_icon,
|
||||
handle_input,
|
||||
launch_on_login
|
||||
launch_on_login,
|
||||
ns_panel::init_ns_panel,
|
||||
ns_panel::show_app,
|
||||
ns_panel::hide_app
|
||||
])
|
||||
.setup(|app| {
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
@@ -47,6 +51,7 @@ fn main() {
|
||||
window.hide().unwrap();
|
||||
Ok(())
|
||||
})
|
||||
.manage(ns_panel::State::default())
|
||||
.system_tray(create_system_tray())
|
||||
.on_system_tray_event(|app, event| match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
use std::sync::{Mutex, Once};
|
||||
|
||||
use objc_id::{Id, ShareId};
|
||||
use tauri::{
|
||||
AppHandle, GlobalShortcutManager, Manager, PhysicalPosition, PhysicalSize, Window, Wry,
|
||||
};
|
||||
|
||||
use cocoa::{
|
||||
appkit::{CGFloat, NSMainMenuWindowLevel, NSWindow, NSWindowCollectionBehavior},
|
||||
base::{id, nil, BOOL, NO, YES},
|
||||
foundation::{NSPoint, NSRect},
|
||||
};
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{self, Class, Object, Protocol, Sel},
|
||||
sel, sel_impl, Message,
|
||||
};
|
||||
use objc_foundation::INSObject;
|
||||
|
||||
#[link(name = "Foundation", kind = "framework")]
|
||||
extern "C" {
|
||||
pub fn NSMouseInRect(aPoint: NSPoint, aRect: NSRect, flipped: BOOL) -> BOOL;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Store {
|
||||
panel: Option<ShareId<RawNSPanel>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State(pub Mutex<Store>);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! set_state {
|
||||
($app_handle:expr, $field:ident, $value:expr) => {{
|
||||
let handle = $app_handle.app_handle();
|
||||
handle
|
||||
.state::<$crate::ns_panel::State>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.$field = $value;
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_state {
|
||||
($app_handle:expr, $field:ident) => {{
|
||||
let handle = $app_handle.app_handle();
|
||||
let value = handle
|
||||
.state::<$crate::ns_panel::State>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.$field;
|
||||
|
||||
value
|
||||
}};
|
||||
($app_handle:expr, $field:ident, $action:ident) => {{
|
||||
let handle = $app_handle.app_handle();
|
||||
let value = handle
|
||||
.state::<$crate::ns_panel::State>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.$field
|
||||
.$action();
|
||||
|
||||
value
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! panel {
|
||||
($app_handle:expr) => {{
|
||||
let handle = $app_handle.app_handle();
|
||||
let panel = handle
|
||||
.state::<$crate::ns_panel::State>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.panel
|
||||
.clone();
|
||||
|
||||
panel.unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! nsstring_to_string {
|
||||
($ns_string:expr) => {{
|
||||
use objc::{sel, sel_impl};
|
||||
let utf8: id = unsafe { objc::msg_send![$ns_string, UTF8String] };
|
||||
let string = if !utf8.is_null() {
|
||||
Some(unsafe {
|
||||
{
|
||||
std::ffi::CStr::from_ptr(utf8 as *const std::ffi::c_char)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
string
|
||||
}};
|
||||
}
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
static PANEL_LABEL: &str = "main";
|
||||
|
||||
#[tauri::command]
|
||||
pub fn init_ns_panel(app_handle: AppHandle<Wry>, window: Window<Wry>, shortcut: &str) {
|
||||
INIT.call_once(|| {
|
||||
set_state!(app_handle, panel, Some(create_ns_panel(&window)));
|
||||
register_shortcut(app_handle, shortcut);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_shortcut(app_handle: AppHandle<Wry>, shortcut: &str) {
|
||||
let mut shortcut_manager = app_handle.global_shortcut_manager();
|
||||
let window = app_handle.get_window(PANEL_LABEL).unwrap();
|
||||
|
||||
let panel = panel!(app_handle);
|
||||
shortcut_manager
|
||||
.register(shortcut, move || {
|
||||
position_window_at_the_center_of_the_monitor_with_cursor(&window);
|
||||
|
||||
if panel.is_visible() {
|
||||
hide_app(window.app_handle());
|
||||
} else {
|
||||
show_app(window.app_handle());
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_app(app_handle: AppHandle<Wry>) {
|
||||
panel!(app_handle).show();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hide_app(app_handle: AppHandle<Wry>) {
|
||||
panel!(app_handle).order_out(None);
|
||||
}
|
||||
|
||||
/// Positions a given window at the center of the monitor with cursor
|
||||
fn position_window_at_the_center_of_the_monitor_with_cursor(window: &Window<Wry>) {
|
||||
if let Some(monitor) = get_monitor_with_cursor() {
|
||||
let display_size = monitor.size.to_logical::<f64>(monitor.scale_factor);
|
||||
let display_pos = monitor.position.to_logical::<f64>(monitor.scale_factor);
|
||||
|
||||
let handle: id = window.ns_window().unwrap() as _;
|
||||
let win_frame: NSRect = unsafe { handle.frame() };
|
||||
let rect = NSRect {
|
||||
origin: NSPoint {
|
||||
x: (display_pos.x + (display_size.width / 2.0)) - (win_frame.size.width / 2.0),
|
||||
y: (display_pos.y + (display_size.height / 2.0)) - (win_frame.size.height / 2.0),
|
||||
},
|
||||
size: win_frame.size,
|
||||
};
|
||||
let _: () = unsafe { msg_send![handle, setFrame: rect display: YES] };
|
||||
}
|
||||
}
|
||||
|
||||
struct Monitor {
|
||||
#[allow(dead_code)]
|
||||
pub name: Option<String>,
|
||||
pub size: PhysicalSize<u32>,
|
||||
pub position: PhysicalPosition<i32>,
|
||||
pub scale_factor: f64,
|
||||
}
|
||||
|
||||
/// Gets the Monitor with cursor
|
||||
fn get_monitor_with_cursor() -> Option<Monitor> {
|
||||
objc::rc::autoreleasepool(|| {
|
||||
let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
|
||||
let screens: id = unsafe { msg_send![class!(NSScreen), screens] };
|
||||
let screens_iter: id = unsafe { msg_send![screens, objectEnumerator] };
|
||||
let mut next_screen: id;
|
||||
|
||||
let frame_with_cursor: Option<NSRect> = loop {
|
||||
next_screen = unsafe { msg_send![screens_iter, nextObject] };
|
||||
if next_screen == nil {
|
||||
break None;
|
||||
}
|
||||
|
||||
let frame: NSRect = unsafe { msg_send![next_screen, frame] };
|
||||
let is_mouse_in_screen_frame: BOOL =
|
||||
unsafe { NSMouseInRect(mouse_location, frame, NO) };
|
||||
if is_mouse_in_screen_frame == YES {
|
||||
break Some(frame);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(frame) = frame_with_cursor {
|
||||
let name: id = unsafe { msg_send![next_screen, localizedName] };
|
||||
let screen_name = nsstring_to_string!(name);
|
||||
let scale_factor: CGFloat = unsafe { msg_send![next_screen, backingScaleFactor] };
|
||||
let scale_factor: f64 = scale_factor;
|
||||
|
||||
return Some(Monitor {
|
||||
name: screen_name,
|
||||
position: PhysicalPosition {
|
||||
x: (frame.origin.x * scale_factor) as i32,
|
||||
y: (frame.origin.y * scale_factor) as i32,
|
||||
},
|
||||
size: PhysicalSize {
|
||||
width: (frame.size.width * scale_factor) as u32,
|
||||
height: (frame.size.height * scale_factor) as u32,
|
||||
},
|
||||
scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
pub fn object_setClass(obj: id, cls: id) -> id;
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
|
||||
|
||||
const CLS_NAME: &str = "RawNSPanel";
|
||||
|
||||
pub struct RawNSPanel;
|
||||
|
||||
impl RawNSPanel {
|
||||
fn get_class() -> &'static Class {
|
||||
Class::get(CLS_NAME).unwrap_or_else(Self::define_class)
|
||||
}
|
||||
|
||||
fn define_class() -> &'static Class {
|
||||
let mut cls = ClassDecl::new(CLS_NAME, class!(NSPanel))
|
||||
.unwrap_or_else(|| panic!("Unable to register {} class", CLS_NAME));
|
||||
|
||||
unsafe {
|
||||
cls.add_method(
|
||||
sel!(canBecomeKeyWindow),
|
||||
Self::can_become_key_window as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
}
|
||||
|
||||
cls.register()
|
||||
}
|
||||
|
||||
/// Returns YES to ensure that RawNSPanel can become a key window
|
||||
extern "C" fn can_become_key_window(_: &Object, _: Sel) -> BOOL {
|
||||
YES
|
||||
}
|
||||
}
|
||||
unsafe impl Message for RawNSPanel {}
|
||||
|
||||
impl RawNSPanel {
|
||||
fn show(&self) {
|
||||
self.make_first_responder(Some(self.content_view()));
|
||||
self.order_front_regardless();
|
||||
self.make_key_window();
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
let flag: BOOL = unsafe { msg_send![self, isVisible] };
|
||||
flag == YES
|
||||
}
|
||||
|
||||
fn make_key_window(&self) {
|
||||
let _: () = unsafe { msg_send![self, makeKeyWindow] };
|
||||
}
|
||||
|
||||
fn order_front_regardless(&self) {
|
||||
let _: () = unsafe { msg_send![self, orderFrontRegardless] };
|
||||
}
|
||||
|
||||
fn order_out(&self, sender: Option<id>) {
|
||||
let _: () = unsafe { msg_send![self, orderOut: sender.unwrap_or(nil)] };
|
||||
}
|
||||
|
||||
fn content_view(&self) -> id {
|
||||
unsafe { msg_send![self, contentView] }
|
||||
}
|
||||
|
||||
fn make_first_responder(&self, sender: Option<id>) {
|
||||
if let Some(responder) = sender {
|
||||
let _: () = unsafe { msg_send![self, makeFirstResponder: responder] };
|
||||
} else {
|
||||
let _: () = unsafe { msg_send![self, makeFirstResponder: self] };
|
||||
}
|
||||
}
|
||||
|
||||
fn set_level(&self, level: i32) {
|
||||
let _: () = unsafe { msg_send![self, setLevel: level] };
|
||||
}
|
||||
|
||||
fn set_style_mask(&self, style_mask: i32) {
|
||||
let _: () = unsafe { msg_send![self, setStyleMask: style_mask] };
|
||||
}
|
||||
|
||||
fn set_collection_behaviour(&self, behaviour: NSWindowCollectionBehavior) {
|
||||
let _: () = unsafe { msg_send![self, setCollectionBehavior: behaviour] };
|
||||
}
|
||||
|
||||
fn set_delegate(&self, delegate: Option<Id<RawNSPanelDelegate>>) {
|
||||
if let Some(del) = delegate {
|
||||
let _: () = unsafe { msg_send![self, setDelegate: del] };
|
||||
} else {
|
||||
let _: () = unsafe { msg_send![self, setDelegate: self] };
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an NSPanel from Tauri's NSWindow
|
||||
fn from(ns_window: id) -> Id<Self> {
|
||||
let ns_panel: id = unsafe { msg_send![Self::class(), class] };
|
||||
unsafe {
|
||||
object_setClass(ns_window, ns_panel);
|
||||
Id::from_retained_ptr(ns_window as *mut Self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl INSObject for RawNSPanel {
|
||||
fn class() -> &'static runtime::Class {
|
||||
RawNSPanel::get_class()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const DELEGATE_CLS_NAME: &str = "RawNSPanelDelegate";
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct RawNSPanelDelegate {}
|
||||
|
||||
impl RawNSPanelDelegate {
|
||||
#[allow(dead_code)]
|
||||
fn get_class() -> &'static Class {
|
||||
Class::get(DELEGATE_CLS_NAME).unwrap_or_else(Self::define_class)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn define_class() -> &'static Class {
|
||||
let mut cls = ClassDecl::new(DELEGATE_CLS_NAME, class!(NSObject))
|
||||
.unwrap_or_else(|| panic!("Unable to register {} class", DELEGATE_CLS_NAME));
|
||||
|
||||
cls.add_protocol(
|
||||
Protocol::get("NSWindowDelegate").expect("Failed to get NSWindowDelegate protocol"),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
cls.add_ivar::<id>("panel");
|
||||
|
||||
cls.add_method(
|
||||
sel!(setPanel:),
|
||||
Self::set_panel as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
|
||||
cls.add_method(
|
||||
sel!(windowDidBecomeKey:),
|
||||
Self::window_did_become_key as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
cls.add_method(
|
||||
sel!(windowDidResignKey:),
|
||||
Self::window_did_resign_key as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
}
|
||||
|
||||
cls.register()
|
||||
}
|
||||
|
||||
extern "C" fn set_panel(this: &mut Object, _: Sel, panel: id) {
|
||||
unsafe { this.set_ivar("panel", panel) };
|
||||
}
|
||||
|
||||
extern "C" fn window_did_become_key(_: &Object, _: Sel, _: id) {}
|
||||
|
||||
/// Hide panel when it's no longer the key window
|
||||
extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) {
|
||||
let panel: id = unsafe { *this.get_ivar("panel") };
|
||||
let _: () = unsafe { msg_send![panel, orderOut: nil] };
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Message for RawNSPanelDelegate {}
|
||||
|
||||
impl INSObject for RawNSPanelDelegate {
|
||||
fn class() -> &'static runtime::Class {
|
||||
Self::get_class()
|
||||
}
|
||||
}
|
||||
|
||||
impl RawNSPanelDelegate {
|
||||
pub fn set_panel_(&self, panel: ShareId<RawNSPanel>) {
|
||||
let _: () = unsafe { msg_send![self, setPanel: panel] };
|
||||
}
|
||||
}
|
||||
|
||||
fn create_ns_panel(window: &Window<Wry>) -> ShareId<RawNSPanel> {
|
||||
// Convert NSWindow Object to NSPanel
|
||||
let handle: id = window.ns_window().unwrap() as _;
|
||||
let panel = RawNSPanel::from(handle);
|
||||
let panel = panel.share();
|
||||
|
||||
// Set panel above the main menu window level
|
||||
panel.set_level(NSMainMenuWindowLevel + 1);
|
||||
|
||||
// Ensure that the panel can display over the top of fullscreen apps
|
||||
panel.set_collection_behaviour(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorTransient
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
);
|
||||
|
||||
// Ensures panel does not activate
|
||||
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
|
||||
|
||||
// Setup delegate for an NSPanel to listen for window resign key and hide the panel
|
||||
let delegate = RawNSPanelDelegate::new();
|
||||
delegate.set_panel_(panel.clone());
|
||||
panel.set_delegate(Some(delegate));
|
||||
|
||||
panel
|
||||
}
|
||||
+2
-2
@@ -43,6 +43,8 @@ const reloadTheme = async () => {
|
||||
await fetchPreferencesData();
|
||||
await reloadTheme();
|
||||
|
||||
await invoke("init_ns_panel", {shortcut: preferences.get('shortcut')});
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') {
|
||||
appWindow.hide();
|
||||
@@ -62,8 +64,6 @@ const reloadTheme = async () => {
|
||||
await invoke('launch_on_login', {
|
||||
enable: preferences.get('launch_on_login'),
|
||||
});
|
||||
|
||||
await listenForHotkey(preferences.get('shortcut'));
|
||||
})();
|
||||
|
||||
export async function listenForHotkey(shortcut: string) {
|
||||
|
||||
Reference in New Issue
Block a user