web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
feat(fields): an input method composes into the URL bar, search and dialogs
The chrome's line fields now show an input method's composition (fcitx5,
IBus) through cce-ui's `ime` model, pinned to the cce-ui commit that adds
it to LineEdit (49bee92, on its claude/cce-ui-web-mac-support-mkxqq6
branch).
- `keyboard_field` names the field with the keyboard (a dialog's focused
field, else the bookmarks search, else a focused URL bar); `focused_edit`
now answers through it.
- Before each frame `sync_ime` hands the composition to that field and
drops it from the one that lost the keyboard; one that went away with it
(a dialog answered, the menu closed) is cancelled at the handover.
- Every field draws `display()` through one painter (`FieldMarks`,
`paint_field`): the composition at the caret, underlined, bullets in a
password field, the caret where the input method has it. Presses are
hit-tested on that string and carried back with `text_index`. The field
with the keyboard reports its caret, which is where the candidates open.
The composition is never in a field's text: navigation, the bookmark
filter and a dialog's answer see only what is committed.
Verified: cargo check --all-targets with the wpe feature (WPE 2.52
headers); the suite, 64 passed, linked against stub WPE libraries. The new
test fails against the old drawing (the held text, "ab" for "axyzb").
Not run against a live input method: WPE cannot be built or run here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjL3pejMNY95NHv9BcmXaZ
CLAUDE.md | 30 +++++
Cargo.toml | 2 +-
src/main.rs | 365 ++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 261 insertions(+), 136 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 81f26fa..eceb599 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -498,6 +498,36 @@ Wheel events pass **winit-signed deltas** (positive = up) with no separate scrol
event: the engine hit-tests the wheel, gives the page its `preventDefault` chance, and
applies the inverted delta itself.
+### An input method composes into the chrome's fields
+
+The URL bar, the bookmarks search and a dialog's fields are each a cce-ui
+`LineEdit`, and an input method (fcitx5, IBus) composes into whichever has the
+keyboard (`keyboard_field`: a dialog's focused field, else the open bookmarks
+search, else a focused URL bar — `focused_edit` is the same answer). Three
+things make that work, all against cce-ui's `ime` model:
+
+- **Before each frame `sync_ime` hands the composition over.** The field with
+ the keyboard takes it up (`LineEdit::sync_ime`); the one that just lost the
+ keyboard drops what it showed (`drop_composition`, which cancels it in the
+ input method too). A field that went away with the keyboard — a dialog
+ answered, the menu closed — cannot drop anything, so a composition still up
+ at the handover is cancelled there: the new field has not taken it yet.
+- **A field draws `display()`, never its text**: the composition spliced in at
+ the caret (bullets in a password field), underlined, with the caret where
+ the input method has its cursor. `FieldMarks::of` measures all of it on the
+ run `paint_field` draws, and a press is hit-tested on the same string and
+ carried back with `text_index`. The composition is never in `text`, so
+ nothing that reads a field — navigation, the bookmark filter, the dialog's
+ answer — sees it until it is committed, when it arrives as typed keys.
+- **The field with the keyboard reports its caret as it paints**
+ (`ime::report_caret`): that is where the candidate window opens, and what
+ tells the shell text is wanted at all. A folded bar paints no URL field, so
+ it asks for nothing.
+
+A page's own fields get none of this: with the page focused no chrome field
+reports a caret, so the toolkit leaves text input off and the page gets
+plain keys. `a_composition_is_drawn_at_the_caret_and_never_held` is the test.
+
### The wheel eases; the trackpad does not
A notch used to move the page `LINE_PX` in one step, which is the browser
diff --git a/Cargo.toml b/Cargo.toml
index 81cd3a3..132228b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,7 @@ wpe = ["dep:rustix"]
servo = ["dep:servo", "dep:dpi", "dep:euclid", "dep:rustls", "dep:http"]
[dependencies]
-cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "32621051c8f1301a905abb0a2f5d7bcf77941e7a" }
+cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "49bee9238b85b7322e071d0dee9f46cd450a2dd9" }
calloop = "0.13.0"
wayland-client = { version = "0.31", features = ["system"] }
servo = { version = "0.4", optional = true }
diff --git a/src/main.rs b/src/main.rs
index 845d630..1626afd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -188,6 +188,97 @@ const SEL_BG: [f32; 4] = [0.24, 0.38, 0.60, 0.95];
const ACCENT: [f32; 4] = [0.35, 0.55, 0.85, 1.0];
const TEXT: [u8; 3] = [220, 220, 225];
const TEXT_DIM: [u8; 3] = [120, 122, 128];
+/// A line field's caret, and the underline under an input method's
+/// composition.
+const CARET: [f32; 4] = [0.85, 0.87, 0.92, 1.0];
+
+/// One of the chrome's line fields. The one with the keyboard
+/// (`BrowserApp::keyboard_field`) is the one keys edit and an input method
+/// composes into.
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum LineField {
+ Url,
+ BmSearch,
+ /// The open dialog's field at this index.
+ #[cfg(feature = "wpe")]
+ Dialog(usize),
+}
+
+/// What a line field draws, and where on it its caret, selection and an
+/// input method's composition fall — x offsets from the text's origin, read
+/// off the same shaped run the text is drawn as.
+struct FieldMarks {
+ /// `LineEdit::display`: the text with a composition at the caret,
+ /// bullets for a password.
+ shown: String,
+ caret: f32,
+ selection: Option<(f32, f32)>,
+ composition: Option<(f32, f32)>,
+}
+
+impl FieldMarks {
+ /// Measure `edit` as it will be drawn. Every offset the field holds is
+ /// into its text, so each goes through `display_index` onto `shown`.
+ fn of(fs: &mut cce_ui::cosmic_text::FontSystem, edit: &cce_ui::widget::LineEdit) -> Self {
+ let shown = edit.display();
+ let mut x = |byte: usize| x_of_boundary_in(fs, &shown, byte);
+ let caret = x(edit.display_index(edit.cursor));
+ let selection = edit
+ .selection
+ .filter(|&(a, b)| a < b)
+ .map(|(a, b)| (x(edit.display_index(a)), x(edit.display_index(b))));
+ let composition = edit.composition_range().map(|(a, b)| (x(a), x(b)));
+ Self { shown, caret, selection, composition }
+ }
+}
+
+/// Draw a line field's text in `f` with its marks: the selection under the
+/// text, `placeholder` dimmed in place of an empty field, the composition
+/// underlined, and the caret when `caret` is set. Returns the caret's rect,
+/// which the field with the keyboard reports to the input method.
+///
+/// style: deliberate — caret and selection stand 4px inside the rim: the
+/// glyph box's inset, not a gap.
+fn paint_field(pc: &mut PaintCtx, f: Rect, marks: &FieldMarks, placeholder: &str, caret: bool) -> Rect {
+ let ty = cce_ui::layout::align_text_y(f.y, f.height, URL_FONT, 0.0);
+ let x0 = f.x + text_pad();
+ let caret_rect = Rect { x: x0 + marks.caret, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 };
+ pc.clip(f, |pc| {
+ if let Some((a, b)) = marks.selection {
+ pc.quad(Rect { x: x0 + a, y: f.y + 4.0, width: b - a, height: f.height - 8.0 }, SEL_BG);
+ }
+ if marks.shown.is_empty() && !placeholder.is_empty() {
+ pc.text(placeholder.to_string(), x0, ty, URL_FONT, TEXT_DIM);
+ } else {
+ pc.text(marks.shown.clone(), x0, ty, URL_FONT, TEXT);
+ }
+ if let Some((a, b)) = marks.composition {
+ pc.quad(Rect { x: x0 + a, y: f.y + f.height - 5.0, width: b - a, height: 1.0 }, CARET);
+ }
+ if caret {
+ pc.quad(caret_rect, CARET);
+ }
+ });
+ caret_rect
+}
+
+/// Tell the input method where the caret is: where its candidates open, and
+/// that text is wanted at all. Only the field with the keyboard calls it,
+/// as it paints.
+fn report_caret(r: Rect) {
+ cce_ui::ime::report_caret(r.x, r.y, r.width, r.height);
+}
+
+/// X offset (text-origin relative) of byte `byte` of `text`, on the shaped
+/// run a field draws (`URL_FONT`, font=None, matching `pc.text`).
+fn x_of_boundary_in(fs: &mut cce_ui::cosmic_text::FontSystem, text: &str, byte: usize) -> f32 {
+ cce_ui::engine::shaped_cluster_offsets(fs, text, URL_FONT, None)
+ .iter()
+ .rev()
+ .find(|&&(b, _)| b <= byte)
+ .map(|&(_, x)| x)
+ .unwrap_or(0.0)
+}
/// A page-blocking prompt drawn over the content.
///
@@ -801,6 +892,9 @@ struct BrowserApp {
/// with) for URL-bar caret/click metrics via `shaped_cluster_offsets` —
/// `measure_text_width`'s inked-extent numbers drift off the drawn glyphs.
font_system: cce_ui::cosmic_text::FontSystem,
+ /// The line field that had the keyboard when the last frame was built,
+ /// so the frame it loses it can drop a composition it was showing.
+ ime_field: Option<LineField>,
/// The open-tab set, persisted across restarts (see `session.rs`).
session: session::Session,
/// The favorites store, shared with the host (and so with the
@@ -1664,8 +1758,9 @@ impl BrowserApp {
/// Byte of the search query under pointer x `x`.
fn bm_query_index_at(&mut self, x: f32) -> Option<usize> {
let field = self.bm_layout()?.search;
- let text = self.bm_menu.as_ref()?.query.text.clone();
- Some(self.boundary_at_x(&text, x - field.x - text_pad()))
+ let shown = self.bm_menu.as_ref()?.query.display();
+ let at = self.boundary_at_x(&shown, x - field.x - text_pad());
+ Some(self.bm_menu.as_ref()?.query.text_index(at))
}
/// The menu's geometry for the current window, bar edge and item count:
@@ -2519,21 +2614,22 @@ impl BrowserApp {
TEXT_DIM,
);
- // The focused field's caret and selection, measured on what it draws
- // (bullets for a password) before the fields are borrowed to paint.
- let focus_marks = m.fields.get(m.focused).map(|(_, edit)| {
- let shown = edit.display();
- let sel = edit.selection.filter(|&(a, b)| a < b);
- (m.focused, shown, edit.display_index(edit.cursor), sel.map(|(a, b)| (edit.display_index(a), edit.display_index(b))))
- });
- let focus_marks = focus_marks.map(|(i, shown, caret, sel)| {
- let caret_x = self.x_of_boundary(&shown, caret);
- let sel_x = sel.map(|(a, b)| (self.x_of_boundary(&shown, a), self.x_of_boundary(&shown, b)));
- (i, caret_x, sel_x)
- });
- let Some(m) = self.modal.as_ref() else { return };
+ // What each field draws (bullets for a password) and where its marks
+ // fall; only the focused field shows a selection.
+ let marks: Vec<FieldMarks> = m
+ .fields
+ .iter()
+ .enumerate()
+ .map(|(i, (_, edit))| {
+ let mut marks = FieldMarks::of(&mut self.font_system, edit);
+ if i != m.focused {
+ marks.selection = None;
+ }
+ marks
+ })
+ .collect();
- for (i, (label, edit)) in m.fields.iter().enumerate() {
+ for (i, ((label, _), marks)) in m.fields.iter().zip(&marks).enumerate() {
let f = m.field_rect(&r, i);
let focused = i == m.focused;
pc.rounded_rect(
@@ -2543,32 +2639,13 @@ impl BrowserApp {
if focused { RIM_FOCUS } else { RIM },
);
pc.rounded_rect(f, 6.0, (true, true, true, true), FIELD_BG);
- let ty = cce_ui::layout::align_text_y(f.y, f.height, URL_FONT, 0.0);
// `display()` masks a password field; the text itself never
- // reaches the paint list.
- let shown = edit.display();
- let marks = focus_marks.filter(|&(fi, ..)| fi == i);
- // style: deliberate — caret and selection 4px inside the rim, as
- // the URL bar draws them.
- pc.clip(f, |pc| {
- if let Some((_, _, Some((x0, x1)))) = marks {
- pc.quad(
- Rect { x: f.x + text_pad() + x0, y: f.y + 4.0, width: x1 - x0, height: f.height - 8.0 },
- SEL_BG,
- );
- }
- if shown.is_empty() && !label.is_empty() {
- pc.text(*label, f.x + text_pad(), ty, URL_FONT, TEXT_DIM);
- } else {
- pc.text(shown, f.x + text_pad(), ty, URL_FONT, TEXT);
- }
- if let Some((_, caret_x, _)) = marks {
- pc.quad(
- Rect { x: f.x + text_pad() + caret_x, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 },
- [0.85, 0.87, 0.92, 1.0],
- );
- }
- });
+ // reaches the paint list. The dialog blocks everything else, so
+ // its focused field is the one with the keyboard.
+ let caret = paint_field(pc, f, marks, label, focused);
+ if focused {
+ report_caret(caret);
+ }
}
let (ok, cancel) = m.button_rects(&r);
@@ -2726,24 +2803,21 @@ impl BrowserApp {
/// pages already saved, and the way out to the full collection.
fn paint_bm_menu(&mut self, pc: &mut PaintCtx, sans: &str) {
let Some(l) = self.bm_layout() else { return };
- let (total, items, hover, scroll, searching, query, caret, sel, selected) = match self.bm_menu.as_ref() {
+ // The field's marks are measured here, before painting borrows
+ // anything, on the same shaped run it draws.
+ let (total, items, hover, scroll, searching, marks, selected) = match self.bm_menu.as_ref() {
Some(m) => (
m.all.len(),
m.items.clone(),
m.hover,
m.scroll,
!m.query.text.is_empty(),
- m.query.text.clone(),
- m.query.cursor,
- m.query.selection.filter(|&(a, b)| a < b),
+ FieldMarks::of(&mut self.font_system, &m.query),
m.selected,
),
None => return,
};
- // Measured before painting borrows anything: the field's caret and
- // selection, on the same shaped run it draws.
- let caret_x = self.x_of_boundary(&query, caret);
- let sel_x = sel.map(|(a, b)| (self.x_of_boundary(&query, a), self.x_of_boundary(&query, b)));
+ let has_keyboard = self.bm_search_has_keyboard();
pc.plate(
l.plate,
(8.0, 8.0, 8.0, 8.0),
@@ -2761,26 +2835,10 @@ impl BrowserApp {
RIM_FOCUS,
);
pc.rounded_rect(f, 6.0, (true, true, true, true), FIELD_BG);
- let ty = cce_ui::layout::align_text_y(f.y, f.height, URL_FONT, 0.0);
- // style: deliberate — caret and selection 4px inside the rim, as the
- // URL bar and the dialog fields draw them.
- pc.clip(f, |pc| {
- if let Some((x0, x1)) = sel_x {
- pc.quad(
- Rect { x: f.x + text_pad() + x0, y: f.y + 4.0, width: x1 - x0, height: f.height - 8.0 },
- SEL_BG,
- );
- }
- if query.is_empty() {
- pc.text("Search bookmarks", f.x + text_pad(), ty, URL_FONT, TEXT_DIM);
- } else {
- pc.text(query.clone(), f.x + text_pad(), ty, URL_FONT, TEXT);
- }
- pc.quad(
- Rect { x: f.x + text_pad() + caret_x, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 },
- [0.85, 0.87, 0.92, 1.0],
- );
- });
+ let caret = paint_field(pc, f, &marks, "Search bookmarks", true);
+ if has_keyboard {
+ report_caret(caret);
+ }
let text_at = |pc: &mut PaintCtx, r: &Rect, s: String, color: [u8; 3]| {
pc.text(
@@ -2983,15 +3041,13 @@ impl BrowserApp {
}
}
+ /// The byte of the URL under pointer x `click_x`. The bar draws
+ /// `display()` — with a composition in it while one is up — so the hit
+ /// lands in that and `text_index` carries it back to the text.
fn cursor_from_click(&mut self, click_x: f32, field: &Rect) -> usize {
- let text = self.url.text.clone();
- self.boundary_at_x(&text, click_x - field.x - text_pad())
- }
-
- /// X offset (text-origin relative) of a byte index in the URL.
- fn x_offset(&mut self, byte: usize) -> f32 {
- let text = self.url.text.clone();
- self.x_of_boundary(&text, byte)
+ let shown = self.url.display();
+ let at = self.boundary_at_x(&shown, click_x - field.x - text_pad());
+ self.url.text_index(at)
}
/// The char boundary of `text` nearest `rel_x` (text-origin relative),
@@ -3008,19 +3064,6 @@ impl BrowserApp {
.unwrap_or(text.len())
}
- /// X offset (text-origin relative) of byte `byte` of `text`, off the same
- /// shaped run as `boundary_at_x`.
- fn x_of_boundary(&mut self, text: &str, byte: usize) -> f32 {
- let offsets =
- cce_ui::engine::shaped_cluster_offsets(&mut self.font_system, text, URL_FONT, None);
- offsets
- .iter()
- .rev()
- .find(|&&(b, _)| b <= byte)
- .map(|&(_, x)| x)
- .unwrap_or(0.0)
- }
-
/// The byte of dialog field `i`'s text under pointer x `x`. The field
/// draws `display()` — bullets, for a password — so the hit lands in
/// that and `text_index` carries it back to the text.
@@ -3033,11 +3076,6 @@ impl BrowserApp {
Some(self.modal.as_ref()?.fields[i].1.text_index(at))
}
- /// Caret x offset for the current byte cursor.
- fn caret_offset(&mut self) -> f32 {
- self.x_offset(self.url.cursor)
- }
-
/// Select the whole URL, caret at the end — what entering the bar does,
/// whether from a click, Ctrl+L or Ctrl+A. No-op on an empty field.
fn select_all_url(&mut self) {
@@ -3048,25 +3086,61 @@ impl BrowserApp {
/// while one is up (it blocks everything else), else the URL bar while
/// it is focused. Undo and redo act on this.
fn focused_edit(&mut self) -> Option<&mut cce_ui::widget::LineEdit> {
+ let field = self.keyboard_field()?;
+ self.line_field(field)
+ }
+
+ /// Which line field has the keyboard: a dialog's focused field while
+ /// one is up, else the bookmarks menu's search while it is open, else
+ /// the URL bar while it is focused.
+ fn keyboard_field(&self) -> Option<LineField> {
#[cfg(feature = "wpe")]
- if let Some(m) = self.modal.as_mut() {
- let i = m.focused;
- return m.fields.get_mut(i).map(|(_, e)| e);
+ if let Some(m) = self.modal.as_ref() {
+ return (m.focused < m.fields.len()).then_some(LineField::Dialog(m.focused));
}
- if let Some(m) = self.bm_menu.as_mut() {
- return Some(&mut m.query);
+ if self.bm_menu.is_some() {
+ return Some(LineField::BmSearch);
+ }
+ self.url_focused.then_some(LineField::Url)
+ }
+
+ /// The editor behind `field`, while it exists.
+ fn line_field(&mut self, field: LineField) -> Option<&mut cce_ui::widget::LineEdit> {
+ match field {
+ LineField::Url => Some(&mut self.url),
+ LineField::BmSearch => self.bm_menu.as_mut().map(|m| &mut m.query),
+ #[cfg(feature = "wpe")]
+ LineField::Dialog(i) => self.modal.as_mut()?.fields.get_mut(i).map(|(_, e)| e),
+ }
+ }
+
+ /// Before a frame is drawn: the field with the keyboard takes up the
+ /// input method's composition, and one that has just lost the keyboard
+ /// drops what it was showing. A field that is gone with it — a dialog
+ /// answered, the bookmarks menu closed — cannot, so a composition still
+ /// up once the old field has let go is cancelled here: it was that
+ /// field's, since the new one has not taken anything yet.
+ fn sync_ime(&mut self) {
+ let now = self.keyboard_field();
+ if now != self.ime_field {
+ let before = self.ime_field;
+ if let Some(edit) = before.and_then(|f| self.line_field(f)) {
+ edit.drop_composition();
+ }
+ if cce_ui::ime::preedit().is_some() {
+ cce_ui::ime::request_reset();
+ }
+ self.ime_field = now;
+ }
+ if let Some(edit) = now.and_then(|f| self.line_field(f)) {
+ edit.sync_ime();
}
- self.url_focused.then_some(&mut self.url)
}
/// Whether `focused_edit` is the bookmarks menu's search field — an undo
/// there changes the query, so the list has to follow.
fn bm_search_has_keyboard(&self) -> bool {
- #[cfg(feature = "wpe")]
- if self.modal.is_some() {
- return false;
- }
- self.bm_menu.is_some()
+ self.keyboard_field() == Some(LineField::BmSearch)
}
@@ -3184,6 +3258,7 @@ impl Application for BrowserApp {
#[cfg(feature = "wpe")]
sender,
font_system: cce_ui::create_font_system(),
+ ime_field: None,
session,
favorites,
favs,
@@ -4302,6 +4377,7 @@ impl Application for BrowserApp {
#[cfg(feature = "wpe")]
self.host.frame_drawn();
self.win = (size.width, size.height);
+ self.sync_ime();
let mut pc = PaintCtx::new();
let w = size.width;
@@ -4485,35 +4561,13 @@ impl Application for BrowserApp {
rim,
);
pc.rounded_rect(f, 6.0, (true, true, true, true), FIELD_BG);
- let ty = cce_ui::layout::align_text_y(f.y, f.height, URL_FONT, 0.0);
- let caret_x = if self.url_focused { Some(self.caret_offset()) } else { None };
- let sel_x = self
- .url
- .selection
- .filter(|&(a, b)| a < b)
- .map(|(a, b)| (self.x_offset(a), self.x_offset(b)));
- // style: deliberate — the caret and selection stand 4px inside
- // the field's rim: the glyph box's inset, not a gap.
- pc.clip(f, |pc| {
- if let Some((x0, x1)) = sel_x {
- pc.quad(
- Rect {
- x: f.x + text_pad() + x0,
- y: f.y + 4.0,
- width: x1 - x0,
- height: f.height - 8.0,
- },
- SEL_BG,
- );
- }
- pc.text(self.url.text.clone(), f.x + text_pad(), ty, URL_FONT, TEXT);
- if let Some(offset) = caret_x {
- pc.quad(
- Rect { x: f.x + text_pad() + offset, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 },
- [0.85, 0.87, 0.92, 1.0],
- );
- }
- });
+ let marks = FieldMarks::of(&mut self.font_system, &self.url);
+ let caret = paint_field(pc, f, &marks, "", self.url_focused);
+ // Drawn here only while the bar is out, so a folded bar asks
+ // for no text.
+ if self.keyboard_field() == Some(LineField::Url) {
+ report_caret(caret);
+ }
});
}
@@ -4609,6 +4663,47 @@ mod tests {
const SEARCH: &str = "https://duckduckgo.com/?q=";
+ #[test]
+ fn a_composition_is_drawn_at_the_caret_and_never_held() {
+ use cce_ui::ime::{set_preedit, Preedit};
+ use cce_ui::widget::LineEdit;
+ let mut fs = cce_ui::create_font_system_with_system_fonts();
+ let mut x = |s: &str, b: usize| x_of_boundary_in(&mut fs, s, b);
+ let (ax, axyzb_1, axyzb_2, axyzb_4) = (x("ab", 1), x("axyzb", 1), x("axyzb", 2), x("axyzb", 4));
+ assert!(ax > 0.0 && axyzb_2 > axyzb_1 && axyzb_4 > axyzb_2, "the run has widths to measure");
+
+ let mut fs = cce_ui::create_font_system_with_system_fonts();
+ let mut url = LineEdit::with_text("ab");
+ url.cursor = 1;
+ let before = FieldMarks::of(&mut fs, &url);
+ assert_eq!((before.shown.as_str(), before.caret, before.composition), ("ab", ax, None));
+
+ // "xyz" composing between a and b, the input method's cursor after x.
+ set_preedit(Some(Preedit::new("xyz", Some((1, 1)))));
+ assert!(url.sync_ime());
+ let marks = FieldMarks::of(&mut fs, &url);
+ assert_eq!(marks.shown, "axyzb", "drawn at the caret");
+ assert_eq!(url.text, "ab", "never in what the bar holds");
+ assert_eq!(marks.composition, Some((axyzb_1, axyzb_4)), "underlined where it is drawn");
+ assert_eq!(marks.caret, axyzb_2, "the caret where the input method has it");
+
+ // A press in it drops it, and lands on the text as held.
+ let at = url.text_index(2);
+ url.press(at, false);
+ url.release();
+ assert!(!url.composing() && cce_ui::ime::preedit().is_none());
+ assert_eq!(FieldMarks::of(&mut fs, &url).shown, "ab");
+
+ // A password field's composition is bullets too.
+ let mut password = LineEdit::masked();
+ set_preedit(Some(Preedit::new("pw", None)));
+ password.sync_ime();
+ let marks = FieldMarks::of(&mut fs, &password);
+ assert_eq!(marks.shown, "\u{2022}\u{2022}");
+ assert!(password.text.is_empty());
+ set_preedit(None);
+ }
+
#[test]
fn the_bookmarks_search_keeps_entries_matching_every_word() {
let link = |label: &str, url: &str| pages::Link { label: label.into(), url: url.into() };