Fix titlebar double-click zoom handling (#2130)

This commit is contained in:
Austin Wang 2026-03-25 02:15:15 -07:00 committed by GitHub
parent 0ea16b12c2
commit 71a64a1234
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 140 additions and 19 deletions

View file

@ -2489,6 +2489,7 @@ struct ContentView: View {
.frame(height: titlebarPadding)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.background(TitlebarDoubleClickMonitorView())
.background({
// The terminal area has two stacked semi-transparent layers: the Bonsplit
// container chrome background plus Ghostty's own Metal-rendered background.
@ -8610,6 +8611,7 @@ struct VerticalTabsSidebar: View {
// drag-to-move and double-click action (zoom/minimize).
WindowDragHandleView()
.frame(height: trafficLightPadding)
.background(TitlebarDoubleClickMonitorView())
}
.overlay(alignment: .topLeading) {
if isMinimalMode {

View file

@ -84,23 +84,23 @@ private func windowDragHandleShouldResolveActiveHitCapture(
/// Runs the same action macOS titlebars use for double-click:
/// zoom by default, or minimize when the user preference is set.
@discardableResult
func performStandardTitlebarDoubleClick(window: NSWindow?) -> Bool {
guard let window else { return false }
enum StandardTitlebarDoubleClickAction: Equatable {
case miniaturize
case zoom
case none
}
let globalDefaults = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain) ?? [:]
func resolvedStandardTitlebarDoubleClickAction(globalDefaults: [String: Any]) -> StandardTitlebarDoubleClickAction {
if let action = (globalDefaults["AppleActionOnDoubleClick"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() {
switch action {
case "minimize":
window.miniaturize(nil)
return true
case "none":
return false
case "maximize", "zoom":
window.zoom(nil)
return true
case "minimize", "miniaturize":
return .miniaturize
case "maximize", "zoom", "fill":
return .zoom
case "none", "no action":
return .none
default:
break
}
@ -108,12 +108,29 @@ func performStandardTitlebarDoubleClick(window: NSWindow?) -> Bool {
if let miniaturizeOnDoubleClick = globalDefaults["AppleMiniaturizeOnDoubleClick"] as? Bool,
miniaturizeOnDoubleClick {
window.miniaturize(nil)
return true
return .miniaturize
}
window.zoom(nil)
return true
return .zoom
}
/// Runs the same action macOS titlebars use for double-click:
/// zoom by default, or minimize when the user preference is set.
@discardableResult
func performStandardTitlebarDoubleClick(window: NSWindow?) -> StandardTitlebarDoubleClickAction? {
guard let window else { return nil }
let globalDefaults = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain) ?? [:]
let action = resolvedStandardTitlebarDoubleClickAction(globalDefaults: globalDefaults)
switch action {
case .miniaturize:
window.miniaturize(nil)
case .zoom:
window.zoom(nil)
case .none:
break
}
return action
}
private enum WindowDragHandleAssociatedObjectKeys {
@ -410,11 +427,11 @@ struct WindowDragHandleView: NSViewRepresentable {
#endif
if event.clickCount >= 2 {
let handled = performStandardTitlebarDoubleClick(window: window)
let action = performStandardTitlebarDoubleClick(window: window)
#if DEBUG
dlog("titlebar.dragHandle.mouseDownDoubleClick handled=\(handled ? 1 : 0)")
dlog("titlebar.dragHandle.mouseDownDoubleClick action=\(String(describing: action))")
#endif
if handled {
if action != nil {
return
}
}
@ -440,3 +457,48 @@ struct WindowDragHandleView: NSViewRepresentable {
}
}
}
/// Local monitor that guarantees double-clicks in custom titlebar surfaces trigger
/// the standard macOS titlebar action even when the visible strip is hosted by
/// higher-level SwiftUI/AppKit container views.
struct TitlebarDoubleClickMonitorView: NSViewRepresentable {
final class Coordinator {
weak var view: NSView?
var monitor: Any?
deinit {
if let monitor {
NSEvent.removeMonitor(monitor)
}
}
}
func makeCoordinator() -> Coordinator { Coordinator() }
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor
context.coordinator.view = view
let coordinator = context.coordinator
coordinator.monitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { [weak coordinator] event in
guard event.clickCount >= 2 else { return event }
guard let coordinator, let view = coordinator.view, let window = view.window else { return event }
guard event.window === window else { return event }
let point = view.convert(event.locationInWindow, from: nil)
guard view.bounds.contains(point) else { return event }
let action = performStandardTitlebarDoubleClick(window: window)
return action == nil ? event : nil
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
context.coordinator.view = nsView
}
}

View file

@ -44,6 +44,12 @@ struct TmuxOverlayExperimentSettings {
}
}
private enum WorkspaceTitlebarInteractionMetrics {
// Keep in sync with Bonsplit's tab bar height so the monitor only covers
// the minimal-mode titlebar strip.
static let minimalModeTopStripHeight: CGFloat = 30
}
struct TmuxPaneLayoutPane: Codable, Equatable, Sendable {
let paneId: String
let left: Int
@ -373,6 +379,12 @@ struct WorkspaceContentView: View {
if isMinimalMode {
bonsplitView
.ignoresSafeArea(.container, edges: .top)
.overlay(alignment: .top) {
if isWorkspaceInputActive {
TitlebarDoubleClickMonitorView()
.frame(height: WorkspaceTitlebarInteractionMetrics.minimalModeTopStripHeight)
}
}
} else {
bonsplitView
}

View file

@ -1258,6 +1258,51 @@ final class WorkspaceRemoteConfigurationTransportKeyTests: XCTestCase {
}
}
final class TitlebarDoubleClickPreferenceTests: XCTestCase {
func testResolvesZoomForFillPreference() {
XCTAssertEqual(
resolvedStandardTitlebarDoubleClickAction(globalDefaults: [
"AppleActionOnDoubleClick": "Fill",
]),
.zoom
)
}
func testResolvesMiniaturizeForExplicitMinimizePreference() {
XCTAssertEqual(
resolvedStandardTitlebarDoubleClickAction(globalDefaults: [
"AppleActionOnDoubleClick": "Minimize",
]),
.miniaturize
)
}
func testResolvesNoneForNoActionPreference() {
XCTAssertEqual(
resolvedStandardTitlebarDoubleClickAction(globalDefaults: [
"AppleActionOnDoubleClick": "No Action",
]),
.none
)
}
func testFallsBackToLegacyMiniaturizePreference() {
XCTAssertEqual(
resolvedStandardTitlebarDoubleClickAction(globalDefaults: [
"AppleMiniaturizeOnDoubleClick": true,
]),
.miniaturize
)
}
func testDefaultsToZoomWhenPreferenceIsMissing() {
XCTAssertEqual(
resolvedStandardTitlebarDoubleClickAction(globalDefaults: [:]),
.zoom
)
}
}
final class WorkspaceRemoteDaemonPendingCallRegistryTests: XCTestCase {
func testSupportsMultiplePendingCallsResolvedOutOfOrder() {
let registry = WorkspaceRemoteDaemonPendingCallRegistry()