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.
This commit is contained in:
KENZ 2026-04-28 07:05:22 +09:00 committed by GitHub
parent 5f96f6191b
commit 4c53d607f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<Option<Rectangle<f32>>>,
handle_opt: Mutex<Option<image::Handle>>,
shift_anchor: Mutex<Option<Cursor>>,
caret_position: (i32, i32),
preedit: Option<Preedit>,
}
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,
}
}
}