winit icon indicating copy to clipboard operation
winit copied to clipboard

IME: reset editor (changed input focus)

Open dhardy opened this issue 7 months ago • 9 comments

So, in the normal case:

  1. A user clicks on an IME-supporting input field, thus the app calls window.set_ime_allowed(true) and (optionally) window.set_ime_purpose(..)
  2. The app receives Ime::Enabled, and calls window.set_ime_cursor_area (also causing itself to call this method again any time the area changes)
  3. The input field handles Ime::Preedit, ImeCommit, ...
  4. The input field loses focus: the app calls window.set_ime_allowed(false) and receives Ime::Disabled (or potentially the other way around)

But if instead during step 3 (while the field has IME focus), the user clicks on a different IME-supporting input field. What should the app do?

  • Only call window.set_ime_purpose(..) and window.set_ime_cursor_area(..)? But then the IME editor is not notified of a context switch and will presumably copy the current content into the new input field.
  • Call window.set_ime_allowed(false) then immediately call window.set_ime_allowed(true)? This may reset the IME editor (not tested), but presents a new problem: when the app receives Ime::Disabled it does not know whether this is due to the context switch or due to some external cause cancelling all IME input.
  • Call window.set_ime_allowed(false) then wait until receiving Ime::Disabled before calling window.set_ime_allowed(true)?

Documentation could be improved here.

Potentially it would also be useful if I could pass a u64 identifier when enabling IME to identify the input ~~field~~ location receiving focus, with this identifier returned through all Ime events: this way my code can be certain that the correct input field is being notified that IME was enabled/disabled and is receiving IME input.

Potentially it would be useful to know whether Ime::Disabled is received due to a call to set_ime_allowed(false) or due to some external cause such as the app losing focus. (This is not really important if an identifier is included as in the previous paragraph.)

dhardy avatar May 10 '25 09:05 dhardy

Further note: moving the text cursor within a text field should also reset the IME.

According to the wayland_protocols docs, change of focus should be handled via disable/enable and "the compositor must be able to handle consecutive series of the same request".

Also missing in winit's API is the ability to set the surrounding text, to specify the cause of change to the surrounding text, and to commit.

Wayland supports many more ContentPurpose variants than winit.

I see @kchibisov wrote a similar comment five years ago, so this is nothing new... yet the status of #1497 indicates that the API is done.

dhardy avatar May 10 '25 10:05 dhardy

According to the wayland_protocols docs, change of focus should be handled via disable/enable and "the compositor must be able to handle consecutive series of the same request".

When you have focus in your app, you're the one doing enable/disable. This API is present.

Also missing in winit's API is the ability to set the surrounding text, to specify the cause of change to the surrounding text, and to commit.

commit is internal detail, surrounding text API is indeed missing, and someone is free to add it.

kchibisov avatar May 10 '25 11:05 kchibisov

The big chunk of API is done, the rest could be added on demand. The issue is also not really closed and IIRC we have individual issues for smaller parts.

kchibisov avatar May 10 '25 11:05 kchibisov

Call window.set_ime_allowed(false) then immediately call window.set_ime_allowed(true)? This may reset the IME editor (not tested), but presents a new problem: when the app receives Ime::Disabled it does not know whether this is due to the context switch or due to some external cause cancelling all IME input.

You should disable/enable.

Potentially it would be useful to know whether Ime::Disabled is received due to a call to set_ime_allowed(false) or due to some external cause such as the app losing focus. (This is not really important if an identifier is included as in the previous paragraph.)

This sounds fine. We can add Enter/Left for that reason.

kchibisov avatar May 10 '25 11:05 kchibisov

Potentially it would also be useful if I could pass a u64 identifier when enabling IME to identify the input field location receiving focus, with this identifier returned through all Ime events: this way my code can be certain that the correct input field is being notified that IME was enabled/disabled and is receiving IME input.

This one is solved via disable/enable being dedicated event, you know exactly which field is targeted, since in case of change, the events will arrive sequentially.

kchibisov avatar May 10 '25 11:05 kchibisov

When you have focus in your app, you're the one doing enable/disable. This API is present.

This much appears to be fine. And I seem to be having success with code like this, where old_ime_target is set when changing/clearing IME focus:

            Ime(winit::event::Ime::Disabled) => {
                let mut target = self.old_ime_target.take();
                if target.is_none() && self.ime.is_some() {
                    target = self.sel_focus.clone();
                    self.ime = None;
                }
                if let Some(id) = target {
                    // Notify my widget that it lost IME focus
                    self.send_event(win.as_node(data), id, Event::LostImeFocus);
                }
            }

I think the main gap here is really just winit's API docs clarifying that it is expected to call set_ime_allowed(false) then set_ime_allowed(true) when the text position or input field focus changes.

Clarification wanted: are spurious calls to set_ime_allowed(true) ineffective or do they reset the IME? If these are ineffective then it should be safe to merge set_ime_purpose into the same function (i.e. fn set_ime_allowed(purpose: Option<ImePurpose>)).

So I think this issue could be closed with only doc changes.

dhardy avatar May 11 '25 10:05 dhardy

I'd say that separate event for who closed sounds fine. That why you don't have to deal with the LostImeFocus yourself.

Clarification wanted: are spurious calls to set_ime_allowed(true) ineffective or do they reset the IME? If these are ineffective then it should be safe to merge set_ime_purpose into the same function (i.e. fn set_ime_allowed(purpose: Option<ImePurpose>)).

I think it's more like you communicate the intent, the state is send back to you eventually. In general, I'd change the API to set_ime_state and in the state you can enable/disable and set all other sort of properties to better accommodate for atomic APIs like Wayland.

kchibisov avatar May 11 '25 10:05 kchibisov

Yes, those changes would be nice. A to-do list to recap:

  • [ ] Let Ime::Disabled distinguish the source: requested by the user or external
  • [x] Potentially merge fns set_ime_allowed and set_ime_purpose into a new fn, set_ime_state
  • [x] Add ability to set surrounding text, likely similar to wayland-protocols.

Link to my implementation: https://github.com/kas-gui/kas/pull/497

dhardy avatar May 13 '25 07:05 dhardy

Yeah, I'd just merge everything in a builder like thing which you pass in, so it'll be atomically applied.

kchibisov avatar May 13 '25 09:05 kchibisov

We've merged Window::request_ime_update that does resolve parts of the issue.

kchibisov avatar Jun 28 '25 04:06 kchibisov

I don't think there's value in keeping this open any more: changes are mostly implemented (thanks!).

The exception is knowing the cause of Ime::Disabled, but I think it's not a problem: the application can track whether or not this has been requested.

dhardy avatar Nov 25 '25 10:11 dhardy