mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
fix(mobile): typing into a pane shows its cursor and what is being composed
Tapping the terminal to type into it showed no cursor: xterm reports its cursor as shown and in place, but leaves it undrawn on the phone. And an input method's composing text — pinyin before a candidate is picked, dictation before it is done — lived only in the hidden field typing goes through, so nothing showed until it was committed, often only on Return. While the pane has the keys, the app draws the cursor itself, a blinking block at the pane's cursor cell, with the composing text underlined in front of it. A program that hides the terminal's cursor to draw its own (an agent's input box) gets only the composing text, where it lands.
This commit is contained in:
@@ -3333,6 +3333,60 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string,
|
||||
// The committed text is in the field after this event, not during it.
|
||||
setTimeout(flush);
|
||||
});
|
||||
// The pane's cursor while typing straight into it, and what an input
|
||||
// method is still composing, drawn here over the terminal. xterm leaves
|
||||
// its own cursor undrawn on the phone, and the composing text lives in
|
||||
// a field no one sees: pinyin before a candidate is picked, dictation
|
||||
// before it is done, showed nothing until it was committed.
|
||||
let composing = "";
|
||||
const preedit = h("span", { class: "term-preedit" });
|
||||
const caret = h("div", { class: "term-caret", hidden: true, ariaHidden: "true" }, preedit, h("span", { class: "term-caret-block" }));
|
||||
view.querySelector(".term-wrap")?.append(caret);
|
||||
let caretFrame = 0;
|
||||
const placeCaret = () => {
|
||||
caretFrame = 0;
|
||||
const buf = term.buffer.active;
|
||||
const drawn = screenEl.querySelector<HTMLElement>(".xterm-screen");
|
||||
const row = buf.baseY + buf.cursorY - buf.viewportY;
|
||||
// A program that draws its own cursor (an agent's input box) hides
|
||||
// the terminal's; then only composing text is shown, where it lands.
|
||||
const hidden = (term as unknown as { _core?: { coreService?: { isCursorHidden?: boolean } } })._core?.coreService?.isCursorHidden;
|
||||
if (document.activeElement !== typing || !drawn || !caret.parentElement || row < 0 || row >= term.rows || (hidden && !composing)) {
|
||||
caret.hidden = true;
|
||||
return;
|
||||
}
|
||||
const box = drawn.getBoundingClientRect();
|
||||
const wrap = caret.parentElement.getBoundingClientRect();
|
||||
const cell = box.width / term.cols;
|
||||
const rowH = box.height / term.rows;
|
||||
caret.hidden = false;
|
||||
caret.classList.toggle("bare", !!hidden);
|
||||
caret.style.left = `${box.left - wrap.left + buf.cursorX * cell}px`;
|
||||
caret.style.top = `${box.top - wrap.top + row * rowH}px`;
|
||||
caret.style.height = `${rowH}px`;
|
||||
caret.style.fontSize = `${term.options.fontSize}px`;
|
||||
caret.style.setProperty("--cell", `${cell}px`);
|
||||
preedit.textContent = composing;
|
||||
};
|
||||
const caretSoon = () => {
|
||||
if (!caretFrame) caretFrame = requestAnimationFrame(placeCaret);
|
||||
};
|
||||
term.onRender(caretSoon);
|
||||
term.onCursorMove(caretSoon);
|
||||
screenEl.addEventListener("scroll", caretSoon, { passive: true });
|
||||
typing.addEventListener("focus", caretSoon);
|
||||
typing.addEventListener("blur", () => {
|
||||
composing = "";
|
||||
caretSoon();
|
||||
});
|
||||
typing.addEventListener("compositionupdate", (e) => {
|
||||
composing = e.data ?? "";
|
||||
caretSoon();
|
||||
});
|
||||
typing.addEventListener("compositionend", () => {
|
||||
composing = "";
|
||||
caretSoon();
|
||||
});
|
||||
typing.addEventListener("input", (e) => {
|
||||
if (!(e as InputEvent).isComposing) flush();
|
||||
});
|
||||
|
||||
@@ -2837,3 +2837,50 @@ html[data-nav="pop"]::view-transition-new(root) {
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
}
|
||||
|
||||
/* The cursor and an input method's composing text, over the pane while it
|
||||
is typed into (main.ts `caret`). */
|
||||
.term-caret {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
pointer-events: none;
|
||||
font-family: var(--mono);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.term-caret[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.term-preedit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
background: var(--term);
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.term-preedit:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.term-caret-block {
|
||||
width: var(--cell);
|
||||
background: var(--ink);
|
||||
opacity: 0.75;
|
||||
animation: caret-blink 1.1s steps(1) infinite;
|
||||
}
|
||||
|
||||
.term-caret.bare .term-caret-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes caret-blink {
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user