From 48fcb96f4256bd6eb17c21211e29a6d3a959c448 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 25 Oct 2026 15:22:24 +0800 Subject: [PATCH] fix: connect toolbar double clicks to native window zoom --- Cargo.toml | 4 ++-- docs/ui-workspace-audit.md | 29 ++++++++++++++++++++++++ src/app.rs | 4 ++++ src/native_chrome.rs | 45 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b722ee..4d2e1fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ winit = "0.30.13" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5.2" -objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSButton", "NSView", "NSWindow"] } -objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSGeometry"] } +objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSApplication", "NSButton", "NSEvent", "NSView", "NSWindow"] } +objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSGeometry", "NSString", "NSUserDefaults"] } [profile.release] lto = "thin" diff --git a/docs/ui-workspace-audit.md b/docs/ui-workspace-audit.md index 810cb84..a74c81b 100644 --- a/docs/ui-workspace-audit.md +++ b/docs/ui-workspace-audit.md @@ -201,3 +201,32 @@ selection changes to the clicked tab and idle separators return after leaving. The capture API does not establish frame-accurate 160 ms timing. Offline all-target Clippy, formatting, release bundling and whitespace checks passed; no cargo test, automated smoke script or Docker task was run. + + +### Native toolbar double-click — 2026-10-25 + +The custom row previously called only winit's native window-drag operation; +a manual double-click in its empty area did not resize the window. The same +empty-area hit test now consults AppKit's current NSEvent clickCount before +starting a drag. On the second press it calls NSWindow.performZoom, preserving +AppKit's standard/user frame toggle and avoiding a hand-built resize animation. +Tabs, close buttons and + remain excluded from this handler. + +The adapter reads AppleActionOnDoubleClick without changing system preferences: +Minimize calls performMiniaturize; absent/Maximize calls performZoom; disabled +or unknown values do nothing. Fill also uses the public performZoom action, +whose standard frame fills this application's available screen in the checked +configuration. It does not invoke the private _zoomFill selector used by Chrome. +Consequently Fill is not claimed to reproduce every macOS tiling margin rule. +Native fullscreen uses a different window state and is left to AppKit. + +References: Apple's [performZoom](https://developer.apple.com/documentation/appkit/nswindow/performzoom(_:)) +and Chromium's [custom draggable-area handling](https://github.com/chromium/chromium/blob/main/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm). + +Manual native verification: an empty-row double-click changed the Retina window +from 2560×1600 to 3024×1888; a second double-click restored 2560×1600. Clicking ++ and double-clicking a tab did not zoom the window. Dragging empty toolbar +space continued to move the window. The user's double-click preference was +absent (default zoom); other preference branches were not changed or manually +tested. Offline all-target Clippy, formatting, release bundling and whitespace +checks passed. No cargo test or automated smoke script was run. diff --git a/src/app.rs b/src/app.rs index d728151..0a53aaa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1410,6 +1410,10 @@ impl ApplicationHandler for TesseraApp { && self.pointer.x >= crate::tabs::LEADING && self.tab_hit().is_none() { + #[cfg(target_os = "macos")] + if crate::native_chrome::titlebar_double_click(&window) { + return; + } let _ = window.drag_window(); } if self.pointer.y >= crate::tabs::height(&[]) { diff --git a/src/native_chrome.rs b/src/native_chrome.rs index 793f90c..e8e7dc5 100644 --- a/src/native_chrome.rs +++ b/src/native_chrome.rs @@ -1,11 +1,52 @@ //! Keep AppKit's real window buttons aligned with the custom tab strip. -use objc2_app_kit::{NSView, NSWindowButton, NSWindowStyleMask}; -use objc2_foundation::NSPoint; +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::(); + 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. pub fn align_controls(window: &Window) {