Repository: iced-rs/iced (language: rust)

Issue:
The initial IME candidate window position is wrong
### Is your issue REALLY a bug?

- [x] My issue is indeed a bug!
- [x] I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise.

### Is there an existing issue for this?

- [x] I have searched the existing issues.

### Is this issue related to iced?

- [x] My hardware is compatible and my graphics drivers are up-to-date.

### What happened?

1. Open TODO example
2. Focus the text input
3. Activate IME
4. Type any key
5. The candidate window appears at the top left corner of the window

This only happens when the candidate is firstly opened (`あいうえお`). When opening the window for the second time, the position is correct (`かきくけこ`).

https://github.com/user-attachments/assets/49c0bbfe-44e8-4058-b70a-d524b9d7dc12

### What is the expected behavior?

The position of the candidate window should be near the cursor on opening it for the first time.

### Version

master

### Operating System

macOS

### Do you have any log output?

```shell

```
Merge InputMethod::Allowed and Open
To simplify the states after #2793 

I opened this PR to collaborate with other contributor.

There are some downsides should be fixed
- ~~It is that `window.set_ime_allowed` is called every time when the preedit content and cursor rendering is updated.~~
  - fixed in https://github.com/iced-rs/iced/pull/2800/commits/4d5ab2b28868bb10f06844f79d50ff4c9a98244f
- It should express that the text widget opens on-the-spot Preedit in other (better) way


Discussion / hints:

@rhysd I made one for your suggestion. Sorry for late as I misunderstood you tell.

> So I'd like to suggest to make a PR with the commit by @kenz-gelsoft and then I will send a PR to your fork.
@kenz-gelsoft Never mind. Thank you for proposing the better approach. I have two requests to you.

- I have opened my PR on your fork. Would you review it?: https://github.com/kenz-gelsoft/iced/pull/1

  > It is that window.set_ime_allowed is called every time when the preedit content and cursor rendering is updated.

  This downside should be fixed by the PR.

- Please add the following line to the description of this PR so that merging this PR can automatically close the issue.
  ```
  Fixes #2792
  ```

Fix patch:
diff --git a/core/src/input_method.rs b/core/src/input_method.rs
index 9c83b083ca..cd8d459dec 100644
--- a/core/src/input_method.rs
+++ b/core/src/input_method.rs
@@ -6,14 +6,10 @@ use std::ops::Range;
 /// The input method strategy of a widget.
 #[derive(Debug, Clone, PartialEq)]
 pub enum InputMethod<T = String> {
-    /// No input method strategy has been specified.
-    None,
-    /// No input method is allowed.
+    /// Input method is disabled.
     Disabled,
-    /// Input methods are allowed, but not open yet.
-    Allowed,
-    /// Input method is open.
-    Open {
+    /// Input method is enabled.
+    Enabled {
         /// The position at which the input method dialog should be placed.
         position: Point,
         /// The [`Purpose`] of the input method.
@@ -91,13 +87,13 @@ impl InputMethod {
     /// # use iced_core::input_method::{InputMethod, Purpose, Preedit};
     /// # use iced_core::Point;
     ///
-    /// let open = InputMethod::Open {
+    /// let open = InputMethod::Enabled {
     ///     position: Point::ORIGIN,
     ///     purpose: Purpose::Normal,
     ///     preedit: Some(Preedit { content: "1".to_owned(), selection: None, text_size: None }),
     /// };
     ///
-    /// let open_2 = InputMethod::Open {
+    /// let open_2 = InputMethod::Enabled {
     ///     position: Point::ORIGIN,
     ///     purpose: Purpose::Secure,
     ///     preedit: Some(Preedit { content: "2".to_owned(), selection: None, text_size: None }),
@@ -105,12 +101,6 @@ impl InputMethod {
     ///
     /// let mut ime = InputMethod::Disabled;
     ///
-    /// ime.merge(&InputMethod::<String>::Allowed);
-    /// assert_eq!(ime, InputMethod::Allowed);
-    ///
-    /// ime.merge(&InputMethod::<String>::Disabled);
-    /// assert_eq!(ime, InputMethod::Allowed);
-    ///
     /// ime.merge(&open);
     /// assert_eq!(ime, open);
     ///
@@ -118,22 +108,16 @@ impl InputMethod {
     /// assert_eq!(ime, open);
     /// ```
     pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) {
-        match (&self, other) {
-            (InputMethod::Open { .. }, _)
-            | (
-                InputMethod::Allowed,
-                InputMethod::None | InputMethod::Disabled,
-            )
-            | (InputMethod::Disabled, InputMethod::None) => {}
-            _ => {
-                *self = other.to_owned();
-            }
+        if let InputMethod::Enabled { .. } = self {
+            return;
         }
+
+        *self = other.to_owned();
     }
 
     /// Returns true if the [`InputMethod`] is open.
-    pub fn is_open(&self) -> bool {
-        matches!(self, Self::Open { .. })
+    pub fn is_enabled(&self) -> bool {
+        matches!(self, Self::Enabled { .. })
     }
 }
 
@@ -144,14 +128,12 @@ impl<T> InputMethod<T> {
         T: AsRef<str>,
     {
         match self {
-            Self::None => InputMethod::None,
             Self::Disabled => InputMethod::Disabled,
-            Self::Allowed => InputMethod::Allowed,
-            Self::Open {
+            Self::Enabled {
                 position,
                 purpose,
                 preedit,
-            } => InputMethod::Open {
+            } => InputMethod::Enabled {
                 position: *position,
                 purpose: *purpose,
                 preedit: preedit.as_ref().map(Preedit::to_owned),
diff --git a/core/src/shell.rs b/core/src/shell.rs
index 509e3822fc..56250e2e56 100644
--- a/core/src/shell.rs
+++ b/core/src/shell.rs
@@ -27,7 +27,7 @@ impl<'a, Message> Shell<'a, Message> {
             redraw_request: window::RedrawRequest::Wait,
             is_layout_invalid: false,
             are_widgets_invalid: false,
-            input_method: InputMethod::None,
+            input_method: InputMethod::Disabled,
         }
     }
 
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
index c73d189c8e..765de07e00 100644
--- a/graphics/src/text/editor.rs
+++ b/graphics/src/text/editor.rs
@@ -11,7 +11,7 @@ use cosmic_text::Edit as _;
 
 use std::borrow::Cow;
 use std::fmt;
-use std::sync::{self, Arc};
+use std::sync::{self, Arc, RwLock};
 
 /// A multi-line text editor.
 #[derive(Debug, PartialEq)]
@@ -19,6 +19,7 @@ pub struct Editor(Option<Arc<Internal>>);
 
 struct Internal {
     editor: cosmic_text::Editor<'static>,
+    cursor: RwLock<Option<Cursor>>,
     font: Font,
     bounds: Size,
     topmost_line_changed: Option<usize>,
@@ -114,10 +115,14 @@ impl editor::Editor for Editor {
     fn cursor(&self) -> editor::Cursor {
         let internal = self.internal();
 
+        if let Ok(Some(cursor)) = internal.cursor.read().as_deref() {
+            return cursor.clone();
+        }
+
         let cursor = internal.editor.cursor();
         let buffer = buffer_from_editor(&internal.editor);
 
-        match internal.editor.selection_bounds() {
+        let cursor = match internal.editor.selection_bounds() {
             Some((start, end)) => {
                 let line_height = buffer.metrics().line_height;
                 let selected_lines = end.line - start.line + 1;
@@ -237,7 +242,12 @@ impl editor::Editor for Editor {
                         - buffer.scroll().vertical,
                 ))
             }
-        }
+        };
+
+        *internal.cursor.write().expect("Write to cursor cache") =
+            Some(cursor.clone());
+
+        cursor
     }
 
     fn cursor_position(&self) -> (usize, usize) {
@@ -259,6 +269,13 @@ impl editor::Editor for Editor {
 
         let editor = &mut internal.editor;
 
+        // Clear cursor cache
+        let _ = internal
+            .cursor
+            .write()
+            .expect("Write to cursor cache")
+            .take();
+
         match action {
             // Motion events
             Action::Move(motion) => {
@@ -527,6 +544,13 @@ impl editor::Editor for Editor {
 
         internal.editor.shape_as_needed(font_system.raw(), false);
 
+        // Clear cursor cache
+        let _ = internal
+            .cursor
+            .write()
+            .expect("Write to cursor cache")
+            .take();
+
         self.0 = Some(Arc::new(internal));
     }
 
@@ -635,6 +659,7 @@ impl Default for Internal {
                     line_height: 1.0,
                 },
             )),
+            cursor: RwLock::new(None),
             font: Font::default(),
             bounds: Size::ZERO,
             topmost_line_changed: None,
diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs
index cb4416788f..9b396c6939 100644
--- a/runtime/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -189,7 +189,7 @@ where
 
         let mut outdated = false;
         let mut redraw_request = window::RedrawRequest::Wait;
-        let mut input_method = InputMethod::None;
+        let mut input_method = InputMethod::Disabled;
 
         let mut manual_overlay = ManuallyDrop::new(
             self.root
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index fe71fd6bc7..0cf75c0458 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -729,7 +729,7 @@ where
                     _ => mouse::Cursor::Unavailable,
                 };
 
-                let had_input_method = shell.input_method().is_open();
+                let had_input_method = shell.input_method().is_enabled();
 
                 let translation =
                     state.translation(self.direction, bounds, content_bounds);
@@ -750,7 +750,7 @@ where
                 );
 
                 if !had_input_method {
-                    if let InputMethod::Open { position, .. } =
+                    if let InputMethod::Enabled { position, .. } =
                         shell.input_method_mut()
                     {
                         *position = *position - translation;
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index ce5da9efd2..7e40a56ade 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -339,10 +339,6 @@ where
             return InputMethod::Disabled;
         };
 
-        let Some(preedit) = &state.preedit else {
-            return InputMethod::Allowed;
-        };
-
         let bounds = layout.bounds();
         let internal = self.content.0.borrow_mut();
 
@@ -363,10 +359,10 @@ where
         let position =
             cursor + translation + Vector::new(0.0, f32::from(line_height));
 
-        InputMethod::Open {
+        InputMethod::Enabled {
             position,
             purpose: input_method::Purpose::Normal,
-            preedit: Some(preedit.as_ref()),
+            preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
         }
     }
 }
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index bb2685bd21..ae3dfe4c01 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -406,10 +406,6 @@ where
             return InputMethod::Disabled;
         };
 
-        let Some(preedit) = &state.is_ime_open else {
-            return InputMethod::Allowed;
-        };
-
         let secure_value = self.is_secure.then(|| value.secure());
         let value = secure_value.as_ref().unwrap_or(value);
 
@@ -433,14 +429,14 @@ where
         let x = (text_bounds.x + cursor_x).floor() - scroll_offset
             + alignment_offset;
 
-        InputMethod::Open {
+        InputMethod::Enabled {
             position: Point::new(x, text_bounds.y + text_bounds.height),
             purpose: if self.is_secure {
                 input_method::Purpose::Secure
             } else {
                 input_method::Purpose::Normal
             },
-            preedit: Some(preedit.as_ref()),
+            preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
         }
     }
 
@@ -584,7 +580,7 @@ where
         let draw = |renderer: &mut Renderer, viewport| {
             let paragraph = if text.is_empty()
                 && state
-                    .is_ime_open
+                    .preedit
                     .as_ref()
                     .map(|preedit| preedit.content.is_empty())
                     .unwrap_or(true)
@@ -1260,7 +1256,7 @@ where
                 input_method::Event::Opened | input_method::Event::Closed => {
                     let state = state::<Renderer>(tree);
 
-                    state.is_ime_open =
+                    state.preedit =
                         matches!(event, input_method::Event::Opened)
                             .then(input_method::Preedit::new);
 
@@ -1270,7 +1266,7 @@ where
                     let state = state::<Renderer>(tree);
 
                     if state.is_focused.is_some() {
-                        state.is_ime_open = Some(input_method::Preedit {
+                        state.preedit = Some(input_method::Preedit {
                             content: content.to_owned(),
                             selection: selection.clone(),
                             text_size: self.size,
@@ -1323,23 +1319,30 @@ where
                 let state = state::<Renderer>(tree);
 
                 if let Some(focus) = &mut state.is_focused {
-                    if focus.is_window_focused
-                        && matches!(
+                    if focus.is_window_focused {
+                        if matches!(
                             state.cursor.state(&self.value),
                             cursor::State::Index(_)
-                        )
-                    {
-                        focus.now = *now;
-
-                        let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
-                            - (*now - focus.updated_at).as_millis()
-                                % CURSOR_BLINK_INTERVAL_MILLIS;
-
-                        shell.request_redraw_at(
-                            *now + Duration::from_millis(
-                                millis_until_redraw as u64,
-                            ),
-                        );
+                        ) {
+                            focus.now = *now;
+
+                            let millis_until_redraw =
+                                CURSOR_BLINK_INTERVAL_MILLIS
+                                    - (*now - focus.updated_at).as_millis()
+                                        % CURSOR_BLINK_INTERVAL_MILLIS;
+
+                            shell.request_redraw_at(
+                                *now + Duration::from_millis(
+                                    millis_until_redraw as u64,
+                                ),
+                            );
+                        }
+
+                        shell.request_input_method(&self.input_method(
+                            state,
+                            layout,
+                            &self.value,
+                        ));
                     }
                 }
             }
@@ -1363,12 +1366,6 @@ where
 
         if let Event::Window(window::Event::RedrawRequested(_now)) = event {
             self.last_status = Some(status);
-
-            shell.request_input_method(&self.input_method(
-                state,
-                layout,
-                &self.value,
-            ));
         } else if self
             .last_status
             .is_some_and(|last_status| status != last_status)
@@ -1528,9 +1525,9 @@ pub struct State<P: text::Paragraph> {
     placeholder: paragraph::Plain<P>,
     icon: paragraph::Plain<P>,
     is_focused: Option<Focus>,
-    is_ime_open: Option<input_method::Preedit>,
     is_dragging: bool,
     is_pasting: Option<Value>,
+    preedit: Option<input_method::Preedit>,
     last_click: Option<mouse::Click>,
     cursor: Cursor,
     keyboard_modifiers: keyboard::Modifiers,
diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs
index d5b334df1d..2730643909 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -75,6 +75,7 @@ where
                 mouse_interaction: mouse::Interaction::None,
                 redraw_at: None,
                 preedit: None,
+                ime_state: None,
             },
         );
 
@@ -166,6 +167,7 @@ where
     pub renderer: P::Renderer,
     pub redraw_at: Option<Instant>,
     preedit: Option<Preedit<P::Renderer>>,
+    ime_state: Option<(Point, input_method::Purpose)>,
 }
 
 impl<P, C> Window<P, C>
@@ -206,52 +208,36 @@ where
 
     pub fn request_input_method(&mut self, input_method: InputMethod) {
         match input_method {
-            InputMethod::None => {}
             InputMethod::Disabled => {
-                self.raw.set_ime_allowed(false);
+                self.disable_ime();
             }
-            InputMethod::Allowed | InputMethod::Open { .. } => {
-                self.raw.set_ime_allowed(true);
-            }
-        }
-
-        if let InputMethod::Open {
-            position,
-            purpose,
-            preedit,
-        } = input_method
-        {
-            self.raw.set_ime_cursor_area(
-                LogicalPosition::new(position.x, position.y),
-                LogicalSize::new(10, 10), // TODO?
-            );
-
-            self.raw.set_ime_purpose(conversion::ime_purpose(purpose));
-
-            if let Some(preedit) = preedit {
-                if preedit.content.is_empty() {
-                    self.preedit = None;
-                } else if let Some(overlay) = &mut self.preedit {
-                    overlay.update(
-                        position,
-                        &preedit,
-                        self.state.background_color(),
-                        &self.renderer,
-                    );
+            InputMethod::Enabled {
+                position,
+                purpose,
+                preedit,
+            } => {
+                self.enable_ime(position, purpose);
+
+                if let Some(preedit) = preedit {
+                    if preedit.content.is_empty() {
+                        self.preedit = None;
+                    } else {
+                        let mut overlay =
+                            self.preedit.take().unwrap_or_else(Preedit::new);
+
+                        overlay.update(
+                            position,
+                            &preedit,
+                            self.state.background_color(),
+                            &self.renderer,
+                        );
+
+                        self.preedit = Some(overlay);
+                    }
                 } else {
-                    let mut overlay = Preedit::new();
-                    overlay.update(
-                        position,
-                        &preedit,
-                        self.state.background_color(),
-                        &self.renderer,
-                    );
-
-                    self.preedit = Some(overlay);
+                    self.preedit = None;
                 }
             }
-        } else {
-            self.preedit = None;
         }
     }
 
@@ -268,6 +254,31 @@ where
             );
         }
     }
+
+    fn enable_ime(&mut self, position: Point, purpose: input_method::Purpose) {
+        if self.ime_state.is_none() {
+            self.raw.set_ime_allowed(true);
+        }
+
+        if self.ime_state != Some((position, purpose)) {
+            self.raw.set_ime_cursor_area(
+                LogicalPosition::new(position.x, position.y),
+                LogicalSize::new(10, 10), // TODO?
+            );
+            self.raw.set_ime_purpose(conversion::ime_purpose(purpose));
+
+            self.ime_state = Some((position, purpose));
+        }
+    }
+
+    fn disable_ime(&mut self) {
+        if self.ime_state.is_some() {
+            self.raw.set_ime_allowed(false);
+            self.ime_state = None;
+        }
+
+        self.preedit = None;
+    }
 }
 
 struct Preedit<Renderer>


Test patch: