Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-add IME support #762

Merged
merged 14 commits into from
Nov 30, 2024
Merged

Re-add IME support #762

merged 14 commits into from
Nov 30, 2024

Conversation

tomcur
Copy link
Member

@tomcur tomcur commented Nov 28, 2024

This adds IME support back into Masonry. This sticks close to linebender/parley#111, except that during IME compose, this version doesn't allow changing the selection anchor, making the code simpler. For reference, there's also linebender/parley#136.

This tweaks the focus update pass: when a widget with IME is unfocused, Masonry sends the platform's IME disable event to the newly focused widget. As a workaround, we synthesize an IME disable event in the focus pass and send it to the widget that is about to be unfocused. A complication is that the handling of that event can request focus to a different widget, and in particular, can request itself to be focused again. This handles that case, too.

Remaining work is setting the IME candidate region to be near the current selection and to make a decision on cursor/selection hiding when the platform sends a None cursor.

masonry/src/text/editor.rs Outdated Show resolved Hide resolved
masonry/src/text/editor.rs Outdated Show resolved Hide resolved
tomcur added a commit to tomcur/parley that referenced this pull request Nov 28, 2024
I believe the cursor should still land at index `self.buffer.len()`
(logically following the last cluster).

E.g., in linebender/xilem#762, if used without this
change, the selection can't span the last cluster using `cursor_at`.
github-merge-queue bot pushed a commit to linebender/parley that referenced this pull request Nov 28, 2024
I believe the cursor should still land at index `self.buffer.len()`
(logically following the last cluster).

E.g., in linebender/xilem#762, if used without
this change, the selection can't span the last cluster using
`cursor_at`.
github-merge-queue bot pushed a commit that referenced this pull request Nov 28, 2024
Another regression from #754 ...

Needed for #762

---------

Co-authored-by: Tom Churchman <[email protected]>
@tomcur tomcur marked this pull request as ready for review November 29, 2024 10:03
Comment on lines +728 to +737
if !self.editor.is_composing() && self.editor.selected_text().is_some()
{
// The IME has started composing. Delete the current selection and
// send a TextChange event with the selection removed, but without
// the composing preedit text.
self.editor.transact(fctx, lctx, |txn| {
txn.delete_selection();
});
submit_text = Some(self.text().to_string());
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note PlainEditorTxn::set_compose also removes the selection from the buffer when the IME newly starts composing (i.e., the first compose after or after a clear_compose). That seemed like a useful abstraction for Parley. The Masonry code here as a special-case also removes the selection, because Masonry might not want to send preedit text in TextChange actions, but does want to notify about the removed selection.

Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works really well for me. It does show that underlines for an emoji is in the wrong place, but that's not the fault of this PR.

Some minor points to be considered. I don't think there's anything blocking, but we should also check-in with Matt.

));
} else {
// IME indicates nothing is to be selected: collapse the selection to a
// caret just in front of the preedit.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd normally expect the cursor to be at the end of the region in this circumstance. Is this working on some prior art?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My platform IME can get into a state where it's sending None, in which case this is quite ugly:
image
typing "body" gives me:
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was more a less an arbitrary choice I lifted from linebender/parley#111, but on my platform, Firefox, Chrome and LibreOffice Writer all place the caret in front of the preedit text (and they appear to never make a spanning selection).

Firefox:

20241129_14h10m32s_grim

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Is that hitting this condition where the IME doesn't provide a preedit cursor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performing the same IME actions on Winit as on Firefox, provides a preedit cursor that spans the entire preedit text.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I haven't figured out steps yet with this IME that sends a cursor: None.)

Copy link
Member

@DJMcNab DJMcNab Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done a little bit more research into this, and it seems like Winit gets into a borked state. Any choice seems reasonable here. Alternatively, maybe we should just not have a visible cursor if we're asked to display none?

To clarify, I have confirmed that my IME is still sending cursor positions, but winit is sending us None. I don't know why

Copy link
Member Author

@tomcur tomcur Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's all slightly messy, with different implementations making different choices. Hiding the cursor completely is what Winit suggests, so perhaps hiding the caret is the best option.

Maybe we should then also call accesskit::node::clear_text_selection, but I'm not sure if that messes with something.

Comment on lines +290 to +293
self.update_layout();

self.editor
.set_selection(self.editor.cursor_at(preedit_range.start).into());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unfortunate in the commit case, because we do a layout here to be able to set the selection, then moments later we change the text, forcing another layout. Practically this is probably fine, but it would be nice to think about whether it can be avoided.

Copy link
Member Author

@tomcur tomcur Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlainEditor::layout_dirty gets us part of the way there, but as you mentioned, we can't set the next selection until the layout is updated (it is forced to fit within the current layout). It might work if we have some (private) PlainEditor::defer_set_selection or something similar, that simply waits with setting the selection until inside the update_layout method.

masonry/src/widget/text_area.rs Show resolved Hide resolved
masonry/src/widget/text_area.rs Outdated Show resolved Hide resolved
}

fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
if event.action == accesskit::Action::SetTextSelection {
if self.editor.is_composing() {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check with @mwcampbell if this is right. The scenario is:

  1. The user is inputting some text which uses a pre-edit through their IME
  2. Through their accessibility tool, they have tried to select some text, without their IME finishing the input

What should we do in that case? This code just ignores the second request, which is the easiest method to reason about. The alternatives are quite messy.

masonry/src/widget/text_area.rs Outdated Show resolved Hide resolved
masonry/src/widget/text_area.rs Outdated Show resolved Hide resolved
Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A potential simplification of the new pass code.

Either applying this or not, I think we can still land this. The only accessibilty integration problem might occur if there is a pre-edit, and not having IME is a much bigger accessibility fail. I'm unlikely to be able to review again before Monday (at least), but I'm happy for it to land as-is (presuming there aren't any massive new changes, of course)

masonry/src/passes/update.rs Show resolved Hide resolved
@tomcur
Copy link
Member Author

tomcur commented Nov 30, 2024

I'm going to defer the decision on cursor hiding to a different PR, as it may hit on AccessKit accessibility, too.

@tomcur tomcur added this pull request to the merge queue Nov 30, 2024
Merged via the queue into linebender:main with commit e9edd38 Nov 30, 2024
17 checks passed
@tomcur tomcur deleted the new-ime branch November 30, 2024 12:50
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.

2 participants