-
-
Save charrondev/43150e940bd2771b1ea88256d491c7a9 to your computer and use it in GitHub Desktop.
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 = 15.0; | |
const WINDOW_CONTROL_PAD_Y: f64 = 23.0; | |
struct UnsafeWindowHandle(*mut std::ffi::c_void); | |
unsafe impl Send for UnsafeWindowHandle {} | |
unsafe impl Sync for UnsafeWindowHandle {} | |
pub fn init<R: Runtime>() -> TauriPlugin<R> { | |
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<R: Runtime> { | |
window: Window<R>, | |
} | |
#[cfg(target_os = "macos")] | |
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) { | |
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<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>( | |
this: &Object, | |
func: F, | |
) { | |
let ptr = unsafe { | |
let x: *mut c_void = *this.get_ivar("app_box"); | |
&mut *(x as *mut WindowState<R>) | |
}; | |
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<R: Runtime>(this: &Object, _cmd: Sel, notification: id) { | |
unsafe { | |
with_window_state(&*this, |state: &mut WindowState<R>| { | |
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<R: Runtime>( | |
this: &Object, | |
_cmd: Sel, | |
notification: id, | |
) { | |
unsafe { | |
with_window_state(&*this, |state: &mut WindowState<R>| { | |
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<R: Runtime>( | |
this: &Object, | |
_cmd: Sel, | |
notification: id, | |
) { | |
unsafe { | |
with_window_state(&*this, |state: &mut WindowState<R>| { | |
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<R: Runtime>( | |
this: &Object, | |
_cmd: Sel, | |
notification: id, | |
) { | |
unsafe { | |
with_window_state(&*this, |state: &mut WindowState<R>| { | |
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<R: Runtime>( | |
this: &Object, | |
_cmd: Sel, | |
notification: id, | |
) { | |
unsafe { | |
with_window_state(&*this, |state: &mut WindowState<R>| { | |
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_(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::<R> 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::<R> as extern fn(&Object, Sel, id), | |
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id), | |
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id), | |
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> 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) | |
})) | |
} | |
} |
@reyamir You'll need this in your main rust entrypoint (lib.rs
for me)
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
AFAIK you can only add a #[macro_use]
in the top level of your crate, so it doesn't really fit into the single file of this gist.
Hi, thank you
It worked on my project now
Hi, I've found another issue,
When my app is running, if I switch to dark mode, the traffic light position will be reset
@reyamir - I'm facing the same issue. When I change the theme color, the traffic light position will be reset. I'm using tauri_plugin_theme.
Have you found a fix for this?
Sadly no
@liyasthomas @reyamir use this to fix the issue:
let win_ = win.clone();
win.on_window_event(move |event| {
if let tauri::WindowEvent::ThemeChanged(theme) = event {
// traffic lights position is reset after theme change, so apply it again
setup_traffic_light_positioner(&win_)
}
});
Thank you, @morajabi, but I'm having this below type mismatch issue.
@liyasthomas Remove the &
before the window_
and it should fix it!
@liyasthomas error says you cannot send this variable, so add .clone()
to make a clone
Thank you, @morajabi, for the help. Here's the entire code worked for me:
tauri::Builder::default()
.setup(|app| {
let window = app.get_window("main").unwrap();
let window_ = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::ThemeChanged(_theme) = event {
setup_traffic_light_positioner(window_.clone())
}
});
Ok(())
})
Even tho, I got it slightly working with the above implementation, but the traffic lights seem to be flickering a bit in between theme change.
Demo: wyhaya/tauri-plugin-theme#14 (comment)
Edit: this code will only detect and reposition traffic lights when system theme is changed. This doesn't cover manually changing theme, eg. via tauri-plugin-theme. I've opened a thread to fix the flickering issue.
are there any examples of how to use this?
it would be great to see how things are called in main.rs / lib.rs
@morajabi is this still working for you?
I'm having a similar type mismatch when passing
window.clone() as an argument to setup_traffic_light_positioner,
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::Manager;
mod tlpos; // this is the gist originally posted by charrondev
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
fn main() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
window.on_window_event(move |event| {
tlpos::setup_traffic_light_positioner(window.clone())
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
@Fake-User Please post the error you're getting. Maybe also try pasting the code and the error into chatgpt.
For anyone interested, I made this into a Tauri v1 plugin:
https://github.com/itseeleeya/tauri-plugin-trafficlights-positioner/
Hi! Has anyone adapted the traffic light position setup for Tauri v2? I would appreciate any information or links to similar solutions!
Hi, thank for great gist
I got error about
ns_win.setDelegate_(delegate!(&delegate_name, {}
, do you know how to fixed it? I'm in latest tauri v2 betaMy cargo.toml