This commit is contained in:
alonso.torres 2026-07-20 12:35:16 +02:00
parent 21d6050b7f
commit 24466897db
5 changed files with 423 additions and 20 deletions

View File

@ -87,6 +87,41 @@
(or (.-isComposing native)
(= 229 (.-keyCode event)))))
(defn- update-ime-caret!
"Move the hidden contenteditable capture surface onto the WASM caret so the
browser's IME candidate window opens next to the edited text.
Must only be called while the editor is idle, never during or immediately
before an IME composition: real IMEs (ibus-mozc) abort and commit the
pending text on any mutation of the composing element, including style
writes on the keydown-229 that precedes compositionstart. The surface is
therefore kept on the caret at all times, updated after every
caret-affecting operation (click, arrows, typing, commit).
For vertical text the surface gets `writing-mode: vertical-rl` and is
anchored by its right edge on the caret column, so the in-flight composition
— and the candidate window that tracks it — flows down the column. The
surface font-size is taken from the caret cell (line height horizontally,
column width vertically) so the browser caret advances at roughly the same
rate as the rendered preview text.
The caret rectangle is in the shape's local selrect space; `origin` is the
overlay foreignObject's top-left in that same space."
[^js node origin]
(when (and (some? node) (some? origin))
(when-let [{:keys [x y width height]} (text-editor/text-editor-get-cursor-rect)]
(let [style (.-style node)]
(if (:vertical? origin)
(do
(set! (.-writingMode style) "vertical-rl")
(set! (.-fontSize style) (dm/str width "px"))
(set! (.-left style) (dm/str (- (+ x width) (:x origin) (:width origin)) "px")))
(do
(set! (.-writingMode style) "")
(set! (.-fontSize style) (dm/str height "px"))
(set! (.-left style) (dm/str (- x (:x origin)) "px"))))
(set! (.-top style) (dm/str (- y (:y origin)) "px"))))))
(mf/defc text-editor*
"Contenteditable element positioned over the text shape to capture input events."
[{:keys [shape]}]
@ -94,8 +129,18 @@
clip-id (dm/str "text-edition-clip" shape-id)
vertical? (jl/vertical-text-content? (:content shape))
contenteditable-ref (mf/use-ref nil)
;; Foreign object top-left in shape-local space, used to translate the
;; WASM caret rectangle into an offset inside the capture surface.
origin-ref (mf/use-ref nil)
;; True between compositionstart and compositionend; guards the surface
;; against any repositioning while the IME owns it.
composing-ref (mf/use-ref false)
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
fallback-families (map (fn [font]
(font-family-from-font-id (:font-id font))) fallback-fonts)
@ -126,9 +171,46 @@
y)]
[(assoc selrect :y y :width overlay-width :height max-height) transform])
schedule-ime-caret!
(mf/use-fn
(fn []
;; Wait two frames so the pending WASM render rebuilds the text
;; layout the caret rect reads, then retry for a while: right after
;; mount the rect can stay unavailable until the first full layout.
;; Skipped when a composition began in the meantime — the surface
;; must not move while the IME owns it.
(letfn [(attempt [tries]
(when-not (mf/ref-val composing-ref)
(when (and (nil? (update-ime-caret!
(mf/ref-val contenteditable-ref)
(mf/ref-val origin-ref)))
(pos? tries))
(js/requestAnimationFrame #(attempt (dec tries))))))]
(js/requestAnimationFrame
(fn []
(js/requestAnimationFrame #(attempt 30)))))))
;; Programmatic .focus() on the surface does not reliably fire
;; focus/focusin in Chromium (contenteditable inside an SVG
;; foreignObject): the node becomes document.activeElement but React's
;; on-focus never dispatches, leaving the WASM editor unfocused and
;; every caret-rect read null. So wherever we focus programmatically,
;; the WASM focus is established explicitly instead of relying on the
;; focus event.
focus-editor!
(mf/use-fn
(mf/deps shape-id)
(fn []
(when-let [node (mf/ref-val contenteditable-ref)]
(when (not= (.-activeElement js/document) node)
(.focus node)))
(wasm.api/text-editor-focus shape-id)
(schedule-ime-caret!)))
on-composition-start
(mf/use-fn
(fn [_event]
(mf/set-ref-val! composing-ref true)
(text-editor/text-editor-composition-start)))
on-composition-update
@ -136,21 +218,32 @@
(fn [event]
;; IME cancel (e.g. Escape on Linux ibus-mozc) fires compositionupdate
;; with an empty string; that must reach WASM to clear the preview text.
;;
;; While the composition is in flight the browser owns the capture
;; surface, so this handler must cause zero DOM mutation anywhere near
;; it: no clearing, no style writes, and no store dispatch. A store
;; sync re-renders this component (shape content/name/dimensions,
;; foreignObject attributes, CSS vars), and any such churn makes a
;; real IME commit the pending kana and restart (typing "ni" commits
;; ん then い instead of composing に). The WASM editor keeps the
;; preview in its own state and paints it via request-render; the
;; store is synced once on compositionend.
(let [data (.-data event)]
(when (some? data)
(text-editor/text-editor-composition-update data)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-composition"))
(reset-input-node (mf/ref-val contenteditable-ref)))))
(wasm.api/request-render "text-composition")))))
on-composition-end
(mf/use-fn
(fn [^js event]
(mf/set-ref-val! composing-ref false)
(let [data (or (.-data event) "")]
(text-editor/text-editor-composition-end data)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-composition"))
(reset-input-node (mf/ref-val contenteditable-ref))))
(reset-input-node (mf/ref-val contenteditable-ref))
;; Move the surface onto the caret, now past the committed text.
(schedule-ime-caret!)))
on-paste
(mf/use-fn
@ -162,7 +255,8 @@
(text-editor/text-editor-insert-text text)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-paste"))
(reset-input-node (mf/ref-val contenteditable-ref)))))
(reset-input-node (mf/ref-val contenteditable-ref))
(schedule-ime-caret!))))
on-copy
(mf/use-fn
@ -185,11 +279,17 @@
(text-editor/text-editor-delete-backward)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-cut"))))
(reset-input-node (mf/ref-val contenteditable-ref)))))
(reset-input-node (mf/ref-val contenteditable-ref))
(schedule-ime-caret!))))
on-key-down
(mf/use-fn
(fn [^js event]
;; IME keydowns (keyCode 229) must not touch the surface at all: by
;; the time the 229 reaches the DOM the IME already owns the surface,
;; and even a pre-compositionstart style write makes ibus abort and
;; commit. The surface is already sitting on the caret from the last
;; idle repositioning (see `update-ime-caret!`).
(when (and (text-editor/text-editor-has-focus?)
(not (composing-event? event)))
(let [key (.-key event)
@ -281,7 +381,11 @@
(wasm.api/request-render "text-cursor-move"))
;; Let contenteditable handle text input via on-input
:else nil)))))
:else nil)
;; Any handled key may have moved the caret; keep the surface on
;; it so the next IME sequence anchors correctly. Plain character
;; keys reschedule again from on-input after the insert.
(schedule-ime-caret!)))))
on-input
(mf/use-fn
@ -296,7 +400,8 @@
(text-editor/text-editor-insert-text data)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-input"))
(reset-input-node (mf/ref-val contenteditable-ref))))))
(reset-input-node (mf/ref-val contenteditable-ref))
(schedule-ime-caret!)))))
;; Pointer/click handlers must request a render so caret and selection
;; updates are painted immediately. Without it the overlay only redraws
@ -305,6 +410,12 @@
on-pointer-down
(mf/use-fn
(fn [^js event]
;; The capture surface is pointer-events:none (it sits on the caret,
;; not under the pointer), so it must be focused programmatically.
;; preventDefault stops the browser from moving focus to the body on
;; mousedown, which would blur the surface right back.
(dom/prevent-default event)
(focus-editor!)
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-pointer-down off-pt)
@ -324,7 +435,8 @@
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-pointer-up off-pt)
(wasm.api/request-render "text-pointer-up"))))
(wasm.api/request-render "text-pointer-up")
(schedule-ime-caret!))))
on-click
(mf/use-fn
@ -332,7 +444,8 @@
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-set-cursor-from-offset off-pt)
(wasm.api/request-render "text-set-cursor"))))
(wasm.api/request-render "text-set-cursor")
(schedule-ime-caret!))))
on-double-click
(mf/use-fn
@ -340,12 +453,14 @@
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-select-word-boundary off-pt)
(wasm.api/request-render "text-select-word"))))
(wasm.api/request-render "text-select-word")
(schedule-ime-caret!))))
on-focus
(mf/use-fn
(fn [^js _event]
(wasm.api/text-editor-focus shape-id)))
(wasm.api/text-editor-focus shape-id)
(schedule-ime-caret!)))
on-blur
(mf/use-fn
@ -365,13 +480,17 @@
"--editor-container-height" (dm/str height "px")
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")}]
;; Keep the caret-translation origin in sync with the current render.
(mf/set-ref-val! origin-ref {:x x :y y :width width :vertical? vertical?})
;; Focus contenteditable on mount
(mf/use-effect
(mf/deps contenteditable-ref)
(fn []
(when-let [node (mf/ref-val contenteditable-ref)]
;; Focus and select all text on mount (this will trigger on-focus)
(.focus node)
(when (some? (mf/ref-val contenteditable-ref))
;; Focus and select all text on mount. `focus-editor!` establishes the
;; WASM focus explicitly; select-all is a no-op without it.
(focus-editor!)
(text-editor/text-editor-select-all)
(wasm.api/request-render "text-editor-select-all-on-mount"))
;; On unmount (edition actually ended), finalize the editor content and
@ -410,7 +529,11 @@
:on-pointer-down on-pointer-down
:on-pointer-move on-pointer-move
:on-pointer-up on-pointer-up
:class (stl/css :text-editor)
;; The hover cursor lives here: the capture surface below is
;; pointer-events:none so it never receives hover itself.
:class (dm/str (cur/get-text (:rotation shape) vertical?)
" "
(stl/css :text-editor))
:style style}
[:div
{:ref contenteditable-ref
@ -427,7 +550,5 @@
:on-focus on-focus
:on-blur on-blur
:id "text-editor-wasm-input"
:class (dm/str (cur/get-text (:rotation shape) (jl/vertical-text-content? (:content shape)))
" "
(stl/css :text-editor-container))
:class (stl/css :text-editor-container)
:data-testid "text-editor-container"}]]]]))

View File

@ -9,4 +9,8 @@
opacity: 0;
overflow: hidden;
white-space: pre;
// The surface is positioned on the caret while idle, so clicks must always
// hit the outer overlay (keeping offset coordinates stable); focus is given
// to the surface programmatically from the overlay's pointer-down.
pointer-events: none;
}

View File

@ -364,6 +364,25 @@
(mem/free)
result)))))
(defn text-editor-get-cursor-rect
"Read the caret rectangle from the WASM editor.
Coordinates are in the shape's local selrect space (the same space as the
text overlay's foreignObject). Returns a map with :x :y :width :height, or
nil when there is no active caret."
[]
(when wasm/context-initialized?
(let [ptr (h/call wasm/internal-module "_text_editor_get_cursor_rect")]
(when (and ptr (not (zero? ptr)))
(let [heap-f32 (mem/get-heap-f32)
offset (mem/->offset-32 ptr)
x (aget heap-f32 offset)
y (aget heap-f32 (+ offset 1))
width (aget heap-f32 (+ offset 2))
height (aget heap-f32 (+ offset 3))]
(mem/free)
{:x x :y y :width width :height height})))))
(defn text-editor-encode-text-pre
[text]
(when (and (not (empty? text))

256
handoff-ime-bug.md Normal file
View File

@ -0,0 +1,256 @@
# Handoff — v3 editor IME input (candidate window placement + premature commit)
> Context for continuing the IME work on branch `poc-japanese-text`.
> Rewritten 2026-07-19 after the premature-commit fix was confirmed with the
> real IME. Companion to `handoff-japanese-text.md` (the overall feature
> handoff); this file covers only the in-flight IME editor work.
## Problem statement
When typing Japanese with an IME in the v3 (WASM) text editor:
1. composition must not be interrupted (the user must be able to compose kana
and convert to kanji) — **FIXED, user-confirmed with real fcitx5-mozc**;
2. the IME candidate/suggestion popup must appear next to the current input
point, in both horizontal and vertical layouts — **partially working;
remaining work below**.
The user's environment is Linux/X11 + Chromium + **fcitx5 with mozc**
(`fcitx5 -d` + `/usr/lib/mozc/mozc_server`; the previous handoff said
ibus-mozc — that was wrong).
## The two root causes found for premature commit
Real IMEs abort the composition and commit the pending kana on ANY DOM
disturbance around the composing element. Two distinct triggers were found and
removed:
1. **Store dispatch in `on-composition-update`.**
`sync-wasm-text-editor-content!` dispatched
`dwt/v2-update-text-shape-content` per kana → React re-render of the editor
component mid-composition (shape content/name/dimensions, foreignObject
attrs, CSS vars). Removed: the WASM `text_editor_composition_update`
already writes the preview into WASM shape state and marks it touched, so
`request-render` alone paints the preview. The store is synced once on
`compositionend`.
2. **Style writes on the keydown-229 that precedes `compositionstart`.**
The old design repositioned the capture surface on the first keydown of an
IME sequence (`keyCode 229 && !isComposing`), believing that was "before
the composition". It is not safe: by the time keydown-229 reaches the DOM,
the IME has already established its composition context, and the style
write still aborts real mozc (CDP tolerates it — see Verification below).
The user confirmed the abort persisted after fix 1 alone; it stopped after
the redesign in fix 2.
## Current design (working tree)
All in `frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs` +
`v3_editor.scss`:
- **Idle positioning**: the capture surface (`#text-editor-wasm-input`) sits
ON the WASM caret at all times. `schedule-ime-caret!` runs after every
caret-affecting operation (click, pointer-up, double-click, handled keydown,
on-input, paste, cut, composition end, focus, mount) — never during a
composition (`composing-ref` guard, set in `on-composition-start`, cleared
in `on-composition-end`). No handler touches the surface or the store
between `compositionstart` and `compositionend`.
- `schedule-ime-caret!` waits two `requestAnimationFrame`s (so the pending
WASM render can rebuild the layout the caret rect reads) and then retries up
to 30 frames because the rect can be unavailable right after mount.
**Gotcha: rAF does not fire in occluded/background windows** (see Tooling).
- `update-ime-caret!` (unchanged semantics): horizontal → `left/top` at caret,
`font-size` = caret rect height; vertical → `writing-mode: vertical-rl`,
right-edge anchor `left = (x+width) - origin.x - origin.width`,
`font-size` = caret rect width. Returns nil when it could not position
(no node/origin/rect) — the scheduler retries on nil.
- **`pointer-events: none` on the surface** (`v3_editor.scss`): clicks always
land on the outer overlay div, so `offsetX/offsetY` remain in the shape's
local space regardless of where the surface currently sits. The hover text
cursor class moved to the outer div.
- **`focus-editor!`**: programmatic `.focus()` on the surface does NOT
reliably fire focus/focusin in Chromium (contenteditable inside SVG
foreignObject): the node becomes `document.activeElement` but React's
`on-focus` never dispatches, so the WASM editor stayed unfocused and every
caret-rect read returned null (this made the vertical popup anchor to the
default full-size surface with no writing-mode — the "jumping" popup).
`focus-editor!` therefore focuses the node AND calls
`wasm.api/text-editor-focus` + `schedule-ime-caret!` explicitly. Used by the
mount effect and `on-pointer-down` (which also `preventDefault`s so
mousedown cannot move focus to the body).
Note `text-editor/text-editor-focus` THROWS when the shape id is not in
WASM state — a possible mount race to keep in mind.
- `on-composition-end`: WASM commit + store sync + `reset-input-node` +
`schedule-ime-caret!` (surface moves to the caret past the committed text).
There is no full-size "reset" anymore; the surface stays caret-positioned
for the whole editing session.
Supporting changes (unchanged from before, already built):
- `frontend/src/app/render_wasm/text_editor.cljs`:
`text-editor-get-cursor-rect` reads 4 f32 (x/y/w/h) from
`_text_editor_get_cursor_rect`. Coordinates are ABSOLUTE page coords (selrect
origin included); `update-ime-caret!` subtracts the foreignObject origin
(`origin-ref`, kept fresh each render, includes the valign-adjusted y).
- `render-wasm/src/wasm/text_editor.rs`: cursor rect independent of the caret
blink phase (null only when the editor lacks focus). The WASM artifact with
this change is built and loaded in the ws1 devenv.
## Verification status
### Horizontal — real IME, PASSED (2026-07-19)
Automated real-IME run (see Tooling): typed `nihongo`, Space-converted,
Return-committed through actual fcitx5-mozc:
- ONE composition `n→に→にほ→にほんご→日本語`, no aborts, no fragment commits;
committed text landed in the shape; store synced at compositionend; no crash.
- Surface style byte-identical through the whole composition; a
MutationObserver over the entire foreignObject subtree recorded ZERO
attribute/childList mutations between compositionstart and compositionend.
- The fcitx candidate window tracked the preedit (window x advanced 866→967 at
constant y, matching the DOM caret anchor at the end of the preedit).
- The user separately confirmed typing works with their own keyboard.
### Vertical — NOT yet verified with the real IME
The vertical real-IME run was voided: the user's screen was locked (slock) so
the synthetic keystrokes never reached the browser (all-blue screenshots, no
composition events, text unchanged). **Rerun `vime-test.sh` when the screen is
unlocked.** Under CDP the vertical mechanics passed earlier (single
composition, style byte-identical, commit lands, caret advances down the
column), but CDP proves mechanics only.
## Remaining known bugs (the actual work left)
1. **Horizontal caret rect / hit test broken for CJK-heavy content → popup
anchors at the shape origin.** With content `日本語ab`:
- `text_editor_get_cursor_rect` always returns the LINE-START x (rect x ==
selrect.x) regardless of the real caret offset. In
`render-wasm/src/wasm/text_editor.rs` (`get_cursor_rect`, ~line 1103):
`get_rects_for_range(char_pos..char_pos, Tight, Tight)` comes back empty
and the fallback uses `get_glyph_position_at_coordinate((0.0,0.0))`
`.position as f32` — a glyph INDEX used as an X COORDINATE. That is a bug
regardless of the root cause of the empty rects.
- `text_editor_set_cursor_from_offset` maps EVERY (x,y) to offset 0 for the
same shape (verified with direct WASM calls bypassing all CLJS changes,
x swept 1..200, y swept 0..40; valign top). So click-to-place-caret is
broken on that content too.
- Both symptoms suggest the layout the hit-test paths read
(`text_content.layout.paragraphs`) differs from what is painted —
possibly laid out without the Japanese fallback fonts (zero-width
glyphs collapse every x to 0). The canvas paints correctly, and
`get_text_dimensions`/auto-width grow correctly, so some other layout is
fine. Compare how `layout.paragraphs` is (re)built after edits vs the
render path in `render/text.rs` / `render/text_editor.rs`.
- The vertical paths are fine (they recompute from
`text_vertical::layout_from_content` on demand).
2. **Vertical popup verification pending** (screen lock, above). After the
focus fix the mount-time WASM focus is established and the caret rect for
vertical returns correct values (measured: end-of-text caret
x=3728.6/y=1278/w=16.8/h=1 on the test shape), so the surface should now
get `writing-mode: vertical-rl` + caret position before the first
composition. Needs the on-screen run to confirm popup placement.
3. Possible mount race: `text-editor-focus` throws if the WASM shape is not
yet in state when the mount effect runs. Not reproduced conclusively, but
one entry attempt showed WASM focus false with no error until a manual call
at +300ms succeeded. If editor entry ever silently loses WASM focus again,
look here (make `focus-editor!` retry or make the throw a no-op+retry).
4. Compositions that start WITHOUT keydown (voice input, on-screen keyboards)
rely on the last idle position — now always correct by design (the surface
is always on the caret), which retires the old concern.
5. v2 editor untouched; macOS/Safari behavior unknown.
## Files touched (uncommitted, on `poc-japanese-text`)
- `frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs` — the design
above (`update-ime-caret!` idle model, `schedule-ime-caret!` retries,
`composing-ref`, `focus-editor!`, `pointer-events`-aware pointer handlers,
no store sync in `on-composition-update`).
- `frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss` —
`pointer-events: none` on `.text-editor-container`.
- `frontend/src/app/render_wasm/text_editor.cljs` —
`text-editor-get-cursor-rect`.
- `render-wasm/src/wasm/text_editor.rs` — cursor rect independent of blink
phase.
Lint/format for the CLJS file not yet run this round (`clj-kondo`/`cljfmt`
live in the devenv container, not on the host PATH).
## Tooling — REAL-IME automated testing (new, important)
CDP (`Input.imeSetComposition`) bypasses the OS input method entirely: it
cannot reproduce IME aborts and cannot show the candidate popup (an OS window
fcitx draws outside the browser — Playwright screenshots never contain it).
The real pipeline is scriptable because the Playwright Chrome runs headed on
the user's X11 display (`DISPLAY=:0`):
- Switch engine: `fcitx5-remote -s mozc` … restore with
`fcitx5-remote -s keyboard-us-altgr-intl` (query with `-n`).
- Real keystrokes: `xdotool windowactivate --sync <WIN>` then
`xdotool type --delay 250 "nihongo"`, `xdotool key space`, `key Return`.
XTEST events go through fcitx/mozc exactly like hardware input.
- The Chrome window: find by exact title, e.g.
`xdotool search --name "Japanese text - Penpot"` (was id 60817432; do NOT
head -1 a loose pattern).
- Popup observation: full-screen `scrot -o file.png` + geometry via
`for w in $(xdotool search --class fcitx); do xdotool getwindowgeometry --shell $w; done`.
Geometry persists for unmapped windows — corroborate with the screenshot.
- Page-side instrumentation (before typing): composition-event log capturing
`node.getAttribute('style')` and the DOM caret anchor
(`getSelection().getRangeAt(0).getBoundingClientRect()`) per event, plus a
MutationObserver over the foreignObject subtree asserting zero
attribute/childList mutations mid-composition.
- Scripts saved in the session scratchpad as `ime-test.sh` (horizontal) and
`vime-test.sh` (vertical) — recreate from this description if gone.
**Safety checks before sending synthetic input:**
- Screen lock: `pgrep slock` (user uses slock) and
`DISPLAY=:0 xset q | grep Monitor` (Monitor off = user away). If locked, DO
NOT type — keystrokes land in the password prompt.
- The user's fcitx layout must be restored afterwards
(`keyboard-us-altgr-intl`).
**Other environment gotchas (all hit this session):**
- `requestAnimationFrame` does not fire while the Chrome window is occluded —
`schedule-ime-caret!` verification MUST be done with the window visible
(activate it first). Store-side checks are unaffected.
- After `location.reload()` the editor may not mount:
`(reset! app.render-wasm.api/page-transition? false)` then re-enter edition.
- Programmatic edition entry:
`select_shape(uuid)` → `zoom.fit_to_shapes([uuid])` (NOT the arity-0
`fit-to-shapes`; Shift+2 via CDP keyboard is unreliable) →
`start_editing_selected()`.
- Munged names used a lot: `app.render_wasm.text_editor.text_editor_get_cursor_rect`,
`..._has_focus_QMARK_`, `app.main.data.workspace.zoom.fit_to_shapes`,
`cljs.core.clj__GT_js`. Patching `app.render_wasm.text_editor.foo` does NOT
intercept calls made through `app.render_wasm.api.foo` (the api def captured
the original fn object) — patch the api var too.
- The test shapes contain residue text from aborted/committed test runs:
horizontal `0121fd5b-f9b4-8060-8008-586a95941bbe` now
`日本語ab日本語を知覚異常日本語…`, vertical
`f179b962-b099-8026-8008-5641a910fe2e`
`日本語をしますかアリアがとうごいます…`. Content is junk by design; keep using
them.
- File "Japanese text" (Drafts), page `showcase`,
`https://localhost:13449/#/workspace?team-id=9aa629b4-0900-8061-8008-3ac96936247d&file-id=f8554703-a493-807a-8008-454b071d651d&page-id=f8554703-a493-807a-8008-454b071d8665`.
## Suggested next steps (in order)
1. Rerun the vertical real-IME test with the screen unlocked
(`vime-test.sh`); verify the popup follows the caret down the column and
commits cleanly. Also re-verify horizontal once more after any changes.
2. Fix the Rust horizontal caret-rect/hit-test for CJK content (bug 1): find
why `layout.paragraphs` yields empty caret-range rects / zero-width
positions for `日本語ab` while painting is correct; fix the
index-as-coordinate fallback while there. Add a Rust regression test with
CJK content asserting `get_cursor_rect` x monotonically increases with the
caret offset and `set_cursor_from_offset` round-trips.
3. Re-verify click-to-place-caret + surface positioning end-to-end in both
flows, then run `clj-kondo`/`cljfmt` (devenv) on the edited CLJS.
4. Human confirmation from the user in both flows (type, convert, Esc-cancel,
two consecutive compositions, click into text mid-composition-free).
See auto-memory `v3-editor-repl-verification` for the distilled recipes and
`handoff-japanese-text.md` for build/test commands and branch state.

View File

@ -637,7 +637,10 @@ pub extern "C" fn text_editor_move_cursor(
#[no_mangle]
pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
with_state!(state, {
if !get_text_editor_state().has_focus || !get_text_editor_state().cursor_visible {
// Report the caret geometry independent of the blink phase; whether the
// caret is currently painted is a separate `cursor_visible` concern
// handled by the overlay renderer.
if !get_text_editor_state().has_focus {
return std::ptr::null_mut();
}