Description
Chinese IME candidate windows can jump horizontally while composing text in a Textarea. The shared InputBaseState::bounds_for_range returns the input origin when a newly inserted caret cannot be found in the previous painted layout.
The geometry failure is reproducible by updating composition and querying its caret bounds before repaint. This is independent of the IME undo issue in #2761; no undo is involved.
Environment
- Reproduced with GPUI Kit / gpui-base / gpui-component 0.6.4, gpui-pre 0.3.5.
- Automated geometry test: macOS 26.5.2, arm64.
- Also inspected v0.7.0: its
bounds_for_range implementation is identical to unpatched 0.6.4. I have not run the reproduction against 0.7.0, so this is source verification, not a claim of native reproduction on that release.
- The original candidate-window report did not identify the exact input method. Native candidate-panel acceptance remains pending.
Steps to Reproduce
Using a rendered, focused TextareaState:
- Insert
这是一段已经输入的中文文字 with replace_text_in_range(None, text, window, cx), leaving the caret at the end.
- Allow layout/paint to complete, then query the caret bounds.
- In the same UI update, call
replace_and_mark_text_in_range(None, "n", None, window, cx) and immediately query the new caret bounds, without allowing another paint between these operations.
The critical sequence is:
// input is a focused TextareaState whose initial text has already been painted.
let bounds = input.text_bounds().unwrap();
let before = input.selected_text_range(false, window, cx).unwrap().range;
let previous = input
.bounds_for_range(before.end..before.end, bounds, window, cx)
.unwrap();
input.replace_and_mark_text_in_range(None, "n", None, window, cx);
let after = input.selected_text_range(false, window, cx).unwrap().range;
let pending = input
.bounds_for_range(after.end..after.end, bounds, window, cx)
.unwrap();
assert!((pending.origin.x - previous.origin.x).abs() < px(40.0));
Local geometry checks additionally cover CJK/emoji mixed text, wrapping and a scrolled multiline input. The pre-repaint check fails with the original dependency and passes with the fallback change below.
Expected
While waiting for the next paint, the candidate-window anchor should remain at a meaningful text/caret position rather than jumping to the input's left edge.
Actual
The failing test measured:
before composition: origin (475.8 px, 353 px), size (0 px, 20 px)
before repaint: origin (351 px, 353 px), size (0 px, 20 px)
The new UTF-8 offset is beyond the old shaped line, so lookup returns None. Both origins then use unwrap_or_default(), yielding the input origin. When only the end lookup fails, the returned range can also have a negative width.
Diagnosis / possible fix
The fallback is still present in v0.7.0 source.
A local experiment falls back to the last laid-out caret for an unresolved start and to the resolved start for an unresolved end:
let start_origin = start_origin.or_else(|| {
let (_, _, origin) = self.line_and_position_for_offset(self.last_cursor?);
origin.map(|origin| origin - line_number_origin)
})?;
let mut end_origin = end_origin.unwrap_or(start_origin);
This passes the local pre-repaint geometry checks. It is a proposed fallback, not a fully validated native IME fix.
Description
Chinese IME candidate windows can jump horizontally while composing text in a
Textarea. The sharedInputBaseState::bounds_for_rangereturns the input origin when a newly inserted caret cannot be found in the previous painted layout.The geometry failure is reproducible by updating composition and querying its caret bounds before repaint. This is independent of the IME undo issue in #2761; no undo is involved.
Environment
bounds_for_rangeimplementation is identical to unpatched 0.6.4. I have not run the reproduction against 0.7.0, so this is source verification, not a claim of native reproduction on that release.Steps to Reproduce
Using a rendered, focused
TextareaState:这是一段已经输入的中文文字withreplace_text_in_range(None, text, window, cx), leaving the caret at the end.replace_and_mark_text_in_range(None, "n", None, window, cx)and immediately query the new caret bounds, without allowing another paint between these operations.The critical sequence is:
Local geometry checks additionally cover CJK/emoji mixed text, wrapping and a scrolled multiline input. The pre-repaint check fails with the original dependency and passes with the fallback change below.
Expected
While waiting for the next paint, the candidate-window anchor should remain at a meaningful text/caret position rather than jumping to the input's left edge.
Actual
The failing test measured:
The new UTF-8 offset is beyond the old shaped line, so lookup returns
None. Both origins then useunwrap_or_default(), yielding the input origin. When only the end lookup fails, the returned range can also have a negative width.Diagnosis / possible fix
The fallback is still present in v0.7.0 source.
A local experiment falls back to the last laid-out caret for an unresolved start and to the resolved start for an unresolved end:
This passes the local pre-repaint geometry checks. It is a proposed fallback, not a fully validated native IME fix.