mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-23 01:48:37 +03:00

This fixes #786 by adding a check to the callback that unhides the menu bar to only unhide the menu bar if focus was lost to another Ghostty window. That's the desired behaviour: when focus is lost to another app's window, we want the non-native-fullscreen window to stay unchanged in background, but when changing focus to another Ghostty window, we want to unhide the menu bar. Why did this problem even show up? It started to show up with the introduction of the Xib-file based approach, in 3018377. Before that, in 27ddc90, for example, the app would receive the same notifications, but the `.autoHideMenuBar` didn't have an effect on the window. Only after adding the Xib-file did it start to have an effect. So I figured there's two ways we could fix it: 1. Figure out why the `.autoHideMenuBar` now works with Xib-files and suppress it, or 2. Encode in the code the behaviour that we actually want: we only want to show the menu bar when focus shifts to another one of Ghostty's windows, but we want to leave it untouched when focus is lost to another app's window. I went with (2) but I think (1) is also valid and happy to close PR if that's what we want to do.
219 lines
9.5 KiB
Swift
219 lines
9.5 KiB
Swift
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, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
|
let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE
|
|
if isInFullscreen {
|
|
if useNonNativeFullscreen || isInNonNativeFullscreen {
|
|
leaveFullscreen(window: window)
|
|
isInNonNativeFullscreen = false
|
|
} else {
|
|
window.toggleFullScreen(nil)
|
|
}
|
|
isInFullscreen = false
|
|
} else {
|
|
if useNonNativeFullscreen {
|
|
let hideMenu = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU
|
|
enterFullscreen(window: window, hideMenu: hideMenu)
|
|
isInNonNativeFullscreen = true
|
|
} else {
|
|
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 invesitgation 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(FullScreenHandler.hideDock),
|
|
name: NSWindow.didBecomeMainNotification,
|
|
object: window)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(FullScreenHandler.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(FullScreenHandler.hideMenu),
|
|
name: NSWindow.didBecomeMainNotification,
|
|
object: window)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(FullScreenHandler.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)
|
|
|
|
// 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
|
|
}
|
|
}
|