前言
在现代桌面应用开发中,跨平台框架提供了开发和部署的便利,而定制化要求通常需要与平台特有的 API 交互。Tauri 框架利用 Rust 和 Web 技术实现高效且安全的跨平台桌面应用开发。在 Tauri 2.x 中,插件系统的引入极大地增强了灵活性,允许开发者注入和定制原生功能。
本文将深入探讨如何在 Tauri 2.x 中创建和使用插件来定制 macOS 窗口控件(交通灯按钮)。我们将详细分析如何利用 Tauri 插件机制、Rust 与 macOS 的 Cocoa 框架交互,以及事件处理的设计和实现细节。
目标功能
在这段代码的实现中,我们的目标是:
- 定制 macOS 窗口交通灯按钮的位置。
- 在窗口大小调整或状态变化时,动态更新按钮的位置。
- 使用 Tauri 插件机制,使功能可重用且易于维护。
1. Tauri 插件机制概述
Tauri 的插件系统允许开发者通过 Rust 插件向主应用添加额外的功能。插件可以通过 TauriPlugin
类型定义,并通过 Builder
API 注册到 Tauri 应用中。插件不仅可以扩展 Tauri 的功能,还能与原生平台的特性紧密集成。
基本的插件创建流程:
TauriPlugin
定义:插件在 Rust 代码中定义,提供入口函数如init()
。- 事件注册:使用
.on_*
方法(例如.on_window_ready()
)来响应特定的窗口事件。 - 插件集成:通过
builder.plugin()
方法将插件添加到 Tauri 应用中。
2. 获取 macOS 窗口句柄:Cocoa 与 Rust 的交互
macOS 的窗口控制(如关闭、最小化、全屏按钮)是由 NSWindow
管理的,这些控件的行为和位置都由 Cocoa 框架控制。在 Rust 中,我们可以通过 cocoa
crate 来调用 Cocoa API,获取和操作这些控件。
获取 NSWindow
句柄
Tauri 为每个创建的窗口提供了 ns_window()
方法,这个方法返回一个原生的 NSWindow
句柄,我们可以利用它来控制 macOS 上的窗口。为了操作交通灯按钮,我们需要获取到窗口的 NSWindow
对象。
use cocoa::appkit::NSWindow;
use cocoa::base::id;
use tauri::Window;
fn get_ns_window(window: &Window) -> id {
window.ns_window().expect("Failed to get NSWindow handle") as id
}
这行代码获取了 Tauri 窗口对应的 NSWindow
对象,并将其转换为 Cocoa 框架的原生 id
类型。之后,我们可以使用 Cocoa 的 API 操作该对象。
操作交通灯按钮的位置
macOS 上的窗口控制按钮(交通灯按钮)被封装为 NSWindowButton
,这些按钮的位置和大小由 NSWindow
控制。我们可以通过 standardWindowButton_
方法获取各个按钮,并通过 setFrameOrigin
调整它们的位置。
use cocoa::appkit::{NSWindowButton, NSWindow};
use cocoa::base::id;
use cocoa::foundation::NSRect;
use objc::runtime::msg_send;
fn position_traffic_lights(ns_window: id, x: f64, y: f64) {
let close_button = unsafe { msg_send![ns_window, standardWindowButton: NSWindowButton::NSWindowCloseButton] };
let miniaturize_button = unsafe { msg_send![ns_window, standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton] };
let zoom_button = unsafe { msg_send![ns_window, standardWindowButton: NSWindowButton::NSWindowZoomButton] };
let buttons = vec![close_button, miniaturize_button, zoom_button];
let space_between = unsafe {
let close_rect: NSRect = msg_send![close_button, frame];
let miniaturize_rect: NSRect = msg_send![miniaturize_button, frame];
miniaturize_rect.origin.x - close_rect.origin.x
};
for (i, button) in buttons.into_iter().enumerate() {
let mut rect: NSRect = unsafe { msg_send![button, frame] };
rect.origin.x = x + (i as f64 * space_between);
unsafe {
msg_send![button, setFrameOrigin: rect.origin];
}
}
}
此时,我们已获得 close
、miniaturize
和 zoom
按钮的句柄。接下来,我们需要调整它们的位置。首先,我们获取按钮的尺寸(如高度),然后计算新的按钮位置。
use cocoa::foundation::NSRect;
use objc::runtime::msg_send;
fn adjust_traffic_lights_position(close_button: id, miniaturize_button: id, zoom_button: id, x: f64, y: f64) {
let close_rect: NSRect = unsafe { msg_send![close_button, frame] };
let button_height = close_rect.size.height;
let space_between = unsafe {
let mini_rect: NSRect = msg_send![miniaturize_button, frame];
mini_rect.origin.x - close_rect.origin.x
};
let new_rect = NSRect {
origin: NSPoint { x, y },
size: NSSize { width: 100.0, height: button_height },
};
unsafe {
msg_send![close_button, setFrame: new_rect];
msg_send![miniaturize_button, setFrameOrigin: new_rect.origin];
}
}
3. 使用 Delegate 监听窗口事件:保持按钮位置同步
为了在窗口调整大小或其他状态变化时,保持按钮位置的一致性,我们需要监听窗口的相关事件,如 resize
和 move
。这可以通过设置 NSWindowDelegate
来实现。
NSWindowDelegate
与事件处理
在 Cocoa 中,NSWindowDelegate
负责接收窗口的事件,包括窗口大小变化、全屏状态改变等。我们通过设置自定义 delegate 来响应这些事件,并在窗口调整时重新计算按钮的位置。
extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let window_state = &mut *(this.get_ivar("app_box") as *mut WindowState);
let ns_window = window_state.window.ns_window().unwrap();
position_traffic_lights(ns_window, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
}
}
在此示例中,我们定义了 on_window_did_resize
函数,当窗口大小变化时,它会通过 with_window_state
函数获取当前窗口状态,并重新调整交通灯按钮的位置。
with_window_state
和 unsafe
操作
with_window_state
函数是一个帮助函数,它允许我们获取窗口的状态并将其传递给事件处理函数。Rust 的 unsafe
代码块在这里是必要的,因为我们需要访问一些通过 get_ivar
存储的原始指针:
unsafe {
let state = &mut *(this.get_ivar("app_box") as *mut WindowState);
// 其他逻辑
}
这种 unsafe
代码块允许我们绕过 Rust 的严格借用检查,直接操作内存。需要注意的是,这种方式非常高效,但需要开发者小心内存管理,避免出现悬垂指针或内存泄漏。在进行 unsafe
操作时,务必确保内存安全,避免发生内存访问错误。
注册 Delegate
NSWindowDelegate
是一个 Objective-C 类,因此我们需要将 Rust 函数映射为 Objective-C 方法。通过 objc::runtime::Object
和 objc::runtime::Sel
,我们可以将 Rust 函数绑定到 macOS 的窗口代理方法上:
let delegate_name = format!("windowDelegate_{}", random_str);
ns_win.setDelegate_(delegate!(&delegate_name, {
window: id = ns_win,
app_box: *mut c_void = app_box,
super_delegate: id = current_delegate,
(windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id),
// 其他方法
}));
这里的 on_window_did_resize
函数会在窗口大小调整时被调用。通过 msg_send!
宏,我们将事件传递给原生的 super_delegate
,从而保持 Cocoa 的正常事件处理链。
4. 插件集成与事件绑定
Tauri 插件机制允许我们将功能集成到主应用中。通过插件的注册机制,我们可以在应用初始化时自动加载并配置窗口行为。init
函数会在插件加载时被调用,我们可以在这里处理窗口控件的初始化和事件绑定。
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);
})
.build()
}
在 on_window_ready
事件中,我们调用 setup_traffic_light_positioner
函数进行窗口控件位置调整的初始化。
5. 使用插件
通过 Tauri 插件系统,我们可以将上述插件注入到主应用中,使其自动生效。通过 builder.plugin(mac::window::init())
,我们将窗口定制功能注入到应用中。
#[cfg(target_os = "macos")]
let builder = builder.plugin(mac::window::init());
这样,插件会在窗口创建时自动运行,并根据需要调整窗口的交通灯按钮。
6. 完整代码
完整代码修改自 https://github.com/victoralvesf/aonsoku/blob/main/src-tauri/src/mac/window.rs
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime, Window,
}; // 0.8
const WINDOW_CONTROL_PAD_X: f64 = 14.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<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, NSWindowTitleVisibility};
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
unsafe {
// Hide the title bar
ns_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
// Hide the title bar background
ns_window.setTitlebarAppearsTransparent_(cocoa::base::YES);
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::delegate;
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use std::ffi::c_void;
use tauri::Emitter;
// 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();
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)
}))
}
}
7. 参考资料
[0]https://github.com/victoralvesf/aonsoku
[1]https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9
[2]https://github.com/itseeleeya/tauri-plugin-trafficlights-positioner/