mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
macos: style changes for quick terminal sizing
This commit is contained in:
@@ -217,48 +217,30 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
|
||||
// Allow unrestricted resizing - users have full control
|
||||
return frameSize
|
||||
}
|
||||
|
||||
override func windowDidResize(_ notification: Notification) {
|
||||
guard let window = notification.object as? NSWindow,
|
||||
window == self.window,
|
||||
visible,
|
||||
!isHandlingResize else { return }
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
// Prevent recursive loops
|
||||
isHandlingResize = true
|
||||
defer { isHandlingResize = false }
|
||||
|
||||
// For centered positions (top, bottom, center), we need to recenter the window
|
||||
// when it's manually resized to maintain proper positioning
|
||||
switch position {
|
||||
case .top, .bottom, .center:
|
||||
recenterWindow(window)
|
||||
// For centered positions (top, bottom, center), we need to recenter the window
|
||||
// when it's manually resized to maintain proper positioning
|
||||
let newOrigin = position.centeredOrigin(for: window, on: screen)
|
||||
window.setFrameOrigin(newOrigin)
|
||||
case .left, .right:
|
||||
// For side positions, we may need to adjust vertical centering
|
||||
recenterWindowVertically(window)
|
||||
let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen)
|
||||
window.setFrameOrigin(newOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
private func recenterWindow(_ window: NSWindow) {
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
isHandlingResize = true
|
||||
defer { isHandlingResize = false }
|
||||
|
||||
let newOrigin = position.centeredOrigin(for: window, on: screen)
|
||||
window.setFrameOrigin(newOrigin)
|
||||
}
|
||||
|
||||
private func recenterWindowVertically(_ window: NSWindow) {
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
isHandlingResize = true
|
||||
defer { isHandlingResize = false }
|
||||
|
||||
let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen)
|
||||
window.setFrameOrigin(newOrigin)
|
||||
}
|
||||
|
||||
// MARK: Base Controller Overrides
|
||||
|
||||
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||
@@ -376,17 +358,17 @@ class QuickTerminalController: BaseTerminalController {
|
||||
|
||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Grab our last closed frame to use, and clear our state since we're animating in.
|
||||
let lastClosedFrame = self.lastClosedFrame
|
||||
self.lastClosedFrame = nil
|
||||
|
||||
// Restore our previous frame if we have one
|
||||
var preserveSize: NSSize? = nil
|
||||
if let lastClosedFrame {
|
||||
window.setFrame(lastClosedFrame, display: false)
|
||||
preserveSize = lastClosedFrame.size
|
||||
self.lastClosedFrame = nil
|
||||
}
|
||||
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize)
|
||||
// Move our window off screen to the initial animation position.
|
||||
position.setInitial(
|
||||
in: window,
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
@@ -417,7 +399,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize)
|
||||
position.setFinal(
|
||||
in: window.animator(),
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
}, completionHandler: {
|
||||
// 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.
|
||||
@@ -499,7 +485,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// the user's preferred window size and position for when the quick
|
||||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
// would reset the window to its minimum content size.
|
||||
lastClosedFrame = window.frame
|
||||
if window.frame.width > 0 && window.frame.height > 0 {
|
||||
lastClosedFrame = window.frame
|
||||
}
|
||||
|
||||
// If we hid the dock then we unhide it.
|
||||
hiddenDock = nil
|
||||
@@ -541,7 +529,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: window.frame.size)
|
||||
position.setInitial(
|
||||
in: window.animator(),
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: window.frame)
|
||||
}, completionHandler: {
|
||||
// This causes the window to be removed from the screen list and macOS
|
||||
// handles what should be focused next.
|
||||
|
@@ -7,49 +7,57 @@ enum QuickTerminalPosition : String {
|
||||
case right
|
||||
case center
|
||||
|
||||
/// Set the loaded state for a window.
|
||||
/// Set the loaded state for a window. This should only be called when the window is first loaded,
|
||||
/// usually in `windowDidLoad` or in a similar callback. This is the initial state.
|
||||
func setLoaded(_ window: NSWindow, size: QuickTerminalSize) {
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
let dimensions = size.calculate(position: self, screenDimensions: screen.frame.size)
|
||||
window.setFrame(.init(
|
||||
origin: window.frame.origin,
|
||||
size: .init(
|
||||
width: dimensions.width,
|
||||
height: dimensions.height)
|
||||
size: size.calculate(position: self, screenDimensions: screen.frame.size)
|
||||
), display: false)
|
||||
}
|
||||
|
||||
/// Set the initial state for a window for animating out of this position.
|
||||
func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) {
|
||||
// We always start invisible
|
||||
/// Set the initial state for a window NOT yet into position (either before animating in or
|
||||
/// after animating out).
|
||||
func setInitial(
|
||||
in window: NSWindow,
|
||||
on screen: NSScreen,
|
||||
terminalSize: QuickTerminalSize,
|
||||
closedFrame: NSRect? = nil
|
||||
) {
|
||||
// Invisible
|
||||
window.alphaValue = 0
|
||||
|
||||
// Position depends
|
||||
window.setFrame(.init(
|
||||
origin: initialOrigin(for: window, on: screen),
|
||||
size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize)
|
||||
size: closedFrame?.size ?? configuredFrameSize(
|
||||
on: screen,
|
||||
terminalSize: terminalSize)
|
||||
), display: false)
|
||||
}
|
||||
|
||||
/// Set the final state for a window in this position.
|
||||
func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) {
|
||||
func setFinal(
|
||||
in window: NSWindow,
|
||||
on screen: NSScreen,
|
||||
terminalSize: QuickTerminalSize,
|
||||
closedFrame: NSRect? = nil
|
||||
) {
|
||||
// We always end visible
|
||||
window.alphaValue = 1
|
||||
|
||||
// Position depends
|
||||
window.setFrame(.init(
|
||||
origin: finalOrigin(for: window, on: screen),
|
||||
size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize)
|
||||
size: closedFrame?.size ?? configuredFrameSize(
|
||||
on: screen,
|
||||
terminalSize: terminalSize)
|
||||
), display: true)
|
||||
}
|
||||
|
||||
/// Get the configured frame size for initial positioning and animations.
|
||||
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize, preserveExisting: NSSize? = nil) -> NSSize {
|
||||
// If we have existing dimensions from manual resizing, preserve them
|
||||
if let existing = preserveExisting, existing.width > 0 && existing.height > 0 {
|
||||
return existing
|
||||
}
|
||||
|
||||
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
|
||||
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size)
|
||||
return NSSize(width: dimensions.width, height: dimensions.height)
|
||||
}
|
||||
|
@@ -1,5 +1,11 @@
|
||||
import GhosttyKit
|
||||
|
||||
/// Represents the Ghostty `quick-terminal-size` configuration. See the documentation for
|
||||
/// that for more details on exactly how it works. Some of those docs will be reproduced in various comments
|
||||
/// in this file but that is the best source of truth for it.
|
||||
///
|
||||
/// The size determines the size of the quick terminal along the primary and secondary axis. The primary and
|
||||
/// secondary axis is defined by the `quick-terminal-position`.
|
||||
struct QuickTerminalSize {
|
||||
let primary: Size?
|
||||
let secondary: Size?
|
||||
@@ -41,23 +47,20 @@ struct QuickTerminalSize {
|
||||
}
|
||||
}
|
||||
|
||||
struct Dimensions {
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
}
|
||||
|
||||
func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> Dimensions {
|
||||
let dims = Dimensions(width: screenDimensions.width, height: screenDimensions.height)
|
||||
/// This is an almost direct port of th Zig function QuickTerminalSize.calculate
|
||||
func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize {
|
||||
let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height)
|
||||
|
||||
switch position {
|
||||
case .left, .right:
|
||||
return Dimensions(
|
||||
return CGSize(
|
||||
width: primary?.toPixels(parentDimension: dims.width) ?? 400,
|
||||
height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height
|
||||
)
|
||||
|
||||
case .top, .bottom:
|
||||
return Dimensions(
|
||||
return CGSize(
|
||||
width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width,
|
||||
height: primary?.toPixels(parentDimension: dims.height) ?? 400
|
||||
)
|
||||
@@ -65,13 +68,13 @@ struct QuickTerminalSize {
|
||||
case .center:
|
||||
if dims.width >= dims.height {
|
||||
// Landscape
|
||||
return Dimensions(
|
||||
return CGSize(
|
||||
width: primary?.toPixels(parentDimension: dims.width) ?? 800,
|
||||
height: secondary?.toPixels(parentDimension: dims.height) ?? 400
|
||||
)
|
||||
} else {
|
||||
// Portrait
|
||||
return Dimensions(
|
||||
return CGSize(
|
||||
width: secondary?.toPixels(parentDimension: dims.width) ?? 400,
|
||||
height: primary?.toPixels(parentDimension: dims.height) ?? 800
|
||||
)
|
||||
|
@@ -7318,6 +7318,7 @@ pub const QuickTerminalSize = struct {
|
||||
|
||||
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||
}
|
||||
|
||||
test "parse QuickTerminalSize" {
|
||||
const testing = std.testing;
|
||||
var v: QuickTerminalSize = undefined;
|
||||
|
Reference in New Issue
Block a user