From 4c53d607f7ffc247a50bbea7de6f68a8ac353fb1 Mon Sep 17 00:00:00 2001 From: KENZ Date: Tue, 28 Apr 2026 07:05:22 +0900 Subject: [PATCH] feat(ime): minimal IME support for the text_box widget (#539) * Support CJK input method * Fix the input method window occasionally jumps around Use the previous caret(text cursor) position to place the input method window when the position is not available * refactor: Omit calling request_input_method() if doesn't focus - iced UserInterface.update() starts with InputMethod::Disabled state, and merges any input_method_request() which is InputMethod::Enabled in widgets tree traversal. - We don't enable the input method feature if the widget doesn't have focus, we can safely omit request_input_method() call itself. --- src/text_box.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/text_box.rs b/src/text_box.rs index db3e4f4..311570b 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -7,6 +7,7 @@ use cosmic::{ Border, Radians, Shell, Transformation, clipboard::Clipboard, image, + input_method::{Event as InputMethodEvent, InputMethod, Preedit, Purpose}, keyboard::{Key, key::Named}, layout::{self, Layout}, renderer::{self, Quad, Renderer as _}, @@ -16,6 +17,7 @@ use cosmic::{ operation::{self, Operation}, tree, }, + window::Event as WindowEvent, }, iced::{ Color, Element, Length, Padding, Point, Rectangle, Size, Vector, @@ -127,6 +129,35 @@ where self.on_focus = Some(on_focus); self } + + fn input_method<'b>( + &self, + state: &'b State, + scale_factor: f32, + layout: Layout<'_>, + ) -> InputMethod<&'b str> { + if !state.is_focused { + return InputMethod::Disabled; + }; + + let editor_pos = layout.position() + [self.padding.left, self.padding.top].into(); + let (caret_x, caret_y) = state.caret_position; + InputMethod::Enabled { + cursor: Rectangle::new( + Point::new( + editor_pos.x + + (caret_x as f32 + state.editor_offset_x.get() as f32) / scale_factor, + editor_pos.y + (caret_y as f32) / scale_factor, + ), + Size::new( + 1.0, + (self.metrics.scale(scale_factor).line_height) / scale_factor, + ), + ), + purpose: Purpose::Normal, + preedit: state.preedit.as_ref().map(Preedit::as_ref), + } + } } pub fn text_box<'a, Message>( @@ -1101,6 +1132,33 @@ where } state.modifiers = *modifiers; } + Event::InputMethod(event) => match event { + InputMethodEvent::Opened | InputMethodEvent::Closed => { + let metrics = self.metrics.scale(scale_factor); + state.preedit = matches!(event, InputMethodEvent::Opened).then(|| { + let mut preedit = Preedit::new(); + preedit.text_size = Some(metrics.font_size.into()); + preedit + }); + } + InputMethodEvent::Preedit(content, selection) => { + if state.is_focused { + state.preedit = Some(Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: Some(self.metrics.font_size.into()), + }); + } + } + InputMethodEvent::Commit(text) => { + if state.is_focused { + editor.start_change(); + editor.insert_string(&text, None); + editor.finish_change(); + shell.capture_event(); + } + } + }, Event::Mouse(MouseEvent::ButtonPressed(button)) => { if let Some(p) = cursor_position.position_in(layout.bounds()) { state.is_focused = true; @@ -1336,6 +1394,12 @@ where shell.capture_event(); } } + Event::Window(WindowEvent::RedrawRequested(_now)) => { + if state.is_focused { + state.caret_position = editor.cursor_position().unwrap_or(state.caret_position); + shell.request_input_method(&self.input_method(state, scale_factor, layout)); + } + } _ => (), } @@ -1385,6 +1449,8 @@ pub struct State { scrollbar_h_rect: Cell>>, handle_opt: Mutex>, shift_anchor: Mutex>, + caret_position: (i32, i32), + preedit: Option, } impl State { @@ -1402,6 +1468,8 @@ impl State { scrollbar_h_rect: Cell::new(None), handle_opt: Mutex::new(None), shift_anchor: Mutex::new(None), + caret_position: (0, 0), + preedit: None, } } }