mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-26 19:38:37 +03:00
macos: handle non-native fullscreen changing screens
This commit is contained in:
@ -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 {
|
||||||
|
@ -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() {
|
||||||
|
Reference in New Issue
Block a user