mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: new fullscreen implementation
This commit is contained in:
@ -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 = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
8503D7C62A549C66006CFF3D /* FullscreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenHandler.swift; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
@ -107,7 +105,7 @@
|
||||
A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; };
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
||||
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalScreen.swift; sourceTree = "<group>"; };
|
||||
A52FFF582CAA4FF1000C6A5B /* FullscreenHandler2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenHandler2.swift; sourceTree = "<group>"; };
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fullscreen.swift; sourceTree = "<group>"; };
|
||||
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FullscreenMode+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = "<group>"; };
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -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."
|
||||
|
285
macos/Sources/Helpers/Fullscreen.swift
Normal file
285
macos/Sources/Helpers/Fullscreen.swift
Normal file
@ -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) }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user