ghostty/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
Mitchell Hashimoto a5a73f8352 macos: autohide dock if quick terminal would conflict with it
Fixes #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.
2025-01-24 14:51:17 -08:00

140 lines
4.7 KiB
Swift

import Cocoa
enum QuickTerminalPosition : String {
case top
case bottom
case left
case right
case center
/// Set the loaded state for a window.
func setLoaded(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { return }
switch (self) {
case .top, .bottom:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width,
height: screen.frame.height / 4)
), display: false)
case .left, .right:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 4,
height: screen.frame.height)
), display: false)
case .center:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 2,
height: screen.frame.height / 3)
), display: false)
}
}
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
// We always start invisible
window.alphaValue = 0
// Position depends
window.setFrame(.init(
origin: initialOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen) {
// We always end visible
window.alphaValue = 1
// Position depends
window.setFrame(.init(
origin: finalOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
), display: true)
}
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
var finalSize = size
switch (self) {
case .top, .bottom:
finalSize.width = screen.frame.width
case .left, .right:
finalSize.height = screen.visibleFrame.height
case .center:
finalSize.width = screen.frame.width / 2
finalSize.height = screen.frame.height / 3
}
return finalSize
}
/// The initial point origin for this position.
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.frame.maxY)
case .bottom:
return .init(x: screen.frame.minX, y: -window.frame.height)
case .left:
return .init(x: screen.frame.minX-window.frame.width, y: 0)
case .right:
return .init(x: screen.frame.maxX, y: 0)
case .center:
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
}
}
/// The final point origin for this position.
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
case .bottom:
return .init(x: screen.frame.minX, y: screen.frame.minY)
case .left:
return .init(x: screen.frame.minX, y: window.frame.origin.y)
case .right:
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
case .center:
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
}
}
}