Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macos: quick terminal supports fullscreen #2341

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,26 @@ class QuickTerminalController: BaseTerminalController {
) {
self.position = position
super.init(ghostty, baseConfig: base, surfaceTree: tree)

// Setup our notifications for behaviors
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(onToggleFullscreen),
name: Ghostty.Notification.ghosttyToggleFullscreen,
object: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view")
}

deinit {
// Remove all of our notificationcenter subscriptions
let center = NotificationCenter.default
center.removeObserver(self)
}

// MARK: NSWindowController

override func windowDidLoad() {
Expand Down Expand Up @@ -199,6 +213,11 @@ class QuickTerminalController: BaseTerminalController {
// We always animate out to whatever screen the window is actually on.
guard let screen = window.screen ?? NSScreen.main else { return }

// If we are in fullscreen, then we exit fullscreen.
if let fullscreenStyle, fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
}

// If we have a previously active application, restore focus to it. We
// do this BEFORE the animation below because when the animation completes
// macOS will bring forward another window.
Expand Down Expand Up @@ -239,4 +258,19 @@ class QuickTerminalController: BaseTerminalController {
alert.alertStyle = .warning
alert.beginSheetModal(for: window)
}

@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.toggleFullscreen(surface: surface)
}

// MARK: Notifications

@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }

// We ignore the requested mode and always use non-native for the quick terminal
toggleFullscreen(mode: .nonNative)
}
}
69 changes: 67 additions & 2 deletions macos/Sources/Features/Terminal/BaseTerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import GhosttyKit
/// view the TerminalView SwiftUI view must be used and this class is the view model and
/// delegate.
///
/// Special considerations to implement:
///
/// - Fullscreen: you must manually listen for the right notification and implement the
/// callback that calls toggleFullscreen on this base class.
///
/// Notably, things this class does NOT implement (not exhaustive):
///
/// - Tabbing, because there are many ways to get tabbed behavior in macOS and we
/// don't want to be opinionated about it.
/// - Fullscreen
/// - Window restoration or save state
/// - Window visual styles (such as titlebar colors)
///
Expand All @@ -25,7 +29,8 @@ class BaseTerminalController: NSWindowController,
NSWindowDelegate,
TerminalViewDelegate,
TerminalViewModel,
ClipboardConfirmationViewDelegate
ClipboardConfirmationViewDelegate,
FullscreenDelegate
{
/// The app instance that this terminal view will represent.
let ghostty: Ghostty.App
Expand All @@ -46,6 +51,9 @@ class BaseTerminalController: NSWindowController,
/// The clipboard confirmation window, if shown.
private var clipboardConfirmation: ClipboardConfirmationController? = nil

/// Fullscreen state management.
private(set) var fullscreenStyle: FullscreenStyle?

required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported for this view")
}
Expand Down Expand Up @@ -123,6 +131,63 @@ class BaseTerminalController: NSWindowController,

func zoomStateDidChange(to: Bool) {}

// MARK: Fullscreen

/// Toggle fullscreen for the given mode.
func toggleFullscreen(mode: FullscreenMode) {
// We need a window to fullscreen
guard let window = self.window else { return }

// If we have a previous fullscreen style initialized, we want to check if
// 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
// switch the handler.
var newStyle = mode.style(for: window)
newStyle?.delegate = self
old: if let oldStyle = self.fullscreenStyle {
// If we're not fullscreen, we can nil it out so we get the new style
if !oldStyle.isFullscreen {
self.fullscreenStyle = newStyle
break old
}

assert(oldStyle.isFullscreen)

// We consider our mode changed if the types change (obvious) but
// also if its nil (not obvious) because nil means that the style has
// likely changed but we don't support it.
if newStyle == nil || type(of: newStyle) != type(of: oldStyle) {
// Our mode changed. Exit fullscreen (since we're toggling anyways)
// and then unset the style so that we replace it next time.
oldStyle.exit()
self.fullscreenStyle = nil

// We're done
return
}

// Style is the same.
} else {
// We have no previous style
self.fullscreenStyle = newStyle
}
guard let fullscreenStyle else { return }

if fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
} else {
fullscreenStyle.enter()
}
}

func fullscreenDidChange() {
// For some reason focus can get lost when we change fullscreen. Regardless of
// mode above we just move it back.
if let focusedSurface {
Ghostty.moveFocus(to: focusedSurface)
}
}

// MARK: Clipboard Confirmation

@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
Expand Down
65 changes: 1 addition & 64 deletions macos/Sources/Features/Terminal/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ import SwiftUI
import GhosttyKit

/// A classic, tabbed terminal experience.
class TerminalController: BaseTerminalController,
FullscreenDelegate
{
class TerminalController: BaseTerminalController {
override var windowNibName: NSNib.Name? { "Terminal" }

/// Fullscreen state management.
private(set) var fullscreenStyle: FullscreenStyle?

/// This is set to true when we care about frame changes. This is a small optimization since
/// this controller registers a listener for ALL frame change notifications and this lets us bail
/// early if we don't care.
Expand Down Expand Up @@ -200,63 +195,6 @@ class TerminalController: BaseTerminalController,
}
}

// MARK: Fullscreen

/// Toggle fullscreen for the given mode.
func toggleFullscreen(mode: FullscreenMode) {
// We need a window to fullscreen
guard let window = self.window else { return }

// If we have a previous fullscreen style initialized, we want to check if
// 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
// switch the handler.
var newStyle = mode.style(for: window)
newStyle?.delegate = self
old: if let oldStyle = self.fullscreenStyle {
// If we're not fullscreen, we can nil it out so we get the new style
if !oldStyle.isFullscreen {
self.fullscreenStyle = newStyle
break old
}

assert(oldStyle.isFullscreen)

// We consider our mode changed if the types change (obvious) but
// also if its nil (not obvious) because nil means that the style has
// likely changed but we don't support it.
if newStyle == nil || type(of: newStyle) != type(of: oldStyle) {
// Our mode changed. Exit fullscreen (since we're toggling anyways)
// and then unset the style so that we replace it next time.
oldStyle.exit()
self.fullscreenStyle = nil

// We're done
return
}

// Style is the same.
} else {
// We have no previous style
self.fullscreenStyle = newStyle
}
guard let fullscreenStyle else { return }

if fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
} else {
fullscreenStyle.enter()
}
}

func fullscreenDidChange() {
// For some reason focus can get lost when we change fullscreen. Regardless of
// mode above we just move it back.
if let focusedSurface {
Ghostty.moveFocus(to: focusedSurface)
}
}

//MARK: - NSWindowController

override func windowWillLoad() {
Expand Down Expand Up @@ -584,7 +522,6 @@ class TerminalController: BaseTerminalController,
targetWindow.makeKeyAndOrderFront(nil)
}


@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }
Expand Down
5 changes: 4 additions & 1 deletion src/input/Binding.zig
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,12 @@ pub const Action = union(enum) {
///
/// - It is a singleton; only one instance can exist at a time.
/// - It does not support tabs.
/// - It does not support fullscreen.
/// - It will not be restored when the application is restarted
/// (for systems that support window restoration).
/// - It supports fullscreen, but fullscreen will always be a non-native
/// fullscreen (macos-non-native-fullscreen = true). This only applies
/// to the quick terminal window. This is a requirement due to how
/// the quick terminal is rendered.
///
/// See the various configurations for the quick terminal in the
/// configuration file to customize its behavior.
Expand Down
Loading