macos: handle non-native fullscreen changing screens

This commit is contained in:
Mitchell Hashimoto
2024-09-30 13:21:16 -07:00
parent 35462331ae
commit 408c33e174
2 changed files with 60 additions and 15 deletions

View File

@ -4,7 +4,8 @@ import SwiftUI
import GhosttyKit import GhosttyKit
/// A classic, tabbed terminal experience. /// A classic, tabbed terminal experience.
class TerminalController: BaseTerminalController class TerminalController: BaseTerminalController,
FullscreenDelegate
{ {
override var windowNibName: NSNib.Name? { "Terminal" } override var windowNibName: NSNib.Name? { "Terminal" }
@ -199,6 +200,8 @@ class TerminalController: BaseTerminalController
} }
} }
// MARK: Fullscreen
/// Toggle fullscreen for the given mode. /// Toggle fullscreen for the given mode.
func toggleFullscreen(mode: FullscreenMode) { func toggleFullscreen(mode: FullscreenMode) {
// We need a window to fullscreen // We need a window to fullscreen
@ -208,11 +211,12 @@ class TerminalController: BaseTerminalController
// our mode changed. If it changed and we're in fullscreen, we exit so we can // our mode changed. If it changed and we're in fullscreen, we exit so we can
// toggle it next time. If it changed and we're not in fullscreen we can just // toggle it next time. If it changed and we're not in fullscreen we can just
// switch the handler. // switch the handler.
let newStyle = mode.style(for: window) var newStyle = mode.style(for: window)
newStyle?.delegate = self
old: if let oldStyle = self.fullscreenStyle { old: if let oldStyle = self.fullscreenStyle {
// If we're not fullscreen, we can nil it out so we get the new style // If we're not fullscreen, we can nil it out so we get the new style
if !oldStyle.isFullscreen { if !oldStyle.isFullscreen {
self.fullscreenStyle = nil self.fullscreenStyle = newStyle
break old break old
} }
@ -227,28 +231,25 @@ class TerminalController: BaseTerminalController
oldStyle.exit() oldStyle.exit()
self.fullscreenStyle = nil self.fullscreenStyle = nil
// Fix our focus
if let focusedSurface {
Ghostty.moveFocus(to: focusedSurface)
}
// We're done // We're done
return return
} }
// Style is the same. // Style is the same.
} else { } else {
// No old style, so set to our new style. // We have no previous style
self.fullscreenStyle = newStyle self.fullscreenStyle = newStyle
} }
guard let fullscreenStyle else { return } guard let fullscreenStyle else { return }
if fullscreenStyle.isFullscreen { if fullscreenStyle.isFullscreen {
fullscreenStyle.exit() fullscreenStyle.exit()
} else { } else {
fullscreenStyle.enter() fullscreenStyle.enter()
} }
}
func fullscreenDidChange() {
// For some reason focus can get lost when we change fullscreen. Regardless of // For some reason focus can get lost when we change fullscreen. Regardless of
// mode above we just move it back. // mode above we just move it back.
if let focusedSurface { if let focusedSurface {

View File

@ -26,6 +26,7 @@ enum FullscreenMode {
/// Protocol that must be implemented by all fullscreen styles. /// Protocol that must be implemented by all fullscreen styles.
protocol FullscreenStyle { protocol FullscreenStyle {
var delegate: FullscreenDelegate? { get set }
var isFullscreen: Bool { get } var isFullscreen: Bool { get }
var supportsTabs: Bool { get } var supportsTabs: Bool { get }
init?(_ window: NSWindow) init?(_ window: NSWindow)
@ -33,11 +34,23 @@ protocol FullscreenStyle {
func exit() func exit()
} }
/// Delegate that can be implemented for fullscreen implementations.
protocol FullscreenDelegate: AnyObject {
/// Called whenever the fullscreen state changed. You can call isFullscreen to see
/// the current state.
func fullscreenDidChange()
}
extension FullscreenDelegate {
func fullscreenDidChange() {}
}
/// macOS native fullscreen. This is the typical behavior you get by pressing the green fullscreen /// macOS native fullscreen. This is the typical behavior you get by pressing the green fullscreen
/// button on regular titlebars. /// button on regular titlebars.
class NativeFullscreen: FullscreenStyle { class NativeFullscreen: FullscreenStyle {
private let window: NSWindow private let window: NSWindow
weak var delegate: FullscreenDelegate?
var isFullscreen: Bool { window.styleMask.contains(.fullScreen) } var isFullscreen: Bool { window.styleMask.contains(.fullScreen) }
var supportsTabs: Bool { true } var supportsTabs: Bool { true }
@ -58,6 +71,9 @@ class NativeFullscreen: FullscreenStyle {
// Enter fullscreen // Enter fullscreen
window.toggleFullScreen(self) window.toggleFullScreen(self)
// Notify the delegate
delegate?.fullscreenDidChange()
} }
func exit() { func exit() {
@ -67,10 +83,15 @@ class NativeFullscreen: FullscreenStyle {
window.titlebarSeparatorStyle = .automatic window.titlebarSeparatorStyle = .automatic
window.toggleFullScreen(nil) window.toggleFullScreen(nil)
// Notify the delegate
delegate?.fullscreenDidChange()
} }
} }
class NonNativeFullscreen: FullscreenStyle { class NonNativeFullscreen: FullscreenStyle {
weak var delegate: FullscreenDelegate?
// Non-native fullscreen never supports tabs because tabs require // Non-native fullscreen never supports tabs because tabs require
// the "titled" style and we don't have it for non-native fullscreen. // the "titled" style and we don't have it for non-native fullscreen.
var supportsTabs: Bool { false } var supportsTabs: Bool { false }
@ -157,6 +178,13 @@ class NonNativeFullscreen: FullscreenStyle {
object: window) object: window)
} }
// When we change screens we need to redo everything.
NotificationCenter.default.addObserver(
self,
selector: #selector(windowDidChangeScreen),
name: NSWindow.didChangeScreenNotification,
object: window)
// Being untitled let's our content take up the full frame. // Being untitled let's our content take up the full frame.
window.styleMask.remove(.titled) window.styleMask.remove(.titled)
@ -169,6 +197,7 @@ class NonNativeFullscreen: FullscreenStyle {
// https://github.com/ghostty-org/ghostty/issues/1996 // https://github.com/ghostty-org/ghostty/issues/1996
DispatchQueue.main.async { DispatchQueue.main.async {
self.window.setFrame(self.fullscreenFrame(screen), display: true) self.window.setFrame(self.fullscreenFrame(screen), display: true)
self.delegate?.fullscreenDidChange()
} }
} }
@ -176,11 +205,10 @@ class NonNativeFullscreen: FullscreenStyle {
guard isFullscreen else { return } guard isFullscreen else { return }
guard let savedState else { return } guard let savedState else { return }
// Reset all of our dock and menu logic // Remove all our notifications
NotificationCenter.default.removeObserver( NotificationCenter.default.removeObserver(self)
self, name: NSWindow.didBecomeMainNotification, object: window)
NotificationCenter.default.removeObserver( // Unhide our elements
self, name: NSWindow.didResignMainNotification, object: window)
unhideDock() unhideDock()
unhideMenu() unhideMenu()
@ -219,6 +247,9 @@ class NonNativeFullscreen: FullscreenStyle {
// Focus window // Focus window
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
// Notify the delegate
self.delegate?.fullscreenDidChange()
} }
private func fullscreenFrame(_ screen: NSScreen) -> NSRect { private func fullscreenFrame(_ screen: NSScreen) -> NSRect {
@ -243,6 +274,19 @@ class NonNativeFullscreen: FullscreenStyle {
return frame return frame
} }
// MARK: Window Events
@objc func windowDidChangeScreen(_ notification: Notification) {
guard isFullscreen else { return }
// When we change screens, we simply exit fullscreen. Changing
// screens shouldn't naturally be possible, it can only happen
// through external window managers. There's a lot of accounting
// to do to get the screen change right so instead of breaking
// we just exit out. The user can re-enter fullscreen thereafter.
exit()
}
// MARK: Dock // MARK: Dock
@objc private func hideDock() { @objc private func hideDock() {