Skip to content

Commit

Permalink
Re-add IME support (#762)
Browse files Browse the repository at this point in the history
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.

---------

Co-authored-by: Daniel McNab <[email protected]>
  • Loading branch information
tomcur and DJMcNab authored Nov 30, 2024
1 parent e0fee74 commit e9edd38
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 32 deletions.
76 changes: 54 additions & 22 deletions masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use std::collections::HashSet;
use cursor_icon::CursorIcon;
use tracing::{info_span, trace};

use crate::passes::event::run_on_pointer_event_pass;
use crate::passes::event::{run_on_pointer_event_pass, run_on_text_event_pass};
use crate::passes::{enter_span, enter_span_if, merge_state_up, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootSignal, RenderRootState};
use crate::tree_arena::ArenaMut;
use crate::{
PointerEvent, QueryCtx, RegisterCtx, Update, UpdateCtx, Widget, WidgetId, WidgetState,
PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId,
WidgetState,
};

// --- MARK: HELPERS ---
Expand Down Expand Up @@ -393,7 +394,7 @@ pub(crate) fn run_update_focus_chain_pass(root: &mut RenderRoot) {
// --- MARK: UPDATE FOCUS ---
pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
let _span = info_span!("update_focus").entered();
// If the focused widget is disabled, stashed or removed, we set
// If the next-focused widget is disabled, stashed or removed, we set
// the focused id to None
if let Some(id) = root.global_state.next_focused_widget {
if !root.is_still_interactive(id) {
Expand All @@ -402,6 +403,43 @@ pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
}

let prev_focused = root.global_state.focused_widget;
let was_ime_active = root.global_state.is_ime_active;

if was_ime_active && prev_focused != root.global_state.next_focused_widget {
// IME was active, but the next focused widget is going to receive the Ime::Disabled event
// sent by the platform. Synthesize an `Ime::Disabled` event here and send it to the widget
// about to be unfocused.
run_on_text_event_pass(root, &TextEvent::Ime(winit::event::Ime::Disabled));

// Disable the IME, which was enabled specifically for this widget. Note that if the newly
// focused widget also requires IME, we will request it again - this resets the platform's
// state, ensuring that partial IME inputs do not "travel" between widgets
root.global_state.emit_signal(RenderRootSignal::EndIme);

// Note: handling of the Ime::Disabled event sent above may have changed the next focused
// widget. In particular, focus may have changed back to the original widget we just
// disabled IME for.
//
// In this unlikely case, the rest of this handler will short-circuit, and IME would not be
// re-enabled for this widget. Re-enable IME here; the resultant `Ime::Enabled` event sent
// by the platform will be routed to this widget as it remains the focused widget. We don't
// handle this as above to avoid loops.
//
// First do the disabled, stashed or removed check again.
if let Some(id) = root.global_state.next_focused_widget {
if !root.is_still_interactive(id) {
root.global_state.next_focused_widget = None;
}
}
if prev_focused == root.global_state.next_focused_widget {
tracing::warn!(
id = prev_focused.map(|id| id.trace()),
"request_focus called whilst handling Ime::Disabled"
);
root.global_state.emit_signal(RenderRootSignal::StartIme);
}
}

let next_focused = root.global_state.next_focused_widget;

// "Focused path" means the focused widget, and all its parents.
Expand Down Expand Up @@ -458,15 +496,8 @@ pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
}
}

// Refocus if the focused widget changed.
if prev_focused != next_focused {
let was_ime_active = root.global_state.is_ime_active;
let is_ime_active = if let Some(id) = next_focused {
root.widget_arena.get_state(id).item.accepts_text_input
} else {
false
};
root.global_state.is_ime_active = is_ime_active;

// We send FocusChange event to widget that lost and the widget that gained focus.
// We also request accessibility, because build_access_node() depends on the focus state.
if let Some(prev_focused) = prev_focused {
Expand All @@ -485,23 +516,24 @@ pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
ctx.widget_state.request_accessibility = true;
ctx.widget_state.needs_accessibility = true;
});
}

if prev_focused.is_some() && was_ime_active {
root.global_state.emit_signal(RenderRootSignal::EndIme);
}
if next_focused.is_some() && is_ime_active {
root.global_state.emit_signal(RenderRootSignal::StartIme);
}
let widget_state = root.widget_arena.get_state(next_focused).item;

root.global_state.is_ime_active = widget_state.accepts_text_input;
if widget_state.accepts_text_input {
root.global_state.emit_signal(RenderRootSignal::StartIme);
}

if let Some(id) = next_focused {
let ime_area = root.widget_arena.get_state(id).item.get_ime_area();
root.global_state
.emit_signal(RenderRootSignal::new_ime_moved_signal(ime_area));
.emit_signal(RenderRootSignal::new_ime_moved_signal(
widget_state.get_ime_area(),
));
} else {
root.global_state.is_ime_active = false;
}
}

root.global_state.focused_widget = root.global_state.next_focused_widget;
root.global_state.focused_widget = next_focused;
root.global_state.focused_path = next_focused_path;
}

Expand Down
Loading

0 comments on commit e9edd38

Please sign in to comment.