macOS: fix surface focus/render state after dragging in to to another window/tab (#12338)

Fixes 2 bugs

1. After dragging a non-focused surface from window A to window B
**quickly without making B the key window**, the focused surface in
window A is not receiving `keyDown` events.


https://github.com/user-attachments/assets/a8861c0a-9300-470d-bf7e-0f32a9ab2cd1

2. #12343 After dragging a surface from tab A to tab B within the same
window, the dragged surface is not rendering input correctly.
> The reason the thread is stuck is because the surface's occlusion
state is set to invisible after target tab's activate while dragging,
since the dragged surface is still in previous tree before dropping, and
after dropping the occlusion state of this surface is not updated to
visible, which causing the surface is accepting input but not rendering.



https://github.com/user-attachments/assets/d67f5dba-8609-4f67-a956-921982faf796
pull/12779/head
Mitchell Hashimoto 2026-05-22 09:05:11 -07:00 committed by GitHub
commit 3e3705b932
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 19 additions and 3 deletions

View File

@ -292,6 +292,7 @@ class BaseTerminalController: NSWindowController,
if to.isEmpty {
focusedSurface = nil
}
syncSurfaceTreeOcclusionState()
}
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
@ -470,8 +471,12 @@ class BaseTerminalController: NSWindowController,
replaceSurfaceTree(
surfaceTree.removing(node),
moveFocusTo: nextFocus,
moveFocusFrom: focusedSurface,
// When a non-focused surface is removed and this window stays as the key window,
// we should refocus the `focusedSurface` to make sure the window's firstResponder remains as it is.
//
// This is a weird workaround, since `resignFirstResponder` wasn't called on `focusedSurface` after drag,
// but the first responder became the window itself.
moveFocusTo: nextFocus ?? focusedSurface,
undoAction: "Close Terminal"
)
}
@ -1277,10 +1282,15 @@ class BaseTerminalController: NSWindowController,
}
func windowDidChangeOcclusionState(_ notification: Notification) {
syncSurfaceTreeOcclusionState()
}
private func syncSurfaceTreeOcclusionState() {
let visible = self.window?.occlusionState.contains(.visible) ?? false
for view in surfaceTree {
if let surface = view.surface {
if let surface = view.surface, view.isWindowVisible != visible {
ghostty_surface_set_occlusion(surface, visible)
view.isWindowVisible = visible
}
}
}

View File

@ -89,6 +89,12 @@ extension Ghostty {
// Whether the cursor is currently visible (not hidden by typing, etc.)
@Published private(set) var cursorVisible: Bool = true
/// Whether the belonging window is visible
///
/// We track this to restore surface occlusion state
/// after this surface is dragged to another window
var isWindowVisible = false
/// The configuration derived from the Ghostty config so we don't need to rely on references.
@Published private(set) var derivedConfig: DerivedConfig