//! 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 { // 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 { 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::(); 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()); // 按系统偏好执行动作;缺省采用 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 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。 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::(); 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; // 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)); } } } }