mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
Merge pull request #711: fix(mobile): hold the PTY rows while the on-screen keyboard is up
This commit is contained in:
commit
98526527c8
26 files changed
+1227
-15
No files matched your search
@@ -206,6 +206,7 @@ Implements the xterm.js `ITerminalAddon` interface. It deliberately does **not**
|
||||
| `removeChar()` | `'pending'` \| `'flushed'` \| `false` | Remove the last character and drop any IME composition. See [backspace handling](#backspace-handling). |
|
||||
| `clear()` | `void` | Clear all state, the composition included, and hide the overlay. Call on Enter, Ctrl+C, Escape. |
|
||||
| `setComposition(text)` | `void` | Show text an IME is still composing as an underlined tail after the typed text. Pass `''` to remove it. See [IME composition](#ime-composition). |
|
||||
| `setRowLimit(limit)` | `void` | Narrow the row bound the overlay keeps itself on screen with (normally `terminal.rows`) to the first `limit` screen rows, so it grows upward from the last of them; `null` restores all rows, and a limit above `terminal.rows` changes nothing. For hosts that keep the terminal taller than the box showing it, e.g. while an on-screen keyboard covers the bottom. |
|
||||
|
||||
### Backspace handling
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
private _bufferDetectDone = false;
|
||||
// IME text still being composed: drawn after the pending text, never sent.
|
||||
private _composition = '';
|
||||
// Rows from the top of the screen that are actually visible (see setRowLimit);
|
||||
// null means all of them.
|
||||
private _rowLimit: number | null = null;
|
||||
|
||||
// Render cache
|
||||
private _lastRenderKey = '';
|
||||
@@ -329,6 +332,35 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bound the layout to the first `limit` rows of the screen, or to all of them
|
||||
* with `null`. It narrows the bound the overlay already keeps itself on
|
||||
* screen with (`totalRows`, see _layoutRows), never adds a second one.
|
||||
*
|
||||
* For a host that keeps the terminal taller than the box showing it and
|
||||
* scrolls the screen element instead (e.g. holding the PTY's rows while an
|
||||
* on-screen keyboard covers the bottom): the composer must grow upward from
|
||||
* the last row the user can SEE, not from the terminal's last row, which is
|
||||
* under the keyboard.
|
||||
*/
|
||||
setRowLimit(limit: number | null): void {
|
||||
const next = limit !== null && Number.isFinite(limit) && limit > 0 ? Math.trunc(limit) : null;
|
||||
if (next === this._rowLimit) return;
|
||||
this._rowLimit = next;
|
||||
this.rerender();
|
||||
}
|
||||
|
||||
/**
|
||||
* The `totalRows` bound handed to renderOverlay: the one place that decides
|
||||
* where the block stops growing downward. The terminal's rows, narrowed by
|
||||
* setRowLimit when the host shows fewer; a limit above the terminal's rows
|
||||
* changes nothing.
|
||||
*/
|
||||
private _layoutRows(): number {
|
||||
const rows = this._terminal!.rows;
|
||||
return this._rowLimit !== null ? Math.min(this._rowLimit, rows) : rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read font properties from the terminal and re-render.
|
||||
* Call after font size changes, theme changes, etc.
|
||||
@@ -621,9 +653,11 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
// Skip redundant re-renders — include text content to detect
|
||||
// same-length changes (e.g., setFlushed with different text)
|
||||
// `rows` is part of the key: the layout is clamped to the visible rows
|
||||
// (see renderOverlay), so a keyboard opening — which changes rows without
|
||||
// changing the text — must not be skipped as a redundant render.
|
||||
const renderKey = `${displayText}:${compositionStart}:${startCol}:${activePrompt.row}:${activePrompt.col}:${totalCols}:${this._terminal.rows}:${this._flushedOffset}`;
|
||||
// (see renderOverlay), so a keyboard opening, which changes rows or the
|
||||
// row limit without changing the text, must not be skipped as a
|
||||
// redundant render.
|
||||
const totalRows = this._layoutRows();
|
||||
const renderKey = `${displayText}:${compositionStart}:${startCol}:${activePrompt.row}:${activePrompt.col}:${totalCols}:${totalRows}:${this._flushedOffset}`;
|
||||
if (renderKey === this._lastRenderKey && this._overlay.style.display !== 'none') return;
|
||||
this._lastRenderKey = renderKey;
|
||||
|
||||
@@ -671,7 +705,7 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
charTop,
|
||||
charHeight,
|
||||
promptRow: activePrompt.row,
|
||||
totalRows: this._terminal.rows,
|
||||
totalRows,
|
||||
font: this._font,
|
||||
showCursor: this._options.showCursor,
|
||||
cursorColor,
|
||||
|
||||
@@ -756,3 +756,78 @@ describe('viewport scrolled away from the bottom', () => {
|
||||
expect(overlay.style.display).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRowLimit', () => {
|
||||
function rowLimitSetup(promptRow = 10) {
|
||||
const lines = Array.from({ length: 24 }, (_, i) => (i === promptRow ? '$ ' : ''));
|
||||
const mock = createMockTerminal({ buffer: { lines }, cols: 20, rows: 24, cellHeight: 17 });
|
||||
const addon = new ZerolagInputAddon({ prompt: { type: 'character', char: '$', offset: 2 } });
|
||||
mock.terminal.loadAddon(addon);
|
||||
cleanups.push(() => {
|
||||
addon.dispose();
|
||||
mock.cleanup();
|
||||
});
|
||||
const overlay = () => mock.terminal.element.querySelector('.xterm-screen')!.lastElementChild as HTMLDivElement;
|
||||
return { addon, overlay };
|
||||
}
|
||||
|
||||
it('grows the composer upward from the last VISIBLE row, not the terminal last row', () => {
|
||||
const { addon, overlay } = rowLimitSetup();
|
||||
addon.appendText('x'.repeat(50)); // 3 lines at 20 cols from the prompt on row 10
|
||||
expect(overlay().style.top).toBe(`${10 * 17}px`);
|
||||
addon.setRowLimit(12);
|
||||
expect(overlay().style.top).toBe(`${9 * 17}px`);
|
||||
});
|
||||
|
||||
it('null restores the full terminal height, and junk reads as null', () => {
|
||||
const { addon, overlay } = rowLimitSetup();
|
||||
addon.appendText('x'.repeat(50));
|
||||
addon.setRowLimit(12);
|
||||
addon.setRowLimit(null);
|
||||
expect(overlay().style.top).toBe(`${10 * 17}px`);
|
||||
addon.setRowLimit(12);
|
||||
addon.setRowLimit(Number.NaN);
|
||||
expect(overlay().style.top).toBe(`${10 * 17}px`);
|
||||
});
|
||||
|
||||
// setRowLimit narrows the SAME bound renderOverlay already keeps the block on
|
||||
// screen with (RenderParams.totalRows, filled from terminal.rows), so there is
|
||||
// one place that decides where the block stops growing downward.
|
||||
it('narrows the terminal-rows bound and never widens it', () => {
|
||||
const { addon, overlay } = rowLimitSetup(22);
|
||||
addon.appendText('x'.repeat(50)); // 3 lines from row 22 of 24: the terminal bound lifts it to row 21
|
||||
expect(overlay().style.top).toBe(`${21 * 17}px`);
|
||||
addon.setRowLimit(24);
|
||||
expect(overlay().style.top).toBe(`${21 * 17}px`);
|
||||
addon.setRowLimit(100);
|
||||
expect(overlay().style.top).toBe(`${21 * 17}px`);
|
||||
addon.setRowLimit(23);
|
||||
expect(overlay().style.top).toBe(`${20 * 17}px`);
|
||||
});
|
||||
|
||||
it('leaves a block that fits above the limit where it is', () => {
|
||||
const { addon, overlay } = rowLimitSetup();
|
||||
addon.appendText('x'.repeat(50));
|
||||
addon.setRowLimit(13); // rows 10..12 are exactly the three lines
|
||||
expect(overlay().style.top).toBe(`${10 * 17}px`);
|
||||
});
|
||||
|
||||
it('keeps the TAIL of a block taller than the limit', () => {
|
||||
const { addon, overlay } = rowLimitSetup();
|
||||
addon.appendText('x'.repeat(50)); // lines of 18, 20 and 12 characters
|
||||
addon.setRowLimit(2);
|
||||
expect(overlay().style.top).toBe('0px');
|
||||
const xs = [...overlay().querySelectorAll(':scope > div')].map(
|
||||
(line) => (line.textContent ?? '').replace(/[^x]/g, '').length
|
||||
);
|
||||
expect(xs).toEqual([20, 12]);
|
||||
});
|
||||
|
||||
it('holds across later keystrokes, not only the re-render it triggers', () => {
|
||||
const { addon, overlay } = rowLimitSetup();
|
||||
addon.appendText('x'.repeat(50));
|
||||
addon.setRowLimit(12);
|
||||
addon.appendText('y');
|
||||
expect(overlay().style.top).toBe(`${9 * 17}px`);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user