Merge pull request #423 from mitchellh/split-focus

macos: fade unfocused splits
This commit is contained in:
Mitchell Hashimoto
2023-09-10 09:20:00 -07:00
committed by GitHub
2 changed files with 82 additions and 61 deletions

View File

@ -183,6 +183,11 @@ extension Ghostty {
} }
return clone return clone
} }
/// True if there are no neighbors
func isEmpty() -> Bool {
return self.previous == nil && self.next == nil
}
} }
} }
@ -341,7 +346,7 @@ extension Ghostty {
let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface) let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface)
let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface) let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface)
SurfaceWrapper(surfaceView: leaf.surface) SurfaceWrapper(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty())
.onReceive(pub) { onNewSplit(notification: $0) } .onReceive(pub) { onNewSplit(notification: $0) }
.onReceive(pubClose) { onClose(notification: $0) } .onReceive(pubClose) { onClose(notification: $0) }
.onReceive(pubFocus) { onMoveFocus(notification: $0) } .onReceive(pubFocus) { onMoveFocus(notification: $0) }

View File

@ -38,6 +38,10 @@ extension Ghostty {
// remains the same, the surface that is being rendered remains the same. // remains the same, the surface that is being rendered remains the same.
@ObservedObject var surfaceView: SurfaceView @ObservedObject var surfaceView: SurfaceView
// True if this surface is part of a split view. This is important to know so
// we know whether to dim the surface out of focus.
var isSplit: Bool = false
// Maintain whether our view has focus or not // Maintain whether our view has focus or not
@FocusState private var surfaceFocus: Bool @FocusState private var surfaceFocus: Bool
@ -49,73 +53,85 @@ extension Ghostty {
private var hasFocus: Bool { surfaceFocus && windowFocus } private var hasFocus: Bool { surfaceFocus && windowFocus }
var body: some View { var body: some View {
// We use a GeometryReader to get the frame bounds so that our metal surface ZStack {
// is up to date. See TerminalSurfaceView for why we don't use the NSView // We use a GeometryReader to get the frame bounds so that our metal surface
// resize callback. // is up to date. See TerminalSurfaceView for why we don't use the NSView
GeometryReader { geo in // resize callback.
// We use these notifications to determine when the window our surface is GeometryReader { geo in
// attached to is or is not focused. // We use these notifications to determine when the window our surface is
let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView) // attached to is or is not focused.
let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification) let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView)
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification) let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)
let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size) Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
.focused($surfaceFocus) .focused($surfaceFocus)
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
.focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceView, surfaceView)
.onReceive(pubBecomeKey) { notification in .onReceive(pubBecomeKey) { notification in
guard let window = notification.object as? NSWindow else { return } guard let window = notification.object as? NSWindow else { return }
guard let surfaceWindow = surfaceView.window else { return } guard let surfaceWindow = surfaceView.window else { return }
windowFocus = surfaceWindow == window windowFocus = surfaceWindow == window
}
.onReceive(pubResign) { notification in
guard let window = notification.object as? NSWindow else { return }
guard let surfaceWindow = surfaceView.window else { return }
if (surfaceWindow == window) {
windowFocus = false
} }
} .onReceive(pubResign) { notification in
.onReceive(pubBecomeFocused) { notification in guard let window = notification.object as? NSWindow else { return }
// We only want to run this on older macOS versions where the .focused guard let surfaceWindow = surfaceView.window else { return }
// method doesn't work properly. See the dispatch of this notification if (surfaceWindow == window) {
// for more information. windowFocus = false
if #available(macOS 13, *) { return } }
}
.onReceive(pubBecomeFocused) { notification in
// We only want to run this on older macOS versions where the .focused
// method doesn't work properly. See the dispatch of this notification
// for more information.
if #available(macOS 13, *) { return }
DispatchQueue.main.async {
surfaceFocus = true
}
}
.onAppear() {
// Welcome to the SwiftUI bug house of horrors. On macOS 12 (at least
// 12.5.1, didn't test other versions), the order in which the view
// is added to the window hierarchy is such that $surfaceFocus is
// not set to true for the first surface in a window. As a result,
// new windows are key (they have window focus) but the terminal surface
// does not have surface until the user clicks. Bad!
//
// There is a very real chance that I am doing something wrong, but it
// works great as-is on macOS 13, so I've instead decided to make the
// older macOS hacky. A workaround is on initial appearance to "steal
// focus" under certain conditions that seem to imply we're in the
// screwy state.
if #available(macOS 13, *) {
// If we're on a more modern version of macOS, do nothing.
return
}
if #available(macOS 12, *) {
// On macOS 13, the view is attached to a window at this point,
// so this is one extra check that we're a new view and behaving odd.
guard surfaceView.window == nil else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
surfaceFocus = true surfaceFocus = true
} }
} }
.onAppear() {
// Welcome to the SwiftUI bug house of horrors. On macOS 12 (at least
// 12.5.1, didn't test other versions), the order in which the view
// is added to the window hierarchy is such that $surfaceFocus is
// not set to true for the first surface in a window. As a result,
// new windows are key (they have window focus) but the terminal surface
// does not have surface until the user clicks. Bad!
//
// There is a very real chance that I am doing something wrong, but it
// works great as-is on macOS 13, so I've instead decided to make the
// older macOS hacky. A workaround is on initial appearance to "steal
// focus" under certain conditions that seem to imply we're in the
// screwy state.
if #available(macOS 13, *) {
// If we're on a more modern version of macOS, do nothing.
return
}
if #available(macOS 12, *) {
// On macOS 13, the view is attached to a window at this point,
// so this is one extra check that we're a new view and behaving odd.
guard surfaceView.window == nil else { return }
DispatchQueue.main.async {
surfaceFocus = true
}
}
// I don't know how older macOS versions behave but Ghostty only // I don't know how older macOS versions behave but Ghostty only
// supports back to macOS 12 so its moot. // supports back to macOS 12 so its moot.
} }
}
.ghosttySurfaceView(surfaceView)
// 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()
.fill(.white)
.opacity(0.15)
}
} }
.ghosttySurfaceView(surfaceView)
} }
} }