GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
IME: the composition in LineEdit and the DocEditor
Both show an input method's composition at the caret without ever holding
it: a host reads LineEdit::text directly and saves the DocEditor's buffer.
- LineEdit splices it into display() at the caret; display_index and
text_index map across it (the caret where the input method has its
cursor, a point inside the composition is the caret); composition_range
is the span to underline, a masked field shows bullets. The app calls
sync_ime each frame the field has the keyboard, reports the caret it
draws (ime::report_caret), and drop_composition when it loses focus.
- The DocEditor lays out the caret's line with the composition in it and
maps every column read off that layout across it (laid_col /
source_col: the caret, caret_rect, pos_at); it underlines it and reports
its own caret while painted focused.
- Both take no key while a composition is up, take the commit as typed,
and treat a press as dropping the composition (cancelled in the input
method) and placing the caret.
Verified: a test each; cce-notes built against this tree and driven under
the headless sway with a stand-in input-method-v2 client — the composition
underlined at the caret, the commit typed, a second composition left up
through the autosave then dropped by a click, and the note on disk held
the commit and never the composition. Linux: lib suite 548 passed (587
with the markdown and doc_editor features; the 2 known failures), plate
golden identical, the 24-step sway run 0 px; check-mac and check-wasm ok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjL3pejMNY95NHv9BcmXaZ
CLAUDE.md | 28 ++++++-
src/widget/doc_editor/mod.rs | 192 ++++++++++++++++++++++++++++++++++++++++++-
src/widget/line_edit.rs | 171 +++++++++++++++++++++++++++++++++++++-
3 files changed, 383 insertions(+), 8 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 12ef531..b2579f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -335,8 +335,32 @@ the history, and the box takes no key while it composes. It is applied in `prepa
and at the top of `handle_key`, against `ime::generation`; a press, or editing ending,
drops it and asks the input method to cancel (`ime::request_reset`). A composition begun
over a selection replaces it, as typing would. `a_composition_is_shown_in_place_and_the_
-commit_is_typed` is the test. Not there yet: `LineEdit` and the `DocEditor` take the
-commit but do not show the composition; no shell sends surrounding text, so an input
+commit_is_typed` is the test.
+
+**`LineEdit` and the `DocEditor` show it too** (since 2026-10-05), each without letting it
+into what it holds — a host reads `LineEdit::text` directly and saves the `DocEditor`'s
+buffer, so neither ever contains it. `LineEdit` splices it into `display()` at the caret,
+and `display_index` / `text_index` map across it (the caret lands where the input method
+has its cursor, a point inside the composition is the caret, one after it is the text it is
+drawn after); `composition_range` is the span to underline, a masked field shows bullets.
+The app, which draws the field, calls `sync_ime` each frame the field has the keyboard,
+reports the caret it draws (`ime::report_caret` — also what tells the shell text is
+wanted), and `drop_composition` when the field loses it. The `DocEditor` lays out the
+caret's line WITH the composition (an active line, raw anyway) and maps every column read
+off that layout across it (`laid_col` / `source_col`: the caret, `caret_rect`, `pos_at`);
+it underlines it, and reports its caret itself while painted focused; a host calls
+`drop_composition` when the editor loses the keyboard. Both take no key while a
+composition is up, take the commit as typed, and treat a press as dropping the
+composition (cancelled in the input method) and placing the caret — the `DocEditor`'s read
+through the line as drawn. `a_composition_is_shown_at_the_caret_and_never_held` and
+`a_composition_is_laid_out_in_place_and_never_held` are the tests; cce-notes, built against
+this tree, was driven under the headless sway with the stand-in input method (2026-10-05):
+the composition underlined at the caret, the commit typed, a second composition left up
+through the editor's autosave and then dropped by a click — and the note on disk held the
+commit and never the composition. No `LineEdit` host calls the three methods yet
+(cce-browser's URL bar and dialog fields; it cannot be built here, for WPE).
+
+Not there yet: no shell sends surrounding text, so an input
method's `delete_surrounding_text` (text-input-v3) is not applied; and a password box is
announced with the normal content purpose.
diff --git a/src/widget/doc_editor/mod.rs b/src/widget/doc_editor/mod.rs
index 794fe97..6d0bec0 100644
--- a/src/widget/doc_editor/mod.rs
+++ b/src/widget/doc_editor/mod.rs
@@ -87,6 +87,13 @@ pub struct DocEditor {
follow_caret: bool,
/// The host's embedded images, by link text (see [`DocEditor::set_images`]).
images: Option<Box<dyn Fn(&str) -> Option<EmbedImage>>>,
+ /// An input method's composition (see `crate::ime`), at the source
+ /// position it began, and the `ime::generation` it was taken at. It is
+ /// in the caret line's LAYOUT, never in the buffer — the buffer is what
+ /// the host saves — so every column read off that line's layout is
+ /// mapped across it (`laid_col`, `source_col`).
+ composition: Option<(Pos, crate::ime::Preedit)>,
+ ime_seen: u64,
}
impl DocEditor {
@@ -117,6 +124,8 @@ impl DocEditor {
viewport: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
follow_caret: false,
images: None,
+ composition: None,
+ ime_seen: 0,
};
e.sync();
e.caret_to_body();
@@ -301,7 +310,13 @@ impl DocEditor {
}
let act = self.shown_active;
let active = self.is_active(i, act);
- let text = self.buf.line(i);
+ // The caret line with a composition in it, laid out as typed.
+ let composed = self.composition.as_ref().filter(|(at, _)| at.line == i).map(|(at, p)| {
+ let mut t = self.buf.line(i).to_string();
+ t.insert_str(at.col.min(t.len()), &p.text);
+ t
+ });
+ let text = composed.as_deref().unwrap_or(self.buf.line(i));
let line = match self.props.get(i) {
Some(Some(p)) if !active => {
let key = match p {
@@ -357,14 +372,94 @@ impl DocEditor {
(self.content_height() - self.viewport.height).max(0.0)
}
+ // ---- the input method ---------------------------------------------------
+
+ /// Take up the input method's composition, if it has moved since the
+ /// editor last did: the caret line relaid out with it in. A composition
+ /// begun over a selection replaces it, as typing would.
+ fn sync_ime(&mut self) {
+ let generation = crate::ime::generation();
+ if generation == self.ime_seen {
+ return;
+ }
+ self.ime_seen = generation;
+ let next = crate::ime::preedit();
+ if next.is_some() && self.composition.is_none() && self.buf.delete_selection() {
+ self.sync();
+ }
+ if let Some((at, _)) = self.composition.take() {
+ self.relayout(at.line);
+ }
+ if let Some(p) = next {
+ let at = self.buf.caret;
+ self.buf.anchor = None;
+ self.composition = Some((at, p));
+ self.relayout(at.line);
+ self.follow_caret = true;
+ }
+ }
+
+ /// The editor lost the keyboard, or a press moved its caret: a
+ /// composition it was showing is dropped, and the input method asked to
+ /// cancel it.
+ pub fn drop_composition(&mut self) {
+ if let Some((at, _)) = self.composition.take() {
+ self.relayout(at.line);
+ crate::ime::request_reset();
+ }
+ self.ime_seen = crate::ime::generation();
+ }
+
+ /// Whether an input method's composition is showing.
+ pub fn composing(&self) -> bool {
+ self.composition.is_some()
+ }
+
+ fn relayout(&mut self, line: usize) {
+ if let Some(l) = self.layouts.get_mut(line) {
+ *l = None;
+ }
+ self.tops_dirty = true;
+ }
+
+ /// The composition on `line`: its source column, its length in bytes,
+ /// and the bytes into it the input method has its cursor.
+ fn composition_on(&self, line: usize) -> Option<(usize, usize, usize)> {
+ let (at, p) = self.composition.as_ref().filter(|(at, _)| at.line == line)?;
+ let caret = p.text.char_indices().nth(p.caret_chars()).map_or(p.text.len(), |(b, _)| b);
+ Some((at.col, p.text.len(), caret))
+ }
+
+ /// A source column of `line` as a column of its layout: past a
+ /// composition there, moved over it; the caret itself, where the input
+ /// method has its cursor in it.
+ fn laid_col(&self, line: usize, col: usize, is_caret: bool) -> usize {
+ match self.composition_on(line) {
+ Some((c, _, caret)) if is_caret && col == c => c + caret,
+ Some((c, len, _)) if col > c => col + len,
+ _ => col,
+ }
+ }
+
+ /// A column of `line`'s layout as a source column: inside a composition
+ /// is where it began.
+ fn source_col(&self, line: usize, col: usize) -> usize {
+ match self.composition_on(line) {
+ Some((c, len, _)) if col > c && col < c + len => c,
+ Some((c, len, _)) if col >= c + len => col - len,
+ _ => col,
+ }
+ }
+
// ---- caret geometry -----------------------------------------------------
/// The caret's (x, y) in content space and its row height.
fn caret_point(&mut self, p: Pos) -> (f32, f32, f32) {
self.ensure(p.line);
self.retop();
+ let col = self.laid_col(p.line, p.col, p == self.buf.caret);
let l = self.layouts[p.line].as_ref().expect("laid out");
- let (x, row) = l.caret_xy(p.col);
+ let (x, row) = l.caret_xy(col);
(x, self.tops[p.line] + row as f32 * l.row_h, l.row_h)
}
@@ -387,7 +482,8 @@ impl DocEditor {
self.retop();
let l = self.layouts[i].as_ref().unwrap();
let row = (((y - self.tops[i]) / l.row_h).floor().max(0.0) as usize).min(l.rows - 1);
- Pos::new(i, l.col_at(x, row))
+ let col = l.col_at(x, row);
+ Pos::new(i, self.source_col(i, col))
}
fn scroll_to_caret(&mut self) {
@@ -448,6 +544,12 @@ impl DocEditor {
/// sends them to `Application::undo`); call [`DocEditor::undo`] there.
pub fn key(&mut self, ev: &KeyEvent) -> Response {
self.sync();
+ // While an input method composes, its keys are its own: a shell does
+ // not deliver them, and one that does is not editing this text.
+ self.sync_ime();
+ if self.composition.is_some() {
+ return Response::None;
+ }
let rev = self.buf.revision;
let caret = (self.buf.caret, self.buf.anchor);
let (ctrl, shift) = (ev.ctrl, ev.shift);
@@ -700,6 +802,16 @@ impl DocEditor {
self.sync();
let rev = self.buf.revision;
let before = (self.buf.caret, self.buf.anchor);
+ // A press mid-composition places the caret, read through the line
+ // as it is drawn, and the composition is left there, cancelled.
+ if self.composition.is_some() {
+ let p = self.pos_at(sx, sy);
+ self.drop_composition();
+ self.buf.set_caret(p, false);
+ self.dragging = true;
+ self.want_x = None;
+ return self.after_edit(rev, before);
+ }
let (x, y) = (sx - self.origin.0, sy - self.origin.1 + self.scroll);
if y >= 0.0 && !shift {
let i = self.line_at_y(y);
@@ -857,6 +969,7 @@ impl DocEditor {
self.viewport = rect;
self.origin = (rect.x + (rect.width - width) / 2.0, rect.y + self.pad);
self.sync();
+ self.sync_ime();
if std::mem::take(&mut self.follow_caret) {
self.scroll_to_caret();
}
@@ -887,6 +1000,8 @@ impl DocEditor {
let (ox, oy) = (self.origin.0, self.origin.1 - self.scroll);
let sel = self.buf.selection();
let caret = self.buf.caret;
+ let caret_col = self.laid_col(caret.line, caret.col, true);
+ let composition = self.composition_on(caret.line);
let th = self.theme.clone();
let first = self.line_at_y(self.scroll - self.pad);
pc.clip(rect, |pc| {
@@ -945,11 +1060,29 @@ impl DocEditor {
pc.vector(ox + r.x, sy, ox + r.x + r.w, sy, 1.0, r.color, Cap::Flat);
}
}
+ if let Some((c, len, _)) = composition {
+ if i == caret.line {
+ // The composition's underline, a row at a time.
+ let (from, to) = (l.caret_xy(c), l.caret_xy(c + len));
+ for row in from.1..=to.1 {
+ let x0 = if row == from.1 { from.0 } else { l.content_x };
+ let x1 = if row == to.1 { to.0 } else { l.runs.iter().filter(|r| r.row == row).map(|r| r.x + r.w).fold(x0, f32::max) };
+ if x1 > x0 {
+ let uy = top + (row + 1) as f32 * l.row_h - 3.0;
+ pc.quad(Rect { x: ox + x0, y: uy, width: x1 - x0, height: 1.5 }, th.caret);
+ }
+ }
+ }
+ }
if focused && i == caret.line {
- let (x, row) = l.caret_xy(caret.col);
+ let (x, row) = l.caret_xy(caret_col);
let h = l.row_h * 0.8;
let y = top + row as f32 * l.row_h + (l.row_h - h) / 2.0;
pc.quad(Rect { x: ox + x - 0.5, y, width: 2.0, height: h }, th.caret);
+ // Where an input method puts its candidates, and that
+ // text is wanted at all.
+ let (dx, dy) = pc.offset();
+ crate::ime::report_caret(ox + x - 0.5 + dx, y + dy, 2.0, h);
}
i += 1;
}
@@ -986,6 +1119,57 @@ mod tests {
e
}
+ /// A composition is in the caret line's layout and never in the buffer:
+ /// drawn as typed and underlined, the caret where the input method has
+ /// its cursor, columns read off the line mapped across it; keys wait
+ /// while it is up; the commit is typed; a press drops it, asks the input
+ /// method to cancel, and places the caret; the caret is reported.
+ #[test]
+ fn a_composition_is_laid_out_in_place_and_never_held() {
+ use crate::ime::{self, Preedit};
+ let rect = Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 };
+ let mut e = editor("ab\ncd");
+ e.buf.caret = Pos::new(0, 1);
+ e.buf.anchor = None;
+ let mut pc = PaintCtx::new();
+ e.paint(&mut pc, rect, true);
+ let plain_caret = e.caret_rect().x;
+ let _ = ime::take_reset();
+
+ ime::set_preedit(Some(Preedit::new("にほ", Some((3, 3)))));
+ ime::begin_frame();
+ e.paint(&mut pc, rect, true);
+ ime::end_frame();
+ assert_eq!(e.text(), "ab\ncd", "never in the buffer");
+ let l = e.layouts[0].as_ref().unwrap();
+ let drawn: String = l.runs.iter().map(|r| r.text.as_str()).collect();
+ assert_eq!(drawn, "aにほb");
+ assert!(e.caret_rect().x > plain_caret, "the caret is after に");
+ assert!(ime::caret().is_some(), "the caret is reported");
+ // Past the composition on its line is the source's end of line.
+ assert_eq!(e.pos_at(rect.x + 590.0, rect.y + e.pad + 2.0), Pos::new(0, 2));
+ assert_eq!(e.key(&typed("x")), Response::None);
+ assert_eq!(e.text(), "ab\ncd");
+
+ // The commit: the composition ends, its text is typed.
+ ime::set_preedit(None);
+ assert_eq!(e.key(&typed("日本")), Response::Changed);
+ assert_eq!(e.text(), "a日本b\ncd");
+ assert_eq!(e.buf.caret, Pos::new(0, 7));
+
+ // A press drops a composition, asks for it to be cancelled, and
+ // places the caret.
+ ime::set_preedit(Some(Preedit::new("か", None)));
+ e.paint(&mut pc, rect, true);
+ assert!(e.composing());
+ e.press(rect.x + 590.0, rect.y + e.pad + 2.0, false, false);
+ e.release();
+ assert!(!e.composing());
+ assert!(ime::take_reset());
+ assert_eq!(e.text(), "a日本b\ncd");
+ assert_eq!(e.buf.caret, Pos::new(0, "a日本b".len()));
+ }
+
#[test]
fn an_embed_line_shows_its_image_and_the_raw_text_with_the_caret() {
let mut e = DocEditor::new("top\n![[pic.png|200]]\nend", EditorTheme::new(14.0), false);
diff --git a/src/widget/line_edit.rs b/src/widget/line_edit.rs
index 41dc911..a7c859b 100644
--- a/src/widget/line_edit.rs
+++ b/src/widget/line_edit.rs
@@ -11,6 +11,20 @@
//! [`LineEdit::press`] / [`LineEdit::drag_to`] / [`LineEdit::release`]. cce-browser's URL bar and its dialog
//! fields (HTTP auth, JS prompts) are built on it.
//!
+//! **An input method's composition** (see `crate::ime`) is SHOWN, never
+//! held: `text` keeps what was typed, and [`LineEdit::display`] splices the
+//! composition in at the caret, with [`LineEdit::display_index`] /
+//! [`LineEdit::text_index`] mapping across it (the caret lands where the
+//! input method has its cursor; a press inside the composition is the caret)
+//! and [`LineEdit::composition_range`] the span to underline. The app calls
+//! [`LineEdit::sync_ime`] each frame the field has the keyboard, before
+//! drawing it, reports the caret it draws with `ime::report_caret` (that is
+//! also what tells the shell text input is wanted), and calls
+//! [`LineEdit::drop_composition`] when the field loses the keyboard. The
+//! commit arrives as typed text through [`LineEdit::handle_key`], which
+//! takes no other key while a composition is up — they are the input
+//! method's.
+//!
//! Indices are **byte** offsets into `text`, always on a char boundary
//! ([`prev_boundary`] / [`next_boundary`] step them), which is what slicing
//! and shaping want. [`TextEditorState`](super::TextEditorState), the model
@@ -23,6 +37,7 @@ use std::time::Duration;
use web_time::Instant;
use crate::history::History;
+use crate::ime::{self, Preedit};
use crate::widget::{ElementState, Key, KeyEvent, NamedKey};
/// How close two presses at the same offset must be to count as one double
@@ -60,6 +75,10 @@ pub struct LineEdit {
/// Undo and redo for what [`LineEdit::handle_key`] changes. Never kept
/// for a masked field (see [`LineEdit::undo`]).
history: History<Snapshot>,
+ /// The input method's composition, shown at the caret (see the module
+ /// doc), and the `ime::generation` it was taken at.
+ composition: Option<Preedit>,
+ ime_seen: u64,
}
/// What an undo step restores: the text, and where the caret and selection
@@ -129,13 +148,77 @@ impl LineEdit {
Self { masked: true, ..Self::default() }
}
- /// What to draw. Never returns the password itself.
+ /// What to draw: the text, with an input method's composition at the
+ /// caret. Never returns the password itself.
pub fn display(&self) -> String {
- if self.masked {
+ let mut shown = if self.masked {
"\u{2022}".repeat(self.text.chars().count())
} else {
self.text.clone()
+ };
+ if let Some(p) = &self.composition {
+ shown.insert_str(self.display_index_held(self.cursor), &self.shown(p));
+ }
+ shown
+ }
+
+ /// A composition as drawn: its text, or a bullet a char when masked.
+ fn shown(&self, p: &Preedit) -> String {
+ if self.masked {
+ "\u{2022}".repeat(p.text.chars().count())
+ } else {
+ p.text.clone()
+ }
+ }
+
+ // ---- the input method ----
+
+ /// Take up the input method's composition, if it has moved since this
+ /// field last did. Call each frame the field has the keyboard, before
+ /// drawing it; true when what [`LineEdit::display`] shows changed. A
+ /// composition begun over a selection replaces it, as typing would.
+ pub fn sync_ime(&mut self) -> bool {
+ let generation = ime::generation();
+ if generation == self.ime_seen {
+ return false;
+ }
+ self.ime_seen = generation;
+ let next = ime::preedit();
+ if next.is_some() && self.composition.is_none() && self.selection.is_some_and(|(a, b)| a < b) {
+ let before = self.snapshot();
+ self.take_selection();
+ if !self.masked {
+ self.history.record(before);
+ }
+ }
+ if next.is_some() {
+ self.selection = None;
+ }
+ let changed = next != self.composition;
+ self.composition = next;
+ changed
+ }
+
+ /// The field lost the keyboard, or a press moved its caret: a
+ /// composition it was showing is dropped, and the input method asked to
+ /// cancel it.
+ pub fn drop_composition(&mut self) {
+ if self.composition.take().is_some() {
+ ime::request_reset();
}
+ self.ime_seen = ime::generation();
+ }
+
+ /// Whether an input method's composition is showing.
+ pub fn composing(&self) -> bool {
+ self.composition.is_some()
+ }
+
+ /// The composition's span in [`LineEdit::display`], to underline.
+ pub fn composition_range(&self) -> Option<(usize, usize)> {
+ let p = self.composition.as_ref()?;
+ let at = self.display_index_held(self.cursor);
+ Some((at, at + self.shown(p).len()))
}
pub fn select_all(&mut self) {
@@ -166,6 +249,8 @@ impl LineEdit {
}
fn press_at(&mut self, at: usize, extend: bool, now: Instant) {
+ // The caret moves: a composition is left where it was, cancelled.
+ self.drop_composition();
// A click moves the caret: typing after it is a new undo step.
self.history.break_group();
let at = self.boundary(at);
@@ -317,7 +402,19 @@ impl LineEdit {
/// A byte offset into [`LineEdit::display`] as the offset into `text` it
/// stands for: the same offset unless the field is masked, where each
/// bullet stands for one character of the text.
+ /// Across a composition: a point inside it is the caret, one after it
+ /// the text it is drawn after.
pub fn text_index(&self, display_at: usize) -> usize {
+ match self.composition_range() {
+ Some((a, _)) if display_at <= a => self.text_index_held(display_at),
+ Some((_, b)) if display_at < b => self.cursor,
+ Some((a, b)) => self.text_index_held(display_at - (b - a)),
+ None => self.text_index_held(display_at),
+ }
+ }
+
+ /// [`LineEdit::text_index`] over the held text alone.
+ fn text_index_held(&self, display_at: usize) -> usize {
if !self.masked {
return self.boundary(display_at);
}
@@ -328,7 +425,25 @@ impl LineEdit {
/// The other direction: a byte offset into `text` (the caret, a selection
/// edge) as the offset into [`LineEdit::display`] where it is drawn — the
/// same offset unless masked, where it is that many bullets in.
+ ///
+ /// Across a composition: the caret is drawn where the input method has
+ /// its cursor in it, and what follows the caret after it.
pub fn display_index(&self, at: usize) -> usize {
+ let held = self.display_index_held(at);
+ let Some(p) = &self.composition else { return held };
+ let at = self.boundary(at);
+ if at > self.cursor {
+ held + self.shown(p).len()
+ } else if at == self.cursor {
+ let caret = p.caret_chars();
+ held + if self.masked { caret * '\u{2022}'.len_utf8() } else { p.text.char_indices().nth(caret).map_or(p.text.len(), |(b, _)| b) }
+ } else {
+ held
+ }
+ }
+
+ /// [`LineEdit::display_index`] over the held text alone.
+ fn display_index_held(&self, at: usize) -> usize {
let at = self.boundary(at);
if !self.masked {
return at;
@@ -388,6 +503,12 @@ impl LineEdit {
/// redo. (A DE chord bound to Ctrl+Y is offered to the app first, as
/// any chord is, and never gets this far.)
pub fn handle_key(&mut self, event: &KeyEvent) -> EditOutcome {
+ // While an input method composes, its keys are its own: a shell does
+ // not deliver them, and one that does is not editing this text.
+ self.sync_ime();
+ if self.composition.is_some() {
+ return EditOutcome::Edited;
+ }
// Before the recording below, which would file the redo as a fresh
// edit and so drop everything left to redo.
if event.state == ElementState::Pressed
@@ -644,6 +765,52 @@ mod tests {
ev(Key::Named(n), false)
}
+ /// A composition is shown, not held: `display` has it at the caret, the
+ /// indices map across it, `text` never sees it; keys wait while it is up;
+ /// the commit is typed; a press drops it and asks the input method to
+ /// cancel; a masked field shows it as bullets.
+ #[test]
+ fn a_composition_is_shown_at_the_caret_and_never_held() {
+ let mut e = LineEdit::with_text("ab");
+ e.cursor = 1;
+ let _ = ime::take_reset();
+ // "にほ", the input method's cursor after "に" (three bytes in).
+ ime::set_preedit(Some(Preedit::new("にほ", Some((3, 3)))));
+ assert!(e.sync_ime());
+ assert_eq!(e.text, "ab");
+ assert_eq!(e.display(), "aにほb");
+ assert_eq!(e.composition_range(), Some((1, 7)));
+ assert_eq!(e.display_index(1), 4, "the caret after に");
+ assert_eq!(e.display_index(2), 8, "what follows, after the composition");
+ assert_eq!(e.text_index(5), 1, "inside the composition is the caret");
+ assert_eq!(e.text_index(8), 2);
+ assert_eq!(e.handle_key(&ch("x")), EditOutcome::Edited);
+ assert_eq!(e.text, "ab", "keys wait while the input method composes");
+
+ // The commit: the composition ends, its text is typed.
+ ime::set_preedit(None);
+ e.handle_key(&ch("日本"));
+ assert_eq!((e.text.as_str(), e.cursor), ("a日本b", 7));
+ assert_eq!(e.display(), "a日本b");
+
+ // A press drops a composition and asks for it to be cancelled.
+ ime::set_preedit(Some(Preedit::new("か", None)));
+ e.sync_ime();
+ assert!(e.composing());
+ e.press(0, false);
+ assert!(!e.composing());
+ assert!(ime::take_reset());
+ assert_eq!(e.text, "a日本b");
+
+ // Masked: bullets for the composition too.
+ let mut m = LineEdit::masked();
+ ime::set_preedit(Some(Preedit::new("ab", None)));
+ m.sync_ime();
+ assert_eq!(m.display(), "\u{2022}\u{2022}");
+ assert_eq!(m.text, "");
+ ime::set_preedit(None);
+ }
+
fn typed(e: &mut LineEdit, s: &str) {
for c in s.chars() {
e.handle_key(&ch(&c.to_string()));