mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: correct IME candidate window position when typing Japanese in terminal (#798)
* fix: correct IME candidate window position when typing Japanese in terminal Two fixes for the IME candidate window appearing offset from the cursor: 1. CSS: Add \left: 0\ to \.xterm .xterm-helpers\ in terminal.css. xterm.css sets position:absolute;top:0 but omits left, leaving left:auto. In Electron's Blink this can resolve to a non-zero value and shift the IME window away from the cursor. 2. JS: Add a capture-phase compositionstart listener in openTerminal(). xterm.js repositions the textarea on compositionupdate but not on compositionstart. The OS reads the textarea's screen rect at compositionstart to place the IME candidate window, so the window can appear at a stale position. The listener force-syncs the textarea to the exact cursor pixel position before the OS opens the window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove compositionstart listener on pane disposal to prevent memory leak Store the handler reference in ManagedPaneInternal.compositionHandler so disposePane() can call removeEventListener with the exact same function reference, preventing the closure from holding the terminal alive after the pane is closed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(terminal): derive IME cell dimensions from public xterm API Computes cell width/height from the .xterm-screen element's bounding rect and uses terminal.textarea (public) instead of reaching into terminal._core. Avoids the `any`-cast access to xterm internals so future xterm.js upgrades don't silently regress the IME position fix. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
parent
ab81f04fae
commit
82fe59fc23
3 changed files with 50 additions and 1 deletions
|
|
@ -29,6 +29,14 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* xterm.css sets position:absolute;top:0 on .xterm-helpers but omits left,
|
||||
leaving left:auto. In Electron's Blink, left:auto can resolve to a non-zero
|
||||
value and offset the IME candidate window away from the cursor. Anchoring it
|
||||
explicitly to 0 matches the intended layout and fixes the IME position. */
|
||||
.pane-manager-root .xterm .xterm-helpers {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Divider: the element is a wide transparent hit area; the visible line is
|
||||
drawn by ::after so that setting `background` on the element never hides it. */
|
||||
.pane-divider.is-vertical,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,8 @@ export function createPaneDOM(
|
|||
serializeAddon,
|
||||
unicode11Addon,
|
||||
webLinksAddon,
|
||||
webglAddon: null
|
||||
webglAddon: null,
|
||||
compositionHandler: null
|
||||
}
|
||||
|
||||
// Focus handler: clicking a pane makes it active and explicitly focuses
|
||||
|
|
@ -182,6 +183,40 @@ export function openTerminal(pane: ManagedPaneInternal): void {
|
|||
// Activate unicode 11
|
||||
terminal.unicode.activeVersion = '11'
|
||||
|
||||
// Why: the OS reads the focused textarea's screen rect at compositionstart to
|
||||
// decide where to display the IME candidate window. xterm.js only repositions
|
||||
// the textarea on compositionupdate (via updateCompositionElements), not on
|
||||
// compositionstart, so the window can appear at a stale cursor position. We
|
||||
// force-sync the textarea position in a capture-phase listener so the OS sees
|
||||
// the correct location before it opens the candidate window.
|
||||
//
|
||||
// Cell dimensions are derived from the public .xterm-screen element's bounds
|
||||
// (xterm sizes that element to cols*cellWidth × rows*cellHeight) rather than
|
||||
// poking `_core._renderService.dimensions` — keeps us on the public API
|
||||
// surface so upgrades don't silently regress the fix.
|
||||
if (terminal.element && terminal.textarea) {
|
||||
const screenElement = terminal.element.querySelector<HTMLElement>('.xterm-screen')
|
||||
const textarea = terminal.textarea
|
||||
const handler = (): void => {
|
||||
if (!screenElement) {
|
||||
return
|
||||
}
|
||||
const rect = screenElement.getBoundingClientRect()
|
||||
const cellWidth = rect.width / terminal.cols
|
||||
const cellHeight = rect.height / terminal.rows
|
||||
if (!(cellWidth > 0) || !(cellHeight > 0)) {
|
||||
return
|
||||
}
|
||||
const buf = terminal.buffer.active
|
||||
const x = Math.min(buf.cursorX, terminal.cols - 1)
|
||||
textarea.style.top = `${buf.cursorY * cellHeight}px`
|
||||
textarea.style.left = `${x * cellWidth}px`
|
||||
}
|
||||
terminal.element.addEventListener('compositionstart', handler, true)
|
||||
// Store so disposePane() can remove it and avoid a memory leak.
|
||||
pane.compositionHandler = handler
|
||||
}
|
||||
|
||||
if (pane.gpuRenderingEnabled) {
|
||||
attachWebgl(pane)
|
||||
}
|
||||
|
|
@ -243,6 +278,10 @@ export function disposePane(
|
|||
pane: ManagedPaneInternal,
|
||||
panes: Map<number, ManagedPaneInternal>
|
||||
): void {
|
||||
if (pane.compositionHandler) {
|
||||
pane.terminal.element?.removeEventListener('compositionstart', pane.compositionHandler, true)
|
||||
pane.compositionHandler = null
|
||||
}
|
||||
try {
|
||||
pane.webglAddon?.dispose()
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export type ManagedPaneInternal = {
|
|||
serializeAddon: SerializeAddon
|
||||
unicode11Addon: Unicode11Addon
|
||||
webLinksAddon: WebLinksAddon
|
||||
// Stored so disposePane() can remove it and avoid a memory leak.
|
||||
compositionHandler: (() => void) | null
|
||||
} & ManagedPane
|
||||
|
||||
export type DropZone = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
|
|
|||
Loading…
Reference in a new issue