mirror of
https://github.com/Eugeny/tabby.git
synced 2026-08-24 10:14:31 -05:00
another attempt at tab blanking/flicker
This commit is contained in:
@@ -191,6 +191,21 @@ export class AppRootComponent {
|
||||
this.ready = true
|
||||
this.app.emitReady()
|
||||
})
|
||||
|
||||
// While the window is being dragged, suppress the split-pane layout
|
||||
// transition (see splitTab.component.scss). Animating pane geometry on
|
||||
// every resize frame triggers a full-layer repaint that flickers the
|
||||
// terminal; the transition is only wanted for split/close/maximize.
|
||||
let resizeEndTimeout: any = null
|
||||
window.addEventListener('resize', () => {
|
||||
document.body.classList.add('resizing')
|
||||
if (resizeEndTimeout) {
|
||||
clearTimeout(resizeEndTimeout)
|
||||
}
|
||||
resizeEndTimeout = setTimeout(() => {
|
||||
document.body.classList.remove('resizing')
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
|
||||
@HostListener('dragover')
|
||||
|
||||
@@ -28,3 +28,9 @@
|
||||
::ng-deep .no-animations split-tab > .child {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// While the window is being resized, don't animate pane geometry — the
|
||||
// transition would otherwise repaint the whole layer each frame and flicker.
|
||||
::ng-deep .resizing split-tab > .child {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ export class XTermFrontend extends Frontend {
|
||||
// - wheel/keyboard event listeners (below)
|
||||
// - explicit scrollToBottom() calls
|
||||
|
||||
this.resizeHandler = () => {
|
||||
const doResize = () => {
|
||||
try {
|
||||
if (this.xterm.element && getComputedStyle(this.xterm.element).getPropertyValue('height') !== 'auto') {
|
||||
const savedPinned = this.pinnedToBottom
|
||||
@@ -229,6 +229,13 @@ export class XTermFrontend extends Frontend {
|
||||
const targetY = Math.min(savedViewportY, maxScroll)
|
||||
this.xterm.scrollToLine(targetY)
|
||||
}
|
||||
|
||||
// fitAddon.fit() resizes the renderer's drawing buffer,
|
||||
// which blanks it synchronously, but xterm only repaints on
|
||||
// the next animation frame — leaving one blank frame that
|
||||
// reads as flicker during a window drag. Force the repaint
|
||||
// now (after scrolling settles) to close that gap.
|
||||
this.xtermCore._renderService?._renderRows(0, this.xterm.rows - 1)
|
||||
}
|
||||
} catch (e) {
|
||||
// tends to throw when element wasn't shown yet
|
||||
@@ -236,6 +243,36 @@ export class XTermFrontend extends Frontend {
|
||||
}
|
||||
}
|
||||
|
||||
// Rate-limit reflows during a window drag. The window 'resize' event and
|
||||
// the ResizeObserver fire many times per frame; each reflow resizes the
|
||||
// renderer's drawing buffer and re-uploads the glyph atlas texture. At
|
||||
// full frame rate a fast drag issues reflows faster than the GPU can
|
||||
// finish one, so frames composite with the text not yet repainted —
|
||||
// visible as a flicker that only shows up when dragging quickly (slow
|
||||
// drags leave enough time between reflows). Capping the reflow rate and
|
||||
// always running a trailing fit keeps the final size correct without
|
||||
// outrunning the renderer. Tune RESIZE_MIN_INTERVAL if needed.
|
||||
const RESIZE_MIN_INTERVAL = 32
|
||||
let resizePending = false
|
||||
let lastResize = 0
|
||||
const runResize = () => {
|
||||
resizePending = false
|
||||
lastResize = Date.now()
|
||||
doResize()
|
||||
}
|
||||
this.resizeHandler = () => {
|
||||
if (resizePending) {
|
||||
return
|
||||
}
|
||||
resizePending = true
|
||||
const wait = Math.max(0, RESIZE_MIN_INTERVAL - (Date.now() - lastResize))
|
||||
if (wait > 0) {
|
||||
setTimeout(() => requestAnimationFrame(runResize), wait)
|
||||
} else {
|
||||
requestAnimationFrame(runResize)
|
||||
}
|
||||
}
|
||||
|
||||
const oldKeyUp = this.xtermCore._keyUp.bind(this.xtermCore)
|
||||
this.xtermCore._keyUp = (e: KeyboardEvent) => {
|
||||
this.xtermCore.updateCursorStyle(e)
|
||||
@@ -361,7 +398,7 @@ export class XTermFrontend extends Frontend {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
this.resizeObserver = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler()))
|
||||
this.resizeObserver = new window['ResizeObserver'](() => this.resizeHandler())
|
||||
this.resizeObserver.observe(host)
|
||||
}
|
||||
|
||||
@@ -634,9 +671,19 @@ export class XTermFrontend extends Frontend {
|
||||
* hidden, and flushes any GPU context recovery deferred until now.
|
||||
*/
|
||||
reactivate (): void {
|
||||
if (this.pendingRendererRecovery) {
|
||||
// An app- or window-level GPU reset can blank the canvas without firing
|
||||
// xterm's per-canvas contextlost event, so pendingRendererRecovery stays
|
||||
// unset. Treat a WebGL frontend that has lost its addon as needing
|
||||
// recovery too, so a shown-but-blank pane always gets its context back
|
||||
// instead of relying on a manual window resize.
|
||||
if (this.pendingRendererRecovery || this.enableWebGL && !this.webGLAddon) {
|
||||
this.pendingRendererRecovery = true
|
||||
this.recoverRenderer()
|
||||
} else {
|
||||
// The pane is shown with a live renderer, so any earlier transient
|
||||
// losses shouldn't count against a future recovery — reset the budget
|
||||
// to avoid permanently downgrading the pane to the DOM renderer.
|
||||
this.rendererRecoveryAttempts = 0
|
||||
this.redraw()
|
||||
}
|
||||
}
|
||||
@@ -682,6 +729,11 @@ export class XTermFrontend extends Frontend {
|
||||
private redraw (): void {
|
||||
const renderService = this.xtermCore._renderService
|
||||
renderService?.clear()
|
||||
// handleResize() alone is a no-op when cols/rows are unchanged
|
||||
// resizeHandler() runs a real itAddon.fit() followed
|
||||
// by an unconditional viewport._refresh(),
|
||||
// forcing a full repaint
|
||||
this.resizeHandler()
|
||||
renderService?.handleResize(this.xterm.cols, this.xterm.rows)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user