mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 10:46:07 +03:00

This adds support for what's commonly referred to as "non-native
fullscreen": a fullscreen-mode that doesn't use macOS' native fullscreen
mechanism and thus doesn't use animations and a separate space on which
to display the fullscreen window. Instead it's really fast and it allows
the user to `Cmd+tab` to other windows, with the fullscreen-ed window
staying in the background.
Another name for it is "traditional fullscreen" since it was the default
pre Mac OS X Lion, if I remember correctly.
Other applications that offer macOS non-native fullscreen:
- Kitty: https://sw.kovidgoyal.net/kitty/conf/#opt-kitty.macos_traditional_fullscreen
- wezterm: https://wezfurlong.org/wezterm/config/lua/config/native_macos_fullscreen_mode.html
- MacVim
- IINA: fc66b27d50/iina/MainWindowController.swift (L1401-L1423)
- mpv: https://mpv.io/manual/stable/#options-native-fs
- iTerm2
Adding this wasn't straightforward, as it turned out. Mainly because
SwiftUI's app lifecycle management doesn't allow one to use a custom
class for the windows it creates. And without custom classes we'd always
get a warning when entering/leaving fullscreen mode.
So what I did here is the following:
- remove SwiftUI app lifecycle management
- introduce `MainMenu.xib` to define the main menu via interface builder
- add `GhosttyAppController` to handle requests from the app
- add a `main.swift` file to boot up the app without a storyboard and
without SwiftUI lifecycle management
- introduce the `FullScreenHandler` to manage non-native fullscreen -
this is where the "magic" is
But since removing the SwiftUI lifecycle management also means removing
the top-level `App` that means I had to introduce the menu (which I
mentioned), but also tab and window management.
So I also added the `WindowService` which manages open tabs and windows.
It's based on the ideas presented in
https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/
and essentially keeps tracks of windows.
Then there's some auxilliary changes: `CustomWindow` and `WindowController` and so on.
Now everything still works, in addition to non-native fullscreen:
* opening/closing of tabs
* opening/closing of windows
* splits
* `gotoTab`
Worthy of note: when toggling back from non-native fullscreen to
non-fullscreen I had to manually implement the logic to re-add the
window back to a tabgroup. The only other app that supports tabs with
non-native FS is iTerm2 and they have implemented their own tab
management to keep the tab bar even in non-native FS -- that's a bit too
much for me. Every other app has non-native apps and doesn't have to
wory about it.
89 lines
3.2 KiB
Swift
89 lines
3.2 KiB
Swift
import SwiftUI
|
|
|
|
class FullScreenHandler {
|
|
var previousTabGroup: NSWindowTabGroup?
|
|
var previousTabGroupIndex: Int?
|
|
var previousContentFrame: NSRect?
|
|
var previousStyleMask: NSWindow.StyleMask?
|
|
var isInFullscreen: Bool = false
|
|
|
|
func toggleFullscreen(window: NSWindow) {
|
|
if isInFullscreen {
|
|
leaveFullscreen(window: window)
|
|
isInFullscreen = false
|
|
} else {
|
|
enterFullscreen(window: window)
|
|
isInFullscreen = true
|
|
}
|
|
}
|
|
|
|
func enterFullscreen(window: NSWindow) {
|
|
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 style mask
|
|
previousStyleMask = window.styleMask
|
|
// Save previous contentViewFrame and screen
|
|
previousContentFrame = window.convertToScreen(contentView.frame)
|
|
|
|
// Change presentation style to hide menu bar and dock
|
|
NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock]
|
|
// Turn it into borderless window
|
|
window.styleMask.insert(.borderless)
|
|
// This is important: it gives us the full screen, including the
|
|
// notch area on MacBooks.
|
|
window.styleMask.remove(.titled)
|
|
|
|
// Set frame to screen size
|
|
window.setFrame(screen.frame, display: true)
|
|
|
|
// Focus window
|
|
window.makeKeyAndOrderFront(nil)
|
|
}
|
|
|
|
|
|
func leaveFullscreen(window: NSWindow) {
|
|
guard let previousFrame = previousContentFrame else { return }
|
|
guard let previousStyleMask = previousStyleMask else { return }
|
|
|
|
// Restore previous style
|
|
window.styleMask = previousStyleMask
|
|
|
|
// Restore previous presentation options
|
|
NSApp.presentationOptions = []
|
|
|
|
// 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)
|
|
}
|
|
}
|