diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9a693469d..1057ebb2b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; - 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; + 8503D7C72A549C66006CFF3D /* FullscreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullscreenHandler.swift */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; @@ -22,6 +22,8 @@ A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; }; A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */; }; + A52FFF592CAA4FF3000C6A5B /* FullscreenHandler2.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF582CAA4FF1000C6A5B /* FullscreenHandler2.swift */; }; + A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */; }; A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; }; A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; }; @@ -94,7 +96,7 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; - 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = ""; }; + 8503D7C62A549C66006CFF3D /* FullscreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenHandler.swift; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; @@ -105,6 +107,8 @@ A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = ""; }; A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = ""; }; A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalScreen.swift; sourceTree = ""; }; + A52FFF582CAA4FF1000C6A5B /* FullscreenHandler2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenHandler2.swift; sourceTree = ""; }; + A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FullscreenMode+Extension.swift"; sourceTree = ""; }; A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = ""; }; A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = ""; }; A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = ""; }; @@ -229,11 +233,12 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A52FFF582CAA4FF1000C6A5B /* FullscreenHandler2.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, - 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */, + 8503D7C62A549C66006CFF3D /* FullscreenHandler.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, @@ -314,6 +319,7 @@ A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */, A55685DF29A03A9F004303CE /* AppError.swift */, + A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */, ); path = Ghostty; sourceTree = ""; @@ -575,6 +581,7 @@ AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, + A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, @@ -597,8 +604,9 @@ A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, - 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, + 8503D7C72A549C66006CFF3D /* FullscreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, + A52FFF592CAA4FF3000C6A5B /* FullscreenHandler2.swift in Sources */, AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bb8b5665d..873b93bcb 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -9,7 +9,7 @@ class TerminalController: BaseTerminalController override var windowNibName: NSNib.Name? { "Terminal" } /// Fullscreen state management. - let fullscreenHandler = FullScreenHandler() + private(set) var fullscreenStyle: FullscreenStyle? /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail @@ -534,12 +534,34 @@ class TerminalController: BaseTerminalController // We need a window to fullscreen guard let window = self.window else { return } - // Check whether we use non-native fullscreen - guard let fullscreenModeAny = notification.userInfo?[Ghostty.Notification.FullscreenModeKey] else { return } - guard let fullscreenMode = fullscreenModeAny as? ghostty_action_fullscreen_e else { return } - self.fullscreenHandler.toggleFullscreen(window: window, mode: fullscreenMode) + // Get the fullscreen mode we want to toggle + let fullscreenMode: FullscreenMode + if let any = notification.userInfo?[Ghostty.Notification.FullscreenModeKey], + let mode = any as? FullscreenMode { + fullscreenMode = mode + } else { + Ghostty.logger.warning("no fullscreen mode specified or invalid mode, doing nothing") + return + } - // For some reason focus always gets lost when we toggle fullscreen, so we set it back. + // TODO: handle changing fullscreen modes at runtime + // This is where we'd handle this. + + // Initialize our style for the window. This may fail for various reasons so + // we also guard below. + if self.fullscreenStyle == nil { + self.fullscreenStyle = fullscreenMode.style(for: window) + } + guard let fullscreenStyle else { return } + + if fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } else { + fullscreenStyle.enter() + } + + // For some reason focus can get lost when we change fullscreen. Regardless of + // mode above we just move it back. if let focusedSurface { Ghostty.moveFocus(to: focusedSurface) } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 3930012df..cfd726d3f 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -114,7 +114,8 @@ class TerminalManager { // If our parent is in non-native fullscreen, then new tabs do not work. // See: https://github.com/mitchellh/ghostty/issues/392 if let controller = parent.windowController as? TerminalController, - controller.fullscreenHandler.isInNonNativeFullscreen { + let fullscreenStyle = controller.fullscreenStyle, + !fullscreenStyle.supportsTabs { let alert = NSAlert() alert.messageText = "Cannot Create New Tab" alert.informativeText = "New tabs are unsupported while in non-native fullscreen. Exit fullscreen and try again." diff --git a/macos/Sources/Ghostty/FullscreenMode+Extension.swift b/macos/Sources/Ghostty/FullscreenMode+Extension.swift new file mode 100644 index 000000000..fffd8e84b --- /dev/null +++ b/macos/Sources/Ghostty/FullscreenMode+Extension.swift @@ -0,0 +1,20 @@ +import GhosttyKit + +extension FullscreenMode { + /// Initialize from a Ghostty fullscreen action. + static func from(ghostty: ghostty_action_fullscreen_e) -> Self? { + return switch ghostty { + case GHOSTTY_FULLSCREEN_NATIVE: + .native + + case GHOSTTY_FULLSCREEN_NON_NATIVE: + .nonNative + + case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: + .nonNativeVisibleMenu + + default: + nil + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 05c01a75e..70e4ca94c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -598,7 +598,7 @@ extension Ghostty { private static func toggleFullscreen( _ app: ghostty_app_t, target: ghostty_target_s, - mode: ghostty_action_fullscreen_e) { + mode raw: ghostty_action_fullscreen_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle fullscreen does nothing with an app target") @@ -607,6 +607,10 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let mode = FullscreenMode.from(ghostty: raw) else { + Ghostty.logger.warning("unknow fullscreen mode raw=\(raw.rawValue)") + return + } NotificationCenter.default.post( name: Notification.ghosttyToggleFullscreen, object: surfaceView, diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullscreenHandler.swift similarity index 97% rename from macos/Sources/Helpers/FullScreenHandler.swift rename to macos/Sources/Helpers/FullscreenHandler.swift index d12809d71..5311c29be 100644 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ b/macos/Sources/Helpers/FullscreenHandler.swift @@ -1,7 +1,7 @@ import SwiftUI import GhosttyKit -class FullScreenHandler { +class FullscreenHandler { var previousTabGroup: NSWindowTabGroup? var previousTabGroupIndex: Int? var previousContentFrame: NSRect? @@ -24,7 +24,7 @@ class FullScreenHandler { default: false } - + if isInFullscreen { if useNonNativeFullscreen || isInNonNativeFullscreen { leaveFullscreen(window: window) @@ -75,12 +75,12 @@ class FullScreenHandler { // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones NotificationCenter.default.addObserver( self, - selector: #selector(FullScreenHandler.hideDock), + selector: #selector(hideDock), name: NSWindow.didBecomeMainNotification, object: window) NotificationCenter.default.addObserver( self, - selector: #selector(FullScreenHandler.unHideDock), + selector: #selector(unHideDock), name: NSWindow.didResignMainNotification, object: window) } @@ -94,12 +94,12 @@ class FullScreenHandler { // handling this there. NotificationCenter.default.addObserver( self, - selector: #selector(FullScreenHandler.hideMenu), + selector: #selector(Self.hideMenu), name: NSWindow.didBecomeMainNotification, object: window) NotificationCenter.default.addObserver( self, - selector: #selector(FullScreenHandler.onDidResignMain), + selector: #selector(onDidResignMain), name: NSWindow.didResignMainNotification, object: window) } diff --git a/macos/Sources/Helpers/FullscreenHandler2.swift b/macos/Sources/Helpers/FullscreenHandler2.swift new file mode 100644 index 000000000..ed61b2e57 --- /dev/null +++ b/macos/Sources/Helpers/FullscreenHandler2.swift @@ -0,0 +1,68 @@ +import Cocoa +import GhosttyKit + +/// The fullscreen modes we support define how the fullscreen behaves. +enum FullscreenMode { + case native + case nonNative + case nonNativeVisibleMenu + + /// Initializes the fullscreen style implementation for the mode. This will not toggle any + /// fullscreen properties. This may fail if the window isn't configured properly for a given + /// mode. + func style(for window: NSWindow) -> FullscreenStyle? { + switch self { + case .native: + return NativeFullscreen(window) + + case .nonNative, .nonNativeVisibleMenu: + return nil + } + } +} + +/// Protocol that must be implemented by all fullscreen styles. +protocol FullscreenStyle { + var isFullscreen: Bool { get } + var supportsTabs: Bool { get } + init?(_ window: NSWindow) + func enter() + func exit() +} + +/// macOS native fullscreen. This is the typical behavior you get by pressing the green fullscreen +/// button on regular titlebars. +class NativeFullscreen: FullscreenStyle { + private let window: NSWindow + + var isFullscreen: Bool { window.styleMask.contains(.fullScreen) } + var supportsTabs: Bool { true } + + required init?(_ window: NSWindow) { + // TODO: There are many requirements for native fullscreen we should + // check here such as the stylemask. + + self.window = window + } + + func enter() { + guard !isFullscreen else { return } + + // The titlebar separator shows up erroneously in fullscreen if the tab bar + // is made to appear and then disappear by opening and then closing a tab. + // We get rid of the separator while in fullscreen to prevent this. + window.titlebarSeparatorStyle = .none + + // Enter fullscreen + window.toggleFullScreen(self) + } + + func exit() { + guard isFullscreen else { return } + + // Restore titlebar separator style. See enter for explanation. + window.titlebarSeparatorStyle = .automatic + + window.toggleFullScreen(nil) + } +}