Skip to content

fix(input): correct IME candidate popup positioning by clamping composition range - #222

Open
cnaron wants to merge 1 commit into
egoist:mainfrom
cnaron:fix/ime-candidate-window-position
Open

fix(input): correct IME candidate popup positioning by clamping composition range#222
cnaron wants to merge 1 commit into
egoist:mainfrom
cnaron:fix/ime-candidate-window-position

Conversation

@cnaron

@cnaron cnaron commented Sep 11, 2026

Copy link
Copy Markdown

Problem

When typing with an input method editor (such as Chinese Pinyin, Japanese, or other CJK IMEs) on macOS, the candidate selection window frequently pops up at the bottom-left corner of the screen (0, 0) instead of following the text insertion caret.

Root Cause

During IME composition:

  1. macOS NSTextInputClient calls firstRectForCharacterRange: synchronously right after setMarkedText:selectedRange:replacementRange:.
  2. At this moment, GPUI's frame render loop has not yet produced a new TextLayout for the newly inserted/replaced composition text, so self.last_layout only reflects the text before the current composition step.
  3. As a result, the requested range's end index range.end can exceed layout.len(). Previously, layout.position_for_index(range.end)? failed and returned None.
  4. In crates/gpui_macos/src/window.rs, first_rect_for_character_range maps None to NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) (the bottom-left of the display).

Solution

  • Clamp start_idx and end_idx to layout.len() so that position_for_index safely resolves to the valid insertion point even if layout has not refreshed yet.
  • If last_layout is not available, return a fallback caret rect anchored at bounds.origin instead of returning None.
  • Ensure single-line bounds calculate width as (end.x - start.x).max(px(2.0)) with proper line_height.

Tested on macOS with Chinese Pinyin IME; candidate window now consistently follows the insertion caret.

…sition range

When composing text using an IME (e.g. CJK Pinyin), macOS NSTextInputClient calls firstRectForCharacterRange synchronously right after setMarkedText.

At this point in time, the layout (self.last_layout) has not yet been updated for the newly inserted composition text. As a result:
1. range.end can exceed layout.len(). Previously, layout.position_for_index(range.end)? failed and returned None.
2. On macOS, first_rect_for_character_range maps None to (0, 0) in Cocoa screen coordinates, causing the IME candidate window to jump to the bottom-left corner of the screen.

Fix this by:
- Clamping start_idx and end_idx to layout.len() so position_for_index safely resolves to the current insertion caret position.
- Falling back to bounds.origin if last_layout is not yet available instead of returning None.
- Ensuring character bounds are correctly sized with a minimum width of 2px.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant