From 718fa6042c7b2488de2123c8f8283d1052ae3596 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 30 Sep 2024 08:31:11 -0700 Subject: [PATCH] macos: new fullscreen implementation --- macos/Ghostty.xcodeproj/project.pbxproj | 12 +- .../Features/Terminal/TerminalManager.swift | 2 +- macos/Sources/Helpers/Fullscreen.swift | 285 ++++++++++++++++++ macos/Sources/Helpers/FullscreenHandler.swift | 239 --------------- .../Sources/Helpers/FullscreenHandler2.swift | 68 ----- 5 files changed, 290 insertions(+), 316 deletions(-) create mode 100644 macos/Sources/Helpers/Fullscreen.swift delete mode 100644 macos/Sources/Helpers/FullscreenHandler.swift delete mode 100644 macos/Sources/Helpers/FullscreenHandler2.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1057ebb2b..9494b4339 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* 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 */; }; 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,7 +21,7 @@ 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 */; }; + A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52FFF582CAA4FF1000C6A5B /* Fullscreen.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 */; }; @@ -96,7 +95,6 @@ 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 = ""; }; 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 = ""; }; @@ -107,7 +105,7 @@ 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 = ""; }; + A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fullscreen.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 = ""; }; @@ -233,12 +231,11 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( - A52FFF582CAA4FF1000C6A5B /* FullscreenHandler2.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, - 8503D7C62A549C66006CFF3D /* FullscreenHandler.swift */, + A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, @@ -604,9 +601,8 @@ A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, - 8503D7C72A549C66006CFF3D /* FullscreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, - A52FFF592CAA4FF3000C6A5B /* FullscreenHandler2.swift in Sources */, + A52FFF592CAA4FF3000C6A5B /* Fullscreen.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/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index cfd726d3f..6e246de83 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -115,7 +115,7 @@ class TerminalManager { // See: https://github.com/mitchellh/ghostty/issues/392 if let controller = parent.windowController as? TerminalController, let fullscreenStyle = controller.fullscreenStyle, - !fullscreenStyle.supportsTabs { + fullscreenStyle.isFullscreen && !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/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift new file mode 100644 index 000000000..ce91adc14 --- /dev/null +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -0,0 +1,285 @@ +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: + return NonNativeFullscreen(window) + + case .nonNativeVisibleMenu: + return NonNativeFullscreenVisibleMenu(window) + } + } +} + +/// 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) + } +} + +class NonNativeFullscreen: FullscreenStyle { + // Non-native fullscreen never supports tabs because tabs require + // the "titled" style and we don't have it for non-native fullscreen. + var supportsTabs: Bool { false } + + // isFullscreen is dependent on if we have saved state currently. We + // could one day try to do fancier stuff like inspecting the window + // state but there isn't currently a need for it. + var isFullscreen: Bool { savedState != nil } + + // The default properties. Subclasses can override this to change + // behavior. This shouldn't be written to (only computed) because + // it must be immutable. + var properties: Properties { Properties() } + + struct Properties { + var hideMenu: Bool = true + } + + private let window: NSWindow + private var savedState: SavedState? + + required init?(_ window: NSWindow) { + self.window = window + } + + func enter() { + // If we are in fullscreen we don't do it again. + guard !isFullscreen else { return } + + // This is the screen that we're going to go fullscreen on. We use the + // screen the window is currently on. + guard let screen = window.screen else { return } + + // Save the state that we need to exit again + guard let savedState = SavedState(window) else { return } + self.savedState = savedState + + // Change presentation style to hide menu bar and dock if needed + // It's important to do this in two calls, because setting them in a single call guarantees + // that the menu bar will also be hidden on any additional displays (why? nobody knows!) + // When these options are set separately, the menu bar hiding problem will only occur in + // specific scenarios. More investigation is needed to pin these scenarios down precisely, + // but it seems to have something to do with which app had focus last. + // Furthermore, it's much easier to figure out which screen the dock is on if the menubar + // has not yet been hidden, so the order matters here! + + // We always hide the dock. There are many scenarios where we don't + // need to (dock is not on this screen, dock is already hidden, etc.) + // but I don't think there's a downside to just unconditionally doing this. + hideDock() + + // Hide the dock whenever this window becomes focused. + NotificationCenter.default.addObserver( + self, + selector: #selector(hideDock), + name: NSWindow.didBecomeMainNotification, + object: window) + + // Unhide the dock whenever this window becomes unfocused. + NotificationCenter.default.addObserver( + self, + selector: #selector(unhideDock), + name: NSWindow.didResignMainNotification, + object: window) + + // Hide the menu if requested + if (properties.hideMenu) { + self.hideMenu() + + // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones + // This is not the best way to do this, not least because it causes the menu to stay visible + // for a brief moment before being hidden in some cases (e.g. when switching spaces). + // If we end up adding a NSWindowDelegate to PrimaryWindow, then we may be better off + // handling this there. + NotificationCenter.default.addObserver( + self, + selector: #selector(Self.hideMenu), + name: NSWindow.didBecomeMainNotification, + object: window) + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidResignMain), + name: NSWindow.didResignMainNotification, + object: window) + } + + // Being untitled let's our content take up the full frame. + window.styleMask.remove(.titled) + + // Set frame to screen size, accounting for the menu bar if needed + window.setFrame(fullscreenFrame(screen), display: true) + + // Focus window + window.makeKeyAndOrderFront(nil) + } + + func exit() { + guard isFullscreen else { return } + guard let savedState else { return } + + // Reset all of our dock and menu logic + NotificationCenter.default.removeObserver( + self, name: NSWindow.didBecomeMainNotification, object: window) + NotificationCenter.default.removeObserver( + self, name: NSWindow.didResignMainNotification, object: window) + unhideDock() + unhideMenu() + + // Restore our saved state + window.styleMask = savedState.styleMask + window.setFrame(window.frameRect(forContentRect: savedState.contentFrame), display: true) + + // This is a hack that I want to remove from this but for now, we need to + // fix up the titlebar tabs here before we do everything below. + if let window = window as? TerminalWindow, + window.titlebarTabs { + window.titlebarTabs = true + } + + // If the window was previously in a tab group that isn't empty now, + // we re-add it. We have to do this because our process of doing non-native + // fullscreen removes the window from the tab group. + if let tabGroup = savedState.tabGroup, + let tabIndex = savedState.tabGroupIndex, + !tabGroup.windows.isEmpty { + if tabIndex == 0 { + // We were previously the first tab. Add it before ("below") + // the first window in the tab group currently. + tabGroup.windows.first!.addTabbedWindow(window, ordered: .below) + } else if tabIndex <= tabGroup.windows.count { + // We were somewhere in the middle + tabGroup.windows[tabIndex - 1].addTabbedWindow(window, ordered: .above) + } else { + // We were at the end + tabGroup.windows.last!.addTabbedWindow(window, ordered: .below) + } + } + + // Unset our saved state, we're restored! + self.savedState = nil + + // Focus window + window.makeKeyAndOrderFront(nil) + } + + private func fullscreenFrame(_ screen: NSScreen) -> NSRect { + // It would make more sense to use "visibleFrame" but visibleFrame + // will omit space by our dock and isn't updated until an event + // loop tick which we don't have time for. So we use frame and + // calculate this ourselves. + var frame = screen.frame + + if (!properties.hideMenu) { + // We need to subtract the menu height since we're still showing it. + frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 + + // NOTE on macOS bugs: macOS used to have a bug where menuBarHeight + // didn't account for the notch. I reported this as a radar and it + // was fixed at some point. I don't know when that was so I can't + // put an #available check, but it was in a bug fix release so I think + // if a bug is reported to Ghostty we can just advise the user to + // update. + } + + return frame + } + + // MARK: Dock + + @objc private func hideDock() { + NSApp.presentationOptions.insert(.autoHideDock) + } + + @objc private func unhideDock() { + NSApp.presentationOptions.remove(.autoHideDock) + } + + // MARK: Menu + + @objc func hideMenu() { + NSApp.presentationOptions.insert(.autoHideMenuBar) + } + + func unhideMenu() { + NSApp.presentationOptions.remove(.autoHideMenuBar) + } + + @objc func windowDidResignMain(_ notification: Notification) { + unhideMenu() + } + + /// The state that must be saved for non-native fullscreen to exit fullscreen. + class SavedState { + let tabGroup: NSWindowTabGroup? + let tabGroupIndex: Int? + let contentFrame: NSRect + let styleMask: NSWindow.StyleMask + + init?(_ window: NSWindow) { + guard let contentView = window.contentView else { return nil } + + self.tabGroup = window.tabGroup + self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) + self.contentFrame = window.convertToScreen(contentView.frame) + self.styleMask = window.styleMask + } + } +} + +class NonNativeFullscreenVisibleMenu: NonNativeFullscreen { + override var properties: Properties { Properties(hideMenu: false) } +} diff --git a/macos/Sources/Helpers/FullscreenHandler.swift b/macos/Sources/Helpers/FullscreenHandler.swift deleted file mode 100644 index 5311c29be..000000000 --- a/macos/Sources/Helpers/FullscreenHandler.swift +++ /dev/null @@ -1,239 +0,0 @@ -import SwiftUI -import GhosttyKit - -class FullscreenHandler { - var previousTabGroup: NSWindowTabGroup? - var previousTabGroupIndex: Int? - var previousContentFrame: NSRect? - var previousStyleMask: NSWindow.StyleMask? = nil - - // We keep track of whether we entered non-native fullscreen in case - // a user goes to fullscreen, changes the config to disable non-native fullscreen - // and then wants to toggle it off - var isInNonNativeFullscreen: Bool = false - var isInFullscreen: Bool = false - - func toggleFullscreen(window: NSWindow, mode: ghostty_action_fullscreen_e) { - let useNonNativeFullscreen = switch (mode) { - case GHOSTTY_FULLSCREEN_NATIVE: - false - - case GHOSTTY_FULLSCREEN_NON_NATIVE, GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: - true - - default: - false - } - - if isInFullscreen { - if useNonNativeFullscreen || isInNonNativeFullscreen { - leaveFullscreen(window: window) - isInNonNativeFullscreen = false - } else { - // Restore titlebar separator style. See below for explanation. - window.titlebarSeparatorStyle = .automatic - window.toggleFullScreen(nil) - } - isInFullscreen = false - } else { - if useNonNativeFullscreen { - let hideMenu = mode != GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU - enterFullscreen(window: window, hideMenu: hideMenu) - isInNonNativeFullscreen = true - } else { - // 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 - window.toggleFullScreen(nil) - } - isInFullscreen = true - } - } - - func enterFullscreen(window: NSWindow, hideMenu: Bool) { - guard let screen = window.screen else { return } - guard let contentView = window.contentView else { return } - - previousTabGroup = window.tabGroup - previousTabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) - - // Save previous contentViewFrame and screen - previousContentFrame = window.convertToScreen(contentView.frame) - - // Change presentation style to hide menu bar and dock if needed - // It's important to do this in two calls, because setting them in a single call guarantees - // that the menu bar will also be hidden on any additional displays (why? nobody knows!) - // When these options are set separately, the menu bar hiding problem will only occur in - // specific scenarios. More investigation is needed to pin these scenarios down precisely, - // but it seems to have something to do with which app had focus last. - // Furthermore, it's much easier to figure out which screen the dock is on if the menubar - // has not yet been hidden, so the order matters here! - if (shouldHideDock(screen: screen)) { - self.hideDock() - - // Ensure that we always hide the dock bar for this window, but not for non fullscreen ones - NotificationCenter.default.addObserver( - self, - selector: #selector(hideDock), - name: NSWindow.didBecomeMainNotification, - object: window) - NotificationCenter.default.addObserver( - self, - selector: #selector(unHideDock), - name: NSWindow.didResignMainNotification, - object: window) - } - if (hideMenu) { - self.hideMenu() - - // Ensure that we always hide the menu bar for this window, but not for non fullscreen ones - // This is not the best way to do this, not least because it causes the menu to stay visible - // for a brief moment before being hidden in some cases (e.g. when switching spaces). - // If we end up adding a NSWindowDelegate to PrimaryWindow, then we may be better off - // handling this there. - NotificationCenter.default.addObserver( - self, - selector: #selector(Self.hideMenu), - name: NSWindow.didBecomeMainNotification, - object: window) - NotificationCenter.default.addObserver( - self, - selector: #selector(onDidResignMain), - name: NSWindow.didResignMainNotification, - object: window) - } - - // This is important: it gives us the full screen, including the - // notch area on MacBooks. - self.previousStyleMask = window.styleMask - window.styleMask.remove(.titled) - - // Set frame to screen size, accounting for the menu bar if needed - let frame = calculateFullscreenFrame(screen: screen, subtractMenu: !hideMenu) - window.setFrame(frame, display: true) - - // Focus window - window.makeKeyAndOrderFront(nil) - } - - @objc func hideMenu() { - NSApp.presentationOptions.insert(.autoHideMenuBar) - } - - @objc func onDidResignMain(_ notification: Notification) { - guard let resigningWindow = notification.object as? NSWindow else { return } - guard let mainWindow = NSApplication.shared.mainWindow else { return } - - // We're only unhiding the menu bar, if the focus shifted within our application. - // In that case, `mainWindow` is the window of our application the focus shifted - // to. - if !resigningWindow.isEqual(mainWindow) { - NSApp.presentationOptions.remove(.autoHideMenuBar) - } - } - - @objc func hideDock() { - NSApp.presentationOptions.insert(.autoHideDock) - } - - @objc func unHideDock() { - NSApp.presentationOptions.remove(.autoHideDock) - } - - func calculateFullscreenFrame(screen: NSScreen, subtractMenu: Bool)->NSRect { - if (subtractMenu) { - if let menuHeight = NSApp.mainMenu?.menuBarHeight { - var padding: CGFloat = 0 - - // Detect the notch. If there is a safe area on top it includes the - // menu height as a safe area so we also subtract that from it. - if (screen.safeAreaInsets.top > 0) { - padding = screen.safeAreaInsets.top - menuHeight; - } - - return NSMakeRect( - screen.frame.minX, - screen.frame.minY, - screen.frame.width, - screen.frame.height - (menuHeight + padding) - ) - } - } - return screen.frame - } - - func leaveFullscreen(window: NSWindow) { - guard let previousFrame = previousContentFrame else { return } - - // Restore the style mask - window.styleMask = self.previousStyleMask! - - // Restore previous presentation options - NSApp.presentationOptions = [] - - // Stop handling any window focus notifications - // that we use to manage menu bar visibility - NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window) - NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, object: window) - - // Restore frame - window.setFrame(window.frameRect(forContentRect: previousFrame), display: true) - - // Have titlebar tabs set itself up again, since removing the titlebar when fullscreen breaks its constraints. - if let window = window as? TerminalWindow, window.titlebarTabs { - window.titlebarTabs = true - } - - // If the window was previously in a tab group that isn't empty now, we re-add it - if let group = previousTabGroup, let tabIndex = previousTabGroupIndex, !group.windows.isEmpty { - var tabWindow: NSWindow? - var order: NSWindow.OrderingMode = .below - - // Index of the window before `window` - let tabIndexBefore = tabIndex-1 - if tabIndexBefore < 0 { - // If we were the first tab, we add the window *before* (.below) the first one. - tabWindow = group.windows.first - } else if tabIndexBefore < group.windows.count { - // If we weren't the first tab in the group, we add our window after - // the tab that was before it. - tabWindow = group.windows[tabIndexBefore] - order = .above - } else { - // If index is after group, add it after last window - tabWindow = group.windows.last - } - - // Add the window - tabWindow?.addTabbedWindow(window, ordered: order) - } - - // Focus window - window.makeKeyAndOrderFront(nil) - } - - // We only want to hide the dock if it's not already going to be hidden automatically, and if - // it's on the same display as the ghostty window that we want to make fullscreen. - func shouldHideDock(screen: NSScreen) -> Bool { - if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { - if (dockAutohide) { return false } - } - - // There is no public API to directly ask about dock visibility, so we have to figure it out - // by comparing the sizes of visibleFrame (the currently usable area of the screen) and - // frame (the full screen size). We also need to account for the menubar, any inset caused - // by the notch on macbooks, and a little extra padding to compensate for the boundary area - // which triggers showing the dock. - let frame = screen.frame - let visibleFrame = screen.visibleFrame - let menuHeight = NSApp.mainMenu?.menuBarHeight ?? 0 - var notchInset = 0.0 - if #available(macOS 12, *) { - notchInset = screen.safeAreaInsets.top - } - let boundaryAreaPadding = 5.0 - - return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) || visibleFrame.width < frame.width - } -} diff --git a/macos/Sources/Helpers/FullscreenHandler2.swift b/macos/Sources/Helpers/FullscreenHandler2.swift deleted file mode 100644 index ed61b2e57..000000000 --- a/macos/Sources/Helpers/FullscreenHandler2.swift +++ /dev/null @@ -1,68 +0,0 @@ -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) - } -}