From 70acb0d76e2670cf81272349f6297d88318ca1f4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Oct 2024 10:01:28 -0700 Subject: [PATCH] macos: clamp window size to screen size on screen parameter changes Fixes #2462 This sets up a listener for screen parameter changes. This only triggers when a screen is added, removed, or a parameter such as its resolution changes. This doesn't trigger when a window is simply moved from one screen to another. On parameter change, we ensure that the window is within the bounds of the screen. As an exception, if the window was previously already outside the bounds of the screen, we don't move it back in. --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Terminal/BaseTerminalController.swift | 74 +++++++++++++++++++ .../Terminal/TerminalController.swift | 3 +- macos/Sources/Ghostty/Ghostty.Config.swift | 8 +- macos/Sources/Helpers/Xcode.swift | 10 +++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Helpers/Xcode.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b60eb11f5..57070dc47 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 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 */; }; + A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; }; @@ -139,6 +140,7 @@ A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; + A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; 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 = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; @@ -233,6 +235,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, @@ -582,6 +585,7 @@ A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, + A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 77d2d0033..ea35790fd 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -57,6 +57,14 @@ class BaseTerminalController: NSWindowController, /// Event monitor (see individual events for why) private var eventMonitor: Any? = nil + /// The previous frame information from the window + private var savedFrame: SavedFrame? = nil + + struct SavedFrame { + let window: NSRect + let screen: NSRect + } + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } @@ -80,6 +88,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(onConfirmClipboardRequest), name: Ghostty.Notification.confirmClipboard, object: nil) + center.addObserver( + self, + selector: #selector(didChangeScreenParametersNotification), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -89,6 +102,8 @@ class BaseTerminalController: NSWindowController, } deinit { + NotificationCenter.default.removeObserver(self) + if let eventMonitor { NSEvent.removeMonitor(eventMonitor) } @@ -121,6 +136,57 @@ class BaseTerminalController: NSWindowController, } } + // Call this whenever the frame changes + private func windowFrameDidChange() { + // We need to update our saved frame information in case of monitor + // changes (see didChangeScreenParameters notification). + savedFrame = nil + guard let window, let screen = window.screen else { return } + savedFrame = .init(window: window.frame, screen: screen.visibleFrame) + } + + // MARK: Notifications + + @objc private func didChangeScreenParametersNotification(_ notification: Notification) { + // If we have a window that is visible and it is outside the bounds of the + // screen then we clamp it back to within the screen. + guard let window else { return } + guard window.isVisible else { return } + guard let screen = window.screen else { return } + + let visibleFrame = screen.visibleFrame + var newFrame = window.frame + + // Clamp width/height + if newFrame.size.width > visibleFrame.size.width { + newFrame.size.width = visibleFrame.size.width + } + if newFrame.size.height > visibleFrame.size.height { + newFrame.size.height = visibleFrame.size.height + } + + // Ensure the window is on-screen. We only do this if the previous frame + // was also on screen. If a user explicitly wanted their window off screen + // then we let it stay that way. + x: if newFrame.origin.x < visibleFrame.origin.x { + if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x { + break x; + } + + newFrame.origin.x = visibleFrame.origin.x + } + y: if newFrame.origin.y < visibleFrame.origin.y { + if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y { + break y; + } + + newFrame.origin.y = visibleFrame.origin.y + } + + // Apply the new window frame + window.setFrame(newFrame, display: true) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -371,6 +437,14 @@ class BaseTerminalController: NSWindowController, } } + func windowDidResize(_ notification: Notification) { + windowFrameDidChange() + } + + func windowDidMove(_ notification: Notification) { + windowFrameDidChange() + } + // MARK: First Responder @IBAction func close(_ sender: Any) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 70df52b4b..f43454edd 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -358,7 +358,8 @@ class TerminalController: BaseTerminalController { self.fixTabBar() } - func windowDidMove(_ notification: Notification) { + override func windowDidMove(_ notification: Notification) { + super.windowDidMove(notification) self.fixTabBar() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 69c9f992b..29639c39e 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -56,7 +56,13 @@ extension Ghostty { // same filesystem concept. #if os(macOS) ghostty_config_load_default_files(cfg); - ghostty_config_load_cli_args(cfg); + + // We only load CLI args when not running in Xcode because in Xcode we + // pass some special parameters to control the debugger. + if !isRunningInXcode() { + ghostty_config_load_cli_args(cfg); + } + ghostty_config_load_recursive_files(cfg); #endif diff --git a/macos/Sources/Helpers/Xcode.swift b/macos/Sources/Helpers/Xcode.swift new file mode 100644 index 000000000..281bad18b --- /dev/null +++ b/macos/Sources/Helpers/Xcode.swift @@ -0,0 +1,10 @@ +import Foundation + +/// True if we appear to be running in Xcode. +func isRunningInXcode() -> Bool { + if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { + return true + } + + return false +}