Fix terminal blanking after full-surface tab drops
This commit is contained in:
parent
8df13d10b0
commit
f33d65f986
5 changed files with 84 additions and 21 deletions
|
|
@ -1825,6 +1825,12 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
|
||||
flushPendingTextIfNeeded()
|
||||
|
||||
// Kick an initial draw after creation/size setup. On some startup paths Ghostty can
|
||||
// miss the first vsync callback and sit on a blank frame until another focus/visibility
|
||||
// transition nudges the renderer.
|
||||
view.forceRefreshSurface()
|
||||
ghostty_surface_refresh(createdSurface)
|
||||
|
||||
#if DEBUG
|
||||
let runtimeFontText = cmuxCurrentSurfaceFontSizePoints(createdSurface).map {
|
||||
String(format: "%.2f", $0)
|
||||
|
|
@ -1886,14 +1892,15 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
|
||||
/// Force a full size recalculation and surface redraw.
|
||||
func forceRefresh() {
|
||||
let hasSurface = surface != nil
|
||||
let viewState: String
|
||||
if let view = attachedView {
|
||||
let inWindow = view.window != nil
|
||||
let bounds = view.bounds
|
||||
let metalOK = (view.layer as? CAMetalLayer) != nil
|
||||
viewState = "inWindow=\(inWindow) bounds=\(bounds) metalOK=\(metalOK)"
|
||||
viewState = "inWindow=\(inWindow) bounds=\(bounds) metalOK=\(metalOK) hasSurface=\(hasSurface)"
|
||||
} else {
|
||||
viewState = "NO_ATTACHED_VIEW"
|
||||
viewState = "NO_ATTACHED_VIEW hasSurface=\(hasSurface)"
|
||||
}
|
||||
#if DEBUG
|
||||
let ts = ISO8601DateFormatter().string(from: Date())
|
||||
|
|
@ -1907,7 +1914,8 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
FileManager.default.createFile(atPath: logPath, contents: line.data(using: .utf8))
|
||||
}
|
||||
#endif
|
||||
guard let view = attachedView,
|
||||
guard let surface,
|
||||
let view = attachedView,
|
||||
view.window != nil,
|
||||
view.bounds.width > 0,
|
||||
view.bounds.height > 0 else {
|
||||
|
|
|
|||
|
|
@ -8409,20 +8409,20 @@ class TerminalController {
|
|||
// Socket commands are line-based; allow callers to express control chars with backslash escapes.
|
||||
let text = unescapeSocketText(raw)
|
||||
|
||||
var result = "ERROR: No window"
|
||||
DispatchQueue.main.sync {
|
||||
// Like simulate_shortcut, prefer a visible window so debug automation doesn't
|
||||
// fail during key window transitions.
|
||||
guard let window = NSApp.keyWindow
|
||||
?? NSApp.mainWindow
|
||||
?? NSApp.windows.first(where: { $0.isVisible })
|
||||
?? NSApp.windows.first else { return }
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
guard let fr = window.firstResponder else {
|
||||
result = "ERROR: No first responder"
|
||||
return
|
||||
}
|
||||
var result = "ERROR: No window"
|
||||
DispatchQueue.main.sync {
|
||||
// Like simulate_shortcut, prefer a visible window so debug automation doesn't
|
||||
// fail during key window transitions.
|
||||
guard let window = NSApp.keyWindow
|
||||
?? NSApp.mainWindow
|
||||
?? NSApp.windows.first(where: { $0.isVisible })
|
||||
?? NSApp.windows.first else { return }
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
guard let fr = window.firstResponder else {
|
||||
result = "ERROR: No first responder"
|
||||
return
|
||||
}
|
||||
|
||||
if let client = fr as? NSTextInputClient {
|
||||
client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
|
||||
|
|
@ -8430,7 +8430,22 @@ class TerminalController {
|
|||
return
|
||||
}
|
||||
|
||||
// Fall back to the responder chain insertText action.
|
||||
// If workspace handoff temporarily leaves a non-terminal first responder,
|
||||
// route debug typing to the selected terminal's focused panel directly.
|
||||
if let tabManager,
|
||||
let tabId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == tabId }),
|
||||
let panelId = tab.focusedPanelId,
|
||||
let terminalPanel = tab.terminalPanel(for: panelId),
|
||||
!terminalPanel.hostedView.isSurfaceViewFirstResponder() {
|
||||
// Match Enter semantics expected by tests/debug tooling when bypassing AppKit.
|
||||
let directText = text.replacingOccurrences(of: "\n", with: "\r")
|
||||
terminalPanel.surface.sendText(directText)
|
||||
result = "OK"
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to the responder-chain insertText action.
|
||||
(fr as? NSResponder)?.insertText(text)
|
||||
result = "OK"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1254,6 +1254,11 @@ final class WindowTerminalPortal: NSObject {
|
|||
)
|
||||
#endif
|
||||
hostedView.isHidden = false
|
||||
// A reveal can happen without any frame delta (same targetFrame), which means the
|
||||
// normal frame-change refresh path won't run. Nudge geometry + redraw so newly
|
||||
// revealed terminals don't sit on a stale/blank IOSurface until later focus churn.
|
||||
hostedView.reconcileGeometryNow()
|
||||
hostedView.refreshSurfaceNow()
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -2265,7 +2265,11 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
|
||||
hostedView.reconcileGeometryNow()
|
||||
terminalPanel.surface.forceRefresh()
|
||||
if hasSurface {
|
||||
terminalPanel.surface.forceRefresh()
|
||||
} else if isAttached && hasUsableBounds {
|
||||
terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
return needsFollowUpPass
|
||||
|
|
@ -2306,6 +2310,31 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func scheduleMovedTerminalRefresh(panelId: UUID) {
|
||||
guard terminalPanel(for: panelId) != nil else { return }
|
||||
|
||||
// Force an NSViewRepresentable update after drag/move reparenting. This keeps
|
||||
// portal host binding current when a pane auto-closes during tab moves.
|
||||
terminalPanel(for: panelId)?.requestViewReattach()
|
||||
|
||||
let runRefreshPass: (TimeInterval) -> Void = { [weak self] delay in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
guard let self, let panel = self.terminalPanel(for: panelId) else { return }
|
||||
panel.hostedView.reconcileGeometryNow()
|
||||
if panel.surface.surface != nil {
|
||||
panel.surface.forceRefresh()
|
||||
} else {
|
||||
panel.surface.requestBackgroundSurfaceStartIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run once immediately and once on the next turn so rapid split close/reparent
|
||||
// sequences still get a post-layout redraw.
|
||||
runRefreshPass(0)
|
||||
runRefreshPass(0.03)
|
||||
}
|
||||
|
||||
private func closeTabs(_ tabIds: [TabID], skipPinned: Bool = true) {
|
||||
for tabId in tabIds {
|
||||
if skipPinned,
|
||||
|
|
@ -2928,12 +2957,18 @@ extension Workspace: BonsplitDelegate {
|
|||
)
|
||||
#endif
|
||||
applyTabSelection(tabId: tab.id, inPane: destination)
|
||||
#if DEBUG
|
||||
let movedPanelIdAfter = panelIdFromSurfaceId(tab.id)
|
||||
#endif
|
||||
if let movedPanelId = panelIdFromSurfaceId(tab.id) {
|
||||
scheduleMovedTerminalRefresh(panelId: movedPanelId)
|
||||
}
|
||||
#if DEBUG
|
||||
let selectedAfter = controller.selectedTab(inPane: destination)
|
||||
.map { String(String(describing: $0.id).prefix(5)) } ?? "nil"
|
||||
let focusedPaneAfter = controller.focusedPaneId?.id.uuidString.prefix(5) ?? "nil"
|
||||
let focusedPanelAfter = focusedPanelId?.uuidString.prefix(5) ?? "nil"
|
||||
let movedPanelFocused = (movedPanelId != nil && movedPanelId == focusedPanelId) ? 1 : 0
|
||||
let movedPanelFocused = (movedPanelIdAfter != nil && movedPanelIdAfter == focusedPanelId) ? 1 : 0
|
||||
dlog(
|
||||
"split.moveTab.state.after idx=\(debugDidMoveTabEventCount) panel=\(movedPanel) " +
|
||||
"destSelected=\(selectedAfter) focusedPane=\(focusedPaneAfter) focusedPanel=\(focusedPanelAfter) " +
|
||||
|
|
|
|||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit f24ba9222651ecc170869662eec9a5880404a82c
|
||||
Subproject commit 21db26f8a6a0c7707af10da672c0d7cf07076c66
|
||||
Loading…
Add table
Add a link
Reference in a new issue