Skip to content

Commit

Permalink
macos: autohide dock if quick terminal would conflict with it (ghostt…
Browse files Browse the repository at this point in the history
…y-org#5361)

Fixes ghostty-org#5328

The dock sits above the level of the quick terminal, and the quick
terminal frame typical includes the dock. Hence, if the dock is visible
and the quick terminal would conflict with it, then part of the terminal
is obscured.

This commit makes the dock autohide if the quick terminal would conflict
with it. The autohide is disabled when the quick terminal is closed.

We can't set our window level above the dock, as this would prevent
things such as input methods from rendering properly in the quick
terminal window.

iTerm2 (the only other macOS terminal I know of that supports a dropdown
mode) frames the terminal around the dock. I think this looks less
aesthetically pleasing and I prefer autohiding the dock instead.

We can introduce a setting to change this behavior if desired later.

Additionally, this commit introduces a mechanism to safely set
app-global presentation options from multiple sources without stepping
on each other.

## Demo



https://github.com/user-attachments/assets/1f5bb945-dca4-49e7-8bcb-95b55524cfb5
  • Loading branch information
mitchellh authored Jan 24, 2025
2 parents 4f857fc + a5a73f8 commit 136d6e9
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 6 deletions.
8 changes: 8 additions & 0 deletions macos/Ghostty.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
Expand Down Expand Up @@ -163,6 +165,8 @@
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -271,13 +275,15 @@
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
A59630962AEE163600D64628 /* HostingWindow.swift */,
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
Expand Down Expand Up @@ -635,6 +641,7 @@
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
Expand All @@ -657,6 +664,7 @@
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
Expand Down
32 changes: 30 additions & 2 deletions macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class QuickTerminalController: BaseTerminalController {
// The active space when the quick terminal was last shown.
private var previousActiveSpace: size_t = 0

/// This is set to true of the dock was autohid when the terminal animated in. This lets us
/// know if we have to unhide when the terminal is animated out.
private var hidDock: Bool = false

/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig

Expand Down Expand Up @@ -224,6 +228,18 @@ class QuickTerminalController: BaseTerminalController {
animateWindowOut(window: window, to: position)
}

private func hideDock() {
guard !hidDock else { return }
NSApp.acquirePresentationOption(.autoHideDock)
hidDock = true
}

private func unhideDock() {
guard hidDock else { return }
NSApp.releasePresentationOption(.autoHideDock)
hidDock = false
}

private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }

Expand All @@ -240,6 +256,12 @@ class QuickTerminalController: BaseTerminalController {
window.makeKeyAndOrderFront(nil)
}

// If our dock position would conflict with our target location then
// we autohide the dock.
if position.conflictsWithDock(on: screen) {
hideDock()
}

// Run the animation that moves our window into the proper place and makes
// it visible.
NSAnimationContext.runAnimationGroup({ context in
Expand All @@ -250,8 +272,11 @@ class QuickTerminalController: BaseTerminalController {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
DispatchQueue.main.async {
// If we canceled our animation in we do nothing
guard self.visible else { return }
// If we canceled our animation clean up some state.
guard self.visible else {
self.unhideDock()
return
}

// After animating in, we reset the window level to a value that
// is above other windows but not as high as popUpMenu. This allows
Expand Down Expand Up @@ -320,6 +345,9 @@ class QuickTerminalController: BaseTerminalController {
}

private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
// If we hid the dock then we unhide it.
unhideDock()

// If the window isn't on our active space then we don't animate, we just
// hide it.
if !window.isOnActiveSpace {
Expand Down
18 changes: 18 additions & 0 deletions macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,22 @@ enum QuickTerminalPosition : String {
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
}
}

func conflictsWithDock(on screen: NSScreen) -> Bool {
// Screen must have a dock for it to conflict
guard screen.hasDock else { return false }

// Get the dock orientation for this screen
guard let orientation = Dock.orientation else { return false }

// Depending on the orientation of the dock, we conflict if our quick terminal
// would potentially "hit" the dock. In the future we should probably consider
// the frame of the quick terminal.
return switch (orientation) {
case .top: self == .top || self == .left || self == .right
case .bottom: self == .bottom || self == .left || self == .right
case .left: self == .top || self == .bottom
case .right: self == .top || self == .bottom
}
}
}
33 changes: 33 additions & 0 deletions macos/Sources/Helpers/Dock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Cocoa

// Private API to get Dock location
@_silgen_name("CoreDockGetOrientationAndPinning")
func CoreDockGetOrientationAndPinning(
_ outOrientation: UnsafeMutablePointer<Int32>,
_ outPinning: UnsafeMutablePointer<Int32>)

// Private API to get the current Dock auto-hide state
@_silgen_name("CoreDockGetAutoHideEnabled")
func CoreDockGetAutoHideEnabled() -> Bool

enum DockOrientation: Int {
case top = 1
case bottom = 2
case left = 3
case right = 4
}

class Dock {
/// Returns the orientation of the dock or nil if it can't be determined.
static var orientation: DockOrientation? {
var orientation: Int32 = 0
var pinning: Int32 = 0
CoreDockGetOrientationAndPinning(&orientation, &pinning)
return .init(rawValue: Int(orientation)) ?? nil
}

/// Returns true if the dock has auto-hide enabled.
static var autoHideEnabled: Bool {
return CoreDockGetAutoHideEnabled()
}
}
8 changes: 4 additions & 4 deletions macos/Sources/Helpers/Fullscreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,21 +307,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// MARK: Dock

private func hideDock() {
NSApp.presentationOptions.insert(.autoHideDock)
NSApp.acquirePresentationOption(.autoHideDock)
}

private func unhideDock() {
NSApp.presentationOptions.remove(.autoHideDock)
NSApp.releasePresentationOption(.autoHideDock)
}

// MARK: Menu

func hideMenu() {
NSApp.presentationOptions.insert(.autoHideMenuBar)
NSApp.acquirePresentationOption(.autoHideMenuBar)
}

func unhideMenu() {
NSApp.presentationOptions.remove(.autoHideMenuBar)
NSApp.releasePresentationOption(.autoHideMenuBar)
}

/// The state that must be saved for non-native fullscreen to exit fullscreen.
Expand Down
31 changes: 31 additions & 0 deletions macos/Sources/Helpers/NSApplication+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Cocoa

extension NSApplication {
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]

/// Add a presentation option to the application and main a reference count so that and equal
/// number of pops is required to disable it. This is useful so that multiple classes can affect global
/// app state without overriding others.
func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
Self.presentationOptionCounts[option, default: 0] += 1
presentationOptions.insert(option)
}

/// See acquirePresentationOption
func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
guard let value = Self.presentationOptionCounts[option] else { return }
guard value > 0 else { return }
if (value == 1) {
presentationOptions.remove(option)
Self.presentationOptionCounts.removeValue(forKey: option)
} else {
Self.presentationOptionCounts[option] = value - 1
}
}
}

extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
}

0 comments on commit 136d6e9

Please sign in to comment.