//! macOS 原生标题栏适配。定位系统窗口按钮并沿用用户的标题栏双击设置。 //! 仅在 macOS 编译;Objective-C 交互局限在原生窗口层,不参与 Endor 布局与事件分发。 //! //! Keep AppKit's real window buttons aligned with the custom tab strip. //! app::create_native/redraw 负责在事件循环线程调用对齐,app::window_event 的标题空白区按下路径调用双击处理。 //! 两个入口都只临时借用 winit 持有的 NSView/NSWindow;本模块不保存原生指针,也不向 VM 发送伪造标题栏事件。 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. /// 读取系统偏好的标题栏双击行为,委托原生窗口执行相应动作。 // shell/app 在自绘标题控制区左键按下时调用;true 表示本次双击已处理,调用方不再开始普通窗口拖动。 pub fn titlebar_double_click(window: &Window) -> bool { // AppKit 必须在主线程调用;不在主线程、无有效句柄或句柄非 AppKit 时返回未处理。 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 { // 只允许 AppKit 句柄进入下面 NSView 转型,其他平台句柄不具有这些 Objective-C 对象前提。 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 { // 从当前 AppKit 事件读取原生 clickCount;无事件就不猜测双击,也不执行窗口动作。 let Some(event) = NSApplication::sharedApplication(main_thread).currentEvent() else { return false; }; // 依赖系统原生连击计数,不自行推算两次输入时间,保持用户设置与系统行为一致。 if event.clickCount() != 2 { return false; } // 借用 winit 提供的活动 NSView,再取得所属 NSWindow;不创建窗口或把原生指针保存到外部状态。 let view = &*handle.ns_view.as_ptr().cast::(); let Some(native) = view.window() else { return false; }; // 全屏时把双击视作已处理,但不执行缩放/最小化,避免自绘标题行改变全屏状态。 if native.styleMask().contains(NSWindowStyleMask::FullScreen) { return true; } // 读取系统标题栏双击设置,字符串转成 Rust 自有值后按约定选原生动作。 let action = NSUserDefaults::standardUserDefaults() .stringForKey(&NSString::from_str("AppleActionOnDoubleClick")) .map(|value| value.to_string()); // 按系统偏好执行动作;缺省采用 Zoom,禁用或未知选项不动作,但仍返回已处理。 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 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。 // shell/app 创建窗口后和绘制前调用;主线程调用由外层保证,此函数本身没有创建 MainThreadMarker 检查。 pub fn align_controls(window: &Window) { // 取得当前 Window 的句柄,失败或非 AppKit 平台表示无法调整,返回不改变现有原生布局。 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 { // 安全前提由调用方保证:事件循环主线程、仍活着的 Window 借用,以及 winit 提供的有效 NSView;这里只短期访问。 let view = &*handle.ns_view.as_ptr().cast::(); let Some(native) = view.window() else { // 原生视图尚未关联窗口时无需移动控制按钮,不创建替代 NSWindow 来补齐。 return; }; // Fullscreen traffic lights belong to AppKit's separate floating toolbar. if native.styleMask().contains(NSWindowStyleMask::FullScreen) { // 全屏按钮由 AppKit 独立工具栏管理,内容视图坐标不适合作为它们的位置来源。 return; } // 读取内容视图边界,用与侧栏相同的 HEADER 高度确定标题行中心。 let bounds = view.bounds(); let center = f64::from(crate::sidebar::HEADER) / 2.0; // AppKit 视图可能以左下或左上为原点;先在内容视图内算标题行中心,再转到按钮父视图。 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; }; // 按钮可能位于不同 AppKit 容器,必须使用 convertPoint 而不能直接赋内容视图坐标。 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)); } } } }