Skip to main content

kui_core/
edit.rs

1//! Editable text: the retained state of every `text_edit` node, keyed by
2//! node `Key`, built on cosmic-text's editor.
3//!
4//! A view declares an editor with `Ui::text_edit(label, initial, &opts,
5//! spec)`, where `opts` is an [`EditOptions`]. The core keeps the buffer,
6//! caret, selection, IME composition and undo history across frames,
7//! routes `InputEvent::Text` / `InputEvent::Key` to the focused editor and
8//! posts `changed` and `submit` events. An app reads the draft back with
9//! `Core::edit_text` / `Ui::edit_text` and replaces it with
10//! `Core::set_edit_text`; the store itself is not something an app
11//! touches.
12//!
13//! ```rust
14//! use kui_core::{Core, EditOptions, InputEvent, NodeSpec, Size, TextStyle};
15//!
16//! let mut core = Core::new();
17//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
18//! let opts = EditOptions {
19//!     style: TextStyle::new(14.0),
20//!     autofocus: true,
21//!     ..Default::default()
22//! };
23//! let field = ui.text_edit("name", "hello", &opts, NodeSpec::row().grow_width().pad(4.0));
24//! ui.finish();
25//!
26//! // Typing reaches the focused editor; the host reads the draft back.
27//! core.handle_input(InputEvent::Text(" world".into()));
28//! assert_eq!(core.edit_text(field).as_deref(), Some("hello world"));
29//! ```
30
31use std::collections::VecDeque;
32
33use cosmic_text::{
34    Action, Attrs, AttrsList, Buffer, Cursor, Edit as _, Editor, FontSystem, Motion, Selection,
35    Shaping, Wrap,
36};
37use rustc_hash::FxHashMap;
38
39use crate::color::Color;
40use crate::display::{Clip, ClipId, Quad, QuadKind};
41use crate::geom::{Rect, Size, Vec2};
42use crate::input::{EditKey, Mods};
43use crate::key::Key;
44use crate::resources::Resources;
45use crate::spec::{TextStyle, TextWrap};
46use crate::text::TextSystem;
47use crate::tree::OriginId;
48
49/// How a `text_edit` node behaves: its text style, whether it is a
50/// single-line field or a multiline document, and how it takes focus and
51/// paints its selection. Build one with struct update syntax from
52/// `Default`.
53#[derive(Clone, Debug, Default)]
54pub struct EditOptions {
55    pub style: TextStyle,
56    /// A document rather than a field: Enter inserts a newline, the text
57    /// wraps to the box, and the caret opens at the top.
58    pub multiline: bool,
59    /// A single-line editor that folds to its width, by `style.wrap`, the
60    /// way a document does — instead of taking one line and scrolling it
61    /// under the caret. Its keyboard is still a field's:
62    /// Enter submits, a newline is never admitted, the caret opens at the
63    /// end. In the bindings this is the `wrap` row being declared on the
64    /// element; off, a field does not read `style.wrap` at all. A
65    /// `multiline` editor wraps either way and ignores this.
66    pub wrap: bool,
67    /// Takes keyboard focus on the frame this declaration starts — a new
68    /// editor, one back after a gap, one whose flag just turned on — and
69    /// only while nothing holds focus: never from a focused control, and
70    /// never again after a blur.
71    pub autofocus: bool,
72    /// A single-line editor whose Tab is the app's: Tab and Shift-Tab do
73    /// not walk the focus ring from it, and the press goes to the key
74    /// sink above it as a chord the editor does not claim does — so a
75    /// list or an outline built from fields indents on Tab (backlog F146).
76    /// With no sink above, Tab does nothing here. A `multiline` editor
77    /// keeps Tab for indentation either way and ignores this.
78    pub keep_tab: bool,
79    /// What an empty editor shows where its text would be, in the theme's
80    /// `faint` (backlog F149): never part of the value, gone with the
81    /// first character typed or composed, and read by assistive
82    /// technology as the field's description when it declares none.
83    pub placeholder: Option<String>,
84    /// Selection highlight color. `None` is the theme's `selection`,
85    /// which is what a field gets unless the caller says otherwise — so a
86    /// selection over a label and one over a field are the same tint on
87    /// both bases.
88    pub accent: Option<Color>,
89    /// A password field (backlog F167): the text is drawn as a bullet
90    /// (•) per grapheme, and measured, hit and selected as the bullets;
91    /// a copy or a cut of it is refused, a paste is taken; no input method
92    /// composes into it; it keeps no undo history, which would hold what
93    /// was typed; the access tree reads it as a password field whose value
94    /// is the bullets; and while it has the keyboard the window asks for
95    /// secure keyboard entry and the input method off, as
96    /// [`Core::secure_input`](crate::Core::secure_input) and
97    /// [`Core::ime_off`](crate::Core::ime_off) read back. The app still
98    /// reads the text with `edit_text`. Declared off on a later frame —
99    /// a "show password" toggle — the same editor draws the text again,
100    /// the caret where it was; on again, it masks it. A secure editor is
101    /// a field: `multiline` is ignored, and ⌥← ⌥→ (Ctrl elsewhere) and a
102    /// double click take the whole text, there being no words to show.
103    pub secure: bool,
104}
105
106/// What a secure editor draws for each grapheme of its text.
107pub const SECURE_BULLET: char = '\u{2022}';
108/// [`SECURE_BULLET`]'s length in a secure editor's buffer.
109const BULLET_LEN: usize = SECURE_BULLET.len_utf8();
110
111/// `n` bullets, the buffer of a secure editor whose text has `n`
112/// graphemes.
113fn bullets(n: usize) -> String {
114    std::iter::repeat_n(SECURE_BULLET, n).collect()
115}
116
117/// How many graphemes `text` has: the bullets a secure editor draws for it.
118fn graphemes(text: &str) -> usize {
119    use unicode_segmentation::UnicodeSegmentation;
120    text.graphemes(true).count()
121}
122
123/// The byte where grapheme `n` of `text` starts; its length for `n` past
124/// the last.
125fn grapheme_start(text: &str, n: usize) -> usize {
126    use unicode_segmentation::UnicodeSegmentation;
127    text.grapheme_indices(true)
128        .nth(n)
129        .map_or(text.len(), |(at, _)| at)
130}
131
132/// How many graphemes of `text` start before `byte`: the bullet a byte
133/// offset into a secure editor's text is drawn at.
134fn graphemes_before(text: &str, byte: usize) -> usize {
135    use unicode_segmentation::UnicodeSegmentation;
136    text.grapheme_indices(true)
137        .take_while(|(at, _)| *at < byte)
138        .count()
139}
140
141/// What an editing key did to an editor: whether the text changed,
142/// whether it submitted the field, and the edge it met when it could do
143/// neither (backlog F145).
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
145pub(crate) struct KeyOutcome {
146    pub changed: bool,
147    pub submit: bool,
148    pub boundary: Option<Edge>,
149}
150
151/// Which end of an editor's text a key met: the start (↑ on the first
152/// line, ← or Backspace at offset 0) or the end (↓ on the last line, → or
153/// Delete at the end).
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub(crate) enum Edge {
156    Start,
157    End,
158}
159
160impl Edge {
161    pub fn name(self) -> &'static str {
162        match self {
163            Edge::Start => "start",
164            Edge::End => "end",
165        }
166    }
167}
168
169pub(crate) struct EditState {
170    editor: Editor<'static>,
171    pub(crate) style: TextStyle,
172    pub(crate) accent: Color,
173    pub(crate) multiline: bool,
174    /// `EditOptions::keep_tab`, as last declared.
175    keep_tab: bool,
176    /// A secure editor's text (`EditOptions::secure`), whose buffer holds
177    /// a [`SECURE_BULLET`] per grapheme of it. `None` for any other: the
178    /// buffer is the text.
179    secret: Option<String>,
180    /// `EditOptions::placeholder`, shaped in a buffer of its own at the
181    /// editor's metrics, beside the `metrics_rev` it was shaped at.
182    placeholder: Option<(String, u32, Buffer)>,
183    /// Whether the buffer is laid out to its box's width: a document, or a
184    /// field with `wrap` declared. What `wrapped`,
185    /// `line_offset` and emission's clip decide by — a field that does not
186    /// fold takes one line and scrolls it.
187    folds: bool,
188    pub(crate) origin: OriginId,
189    scale: f32,
190    /// Wrap width (physical) the buffer is laid out at; None = unwrapped.
191    wrap: Option<f32>,
192    /// Bumped on every content change.
193    pub version: u64,
194    /// Bumped whenever the same text is laid out under new metrics (a
195    /// style or a scale change), so neither cache below can answer a
196    /// question about this font with a size measured for the last one.
197    metrics_rev: u32,
198    /// Cached wrapped measurement: (version, metrics, wrap bits, size).
199    measured: Option<(u64, u32, u32, Size)>,
200    /// Cached unwrapped measurement: (version, metrics, size). What the
201    /// fit width reads, and the reason it is cached rather than taken off
202    /// the buffer as it stands: the buffer is still carrying whatever
203    /// width `wrapped` last set on it.
204    natural: Option<(u64, u32, Size)>,
205    /// How far a single-line field has scrolled its text left, in physical
206    /// px. A field keeps the caret inside its box by moving the text under
207    /// it, the way a native field does, rather than by wrapping; an
208    /// editor that folds (`folds`) wraps and this stays 0.
209    offset_x: f32,
210    /// In-progress IME composition: a marked, uncommitted range living
211    /// inside the buffer (so the text around it reflows as it grows).
212    preedit: Option<Preedit>,
213    /// Edit history, oldest first. The widget owns its buffer, so it owns
214    /// undo too — hosts with their own text model (on_key sinks) bring
215    /// their own history and never touch this.
216    undo: VecDeque<EditOp>,
217    redo: VecDeque<EditOp>,
218    /// What the top undo op can still absorb (typing bursts, delete runs).
219    coalesce: Option<Coalesce>,
220    /// Whether the last declaration carried `autofocus`: with
221    /// `last_declared`, what makes the next one an edge or a repeat.
222    autofocus: bool,
223    /// The frame this key was last declared in. Only the budget reads it
224    /// (see [`MAX_UNDECLARED_EDITS`]); a state declared every frame never
225    /// looks at it again.
226    last_declared: u64,
227}
228
229/// How many *undeclared* editors the store keeps before the longest
230/// undeclared one is dropped. A declared editor is never evicted, however
231/// many there are: retention across absence is what `text_edit` promises,
232/// so this is a ceiling, not a prune. An editor holding a short line is a
233/// few KB and one holding a long line about 22 KB, so a full budget is a
234/// few MB.
235pub const MAX_UNDECLARED_EDITS: usize = 256;
236
237const UNDO_CAP: usize = 1000;
238/// Max bytes one coalesced op absorbs before a new unit starts.
239const COALESCE_MAX: usize = 64;
240
241/// One reversible edit: `deleted` was removed at `at` and `inserted` put in
242/// its place. Operational, not a snapshot — undo cost tracks the edit size,
243/// never the document size.
244struct EditOp {
245    at: Cursor,
246    deleted: String,
247    inserted: String,
248    /// Caret restore points for undo / redo.
249    cursor_before: Cursor,
250    cursor_after: Cursor,
251}
252
253#[derive(Clone, Copy, PartialEq)]
254enum Coalesce {
255    /// Plain typing: appends to the top op's `inserted`.
256    Insert,
257    /// Backspace runs walking left: prepends to `deleted`.
258    Backspace,
259    /// Forward-delete runs at a fixed spot: appends to `deleted`.
260    Delete,
261}
262
263/// Where a cursor lands after inserting `s` at `at`.
264fn end_cursor(at: Cursor, s: &str) -> Cursor {
265    match s.rsplit_once('\n') {
266        None => Cursor::new(at.line, at.index + s.len()),
267        Some((head, tail)) => Cursor::new(at.line + head.matches('\n').count() + 1, tail.len()),
268    }
269}
270
271/// Position equality, ignoring affinity (which editor cursors carry but
272/// computed ones don't).
273fn same_pos(a: Cursor, b: Cursor) -> bool {
274    a.line == b.line && a.index == b.index
275}
276
277/// IME composition state. The text is *in* the buffer starting at `start`
278/// — inserted without touching the undo history, replaced on every update,
279/// removed on commit or cancel — so the paragraph wraps and the following
280/// text shifts exactly as if it had been typed. The editor's own caret is
281/// parked at the IME-reported offset inside it.
282struct Preedit {
283    start: Cursor,
284    text: String,
285}
286
287/// Byte offset of `c` in the buffer's full text (lines joined by '\n').
288fn abs_offset(b: &Buffer, c: Cursor) -> usize {
289    b.lines
290        .iter()
291        .take(c.line)
292        .map(|l| l.text().len() + 1)
293        .sum::<usize>()
294        + c.index
295}
296
297impl EditState {
298    /// Both measurement caches, dropped together: the text under them
299    /// changed, so neither the wrapped size nor the natural one still
300    /// describes it.
301    fn invalidate_measurements(&mut self) {
302        self.measured = None;
303        self.natural = None;
304    }
305
306    /// Pushes a fresh op (clearing redo), merging into the top op when the
307    /// declared coalesce kind matches and the edits are adjacent.
308    fn record(&mut self, op: EditOp, kind: Option<Coalesce>) {
309        self.redo.clear();
310        // A secure editor keeps no history: it would hold what was typed.
311        if self.secret.is_some() {
312            return;
313        }
314        if let (Some(k), Some(last)) = (kind, self.undo.back_mut())
315            && self.coalesce == Some(k)
316            && last.deleted.len() + last.inserted.len() + op.deleted.len() + op.inserted.len()
317                <= COALESCE_MAX
318        {
319            let merged = match k {
320                Coalesce::Insert => {
321                    same_pos(op.at, end_cursor(last.at, &last.inserted)) && {
322                        last.inserted.push_str(&op.inserted);
323                        true
324                    }
325                }
326                Coalesce::Backspace => {
327                    same_pos(end_cursor(op.at, &op.deleted), last.at) && {
328                        last.at = op.at;
329                        last.deleted.insert_str(0, &op.deleted);
330                        true
331                    }
332                }
333                Coalesce::Delete => {
334                    same_pos(op.at, last.at) && {
335                        last.deleted.push_str(&op.deleted);
336                        true
337                    }
338                }
339            };
340            if merged {
341                last.cursor_after = op.cursor_after;
342                return;
343            }
344        }
345        if self.undo.len() >= UNDO_CAP {
346            self.undo.pop_front();
347        }
348        self.undo.push_back(op);
349        self.coalesce = kind;
350    }
351
352    /// Caret motion, clicks, blur: the next edit starts a new undo unit.
353    fn break_coalesce(&mut self) {
354        self.coalesce = None;
355    }
356
357    /// Replaces `[at .. at+remove]` with `insert`, no recording — the raw
358    /// mechanism undo/redo replay through.
359    fn splice(&mut self, at: Cursor, remove: &str, insert: &str) {
360        if remove.is_empty() {
361            self.editor.set_selection(Selection::None);
362            self.editor.set_cursor(at);
363        } else {
364            self.editor.set_selection(Selection::Normal(at));
365            self.editor.set_cursor(end_cursor(at, remove));
366            self.editor.delete_selection();
367        }
368        if !insert.is_empty() {
369            self.editor.insert_string(insert, None);
370        }
371    }
372
373    /// Replaces the selection (if any) with `text`, as one recorded op.
374    fn insert_recorded(&mut self, text: &str) {
375        if self.secret.is_some() {
376            self.secure_replace(text);
377            return;
378        }
379        let cursor_before = self.editor.cursor();
380        let deleted = self.editor.copy_selection().unwrap_or_default();
381        self.editor.delete_selection();
382        let at = self.editor.cursor();
383        self.editor.insert_string(text, None);
384        let kind = (deleted.is_empty() && !text.contains('\n')).then_some(Coalesce::Insert);
385        self.record(
386            EditOp {
387                at,
388                deleted,
389                inserted: text.to_string(),
390                cursor_before,
391                cursor_after: self.editor.cursor(),
392            },
393            kind,
394        );
395    }
396
397    /// Deletes the selection as one recorded op; false when there is none.
398    fn delete_selection_recorded(&mut self) -> bool {
399        if self.secret.is_some() {
400            return self.selected().is_some() && {
401                self.secure_replace("");
402                true
403            };
404        }
405        let cursor_before = self.editor.cursor();
406        let Some(deleted) = self.editor.copy_selection() else {
407            return false;
408        };
409        if !self.editor.delete_selection() {
410            return false;
411        }
412        let at = self.editor.cursor();
413        self.record(
414            EditOp {
415                at,
416                deleted,
417                inserted: String::new(),
418                cursor_before,
419                cursor_after: at,
420            },
421            None,
422        );
423        true
424    }
425
426    /// Backspace/Delete (plain or word): selects via `motion`, deletes as a
427    /// recorded op. False at the buffer boundary (nothing to delete).
428    fn delete_motion_recorded(
429        &mut self,
430        motion: Motion,
431        kind: Coalesce,
432        fs: &mut FontSystem,
433    ) -> bool {
434        let cursor_before = self.editor.cursor();
435        self.editor.set_selection(Selection::Normal(cursor_before));
436        self.editor.action(fs, Action::Motion(motion));
437        let deleted = self.editor.copy_selection().unwrap_or_default();
438        if deleted.is_empty() {
439            self.editor.set_selection(Selection::None);
440            return false;
441        }
442        if self.secret.is_some() {
443            self.secure_replace("");
444            return true;
445        }
446        self.editor.delete_selection();
447        let at = self.editor.cursor();
448        self.record(
449            EditOp {
450                at,
451                deleted,
452                inserted: String::new(),
453                cursor_before,
454                cursor_after: at,
455            },
456            Some(kind),
457        );
458        true
459    }
460
461    /// The selection's ends in the buffer, when it is not empty.
462    fn selected(&self) -> Option<(Cursor, Cursor)> {
463        self.editor
464            .selection_bounds()
465            .filter(|(a, z)| !same_pos(*a, *z))
466    }
467
468    /// A secure editor's edit: `text` over the selection, or at the caret,
469    /// in its text, and the buffer as many bullets as the text has
470    /// graphemes, the caret after what went in. Every bullet is alike, so
471    /// the buffer moves by its end alone. Unrecorded: a secure editor
472    /// keeps no history.
473    fn secure_replace(&mut self, text: &str) {
474        let Some(secret) = self.secret.as_mut() else {
475            return;
476        };
477        // A field's: one line, whichever door the text came by.
478        let text = admitted(text, false);
479        let caret = self.editor.cursor();
480        let (a, z) = self.editor.selection_bounds().unwrap_or((caret, caret));
481        let (a, z) = (
482            grapheme_start(secret, a.index / BULLET_LEN),
483            grapheme_start(secret, z.index / BULLET_LEN),
484        );
485        secret.replace_range(a..z, &text);
486        let at = graphemes_before(secret, a + text.len());
487        let n = graphemes(secret);
488        self.set_bullets(n);
489        self.editor
490            .set_cursor(Cursor::new(0, at.min(n) * BULLET_LEN));
491    }
492
493    /// Makes a secure editor's buffer `n` bullets, with no selection.
494    fn set_bullets(&mut self, n: usize) {
495        let had = self
496            .editor
497            .with_buffer(|b| b.lines.first().map_or(0, |l| l.text().len()))
498            / BULLET_LEN;
499        self.editor.set_selection(Selection::None);
500        if n > had {
501            self.editor.set_cursor(Cursor::new(0, had * BULLET_LEN));
502            self.editor.insert_string(&bullets(n - had), None);
503        } else if n < had {
504            self.editor
505                .set_selection(Selection::Normal(Cursor::new(0, n * BULLET_LEN)));
506            self.editor.set_cursor(Cursor::new(0, had * BULLET_LEN));
507            self.editor.delete_selection();
508        }
509    }
510
511    /// Masks the text (`on`) or shows it again, the caret on the grapheme
512    /// it was on and no selection. The history goes either way: masked,
513    /// it would hold the text; shown, it holds edits to the bullets.
514    fn set_secure(&mut self, on: bool, attrs: &Attrs<'_>) {
515        self.abandon_preedit();
516        let caret = self.editor.cursor();
517        let (shown, at) = if on {
518            // The text as a field holds it, and how many of its graphemes
519            // are before the caret.
520            let (text, before) = self.editor.with_buffer(|b| {
521                let text = buffer_text(b);
522                let at = abs_offset(b, caret).min(text.len());
523                let before = text.get(..at).map_or(0, |t| graphemes(&admitted(t, false)));
524                (admitted(&text, false).into_owned(), before)
525            });
526            let shown = bullets(graphemes(&text));
527            self.secret = Some(text);
528            (shown, before * BULLET_LEN)
529        } else {
530            let text = self.secret.take().unwrap_or_default();
531            let at = grapheme_start(&text, caret.index / BULLET_LEN);
532            (text, at)
533        };
534        self.editor
535            .with_buffer_mut(|b| b.set_text(&shown, attrs, Shaping::Advanced, None));
536        self.editor.set_selection(Selection::None);
537        self.editor.set_cursor(Cursor::new(0, at));
538        self.undo.clear();
539        self.redo.clear();
540        self.coalesce = None;
541        self.version += 1;
542        self.wrap = None;
543        self.invalidate_measurements();
544    }
545
546    /// Removes an in-progress composition from the buffer (cancel, blur,
547    /// or anything that must act on committed text only), leaving the
548    /// caret where the composition began. Unrecorded, like its insertion.
549    fn abandon_preedit(&mut self) -> bool {
550        let Some(pre) = self.preedit.take() else {
551            return false;
552        };
553        self.splice(pre.start, &pre.text, "");
554        self.editor.set_cursor(pre.start);
555        self.editor.set_selection(Selection::None);
556        true
557    }
558
559    fn undo_one(&mut self) -> bool {
560        let Some(op) = self.undo.pop_back() else {
561            return false;
562        };
563        self.splice(op.at, &op.inserted, &op.deleted);
564        self.editor.set_cursor(op.cursor_before);
565        self.editor.set_selection(Selection::None);
566        self.redo.push_back(op);
567        self.coalesce = None;
568        true
569    }
570
571    fn redo_one(&mut self) -> bool {
572        let Some(op) = self.redo.pop_back() else {
573            return false;
574        };
575        self.splice(op.at, &op.deleted, &op.inserted);
576        self.editor.set_cursor(op.cursor_after);
577        self.editor.set_selection(Selection::None);
578        self.undo.push_back(op);
579        self.coalesce = None;
580        true
581    }
582}
583
584/// A held `set_edit_text` the frame after it did not claim, in whichever
585/// spelling the call used — the two name the same mistake and raise the
586/// same code, but a warning that says "key" for a call that said "label"
587/// sends its reader looking in the wrong place.
588pub(crate) enum Unclaimed {
589    Key(Key),
590    Label(String),
591}
592
593/// The retained editors of one window, owned by `Core`. Reach it through
594/// `Ui::text_edit`, `Core::edit_text` and `Core::set_edit_text` rather
595/// than directly.
596pub struct EditStore {
597    states: FxHashMap<Key, EditState>,
598    /// Text set for a key nothing has declared yet: `set_text` holds it
599    /// here and the next `declare` under that key seeds with it instead of
600    /// with `initial`. An `update` that opens an editor and sets its text
601    /// in the same turn runs a frame ahead of the view that declares it,
602    /// so without this the call lands on nothing and the app sees the
603    /// editor open with `initial`. What the frame after it
604    /// does not claim is dropped by `finish_frame`, with a warning.
605    pending: FxHashMap<Key, String>,
606    /// The same seed named by label instead of by key, for the app that
607    /// has no key to give: the hex key comes from an event the node
608    /// fired, and an editor a rename is opening for the first time has
609    /// fired none. Held until `text_edit` declares an
610    /// editor under the label and claims it — which is also where a
611    /// *retained* editor is reached, since a key kept off screen (F20,
612    /// F26) has a state `declare` would not reseed and a label
613    /// `Core::key_of` cannot resolve while it goes undeclared.
614    pending_labels: FxHashMap<String, String>,
615    pub(crate) focused: Option<Key>,
616    /// Edit node being drag-selected (with its content origin, logical).
617    pub(crate) dragging: Option<(Key, Vec2)>,
618    /// Edit whose caret moved since the last frame; `finish_frame` scrolls
619    /// the nearest scrollable ancestor to keep the caret visible, then clears.
620    pub(crate) caret_moved: Option<Key>,
621    /// Bumped on anything that should restart the caret blink cycle (edits,
622    /// motion, clicks, focus changes). Frame drivers watch it to re-arm
623    /// their blink timer — the core itself stays clock-free.
624    caret_stamp: u64,
625    /// Whether the caret is currently drawn; toggled by the frame driver's
626    /// blink timer. Headless drivers never touch it, so the caret is solid.
627    blink_visible: bool,
628    /// The frame being built, stamped onto every state `declare` touches.
629    frame_no: u64,
630}
631
632impl Default for EditStore {
633    fn default() -> Self {
634        Self {
635            states: FxHashMap::default(),
636            pending: FxHashMap::default(),
637            pending_labels: FxHashMap::default(),
638            focused: None,
639            dragging: None,
640            caret_moved: None,
641            caret_stamp: 0,
642            blink_visible: true,
643            frame_no: 0,
644        }
645    }
646}
647
648/// What an editor's buffer may hold: a field is one line, so a newline that
649/// arrived in a seed or a `set_text` is dropped rather than drawn below a
650/// box measured for one line. The same rule `apply_text` applies to
651/// typing and pasting, so the two doors agree. The paragraph separator
652/// (U+2029) is the break it names, a newline (backlog FZ6: the shaper
653/// panicked on one between text running two ways), and the other
654/// characters that end a bidi paragraph go through
655/// [`one_paragraph`](crate::text::one_paragraph).
656fn admitted(text: &str, multiline: bool) -> std::borrow::Cow<'_, str> {
657    let text: std::borrow::Cow<'_, str> = if text.contains('\u{2029}') {
658        text.replace('\u{2029}', "\n").into()
659    } else {
660        text.into()
661    };
662    let text = if multiline || !text.contains(['\n', '\r']) {
663        text
664    } else {
665        text.chars()
666            .filter(|c| *c != '\n' && *c != '\r')
667            .collect::<String>()
668            .into()
669    };
670    match crate::text::one_paragraph(&text) {
671        std::borrow::Cow::Borrowed(_) => text,
672        std::borrow::Cow::Owned(s) => s.into(),
673    }
674}
675
676/// What typing `c` puts in an editor: the character; the newline the
677/// paragraph separator (U+2029) names, as in `admitted` (backlog FZ6);
678/// or nothing, for a control a field does not take — every one but a tab
679/// and, in a multiline editor, the newline. The other bidi paragraph ends
680/// are controls, and go with them.
681fn typed(c: char, multiline: bool) -> Option<char> {
682    let c = if c == '\u{2029}' { '\n' } else { c };
683    (!c.is_control() || (c == '\n' && multiline) || c == '\t').then_some(c)
684}
685
686/// An IME's composition as the editor holds it: typed text in waiting,
687/// so what typing drops goes (backlog FZ7). A `\r` went in as it came,
688/// cosmic-text broke the line at it, and `end_cursor` - which counts
689/// `\n` alone - put the composition's end past the line it was on: the
690/// next key that ended the composition split the buffer off a character
691/// boundary, a panic in cosmic-text. The IME's byte offsets move with
692/// what stays.
693fn composed(
694    text: &str,
695    cursor: Option<(usize, usize)>,
696    multiline: bool,
697) -> (String, Option<(usize, usize)>) {
698    let mut out = String::with_capacity(text.len());
699    // Where each character of `text` starts, there and in `out`.
700    let mut starts = Vec::with_capacity(text.len());
701    for (at, c) in text.char_indices() {
702        starts.push((at, out.len()));
703        if let Some(c) = typed(c, multiline) {
704            out.push(c);
705        }
706    }
707    let moved = |o: usize| {
708        starts
709            .iter()
710            .find(|(at, _)| *at >= o)
711            .map_or(out.len(), |(_, kept)| *kept)
712    };
713    let cursor = cursor.map(|(a, b)| (moved(a), moved(b)));
714    (out, cursor)
715}
716
717/// At the family's regular weight, or its bold for a bold style, as
718/// text draws it.
719fn attrs_for<'a>(style: &TextStyle, res: &'a Resources) -> Attrs<'a> {
720    res.weights_of(style.family).apply(
721        Attrs::new()
722            .family(res.family_of(style.family))
723            .font_features(crate::text::cosmic_features(&style.features)),
724        style.bold,
725    )
726}
727
728impl EditStore {
729    /// Gives every editor's text the weights its family is asked at now:
730    /// a face of a registered family came or went. The text,
731    /// caret and history stay; every line shapes again, and every
732    /// measurement of it is of the old weights. Whether or not a line's
733    /// attributes moved: a new fallback list (`set_fallback_fonts`)
734    /// changes the faces a line is shaped in and none of its attributes
735    /// (RG114).
736    pub(crate) fn reweigh(&mut self, res: &Resources) {
737        for s in self.states.values_mut() {
738            let a = AttrsList::new(&attrs_for(&s.style, res));
739            s.editor.with_buffer_mut(|b| {
740                for line in &mut b.lines {
741                    line.set_attrs_list(a.clone());
742                    line.reset_shaping();
743                }
744            });
745            s.editor.set_redraw(true);
746            s.wrap = None;
747            s.metrics_rev = s.metrics_rev.wrapping_add(1);
748            s.invalidate_measurements();
749        }
750    }
751
752    pub fn focused(&self) -> Option<Key> {
753        self.focused
754    }
755
756    pub fn set_focus(&mut self, key: Option<Key>) {
757        if self.focused != key {
758            self.caret_stamp += 1;
759            // A blurred editor abandons any in-progress composition.
760            if let Some(old) = self.focused
761                && let Some(s) = self.states.get_mut(&old)
762                && s.abandon_preedit()
763            {
764                s.version += 1;
765                s.invalidate_measurements();
766            }
767        }
768        self.focused = key;
769    }
770
771    /// See `caret_stamp` field: compare across frames to restart blink.
772    pub fn caret_stamp(&self) -> u64 {
773        self.caret_stamp
774    }
775
776    /// Blink-phase toggle for frame drivers; `true` draws the caret.
777    pub fn set_blink_visible(&mut self, visible: bool) {
778        self.blink_visible = visible;
779    }
780
781    /// The phase as last set; `true` draws the caret.
782    pub fn blink_visible(&self) -> bool {
783        self.blink_visible
784    }
785
786    fn touch_caret(&mut self, key: Key) {
787        self.caret_moved = Some(key);
788        self.caret_stamp += 1;
789    }
790
791    /// How many states are retained — declared and undeclared together.
792    /// What a test watches the budget through.
793    pub fn len(&self) -> usize {
794        self.states.len()
795    }
796
797    pub fn is_empty(&self) -> bool {
798        self.states.is_empty()
799    }
800
801    /// Stamps the frame being built and, if the map has grown past the
802    /// budget, drops the longest-undeclared states
803    /// ([`MAX_UNDECLARED_EDITS`]). A state the frame that just ended
804    /// declared is never evicted, and neither is the focused or the
805    /// drag-selected one — the store still points at those.
806    ///
807    /// The length test is what an ordinary frame pays: a store inside the
808    /// budget never walks itself, which is why this can run every frame
809    /// rather than every 240th like the anim store's cutoff sweep.
810    pub(crate) fn begin_frame(&mut self, frame_no: u64) {
811        self.frame_no = frame_no;
812        if self.states.len() > MAX_UNDECLARED_EDITS {
813            self.evict(frame_no.saturating_sub(1));
814        }
815    }
816
817    /// Drops the oldest undeclared states down to the budget. `declared_at`
818    /// is the frame that just ended: a state stamped with it is declared.
819    fn evict(&mut self, declared_at: u64) {
820        let focused = self.focused;
821        let dragging = self.dragging.map(|(k, _)| k);
822        let caret_moved = &mut self.caret_moved;
823        crate::retain::evict_undeclared(
824            &mut self.states,
825            MAX_UNDECLARED_EDITS,
826            declared_at,
827            |s| s.last_declared,
828            |k| Some(k) == focused || Some(k) == dragging,
829            |k| {
830                if *caret_moved == Some(k) {
831                    *caret_moved = None;
832                }
833            },
834        );
835    }
836
837    /// Ensures state exists for `key`, seeding `initial` on first creation
838    /// — or, if a `set_text` for this key arrived before anything declared
839    /// it, that text instead (see [`EditStore::pending`]). Returns whether
840    /// this declaration is an *autofocus edge*: the key was not declared
841    /// with `autofocus` on the frame before this one — a new editor, one
842    /// back after a gap, or one whose `autofocus` just turned on — which
843    /// is the one frame the flag may act on.
844    #[allow(clippy::too_many_arguments)]
845    pub(crate) fn declare(
846        &mut self,
847        key: Key,
848        initial: &str,
849        opts: &EditOptions,
850        origin: OriginId,
851        scale: f32,
852        fs: &mut FontSystem,
853        res: &Resources,
854    ) -> bool {
855        let frame_no = self.frame_no;
856        let edge = self
857            .states
858            .get(&key)
859            .is_none_or(|s| s.last_declared + 1 != frame_no || !s.autofocus);
860        // Only a creation consumes the seed: a key already declared has
861        // no pending text (a `set_text` with state behind it is applied
862        // where it is called), and taking one here would drop it.
863        let seed = if self.states.contains_key(&key) {
864            None
865        } else {
866            self.pending.remove(&key)
867        };
868        let seeded = seed.is_some();
869        let initial = seed.as_deref().unwrap_or(initial);
870        // A secure editor is a field (`EditOptions::secure`).
871        let multiline = opts.multiline && !opts.secure;
872        let state = self.states.entry(key).or_insert_with(|| {
873            let metrics = crate::text::shaper_metrics(
874                opts.style.size * scale,
875                opts.style.line_height * scale,
876            );
877            let mut buffer = Buffer::new(fs, metrics);
878            buffer.set_size(None, None);
879            let text = admitted(initial, multiline);
880            let secret = opts.secure.then(|| text.to_string());
881            let shown = match &secret {
882                Some(s) => bullets(graphemes(s)).into(),
883                None => text,
884            };
885            buffer.set_text(
886                &shown,
887                &attrs_for(&opts.style, res),
888                Shaping::Advanced,
889                None,
890            );
891            let mut editor = Editor::new(buffer);
892            // A single-line field opens with the caret after its seeded
893            // text — what a native field does with a prefilled value, and
894            // what a rename wants, since typing into a name meant to be
895            // extended otherwise prepends to it. A multiline
896            // editor is a document and opens at its top, as native text
897            // views do. Placed, not moved: no `touch_caret`, so nothing
898            // scrolls to reveal it before the user has touched it.
899            //
900            // A held `set_text` is not `initial`: it is that call arriving
901            // where it can land, so it leaves the caret where the call
902            // does — at the end, document or not.
903            if seeded || !multiline {
904                let end = editor.with_buffer(|b| {
905                    let line = b.lines.len().saturating_sub(1);
906                    Cursor::new(line, b.lines.get(line).map_or(0, |l| l.text().len()))
907                });
908                editor.set_cursor(end);
909            }
910            EditState {
911                editor,
912                style: opts.style,
913                accent: opts.accent.unwrap_or(crate::select::TINT),
914                multiline,
915                keep_tab: opts.keep_tab,
916                secret,
917                placeholder: None,
918                folds: false,
919                origin,
920                scale,
921                wrap: None,
922                version: 0,
923                metrics_rev: 0,
924                measured: None,
925                natural: None,
926                offset_x: 0.0,
927                preedit: None,
928                undo: VecDeque::new(),
929                redo: VecDeque::new(),
930                coalesce: None,
931                autofocus: false,
932                last_declared: frame_no,
933            }
934        });
935        state.last_declared = frame_no;
936        state.autofocus = opts.autofocus;
937        state.origin = origin;
938        state.multiline = multiline;
939        state.keep_tab = opts.keep_tab;
940        // Masked or shown again: a "show password" toggle on the same
941        // editor, the caret on the same grapheme.
942        if opts.secure != state.secret.is_some() {
943            state.set_secure(opts.secure, &attrs_for(&opts.style, res));
944        }
945        state.accent = opts.accent.unwrap_or(crate::select::TINT);
946        // A document wraps between words whatever its style says, as it
947        // always has; a field folds only when asked, and then by the mode
948        // the `wrap` row picked — so a rename field breaks where the label
949        // it renames breaks. `wrap="none"` on a field is the
950        // field: one line, scrolled.
951        let folds = multiline || (opts.wrap && opts.style.wrap != TextWrap::None);
952        let mode = match (multiline, opts.style.wrap) {
953            (false, TextWrap::Glyph) => Wrap::Glyph,
954            _ => Wrap::WordOrGlyph,
955        };
956        if state.folds != folds || state.editor.with_buffer(|b| b.wrap()) != mode {
957            state.folds = folds;
958            state.editor.with_buffer_mut(|b| b.set_wrap(mode));
959            state.invalidate_measurements();
960        }
961        // Style/scale changes re-metric the buffer (text and cursor survive).
962        // The text is the same, so `version` does not move — but every
963        // measurement of it is now of the wrong font, which is what
964        // `metrics_rev` is for: without it a cache keyed on the text alone
965        // answers the new frame with the old size.
966        if state.scale != scale || state.style != opts.style {
967            state.style = opts.style;
968            state.scale = scale;
969            let metrics = crate::text::shaper_metrics(
970                opts.style.size * scale,
971                opts.style.line_height * scale,
972            );
973            state.editor.with_buffer_mut(|b| b.set_metrics(metrics));
974            state.wrap = None;
975            state.metrics_rev = state.metrics_rev.wrapping_add(1);
976            state.invalidate_measurements();
977        }
978        // The placeholder, shaped again only when its text or the
979        // editor's metrics moved.
980        match opts.placeholder.as_deref().filter(|p| !p.is_empty()) {
981            None => state.placeholder = None,
982            Some(p) => {
983                let rev = state.metrics_rev;
984                if state
985                    .placeholder
986                    .as_ref()
987                    .is_none_or(|(t, r, _)| t != p || *r != rev)
988                {
989                    let metrics = state.editor.with_buffer(|b| b.metrics());
990                    let mut b = Buffer::new(fs, metrics);
991                    b.set_size(None, None);
992                    b.set_text(
993                        &crate::text::one_paragraph(p),
994                        &attrs_for(&opts.style, res),
995                        Shaping::Advanced,
996                        None,
997                    );
998                    b.shape_until_scroll(fs, false);
999                    state.placeholder = Some((p.to_string(), rev, b));
1000                }
1001            }
1002        }
1003        // `autofocus` is the core's decision (it owns the one focus).
1004        edge
1005    }
1006
1007    /// What the editor shows of its text, which is what the access tree
1008    /// reads: the text, but for a secure editor, whose bullets are all it
1009    /// gives away.
1010    pub(crate) fn shown_text(&self, key: Key) -> Option<String> {
1011        let s = self.states.get(&key)?;
1012        if s.secret.is_some() {
1013            return Some(s.editor.with_buffer(buffer_text));
1014        }
1015        self.text(key)
1016    }
1017
1018    /// Whether `key` is a secure editor (`EditOptions::secure`).
1019    pub fn is_secure(&self, key: Key) -> bool {
1020        self.states.get(&key).is_some_and(|s| s.secret.is_some())
1021    }
1022
1023    /// The placeholder `key` was declared with, if any.
1024    pub fn placeholder(&self, key: Key) -> Option<&str> {
1025        let s = self.states.get(&key)?;
1026        s.placeholder.as_ref().map(|(t, ..)| t.as_str())
1027    }
1028
1029    pub fn contains(&self, key: Key) -> bool {
1030        self.states.contains_key(&key)
1031    }
1032
1033    pub fn origin_of(&self, key: Key) -> Option<OriginId> {
1034        self.states.get(&key).map(|s| s.origin)
1035    }
1036
1037    /// The committed text: an in-progress composition is not part of it.
1038    /// A secure editor's is its text, not the bullets it draws.
1039    pub fn text(&self, key: Key) -> Option<String> {
1040        let s = self.states.get(&key)?;
1041        if let Some(secret) = &s.secret {
1042            return Some(secret.clone());
1043        }
1044        Some(s.editor.with_buffer(|b| {
1045            let mut text = buffer_text(b);
1046            if let Some(pre) = &s.preedit {
1047                let at = abs_offset(b, pre.start);
1048                if text.is_char_boundary(at) && at + pre.text.len() <= text.len() {
1049                    text.replace_range(at..at + pre.text.len(), "");
1050                }
1051            }
1052            text
1053        }))
1054    }
1055
1056    /// Returns whether the text landed in an editor: `false` says it was
1057    /// held for the frame that declares the key, so nothing on screen
1058    /// changed yet and the frame that will change it is the app's — a
1059    /// driver that redraws on every write would re-lower the tree that
1060    /// declares no editor and drop the seed to the warning.
1061    pub fn set_text(&mut self, key: Key, text: &str, fs: &mut FontSystem, res: &Resources) -> bool {
1062        let Some(s) = self.states.get_mut(&key) else {
1063            // Nothing has declared this key yet. The call is not wrong —
1064            // the `update` that opens an editor runs before the view that
1065            // declares it — so hold the text for the frame that does
1066            // rather than falling through silently.
1067            self.pending.insert(key, text.to_string());
1068            return false;
1069        };
1070        s.preedit = None;
1071        let a = attrs_for(&s.style, res);
1072        let text = admitted(text, s.multiline);
1073        let text = match &mut s.secret {
1074            Some(secret) => {
1075                *secret = text.into_owned();
1076                bullets(graphemes(secret)).into()
1077            }
1078            None => text,
1079        };
1080        s.editor
1081            .with_buffer_mut(|b| b.set_text(&text, &a, Shaping::Advanced, None));
1082        s.editor.set_selection(Selection::None);
1083        s.editor.action(fs, Action::Motion(Motion::BufferEnd));
1084        s.version += 1;
1085        s.invalidate_measurements();
1086        s.wrap = None;
1087        // A wholesale replacement invalidates the recorded deltas.
1088        s.undo.clear();
1089        s.redo.clear();
1090        s.coalesce = None;
1091        self.touch_caret(key);
1092        true
1093    }
1094
1095    /// Holds `text` for the next editor declared under `label`, for a
1096    /// `set_edit_text` by a name nothing has declared yet.
1097    /// The key path is [`EditStore::pending`]; this one is claimed by
1098    /// [`EditStore::claim_label`] from inside the build, where the label
1099    /// and the key it resolves to are both in hand.
1100    pub(crate) fn hold_label(&mut self, label: &str, text: &str) {
1101        self.pending_labels
1102            .insert(label.to_string(), text.to_string());
1103    }
1104
1105    /// A declaring editor takes the seed held for its label, if there is
1106    /// one. Two shapes, and the second is the one a seed by key cannot
1107    /// have: a *new* editor takes it as `declare` takes a pending key,
1108    /// over `initial`; an editor that already has a state — retained
1109    /// while its key was off screen — takes it as `set_text`, since
1110    /// `declare` reseeds nothing that exists and the draft would
1111    /// otherwise come back over the model's text.
1112    pub(crate) fn claim_label(
1113        &mut self,
1114        key: Key,
1115        label: &str,
1116        fs: &mut FontSystem,
1117        res: &Resources,
1118    ) {
1119        if self.pending_labels.is_empty() {
1120            return;
1121        }
1122        let Some(text) = self.pending_labels.remove(label) else {
1123            return;
1124        };
1125        if self.states.contains_key(&key) {
1126            self.set_text(key, &text, fs, res);
1127        } else {
1128            self.pending.insert(key, text);
1129        }
1130    }
1131
1132    /// The seeds no frame claimed, dropped: `finish_frame` drains this
1133    /// after the build and raises [`crate::diag::EDIT_TEXT_WITHOUT_EDITOR`]
1134    /// for each, so a `set_edit_text` on a key — or a label — the view
1135    /// never declares is a line rather than nothing at all. Sorted within
1136    /// each spelling, so the order two unclaimed seeds are reported in
1137    /// does not depend on a hash seed.
1138    pub(crate) fn take_unclaimed_seeds(&mut self) -> Vec<Unclaimed> {
1139        let mut out: Vec<Unclaimed> = Vec::new();
1140        if !self.pending.is_empty() {
1141            let mut keys: Vec<Key> = self.pending.drain().map(|(k, _)| k).collect();
1142            keys.sort_unstable();
1143            out.extend(keys.into_iter().map(Unclaimed::Key));
1144        }
1145        if !self.pending_labels.is_empty() {
1146            let mut labels: Vec<String> = self.pending_labels.drain().map(|(l, _)| l).collect();
1147            labels.sort_unstable();
1148            out.extend(labels.into_iter().map(Unclaimed::Label));
1149        }
1150        out
1151    }
1152
1153    pub fn version(&self, key: Key) -> u64 {
1154        self.states.get(&key).map_or(0, |s| s.version)
1155    }
1156
1157    /// The caret as a byte offset into the committed text, and the
1158    /// non-empty selection as a byte range, for the access tree.
1159    pub fn caret_and_selection(&self, key: Key) -> Option<(usize, Option<(usize, usize)>)> {
1160        let s = self.states.get(&key)?;
1161        Some(s.editor.with_buffer(|b| {
1162            let caret = abs_offset(b, s.editor.cursor());
1163            let selection = s
1164                .editor
1165                .selection_bounds()
1166                .map(|(a, z)| (abs_offset(b, a), abs_offset(b, z)))
1167                .filter(|(a, z)| a != z);
1168            (caret, selection)
1169        }))
1170    }
1171
1172    /// The selection's anchor and the caret as (line, byte index) pairs
1173    /// (equal without a selection), for the access tree.
1174    pub fn selection_cursors(&self, key: Key) -> Option<((usize, usize), (usize, usize))> {
1175        let s = self.states.get(&key)?;
1176        let caret = s.editor.cursor();
1177        let anchor = match s.editor.selection() {
1178            Selection::Normal(c) | Selection::Line(c) | Selection::Word(c) => c,
1179            Selection::None => caret,
1180        };
1181        Some(((anchor.line, anchor.index), (caret.line, caret.index)))
1182    }
1183
1184    /// The editor's laid-out lines as access runs (see
1185    /// [`crate::access::AccessRun`]); `origin` is where the content box
1186    /// starts, logical px.
1187    pub(crate) fn runs(
1188        &self,
1189        key: Key,
1190        node: Key,
1191        origin: Vec2,
1192        scale: f32,
1193    ) -> Vec<crate::access::AccessRun> {
1194        let Some(s) = self.states.get(&key) else {
1195            return Vec::new();
1196        };
1197        let mut out = Vec::new();
1198        let mut n = 0;
1199        // Where the glyphs are, not where they would be unscrolled: a
1200        // screen reader's character rects have to land on the pixels
1201        // emission drew (a scrolled field, F41).
1202        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
1203        s.editor.with_buffer(|b| {
1204            crate::access::runs_of_buffer(
1205                b,
1206                crate::access::RunSource {
1207                    key: node,
1208                    line: 0,
1209                    byte_base: 0,
1210                    origin,
1211                    scale,
1212                    newline_after_last: false,
1213                },
1214                &mut n,
1215                &mut out,
1216                |_| (0.0, 0.0),
1217            )
1218        });
1219        out
1220    }
1221
1222    /// Moves the caret to `focus` and the selection's other end to
1223    /// `anchor`, both (line, byte index) pairs clamped into the text; equal
1224    /// pairs clear the selection. What a screen reader's "select from here
1225    /// to there" becomes.
1226    pub fn set_selection(&mut self, key: Key, anchor: (usize, usize), focus: (usize, usize)) {
1227        let Some(s) = self.states.get_mut(&key) else {
1228            return;
1229        };
1230        s.abandon_preedit();
1231        let clamp = |b: &Buffer, (line, index): (usize, usize)| {
1232            let line = line.min(b.lines.len().saturating_sub(1));
1233            let text = b.lines[line].text();
1234            let mut index = index.min(text.len());
1235            while !text.is_char_boundary(index) {
1236                index -= 1;
1237            }
1238            Cursor::new(line, index)
1239        };
1240        let (a, f) = s
1241            .editor
1242            .with_buffer(|b| (clamp(b, anchor), clamp(b, focus)));
1243        s.editor.set_cursor(f);
1244        s.editor.set_selection(if a == f {
1245            Selection::None
1246        } else {
1247            Selection::Normal(a)
1248        });
1249        s.break_coalesce();
1250        self.touch_caret(key);
1251    }
1252
1253    /// Types `text` over the selection (or at the caret), as one undo
1254    /// step; true when the content changed.
1255    pub fn replace_selection(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
1256        let Some(s) = self.states.get_mut(&key) else {
1257            return false;
1258        };
1259        s.abandon_preedit();
1260        if text.is_empty() && s.editor.selection_bounds().is_none() {
1261            return false;
1262        }
1263        s.insert_recorded(text);
1264        s.editor.shape_as_needed(fs, false);
1265        s.version += 1;
1266        s.invalidate_measurements();
1267        self.touch_caret(key);
1268        true
1269    }
1270
1271    /// The selected text, for a copy or a cut. `None` from a secure
1272    /// editor: its text does not leave it by the clipboard.
1273    pub fn copy_selection(&self, key: Key) -> Option<String> {
1274        let s = self.states.get(&key)?;
1275        if s.secret.is_some() {
1276            return None;
1277        }
1278        s.editor.copy_selection()
1279    }
1280
1281    /// Whether a copy or a cut from editor `key` has anything to take: a
1282    /// selection, in an editor that is not secure. What an Edit menu
1283    /// lights Cut and Copy by.
1284    pub fn can_copy(&self, key: Key) -> bool {
1285        self.has_selection(key) && !self.is_secure(key)
1286    }
1287
1288    /// Whether the editor `key` has a non-empty selection — what
1289    /// `copy_selection` would answer, without building the string. A
1290    /// menu asking whether Copy applies asks this every time it opens.
1291    pub fn has_selection(&self, key: Key) -> bool {
1292        self.states
1293            .get(&key)
1294            .and_then(|s| s.editor.selection_bounds())
1295            .is_some_and(|(a, b)| a != b)
1296    }
1297
1298    /// Whether the editor `key` has an edit to undo, and one to redo.
1299    pub fn history(&self, key: Key) -> (bool, bool) {
1300        self.states
1301            .get(&key)
1302            .map_or((false, false), |s| (!s.undo.is_empty(), !s.redo.is_empty()))
1303    }
1304
1305    /// Deletes the selection; returns true if anything was deleted.
1306    pub fn delete_selection(&mut self, key: Key, fs: &mut FontSystem) -> bool {
1307        let Some(s) = self.states.get_mut(&key) else {
1308            return false;
1309        };
1310        let _ = fs;
1311        s.abandon_preedit();
1312        if s.delete_selection_recorded() {
1313            s.version += 1;
1314            s.invalidate_measurements();
1315            self.touch_caret(key);
1316            true
1317        } else {
1318            false
1319        }
1320    }
1321
1322    // -- Input application (focused editor). Returns true if content changed.
1323
1324    pub(crate) fn apply_text(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
1325        let Some(s) = self.states.get_mut(&key) else {
1326            return false;
1327        };
1328        // A commit ends the composition (winit also clears preedit first);
1329        // the committed text goes in where the composition began.
1330        s.abandon_preedit();
1331        let filtered: String = text.chars().filter_map(|c| typed(c, s.multiline)).collect();
1332        if filtered.is_empty() {
1333            return false;
1334        }
1335        s.insert_recorded(&filtered);
1336        s.editor.shape_as_needed(fs, false);
1337        s.version += 1;
1338        s.invalidate_measurements();
1339        self.touch_caret(key);
1340        true
1341    }
1342
1343    /// Returns (content_changed, submit) — submit is Enter in single-line.
1344    pub(crate) fn apply_key(
1345        &mut self,
1346        key: Key,
1347        ek: EditKey,
1348        mods: Mods,
1349        fs: &mut FontSystem,
1350    ) -> KeyOutcome {
1351        let Some(s) = self.states.get_mut(&key) else {
1352            return KeyOutcome::default();
1353        };
1354        // Keys reaching the editor mid-composition mean the IME let them
1355        // through; act on committed text only.
1356        let mut changed = s.abandon_preedit();
1357        let mut submit = false;
1358        // Where the caret was, to tell a key that moved or deleted nothing
1359        // — the caret at an edge of the text — from one that did.
1360        let at = |e: &Editor<'static>| (e.cursor().line, e.cursor().index);
1361        let before = (at(&s.editor), s.editor.selection());
1362        // The visual line the caret is on, for ↑ and ↓: on the first or
1363        // last one the motion moves the caret along it (to an end of the
1364        // text) but not off it, and that is the edge.
1365        let row = |e: &Editor<'static>| e.cursor_position().map(|(_, y)| y);
1366        let row_before = row(&s.editor);
1367        let composing = changed;
1368        // ↑ and ↓ by paragraphs: an edge only where they could not move.
1369        let mut by_paragraph = false;
1370        match ek {
1371            EditKey::Left
1372            | EditKey::Right
1373            | EditKey::Up
1374            | EditKey::Down
1375            | EditKey::Home
1376            | EditKey::End
1377            | EditKey::PageUp
1378            | EditKey::PageDown => {
1379                // What the modifiers mean is each platform's fields'. On
1380                // a Mac ⌥ moves by words, and ⌥↑ ⌥↓ by paragraphs; ⌘ to
1381                // the line's ends, and ⌘↑ ⌘↓ to the text's. Elsewhere Ctrl
1382                // moves by words, and Ctrl+↑ Ctrl+↓ by paragraphs; Alt
1383                // stays a word, as it was here before.
1384                let mac = cfg!(target_os = "macos");
1385                let word = mods.word || (!mac && mods.doc);
1386                // A secure editor has no words to show: a word's move is
1387                // the line's, as a Mac's password field's is.
1388                let (word, line) = if s.secret.is_some() {
1389                    (false, (mac && mods.doc) || word)
1390                } else {
1391                    (word, mac && mods.doc)
1392                };
1393                let motion = match (ek, line, word) {
1394                    (EditKey::Left, true, _) => Motion::Home,
1395                    (EditKey::Right, true, _) => Motion::End,
1396                    (EditKey::Up, true, _) => Motion::BufferStart,
1397                    (EditKey::Down, true, _) => Motion::BufferEnd,
1398                    (EditKey::Left, _, true) => Motion::LeftWord,
1399                    (EditKey::Right, _, true) => Motion::RightWord,
1400                    (EditKey::Up, _, true) => Motion::ParagraphStart,
1401                    (EditKey::Down, _, true) => Motion::ParagraphEnd,
1402                    (EditKey::Left, ..) => Motion::Left,
1403                    (EditKey::Right, ..) => Motion::Right,
1404                    (EditKey::Up, ..) => Motion::Up,
1405                    (EditKey::Down, ..) => Motion::Down,
1406                    (EditKey::Home, ..) if mods.doc => Motion::BufferStart,
1407                    (EditKey::End, ..) if mods.doc => Motion::BufferEnd,
1408                    (EditKey::Home, ..) => Motion::Home,
1409                    (EditKey::End, ..) => Motion::End,
1410                    (EditKey::PageUp, ..) => Motion::PageUp,
1411                    (EditKey::PageDown, ..) => Motion::PageDown,
1412                    _ => unreachable!(),
1413                };
1414                if mods.shift {
1415                    if s.editor.selection() == Selection::None {
1416                        s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1417                    }
1418                } else {
1419                    s.editor.set_selection(Selection::None);
1420                }
1421                // A paragraph's start or end the caret is already at: the
1422                // one before or after, as a Mac's ⌥↑ ⌥↓ go on.
1423                by_paragraph = matches!(motion, Motion::ParagraphStart | Motion::ParagraphEnd);
1424                if by_paragraph {
1425                    let c = s.editor.cursor();
1426                    let len = s
1427                        .editor
1428                        .with_buffer(|b| b.lines.get(c.line).map_or(0, |l| l.text().len()));
1429                    let last = s.editor.with_buffer(|b| b.lines.len().saturating_sub(1));
1430                    if motion == Motion::ParagraphStart && c.index == 0 && c.line > 0 {
1431                        s.editor.action(fs, Action::Motion(Motion::Previous));
1432                    } else if motion == Motion::ParagraphEnd && c.index >= len && c.line < last {
1433                        s.editor.action(fs, Action::Motion(Motion::Next));
1434                    }
1435                }
1436                s.editor.action(fs, Action::Motion(motion));
1437                s.break_coalesce();
1438            }
1439            EditKey::Backspace | EditKey::Delete => {
1440                // ⌘⌫ on a Mac takes the line back to its start and ⌘⌦ on
1441                // to its end, as AppKit's fields do; elsewhere the primary
1442                // modifier is Ctrl, and Ctrl+Backspace takes a word, as
1443                // Windows' and GTK's fields do.
1444                let (line, word) = if cfg!(target_os = "macos") {
1445                    (mods.doc, mods.word)
1446                } else {
1447                    (false, mods.word || mods.doc)
1448                };
1449                // A secure editor's word is its line (see the arrows).
1450                let (line, word) = if s.secret.is_some() {
1451                    (line || word, false)
1452                } else {
1453                    (line, word)
1454                };
1455                let back = ek == EditKey::Backspace;
1456                let motion = match (back, line, word) {
1457                    (true, true, _) => Motion::Home,
1458                    (true, _, true) => Motion::LeftWord,
1459                    (true, ..) => Motion::Left,
1460                    (false, true, _) => Motion::End,
1461                    (false, _, true) => Motion::RightWord,
1462                    (false, ..) => Motion::Right,
1463                };
1464                let kind = if back {
1465                    Coalesce::Backspace
1466                } else {
1467                    Coalesce::Delete
1468                };
1469                changed |=
1470                    s.delete_selection_recorded() || s.delete_motion_recorded(motion, kind, fs);
1471            }
1472            EditKey::Enter => {
1473                if s.multiline {
1474                    s.insert_recorded("\n");
1475                    changed = true;
1476                } else {
1477                    submit = true;
1478                }
1479            }
1480            EditKey::Tab => {
1481                if s.multiline {
1482                    s.insert_recorded("    ");
1483                    changed = true;
1484                }
1485            }
1486            EditKey::Undo => changed |= s.undo_one(),
1487            EditKey::Redo => changed |= s.redo_one(),
1488            EditKey::SelectAll => {
1489                s.editor.action(fs, Action::Motion(Motion::BufferStart));
1490                s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1491                s.editor.action(fs, Action::Motion(Motion::BufferEnd));
1492                s.break_coalesce();
1493            }
1494            EditKey::Escape => {
1495                s.editor.action(fs, Action::Escape);
1496                s.break_coalesce();
1497            }
1498        }
1499        s.editor.shape_as_needed(fs, false);
1500        // A key the editor holds but could not act on (backlog F145): an
1501        // arrow, Backspace or Delete that left the caret where it was and
1502        // changed nothing, from a caret with no selection — the caret was
1503        // at the edge of the text the key moves toward. Not under Shift,
1504        // which extends a selection and has no edge to report, nor
1505        // mid-composition, whose key the IME let through.
1506        let plain = !changed && !composing && !mods.shift && before.1 == Selection::None;
1507        let still = plain && (at(&s.editor), s.editor.selection()) == before;
1508        // ↑ and ↓ meet the edge when they leave the caret on its visual
1509        // line, wherever along it they moved it: a field's ↓ from the
1510        // middle of its one line is the end of the field, not a caret
1511        // walked to the end first and an edge on the second press.
1512        let same_row = plain && row_before.is_some() && row(&s.editor) == row_before;
1513        let boundary = match ek {
1514            EditKey::Up if by_paragraph && still => Some(Edge::Start),
1515            EditKey::Down if by_paragraph && still => Some(Edge::End),
1516            EditKey::Up | EditKey::Down if by_paragraph => None,
1517            EditKey::Up if same_row => Some(Edge::Start),
1518            EditKey::Down if same_row => Some(Edge::End),
1519            EditKey::Left | EditKey::Backspace if still => Some(Edge::Start),
1520            EditKey::Right | EditKey::Delete if still => Some(Edge::End),
1521            _ => None,
1522        };
1523        if changed {
1524            s.version += 1;
1525            s.invalidate_measurements();
1526        }
1527        self.touch_caret(key);
1528        KeyOutcome {
1529            changed,
1530            submit,
1531            boundary,
1532        }
1533    }
1534
1535    /// Mouse press inside the edit at content-local logical position.
1536    /// `clicks` is the driver-counted multi-click: 2 selects the word,
1537    /// 3 the line (cosmic-text's double/triple click actions). With
1538    /// `extend` — a Shift-press — the caret moves there keeping the
1539    /// selection's anchor, seeding one at the caret when there is none
1540    /// (cosmic-text's `Drag` does both), and the
1541    /// click count says nothing. Either way it is a click: a live
1542    /// composition is abandoned and the next edit starts an undo unit.
1543    pub(crate) fn click(
1544        &mut self,
1545        key: Key,
1546        local: Vec2,
1547        clicks: u8,
1548        extend: bool,
1549        fs: &mut FontSystem,
1550    ) {
1551        if let Some(s) = self.states.get_mut(&key) {
1552            if s.abandon_preedit() {
1553                s.version += 1;
1554                s.invalidate_measurements();
1555            }
1556            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1557            let action = match (extend, clicks) {
1558                (true, _) => Action::Drag { x, y },
1559                (false, 0 | 1) => Action::Click { x, y },
1560                // A secure editor's word is its text (see the arrows).
1561                (false, 2) if s.secret.is_none() => Action::DoubleClick { x, y },
1562                (false, _) => Action::TripleClick { x, y },
1563            };
1564            s.editor.action(fs, action);
1565            s.editor.shape_as_needed(fs, false);
1566            s.break_coalesce();
1567            self.caret_stamp += 1;
1568        }
1569    }
1570
1571    /// Mouse motion with the button held: moves the caret to the point
1572    /// and keeps the selection's anchor (seeding one at the caret when
1573    /// there is none — cosmic-text's `Drag` does both). The caret moved,
1574    /// so it is marked like a keyboard motion's: a field scrolls its own
1575    /// text toward the drag at once, and a scroller above a document
1576    /// reveals the caret on the release — not under the held pointer,
1577    /// where the drag's own rate is what moves it
1578    /// (`scroll_caret_into_view`).
1579    pub(crate) fn drag(&mut self, key: Key, local: Vec2, fs: &mut FontSystem) {
1580        if let Some(s) = self.states.get_mut(&key) {
1581            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1582            let before = s.editor.cursor();
1583            s.editor.action(fs, Action::Drag { x, y });
1584            if s.editor.cursor() != before {
1585                self.touch_caret(key);
1586            } else {
1587                self.caret_stamp += 1;
1588            }
1589        }
1590    }
1591
1592    /// Replaces the focused editor's IME composition. The text lives in
1593    /// the buffer from the moment it appears — following text shifts and
1594    /// the paragraph rewraps — but never in the undo history; the caret
1595    /// sits at the IME-reported offset inside it. Empty text cancels
1596    /// (winit sends that before every commit). Returns true if anything
1597    /// visible changed.
1598    pub(crate) fn set_preedit(
1599        &mut self,
1600        key: Key,
1601        text: &str,
1602        cursor: Option<(usize, usize)>,
1603        fs: &mut FontSystem,
1604    ) -> bool {
1605        let Some(s) = self.states.get_mut(&key) else {
1606            return false;
1607        };
1608        // No input method composes into a secure editor: the window has
1609        // it off while one has the keyboard, and a host that sends a
1610        // composition anyway is not drawn one.
1611        if s.secret.is_some() {
1612            return false;
1613        }
1614        let (text, cursor) = composed(text, cursor, s.multiline);
1615        let text = &*text;
1616        if text.is_empty() {
1617            if !s.abandon_preedit() {
1618                return false;
1619            }
1620        } else {
1621            let start = match s.preedit.take() {
1622                Some(pre) => {
1623                    s.splice(pre.start, &pre.text, text);
1624                    pre.start
1625                }
1626                None => {
1627                    // Composing over a selection replaces it, as typing
1628                    // would — that part is a real, recorded edit.
1629                    s.delete_selection_recorded();
1630                    let at = s.editor.cursor();
1631                    s.editor.insert_string(text, None);
1632                    at
1633                }
1634            };
1635            // Caret at the IME's offset inside the composition (its end
1636            // when unreported), clamped to a char boundary.
1637            let mut at = cursor.map_or(text.len(), |(c, _)| c.min(text.len()));
1638            while at > 0 && !text.is_char_boundary(at) {
1639                at -= 1;
1640            }
1641            s.editor.set_cursor(end_cursor(start, &text[..at]));
1642            s.editor.set_selection(Selection::None);
1643            s.preedit = Some(Preedit {
1644                start,
1645                text: text.to_string(),
1646            });
1647            s.break_coalesce();
1648        }
1649        s.editor.shape_as_needed(fs, false);
1650        s.version += 1;
1651        s.invalidate_measurements();
1652        self.touch_caret(key);
1653        true
1654    }
1655
1656    /// Drops the selection of one editor, leaving the caret where the
1657    /// selection's live end was. What a selection started elsewhere in
1658    /// the window calls, so no window ever shows two selections.
1659    pub(crate) fn collapse_selection(&mut self, key: Key) -> bool {
1660        let Some(s) = self.states.get_mut(&key) else {
1661            return false;
1662        };
1663        if matches!(s.editor.selection(), Selection::None) {
1664            return false;
1665        }
1666        s.editor.set_selection(Selection::None);
1667        self.caret_stamp += 1;
1668        true
1669    }
1670
1671    /// The active composition text, if any (for tests and hosts).
1672    pub fn preedit(&self, key: Key) -> Option<&str> {
1673        self.states
1674            .get(&key)?
1675            .preedit
1676            .as_ref()
1677            .map(|p| p.text.as_str())
1678    }
1679
1680    pub fn is_multiline(&self, key: Key) -> bool {
1681        self.states.get(&key).is_some_and(|s| s.multiline)
1682    }
1683
1684    /// Whether Tab in `key` is the app's rather than the focus ring's: a
1685    /// field declared with `keep_tab` (a document's Tab is its own).
1686    pub fn keeps_tab(&self, key: Key) -> bool {
1687        self.states
1688            .get(&key)
1689            .is_some_and(|s| s.keep_tab && !s.multiline)
1690    }
1691
1692    /// Whether `key` lays its text out to its box's width: a document, or
1693    /// a field with `wrap` declared. What emission asks
1694    /// before narrowing a field's clip — an editor that folds never
1695    /// scrolls, so it keeps the node's.
1696    pub(crate) fn folds(&self, key: Key) -> bool {
1697        self.states.get(&key).is_some_and(|s| s.folds)
1698    }
1699
1700    /// Caret rect in physical px, relative to the edit's content origin.
1701    /// None when the caret isn't laid out (e.g. no state for `key`).
1702    pub(crate) fn caret_rect(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
1703        let s = self.states.get_mut(&key)?;
1704        s.editor.shape_as_needed(fs, false);
1705        let (x, y) = s.editor.cursor_position()?;
1706        let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
1707        Some(Rect::new(
1708            x as f32 - s.offset_x,
1709            y as f32,
1710            (2.0 * s.scale).max(2.0),
1711            line_height,
1712        ))
1713    }
1714
1715    /// The caret of `key` in logical px from the start of its text, as laid
1716    /// out and before a field's scroll: zero wide, one line tall.
1717    pub(crate) fn caret_local(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
1718        let s = self.states.get_mut(&key)?;
1719        s.editor.shape_as_needed(fs, false);
1720        let (x, y) = s.editor.cursor_position()?;
1721        let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
1722        Some(Rect::new(
1723            x as f32 / s.scale,
1724            y as f32 / s.scale,
1725            0.0,
1726            line_height / s.scale,
1727        ))
1728    }
1729
1730    /// How far a single-line field's text is scrolled left, in physical
1731    /// px, for a content box `inner_w` wide (physical too). 0 for an
1732    /// editor that folds — a document, or a field with `wrap` — which
1733    /// wraps instead.
1734    ///
1735    /// A field does not wrap ([`EditStore::wrapped`]), so a value that
1736    /// outgrows its box is moved under the caret rather than folded onto a
1737    /// second line — which is what a native field does, and what an app
1738    /// otherwise has to fake by declaring the box wider than the text it
1739    /// is about to hold. Recomputed where the box is known,
1740    /// so a field that grows or shrinks between frames re-anchors with it.
1741    pub(crate) fn line_offset(&mut self, key: Key, inner_w: f32, fs: &mut FontSystem) -> f32 {
1742        let focused = self.focused == Some(key);
1743        let Some(s) = self.states.get_mut(&key) else {
1744            return 0.0;
1745        };
1746        if s.folds {
1747            return 0.0;
1748        }
1749        // Unfocused, a field shows its value from the start: what it says
1750        // is what a reader wants, not where its caret was left.
1751        if !focused {
1752            s.offset_x = 0.0;
1753            return 0.0;
1754        }
1755        s.editor.shape_as_needed(fs, false);
1756        let caret_w = (2.0 * s.scale).max(2.0);
1757        let text_w = s
1758            .editor
1759            .with_buffer(|b| b.layout_runs().map(|r| r.line_w).fold(0.0f32, f32::max));
1760        if let Some((x, _)) = s.editor.cursor_position() {
1761            let x = x as f32;
1762            // Two ends, one rule: keep the caret inside the box, moving
1763            // the text by the least that does it.
1764            if x - s.offset_x > inner_w - caret_w {
1765                s.offset_x = x - inner_w + caret_w;
1766            }
1767            if x < s.offset_x {
1768                s.offset_x = x;
1769            }
1770        }
1771        // Never past the end of the text (a field that shrank, or one
1772        // whose value was replaced by a shorter one, scrolls back).
1773        s.offset_x = s.offset_x.clamp(0.0, (text_w + caret_w - inner_w).max(0.0));
1774        s.offset_x
1775    }
1776
1777    // -- Layout measurement (logical units)
1778
1779    /// What the text wants on its own: the width a `Fit` editor takes, and
1780    /// the floor under a `Min::FIT` one.
1781    ///
1782    /// Measured with the wrap taken *off*, and cached against the text and
1783    /// its metrics rather than read off the buffer as it stands. The
1784    /// buffer is still carrying whatever width `wrapped` last set on it,
1785    /// and a fit width measured under that is a width that feeds back on
1786    /// itself: the box takes the widest wrapped line, the next frame wraps
1787    /// to that, and a field declared to hug its text ratchets down to one
1788    /// character with every keystroke on its own line.
1789    pub(crate) fn intrinsic(&mut self, key: Key, fs: &mut FontSystem) -> Size {
1790        let Some(s) = self.states.get_mut(&key) else {
1791            return Size::ZERO;
1792        };
1793        if let Some((v, m, size)) = s.natural
1794            && (v, m) == (s.version, s.metrics_rev)
1795        {
1796            return size;
1797        }
1798        // Taken off and left off: `wrapped` runs after this in the same
1799        // pass, and putting the frame's width back is its job. Off
1800        // unconditionally rather than when `wrap` says it is on — a style
1801        // change clears that flag without touching the buffer, and
1802        // measuring the wrapped buffer is the whole bug.
1803        s.editor.with_buffer_mut(|b| b.set_size(None, None));
1804        s.wrap = None;
1805        s.editor.shape_as_needed(fs, false);
1806        // The same measurement a text node takes of itself, except that an
1807        // empty editor is still one line tall.
1808        let (w, h) = s.editor.with_buffer(|b| {
1809            let (size, lines) = crate::text::measure_buffer(b, 0);
1810            (size.w, lines.max(1) as f32 * b.metrics().line_height)
1811        });
1812        // Caret margin so the cursor at line end isn't clipped.
1813        let size = Size::new((w + 2.0 * s.scale) / s.scale, h / s.scale);
1814        s.natural = Some((s.version, s.metrics_rev, size));
1815        size
1816    }
1817
1818    /// The editor's height at its final content width — and, for a
1819    /// single-line field, the width it is *not* wrapped to.
1820    ///
1821    /// `multiline: false` is a field, not a short document: it lays out on
1822    /// one line whatever it is given and scrolls that line under the caret
1823    /// (see [`EditStore::line_offset`]), which is what a native field does
1824    /// and what the `<edit>` row has always said it is. Wrapping one was
1825    /// how a name that outgrew its box came to be drawn two lines tall
1826    /// inside a box measured for one. The exception is a
1827    /// field that asked to fold (`EditOptions::wrap`): it
1828    /// wraps to its width exactly as a document does, and keeps a field's
1829    /// keyboard.
1830    /// The first line's baseline of editor `key` as `wrapped` last laid it
1831    /// out, logical px below the top of its text: what a
1832    /// field beside its label lines up by. An empty editor is one line of
1833    /// its own metrics. `NaN` for a key no editor holds.
1834    pub(crate) fn baseline(&self, key: Key) -> f32 {
1835        let Some(s) = self.states.get(&key) else {
1836            return f32::NAN;
1837        };
1838        let b = s.editor.with_buffer(|b| {
1839            b.layout_runs()
1840                .next()
1841                .map_or(b.metrics().line_height * 0.8, |r| r.line_y.round())
1842        });
1843        b / s.scale
1844    }
1845
1846    pub(crate) fn wrapped(&mut self, key: Key, max_w: f32, fs: &mut FontSystem) -> Size {
1847        let Some(s) = self.states.get_mut(&key) else {
1848            return Size::ZERO;
1849        };
1850        if !s.folds {
1851            // The buffer stays unwrapped (an editor that folded last frame
1852            // may be carrying a width), and the box is one line tall
1853            // whatever the text has in it — a `\n` that reached a field
1854            // through `set_text` does not make it two.
1855            if s.wrap.is_some() {
1856                s.editor.with_buffer_mut(|b| b.set_size(None, None));
1857                s.wrap = None;
1858                s.invalidate_measurements();
1859            }
1860            s.editor.shape_as_needed(fs, false);
1861            let line = s.editor.with_buffer(|b| b.metrics().line_height);
1862            return Size::new(max_w, line / s.scale);
1863        }
1864        let target = (max_w * s.scale).max(1.0);
1865        if crate::text::wrap_differs(s.wrap, Some(target)) {
1866            s.editor.with_buffer_mut(|b| b.set_size(Some(target), None));
1867            s.wrap = Some(target);
1868        }
1869        let stamp = (s.version, s.metrics_rev, target.to_bits());
1870        if let Some((v, m, w, size)) = s.measured
1871            && (v, m, w) == stamp
1872        {
1873            return size;
1874        }
1875        s.editor.shape_as_needed(fs, false);
1876        let h = s.editor.with_buffer(|b| {
1877            let (_, lines) = crate::text::measure_buffer(b, 0);
1878            lines.max(1) as f32 * b.metrics().line_height
1879        });
1880        let size = Size::new(max_w, h / s.scale);
1881        s.measured = Some((stamp.0, stamp.1, stamp.2, size));
1882        size
1883    }
1884
1885    // -- Emission
1886
1887    /// Draws selection, glyphs, and caret. `origin` is the content box origin
1888    /// in physical px; everything emitted is clipped by `clip`. `selected`
1889    /// is whether the selection is drawn: an editor that lost the keyboard
1890    /// keeps its range for when focus comes back, and shows none of it
1891    /// meanwhile (backlog F147) — a page of fields would otherwise show a
1892    /// highlight in each, none of which ⌘C copies.
1893    #[allow(clippy::too_many_arguments)]
1894    pub(crate) fn emit(
1895        &mut self,
1896        key: Key,
1897        origin: Vec2,
1898        focused: bool,
1899        selected: bool,
1900        faint: Color,
1901        clip: Clip,
1902        clip_id: ClipId,
1903        fs: &mut FontSystem,
1904        text_system: &mut TextSystem,
1905        atlas: &mut crate::atlas::GlyphAtlas,
1906        out: &mut Vec<Quad>,
1907    ) {
1908        let blink_visible = self.blink_visible;
1909        let Some(s) = self.states.get_mut(&key) else {
1910            return;
1911        };
1912        let raster = text_system.raster_mut();
1913        s.editor.shape_as_needed(fs, false);
1914        let color = s.style.color_or_default();
1915        let accent = s.accent;
1916        let scale = s.scale;
1917        let selection = s.editor.selection_bounds().filter(|_| selected);
1918        // The composition range, marked like a selection but drawn as a
1919        // tint plus underline; the caret stays solid while composing so
1920        // the IME's offset inside the text is never hidden by a blink.
1921        let composing = focused && s.preedit.is_some();
1922        let preedit = s
1923            .preedit
1924            .as_ref()
1925            .map(|p| (p.start, end_cursor(p.start, &p.text)));
1926        let cursor_pos = if focused && (blink_visible || composing) {
1927            s.editor.cursor_position()
1928        } else {
1929            None
1930        };
1931        // A single-line field scrolls its text under the caret; the box it
1932        // scrolls inside is the clip emission was handed.
1933        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
1934
1935        // What can show, in the glyphs' own space (ADR 0043).
1936        let vis = clip.visible();
1937        // An empty editor shows its placeholder where its text would start,
1938        // under the caret (backlog F149).
1939        let empty = s.preedit.is_none()
1940            && s.editor
1941                .with_buffer(|b| b.lines.len() == 1 && b.lines[0].text().is_empty());
1942        if empty && let Some((_, _, b)) = &s.placeholder {
1943            for run in b.layout_runs() {
1944                for glyph in run.glyphs.iter() {
1945                    let physical = glyph.physical((0.0, 0.0), 1.0);
1946                    let Some(slot) =
1947                        crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
1948                    else {
1949                        continue;
1950                    };
1951                    out.push(Quad {
1952                        rect: Rect::new(
1953                            origin.x + physical.x as f32 + slot.left as f32,
1954                            origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32,
1955                            slot.w as f32,
1956                            slot.h as f32,
1957                        ),
1958                        color: faint,
1959                        border_color: Color::TRANSPARENT,
1960                        radius: [0.0; 4],
1961                        border_w: 0.0,
1962                        blur: 0.0,
1963                        kind: crate::text::glyph_kind(&slot),
1964                        uv: [slot.x, slot.y, slot.w, slot.h],
1965                        clip: clip_id,
1966                    });
1967                }
1968            }
1969        }
1970        s.editor.with_buffer(|b| {
1971            let line_height = b.metrics().line_height;
1972            // Runs come in line order: skip everything above the clip and
1973            // stop at the first run past its bottom — a 100k-line document
1974            // emits only the visible screenful of quads.
1975            let runs = b
1976                .layout_runs()
1977                .filter(|run| origin.y + run.line_top + line_height >= vis.y)
1978                .take_while(|run| origin.y + run.line_top <= vis.y + vis.h);
1979            for run in runs {
1980                // Selection highlight for this run (mixed BiDi runs can
1981                // yield several disjoint spans). `highlight` is only valid
1982                // for runs on lines inside the selection span — outside it
1983                // marks the whole run selected.
1984                if let Some((start, end)) = selection
1985                    && run.line_i >= start.line
1986                    && run.line_i <= end.line
1987                {
1988                    let mut any = false;
1989                    for (x, w) in run.highlight(start, end) {
1990                        any = true;
1991                        out.push(Quad {
1992                            rect: Rect::new(
1993                                origin.x + x,
1994                                origin.y + run.line_top,
1995                                w.max(2.0),
1996                                line_height,
1997                            ),
1998                            color: accent,
1999                            border_color: Color::TRANSPARENT,
2000                            radius: [0.0; 4],
2001                            border_w: 0.0,
2002                            blur: 0.0,
2003                            kind: QuadKind::Solid,
2004                            uv: [0; 4],
2005                            clip: clip_id,
2006                        });
2007                    }
2008                    // Empty line inside the selection: a stub for the
2009                    // selected newline keeps the highlight continuous.
2010                    if !any && run.glyphs.is_empty() && end.line > run.line_i {
2011                        out.push(Quad {
2012                            rect: Rect::new(origin.x, origin.y + run.line_top, 2.0, line_height),
2013                            color: accent,
2014                            border_color: Color::TRANSPARENT,
2015                            radius: [0.0; 4],
2016                            border_w: 0.0,
2017                            blur: 0.0,
2018                            kind: QuadKind::Solid,
2019                            uv: [0; 4],
2020                            clip: clip_id,
2021                        });
2022                    }
2023                }
2024                // Composition backdrop + underline under its glyphs.
2025                if let Some((ps, pe)) = preedit
2026                    && run.line_i >= ps.line
2027                    && run.line_i <= pe.line
2028                {
2029                    let underline_h = scale.max(1.0);
2030                    for (x, w) in run.highlight(ps, pe) {
2031                        let solid = |rect: Rect, color: Color| Quad {
2032                            rect,
2033                            color,
2034                            border_color: Color::TRANSPARENT,
2035                            radius: [0.0; 4],
2036                            border_w: 0.0,
2037                            blur: 0.0,
2038                            kind: QuadKind::Solid,
2039                            uv: [0; 4],
2040                            clip: clip_id,
2041                        };
2042                        out.push(solid(
2043                            Rect::new(origin.x + x, origin.y + run.line_top, w, line_height),
2044                            Color { a: 0.3, ..accent },
2045                        ));
2046                        out.push(solid(
2047                            Rect::new(
2048                                origin.x + x,
2049                                origin.y + run.line_top + line_height - underline_h,
2050                                w,
2051                                underline_h,
2052                            ),
2053                            color,
2054                        ));
2055                    }
2056                }
2057                // Glyphs.
2058                for glyph in run.glyphs.iter() {
2059                    let physical = glyph.physical((0.0, 0.0), 1.0);
2060                    let Some(slot) =
2061                        crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
2062                    else {
2063                        continue;
2064                    };
2065                    let x = origin.x + physical.x as f32 + slot.left as f32;
2066                    let y = origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32;
2067                    let glyph_color = glyph
2068                        .color_opt
2069                        .map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a()))
2070                        .unwrap_or(color);
2071                    out.push(Quad {
2072                        rect: Rect::new(x, y, slot.w as f32, slot.h as f32),
2073                        color: glyph_color,
2074                        border_color: Color::TRANSPARENT,
2075                        radius: [0.0; 4],
2076                        border_w: 0.0,
2077                        blur: 0.0,
2078                        kind: crate::text::glyph_kind(&slot),
2079                        uv: [slot.x, slot.y, slot.w, slot.h],
2080                        clip: clip_id,
2081                    });
2082                }
2083            }
2084            // Caret.
2085            if let Some((cx, cy)) = cursor_pos {
2086                out.push(Quad {
2087                    rect: Rect::new(
2088                        origin.x + cx as f32,
2089                        origin.y + cy as f32,
2090                        (2.0 * scale).max(2.0),
2091                        line_height,
2092                    ),
2093                    color,
2094                    border_color: Color::TRANSPARENT,
2095                    radius: [0.0; 4],
2096                    border_w: 0.0,
2097                    blur: 0.0,
2098                    kind: QuadKind::Solid,
2099                    uv: [0; 4],
2100                    clip: clip_id,
2101                });
2102            }
2103        });
2104    }
2105}
2106
2107fn buffer_text(b: &Buffer) -> String {
2108    let mut out = String::new();
2109    for (i, line) in b.lines.iter().enumerate() {
2110        if i > 0 {
2111            out.push('\n');
2112        }
2113        out.push_str(line.text());
2114    }
2115    out
2116}