macos: pass last focused surface as env, use for focus detection (#11024)

Fixes #10935

This is a more robust way to detect "is my surface focused" because that
question usually means "is my surface the last focused surface" if a
_different_ surface is not focused. We already have used this pattern
all over but we should extend it to SwiftUI too.
This commit is contained in:
Mitchell Hashimoto
2026-02-25 14:08:34 -08:00
committed by GitHub
2 changed files with 30 additions and 10 deletions

View File

@@ -47,9 +47,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// An optional delegate to receive information about terminal changes.
weak var delegate: (any TerminalViewDelegate)?
// The most recently focused surface, equal to focusedSurface when
// it is non-nil.
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init()
/// The most recently focused surface, equal to `focusedSurface` when it is non-nil.
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView>?
// This seems like a crutch after switching from SwiftUI to AppKit lifecycle.
@FocusState private var focused: Bool
@@ -84,6 +83,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
tree: viewModel.surfaceTree,
action: { delegate?.performSplitAction($0) })
.environmentObject(ghostty)
.ghosttyLastFocusedSurface(lastFocusedSurface)
.focused($focused)
.onAppear { self.focused = true }
.onChange(of: focusedSurface) { newValue in
@@ -101,13 +101,13 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
guard let size = newValue else { return }
self.delegate?.cellSizeDidChange(to: size)
}
.frame(idealWidth: lastFocusedSurface.value?.initialSize?.width,
idealHeight: lastFocusedSurface.value?.initialSize?.height)
.frame(idealWidth: lastFocusedSurface?.value?.initialSize?.width,
idealHeight: lastFocusedSurface?.value?.initialSize?.height)
}
// Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style
.ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])
if let surfaceView = lastFocusedSurface.value {
if let surfaceView = lastFocusedSurface?.value {
TerminalCommandPaletteView(
surfaceView: surfaceView,
isPresented: $viewModel.commandPaletteIsShowing,

View File

@@ -56,6 +56,11 @@ extension Ghostty {
#endif
@EnvironmentObject private var ghostty: Ghostty.App
@Environment(\.ghosttyLastFocusedSurface) private var lastFocusedSurface
private var isFocusedSurface: Bool {
surfaceFocus || lastFocusedSurface?.value === surfaceView
}
var body: some View {
let center = NotificationCenter.default
@@ -217,10 +222,9 @@ extension Ghostty {
}
// If we're part of a split view and don't have focus, we put a semi-transparent
// rectangle above our view to make it look unfocused. We use "surfaceFocus"
// because we want to keep our focused surface dark even if we don't have window
// focus.
if isSplit && !surfaceFocus {
// rectangle above our view to make it look unfocused. We include the last
// focused surface so this still works while SwiftUI focus is temporarily nil.
if isSplit && !isFocusedSurface {
let overlayOpacity = ghostty.config.unfocusedSplitOpacity
if overlayOpacity > 0 {
Rectangle()
@@ -1201,17 +1205,33 @@ private struct GhosttySurfaceViewKey: EnvironmentKey {
static let defaultValue: Ghostty.SurfaceView? = nil
}
private struct GhosttyLastFocusedSurfaceKey: EnvironmentKey {
/// Optional read-only last-focused surface reference. If a surface view is currently focused this
/// is equal to the currently focused surface.
static let defaultValue: Weak<Ghostty.SurfaceView>? = nil
}
extension EnvironmentValues {
var ghosttySurfaceView: Ghostty.SurfaceView? {
get { self[GhosttySurfaceViewKey.self] }
set { self[GhosttySurfaceViewKey.self] = newValue }
}
var ghosttyLastFocusedSurface: Weak<Ghostty.SurfaceView>? {
get { self[GhosttyLastFocusedSurfaceKey.self] }
set { self[GhosttyLastFocusedSurfaceKey.self] = newValue }
}
}
extension View {
func ghosttySurfaceView(_ surfaceView: Ghostty.SurfaceView?) -> some View {
environment(\.ghosttySurfaceView, surfaceView)
}
/// The most recently focused surface (can be currently focused if the surface is currently focused).
func ghosttyLastFocusedSurface(_ surfaceView: Weak<Ghostty.SurfaceView>?) -> some View {
environment(\.ghosttyLastFocusedSurface, surfaceView)
}
}
// MARK: Surface Focus Keys