Files
endorphin-tessera-gui/src/shell/native_chrome.rs
T

103 lines
4.2 KiB
Rust

//! macOS 原生标题栏适配。定位系统窗口按钮并沿用用户的标题栏双击设置。
//! 仅在 macOS 编译;Objective-C 交互局限在原生窗口层,不参与 Endor 布局与事件分发。
//!
//! Keep AppKit's real window buttons aligned with the custom tab strip.
use objc2_app_kit::{NSApplication, NSView, NSWindowButton, NSWindowStyleMask};
use objc2_foundation::{MainThreadMarker, NSPoint, NSString, NSUserDefaults};
use winit::{
raw_window_handle::{HasWindowHandle, RawWindowHandle},
window::Window,
};
/// The custom draggable row does not receive AppKit's titlebar double-click
/// action automatically. Use the native click count and native window actions.
/// 读取系统偏好的标题栏双击行为,委托原生窗口执行相应动作。
pub fn titlebar_double_click(window: &Window) -> bool {
let Some(main_thread) = MainThreadMarker::new() else {
return false;
};
let Ok(handle) = window.window_handle() else {
return false;
};
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
return false;
};
// SAFETY: called on the event-loop thread during a left-button press, with
// a live winit-owned NSView. No native object outlives the Window borrow.
unsafe {
let Some(event) = NSApplication::sharedApplication(main_thread).currentEvent() else {
return false;
};
if event.clickCount() != 2 {
return false;
}
let view = &*handle.ns_view.as_ptr().cast::<NSView>();
let Some(native) = view.window() else {
return false;
};
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
return true;
}
let action = NSUserDefaults::standardUserDefaults()
.stringForKey(&NSString::from_str("AppleActionOnDoubleClick"))
.map(|value| value.to_string());
match action.as_deref() {
Some("Minimize") => native.performMiniaturize(None),
None | Some("Maximize" | "Fill") => native.performZoom(None),
// Respect disabled/unrecognized system actions without moving it.
_ => {}
}
}
true
}
/// Called only on winit's main thread, after native creation and before drawing.
/// AppKit can restore its default button positions when laying out the title bar.
/// 调整真实 macOS 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。
pub fn align_controls(window: &Window) {
let Ok(handle) = window.window_handle() else {
return;
};
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
return;
};
// SAFETY: winit owns this NSView for the lifetime of the borrowed Window.
// All AppKit access is on the event-loop thread; no native pointers escape.
unsafe {
let view = &*handle.ns_view.as_ptr().cast::<NSView>();
let Some(native) = view.window() else {
return;
};
// Fullscreen traffic lights belong to AppKit's separate floating toolbar.
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
return;
}
let bounds = view.bounds();
let center = f64::from(crate::sidebar::HEADER) / 2.0;
let content_y = if view.isFlipped() {
bounds.origin.y + center
} else {
bounds.origin.y + bounds.size.height - center
};
for kind in [
NSWindowButton::NSWindowCloseButton,
NSWindowButton::NSWindowMiniaturizeButton,
NSWindowButton::NSWindowZoomButton,
] {
let Some(button) = native.standardWindowButton(kind) else {
continue;
};
let Some(parent) = button.superview() else {
continue;
};
let target = parent.convertPoint_fromView(NSPoint::new(0.0, content_y), Some(view));
let frame = button.frame();
let y = target.y - frame.size.height / 2.0;
// Preserve native button size, horizontal spacing and behavior.
if (frame.origin.y - y).abs() > 0.01 {
button.setFrameOrigin(NSPoint::new(frame.origin.x, y));
}
}
}
}