"Return to Default Size" implementation (#5974)

## Added Support for "Return To Default Size"

This update introduces support for the **"Return To Default Size"**
feature.

### Fixes  
- Resolves [#1328](https://github.com/ghostty-org/ghostty/issues/1328)

### Screenshots  

| Description | Screenshot |
|------------|------------|
| **Ghostty** | <img width="1084" alt="Screenshot 2025-02-24 at 21 15
38"
src="https://github.com/user-attachments/assets/4657ccdb-9c7a-4884-873c-bbe0f30f9400"
/> |
| **After changing window size** | <img width="1155" alt="Screenshot
2025-02-24 at 21 16 00"
src="https://github.com/user-attachments/assets/9b3931f2-1c4b-4f86-8d56-8892bd5675cc"
/> |
| **Native Terminal App (for reference)** | <img width="630"
alt="Screenshot 2025-02-24 at 21 16 20"
src="https://github.com/user-attachments/assets/ae049931-b74d-4246-a9e7-d9be079b1a24"
/> |
This commit is contained in:
Mitchell Hashimoto
2025-02-28 15:06:24 -08:00
committed by GitHub
3 changed files with 115 additions and 29 deletions

View File

@ -388,6 +388,13 @@
</menu> </menu>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="dgt-Tx-d4e"/> <menuItem isSeparatorItem="YES" id="dgt-Tx-d4e"/>
<menuItem title="Return To Default Size" id="Gbx-Vi-OGC" userLabel="Return To Default Size">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="returnToDefaultSize:" target="-1" id="Bpt-GO-UU1"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="CpM-rI-Sc1"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ"> <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>

View File

@ -27,6 +27,9 @@ class TerminalController: BaseTerminalController {
/// The notification cancellable for focused surface property changes. /// The notification cancellable for focused surface property changes.
private var surfaceAppearanceCancellables: Set<AnyCancellable> = [] private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
/// This will be set to the initial frame of the window from the xib on load.
private var initialFrame: NSRect? = nil
init(_ ghostty: Ghostty.App, init(_ ghostty: Ghostty.App,
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
withSurfaceTree tree: Ghostty.SplitNode? = nil withSurfaceTree tree: Ghostty.SplitNode? = nil
@ -308,6 +311,55 @@ class TerminalController: BaseTerminalController {
y: frame.maxY - (CGFloat(y) + window.frame.height))) y: frame.maxY - (CGFloat(y) + window.frame.height)))
} }
/// Returns the default size of the window. This is contextual based on the focused surface because
/// the focused surface may specify a different default size than others.
private var defaultSize: NSRect? {
guard let screen = window?.screen ?? NSScreen.main else { return nil }
if derivedConfig.maximize {
return screen.visibleFrame
} else if let focusedSurface,
let initialSize = focusedSurface.initialSize {
// Get the current frame of the window
guard var frame = window?.frame else { return nil }
// Calculate the chrome size (window size minus view size)
let chromeWidth = frame.size.width - focusedSurface.frame.size.width
let chromeHeight = frame.size.height - focusedSurface.frame.size.height
// Calculate the new width and height, clamping to the screen's size
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
// Update the frame size while keeping the window's position intact
frame.size.width = newWidth
frame.size.height = newHeight
// Ensure the window doesn't go outside the screen boundaries
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
return frame
}
guard let initialFrame else { return nil }
guard var frame = window?.frame else { return nil }
// Calculate the new width and height, clamping to the screen's size
let newWidth = min(initialFrame.size.width, screen.visibleFrame.width)
let newHeight = min(initialFrame.size.height, screen.visibleFrame.height)
// Update the frame size while keeping the window's position intact
frame.size.width = newWidth
frame.size.height = newHeight
// Ensure the window doesn't go outside the screen boundaries
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
return frame
}
//MARK: - NSWindowController //MARK: - NSWindowController
override func windowWillLoad() { override func windowWillLoad() {
@ -356,6 +408,9 @@ class TerminalController: BaseTerminalController {
super.windowDidLoad() super.windowDidLoad()
guard let window = window as? TerminalWindow else { return } guard let window = window as? TerminalWindow else { return }
// Store our initial frame so we can know our default later.
initialFrame = window.frame
// I copy this because we may change the source in the future but also because // I copy this because we may change the source in the future but also because
// I regularly audit our codebase for "ghostty.config" access because generally // I regularly audit our codebase for "ghostty.config" access because generally
// you shouldn't use it. Its safe in this case because for a new window we should // you shouldn't use it. Its safe in this case because for a new window we should
@ -372,36 +427,15 @@ class TerminalController: BaseTerminalController {
// If window decorations are disabled, remove our title // If window decorations are disabled, remove our title
if (!config.windowDecorations) { window.styleMask.remove(.titled) } if (!config.windowDecorations) { window.styleMask.remove(.titled) }
// If we have only a single surface (no splits) and that surface requested // If we have only a single surface (no splits) and there is a default size then
// an initial size then we set it here now. // we should resize to that default size.
if case let .leaf(leaf) = surfaceTree { if case let .leaf(leaf) = surfaceTree {
if config.maximize { // If this is our first surface then our focused surface will be nil
if let screen = window.screen ?? NSScreen.main { // so we force the focused surface to the leaf.
window.setFrame(screen.visibleFrame, display: true) focusedSurface = leaf.surface
}
} else if let initialSize = leaf.surface.initialSize,
let screen = window.screen ?? NSScreen.main {
// Get the current frame of the window
var frame = window.frame
// Calculate the chrome size (window size minus view size) if let defaultSize {
let chromeWidth = frame.size.width - leaf.surface.frame.size.width window.setFrame(defaultSize, display: true)
let chromeHeight = frame.size.height - leaf.surface.frame.size.height
// Calculate the new width and height, clamping to the screen's size
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
// Update the frame size while keeping the window's position intact
frame.size.width = newWidth
frame.size.height = newHeight
// Ensure the window doesn't go outside the screen boundaries
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
// Set the updated frame to the window
window.setFrame(frame, display: true)
} }
} }
@ -578,6 +612,11 @@ class TerminalController: BaseTerminalController {
window.close() window.close()
} }
@IBAction func returnToDefaultSize(_ sender: Any) {
guard let defaultSize else { return }
window?.setFrame(defaultSize, display: true)
}
@IBAction override func closeWindow(_ sender: Any?) { @IBAction override func closeWindow(_ sender: Any?) {
guard let window = window else { return } guard let window = window else { return }
guard let tabGroup = window.tabGroup else { guard let tabGroup = window.tabGroup else {
@ -811,15 +850,55 @@ class TerminalController: BaseTerminalController {
struct DerivedConfig { struct DerivedConfig {
let backgroundColor: Color let backgroundColor: Color
let macosTitlebarStyle: String let macosTitlebarStyle: String
let maximize: Bool
init() { init() {
self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundColor = Color(NSColor.windowBackgroundColor)
self.macosTitlebarStyle = "system" self.macosTitlebarStyle = "system"
self.maximize = false
} }
init(_ config: Ghostty.Config) { init(_ config: Ghostty.Config) {
self.backgroundColor = config.backgroundColor self.backgroundColor = config.backgroundColor
self.macosTitlebarStyle = config.macosTitlebarStyle self.macosTitlebarStyle = config.macosTitlebarStyle
self.maximize = config.maximize
} }
} }
} }
extension TerminalController: NSMenuItemValidation {
func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action {
case #selector(returnToDefaultSize):
guard let window else { return false }
// Native fullscreen windows can't revert to default size.
if window.styleMask.contains(.fullScreen) {
return false
}
// If we're fullscreen at all then we can't change size
if fullscreenStyle?.isFullscreen ?? false {
return false
}
// If our window is already the default size or we don't have a
// default size, then disable.
guard let defaultSize,
window.frame.size != .init(
width: defaultSize.size.width,
height: defaultSize.size.height
)
else {
return false
}
return true
default:
return true
}
}
}

View File

@ -140,7 +140,7 @@ extension Ghostty {
guard let ptr = v else { return "" } guard let ptr = v else { return "" }
return String(cString: ptr) return String(cString: ptr)
} }
var windowPositionX: Int16? { var windowPositionX: Int16? {
guard let config = self.config else { return nil } guard let config = self.config else { return nil }
var v: Int16 = 0 var v: Int16 = 0