Skip to content

Commit

Permalink
Use the Parley-reported IME cursor area in TextArea
Browse files Browse the repository at this point in the history
This also adds the required context methods for (re)setting the IME
area. These are added to the mutable context types, which I believe is
the right place.

Note Masonry currently reports new IME cursor areas only when the widget
itself moves or the focus changes to a new widget, but Parley's
`PlainEditor` determines the IME cursor area based on the selection and
preedit state of the editor, meaning the area can change based on events
as well. Other widgets may choose to have different behavior still.

(It appears Masonry currently doesn't account for the IME area
potentially changing based on relayout, which may be a bug.)

This replaces the old IME area reporting with a check at the end of the
rewrite pass to see if IME is active, and if it is, whether the IME area
has moved. If it has, the new area is reported to the platform.
  • Loading branch information
tomcur committed Dec 11, 2024
1 parent d5f1cd1 commit cf76ee5
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 17 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ tree_arena = { version = "0.1.0", path = "tree_arena" }
vello = "0.3"
wgpu = "22.1.0"
kurbo = "0.11.1"
parley = { git = "https://github.com/linebender/parley", rev = "211878fae20ba8b4bf4dcc39e6bf790eece8aa03", features = [
parley = { git = "https://github.com/linebender/parley", rev = "1a8740d8d86ebf751201e45e89bb71019340137d", features = [
"accesskit",
] }
peniko = "0.2.0"
Expand Down
15 changes: 15 additions & 0 deletions masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ impl_context_method!(
.expect("get_child_state_mut: child not found");
child_state_mut.item
}

/// Set the IME cursor area.
///
/// The reported IME area can be used by the platform to, for example, place a candidate
/// box near that area, while ensuring the area is not obscured.
pub fn set_ime_area(&mut self, ime_area: Rect) {
self.widget_state.ime_area = Some(ime_area);
}

/// Remove the IME cursor area.
///
/// See [`LayoutCtx::set_ime_area`](LayoutCtx::set_ime_area) for more details.
pub fn clear_ime_area(&mut self) {
self.widget_state.ime_area = None;
}
}
);

Expand Down
8 changes: 1 addition & 7 deletions masonry/src/passes/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tree_arena::ArenaMut;
use vello::kurbo::Vec2;

use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootSignal, RenderRootState};
use crate::render_root::{RenderRoot, RenderRootState};
use crate::{ComposeCtx, Widget, WidgetState};

// --- MARK: RECURSE ---
Expand Down Expand Up @@ -42,12 +42,6 @@ fn compose_widget(
widget.item.compose(&mut ctx);
}

// TODO - Add unit tests for this.
if moved && state.item.accepts_text_input && global_state.is_focused(state.item.id) {
let ime_area = state.item.get_ime_area();
global_state.emit_signal(RenderRootSignal::new_ime_moved_signal(ime_area));
}

// We need to update the accessibility node's coordinates and repaint it at the new position.
state.item.request_accessibility = true;
state.item.needs_accessibility = true;
Expand Down
5 changes: 0 additions & 5 deletions masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,11 +523,6 @@ pub(crate) fn run_update_focus_pass(root: &mut RenderRoot) {
if widget_state.accepts_text_input {
root.global_state.emit_signal(RenderRootSignal::StartIme);
}

root.global_state
.emit_signal(RenderRootSignal::new_ime_moved_signal(
widget_state.get_ime_area(),
));
} else {
root.global_state.is_ime_active = false;
}
Expand Down
28 changes: 27 additions & 1 deletion masonry/src/render_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub(crate) struct RenderRootState {
pub(crate) text_layout_context: LayoutContext<BrushIndex>,
pub(crate) mutate_callbacks: Vec<MutateCallback>,
pub(crate) is_ime_active: bool,
pub(crate) last_sent_ime_area: Rect,
pub(crate) scenes: HashMap<WidgetId, Scene>,
/// Whether data set in the pointer pass has been invalidated.
pub(crate) needs_pointer_pass: bool,
Expand Down Expand Up @@ -170,6 +171,7 @@ impl RenderRoot {
text_layout_context: LayoutContext::new(),
mutate_callbacks: Vec::new(),
is_ime_active: false,
last_sent_ime_area: Rect::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN),
scenes: HashMap::new(),
needs_pointer_pass: false,
trace: PassTracing::from_env(),
Expand Down Expand Up @@ -263,6 +265,13 @@ impl RenderRoot {
let _span = info_span!("text_event");
let handled = run_on_text_event_pass(self, &event);
run_update_focus_pass(self);

if matches!(event, TextEvent::Ime(winit::event::Ime::Enabled)) {
// Reset the last sent IME area, as the platform reset the IME state and may have
// forgotten it.
self.global_state.last_sent_ime_area =
Rect::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
}
self.run_rewrite_passes();

handled
Expand Down Expand Up @@ -489,6 +498,19 @@ impl RenderRoot {
self.global_state
.emit_signal(RenderRootSignal::RequestRedraw);
}

if self.global_state.is_ime_active {
let widget = self
.global_state
.focused_widget
.expect("IME is active without a focused widget");
let ime_area = self.widget_arena.get_state(widget).item.get_ime_area();
if self.global_state.last_sent_ime_area != ime_area {
self.global_state.last_sent_ime_area = ime_area;
self.global_state
.emit_signal(RenderRootSignal::new_ime_moved_signal(ime_area));
}
}
}

pub(crate) fn request_render_all(&mut self) {
Expand Down Expand Up @@ -580,6 +602,10 @@ impl RenderRootState {
self.focused_widget != self.next_focused_widget
}

#[expect(
dead_code,
reason = "no longer used, but may be useful again in the future"
)]
pub(crate) fn is_focused(&self, id: WidgetId) -> bool {
self.focused_widget == Some(id)
}
Expand All @@ -594,7 +620,7 @@ impl RenderRootSignal {
RenderRootSignal::ImeMoved(
LogicalPosition {
x: area.origin().x,
y: area.origin().y + area.size().height,
y: area.origin().y,
},
LogicalSize {
width: area.size().width,
Expand Down
26 changes: 25 additions & 1 deletion masonry/src/widget/text_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use parley::layout::Alignment;
use parley::PlainEditor;
use smallvec::SmallVec;
use tracing::{trace_span, Span};
use vello::kurbo::Vec2;
use vello::kurbo::{Rect, Vec2};
use vello::peniko::{Brush, Color, Fill};
use vello::Scene;
use winit::keyboard::{Key, NamedKey};
Expand Down Expand Up @@ -299,6 +299,25 @@ impl<const EDITABLE: bool> TextArea<EDITABLE> {
}
}

// --- MARK: HELPERS ---
impl<const EDITABLE: bool> TextArea<EDITABLE> {
/// Get the IME area from the editor, accounting for padding.
///
/// This should only be called when the editor layout is available.
fn ime_area(&self) -> Rect {
debug_assert!(
self.editor.try_layout().is_some(),
"TextArea::ime_area should only be called when the editor layout is available"
);
let is_rtl = self
.editor
.try_layout()
.map(|layout| layout.is_rtl())
.unwrap_or(false);
self.editor.ime_cursor_area() + Vec2::new(self.padding.get_left(is_rtl), self.padding.top)
}
}

// --- MARK: WIDGETMUT ---
impl<const EDITABLE: bool> TextArea<EDITABLE> {
/// Set font styling for an active text area.
Expand Down Expand Up @@ -499,6 +518,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
let new_generation = self.editor.generation();
if new_generation != self.rendered_generation {
ctx.request_render();
ctx.set_ime_area(self.ime_area());
self.rendered_generation = new_generation;
}
ctx.request_focus();
Expand All @@ -515,6 +535,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
let new_generation = self.editor.generation();
if new_generation != self.rendered_generation {
ctx.request_render();
ctx.set_ime_area(self.ime_area());
self.rendered_generation = new_generation;
}
}
Expand All @@ -539,6 +560,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
},
);
let (fctx, lctx) = ctx.text_contexts();
// Whether the text was changed.
let mut edited = false;
// Ideally we'd use key_without_modifiers, but that's broken
match &key_event.logical_key {
Expand Down Expand Up @@ -730,6 +752,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
ctx.request_layout();
} else {
ctx.request_render();
ctx.set_ime_area(self.ime_area());
}
self.rendered_generation = new_generation;
}
Expand Down Expand Up @@ -851,6 +874,7 @@ impl<const EDITABLE: bool> Widget for TextArea<EDITABLE> {
let layout = self.editor.layout(fctx, lctx);
let text_width = max_advance.unwrap_or(layout.full_width());
let text_size = Size::new(text_width.into(), layout.height().into());
ctx.set_ime_area(self.ime_area());

let area_size = Size {
height: text_size.height + padding_size.height,
Expand Down

0 comments on commit cf76ee5

Please sign in to comment.