Fix resize artifacts: rAF-throttle drag + force xterm repaint
Two related fixes for stale glyphs / visual artifacts while dragging a gutter: - Gutter.tsx: pointermove now writes the new ratio into a ref and schedules a single requestAnimationFrame flush per frame. Without this, setTree fires 60+ times per second during a drag and React + ResizeObserver + xterm's DOM renderer get out of sync. The pointerup handler flushes any pending ratio so the final position always lands. - XtermPane.tsx: the ResizeObserver callback now also rAF-coalesces AND calls term.refresh(0, term.rows - 1) after fit.fit(). xterm's DOM renderer doesn't reliably repaint freed-up rows after a shrink, so the explicit refresh wipes any stale glyphs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a4cd82440b
commit
94bdb884ad
2 changed files with 48 additions and 11 deletions
|
|
@ -165,16 +165,27 @@ export default function XtermPane({
|
||||||
if (ta) ta.addEventListener("focus", () => onFocusRef.current?.(), true);
|
if (ta) ta.addEventListener("focus", () => onFocusRef.current?.(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-fit on container resize; forward new size to the PTY.
|
// Re-fit on container resize; forward new size to the PTY. We
|
||||||
|
// coalesce multiple ResizeObserver firings into one rAF so a single
|
||||||
|
// gutter drag tick = one fit/resize/repaint, then explicitly call
|
||||||
|
// term.refresh() so the DOM renderer fully repaints (without this,
|
||||||
|
// shrinking a pane sometimes leaves stale glyphs in the freed rows).
|
||||||
|
let resizeRaf: number | null = null;
|
||||||
ro = new ResizeObserver(() => {
|
ro = new ResizeObserver(() => {
|
||||||
try {
|
if (resizeRaf != null) return;
|
||||||
fit.fit();
|
resizeRaf = requestAnimationFrame(() => {
|
||||||
if (paneId != null && term) {
|
resizeRaf = null;
|
||||||
void resizePane(paneId, term.cols, term.rows);
|
if (!term) return;
|
||||||
|
try {
|
||||||
|
fit.fit();
|
||||||
|
if (paneId != null) {
|
||||||
|
void resizePane(paneId, term.cols, term.rows);
|
||||||
|
}
|
||||||
|
term.refresh(0, term.rows - 1);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("resize failed", e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
});
|
||||||
console.warn("resize failed", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
ro.observe(container);
|
ro.observe(container);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,20 @@ export default function Gutter({
|
||||||
}) {
|
}) {
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const draggingRef = useRef(false);
|
const draggingRef = useRef(false);
|
||||||
|
// rAF-throttle the ratio updates so we don't fire React + ResizeObserver
|
||||||
|
// 60+ times per second during a drag (xterm's DOM renderer can't keep up
|
||||||
|
// and leaves artifacts).
|
||||||
|
const pendingRatioRef = useRef<number | null>(null);
|
||||||
|
const rafRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const flushPending = useCallback(() => {
|
||||||
|
rafRef.current = null;
|
||||||
|
const r = pendingRatioRef.current;
|
||||||
|
if (r != null) {
|
||||||
|
pendingRatioRef.current = null;
|
||||||
|
onRatioChange(info.splitId, r);
|
||||||
|
}
|
||||||
|
}, [info.splitId, onRatioChange]);
|
||||||
|
|
||||||
const onPointerDown = useCallback((e: PointerEvent<HTMLDivElement>) => {
|
const onPointerDown = useCallback((e: PointerEvent<HTMLDivElement>) => {
|
||||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
|
@ -46,9 +60,12 @@ export default function Gutter({
|
||||||
? (xFrac - pb.left) / pb.width
|
? (xFrac - pb.left) / pb.width
|
||||||
: (yFrac - pb.top) / pb.height;
|
: (yFrac - pb.top) / pb.height;
|
||||||
const ratio = Math.max(0.05, Math.min(0.95, rawRatio));
|
const ratio = Math.max(0.05, Math.min(0.95, rawRatio));
|
||||||
onRatioChange(info.splitId, ratio);
|
pendingRatioRef.current = ratio;
|
||||||
|
if (rafRef.current == null) {
|
||||||
|
rafRef.current = requestAnimationFrame(flushPending);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[containerRef, info, onRatioChange],
|
[containerRef, info, flushPending],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPointerUp = useCallback((e: PointerEvent<HTMLDivElement>) => {
|
const onPointerUp = useCallback((e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
|
@ -56,7 +73,16 @@ export default function Gutter({
|
||||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||||
draggingRef.current = false;
|
draggingRef.current = false;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
}, []);
|
// Make sure the final ratio lands even if the rAF hadn't fired.
|
||||||
|
if (rafRef.current != null) {
|
||||||
|
cancelAnimationFrame(rafRef.current);
|
||||||
|
rafRef.current = null;
|
||||||
|
}
|
||||||
|
if (pendingRatioRef.current != null) {
|
||||||
|
onRatioChange(info.splitId, pendingRatioRef.current);
|
||||||
|
pendingRatioRef.current = null;
|
||||||
|
}
|
||||||
|
}, [info.splitId, onRatioChange]);
|
||||||
|
|
||||||
const isH = info.orientation === "h";
|
const isH = info.orientation === "h";
|
||||||
// Visible 4px line, but the draggable hitbox is wider for grabbability.
|
// Visible 4px line, but the draggable hitbox is wider for grabbability.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue