mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): avoid throttling keyboard input
This commit is contained in:
parent
9524d3a580
commit
29d9f087cc
3 changed files with 70 additions and 22 deletions
|
|
@ -13,6 +13,7 @@
|
|||
- Fixed alternate-screen Kitty images crossing vertical layout clip boundaries and overlapping sticky regions while scrolling.
|
||||
- Fixed alternate-screen redraws retransmitting Kitty image data when placements move or recently offscreen images return, dropping adjacent row content when reusing placements, rendering fixed-basis scroll content twice per frame, and scanning clipped transcript rows while painting.
|
||||
- Fixed fullscreen transcript navigation leaving no editor-accessible `Home`, `End`, `PageUp`, or `PageDown` variants by adding Ctrl-modified editor bindings ([#7574](https://github.com/earendil-works/pi/issues/7574)).
|
||||
- Fixed keyboard input rendering latency on Windows by letting input preempt the throttled render timer.
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
||||
public onDebug?: () => void;
|
||||
private renderRequested = false;
|
||||
private immediateRenderScheduled = false;
|
||||
private renderTimer: NodeJS.Timeout | undefined;
|
||||
private lastRenderAt = 0;
|
||||
private static readonly MIN_RENDER_INTERVAL_MS = 16;
|
||||
|
|
@ -743,10 +744,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
|
||||
stop(options: TuiStopOptions = {}): void {
|
||||
this.stopped = true;
|
||||
if (this.renderTimer) {
|
||||
clearTimeout(this.renderTimer);
|
||||
this.renderTimer = undefined;
|
||||
}
|
||||
this.cancelRenderTimer();
|
||||
if (this.terminalColorSchemeNotificationsEnabled) {
|
||||
this.terminal.write("\x1b[?2031l");
|
||||
}
|
||||
|
|
@ -759,10 +757,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
renderNow(force = false): void {
|
||||
if (force) this.resetRenderState();
|
||||
this.renderRequested = false;
|
||||
if (this.renderTimer) {
|
||||
clearTimeout(this.renderTimer);
|
||||
this.renderTimer = undefined;
|
||||
}
|
||||
this.cancelRenderTimer();
|
||||
this.lastRenderAt = performance.now();
|
||||
this.doRender();
|
||||
}
|
||||
|
|
@ -770,19 +765,7 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
requestRender(force = false): void {
|
||||
if (force) {
|
||||
this.resetRenderState();
|
||||
if (this.renderTimer) {
|
||||
clearTimeout(this.renderTimer);
|
||||
this.renderTimer = undefined;
|
||||
}
|
||||
this.renderRequested = true;
|
||||
process.nextTick(() => {
|
||||
if (this.stopped || !this.renderRequested) {
|
||||
return;
|
||||
}
|
||||
this.renderRequested = false;
|
||||
this.lastRenderAt = performance.now();
|
||||
this.doRender();
|
||||
});
|
||||
this.requestImmediateRender();
|
||||
return;
|
||||
}
|
||||
if (this.renderRequested) return;
|
||||
|
|
@ -790,6 +773,29 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
process.nextTick(() => this.scheduleRender());
|
||||
}
|
||||
|
||||
private requestImmediateRender(): void {
|
||||
this.cancelRenderTimer();
|
||||
this.renderRequested = true;
|
||||
if (this.immediateRenderScheduled) return;
|
||||
this.immediateRenderScheduled = true;
|
||||
process.nextTick(() => {
|
||||
this.immediateRenderScheduled = false;
|
||||
if (this.stopped || !this.renderRequested) return;
|
||||
// A previously queued scheduleRender() can create a timer before this
|
||||
// callback runs. User input must preempt that throttled frame.
|
||||
this.cancelRenderTimer();
|
||||
this.renderRequested = false;
|
||||
this.lastRenderAt = performance.now();
|
||||
this.doRender();
|
||||
});
|
||||
}
|
||||
|
||||
private cancelRenderTimer(): void {
|
||||
if (!this.renderTimer) return;
|
||||
clearTimeout(this.renderTimer);
|
||||
this.renderTimer = undefined;
|
||||
}
|
||||
|
||||
private scheduleRender(): void {
|
||||
if (this.stopped || this.renderTimer || !this.renderRequested) {
|
||||
return;
|
||||
|
|
@ -882,7 +888,9 @@ export abstract class TuiBase extends Container implements TUI {
|
|||
return;
|
||||
}
|
||||
this.focusedComponent.handleInput(data);
|
||||
this.requestRender();
|
||||
// Keyboard input is latency-sensitive. Avoid the throttled timer path,
|
||||
// where even setTimeout(0) can take a full 16 ms tick on Windows.
|
||||
this.requestImmediateRender();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ class TestComponent implements Component {
|
|||
invalidate(): void {}
|
||||
}
|
||||
|
||||
class InputComponent extends TestComponent {
|
||||
renderCount = 0;
|
||||
|
||||
override render(width: number): string[] {
|
||||
this.renderCount += 1;
|
||||
return super.render(width);
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
this.lines = [data];
|
||||
}
|
||||
}
|
||||
|
||||
class LoggingVirtualTerminal extends VirtualTerminal {
|
||||
private writes: string[] = [];
|
||||
|
||||
|
|
@ -75,6 +88,32 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
|||
return cell.isItalic();
|
||||
}
|
||||
|
||||
describe("TUI render scheduling", () => {
|
||||
it("renders keyboard input without waiting for a throttled frame", async () => {
|
||||
const terminal = new VirtualTerminal(40, 10);
|
||||
const tui: TUI = new TuiMainScreen(terminal);
|
||||
const component = new InputComponent();
|
||||
component.lines = ["initial"];
|
||||
tui.addChild(component);
|
||||
tui.setFocus(component);
|
||||
tui.start();
|
||||
tui.renderNow();
|
||||
const renderCountBeforeInput = component.renderCount;
|
||||
|
||||
// Queue a normal throttled render first. Keyboard input should preempt it.
|
||||
component.lines = ["pending"];
|
||||
tui.requestRender();
|
||||
terminal.sendInput("first");
|
||||
terminal.sendInput("second");
|
||||
terminal.sendInput("typed");
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
|
||||
assert.strictEqual(component.renderCount, renderCountBeforeInput + 1);
|
||||
assert.deepStrictEqual(component.lines, ["typed"]);
|
||||
tui.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TUI debug logging", () => {
|
||||
it("writes redraw logs to the provided directory", async () => {
|
||||
const logDir = mkdtempSync(join(tmpdir(), "pi-tui-log-"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue