Skip to main content

kui_core/
edit.rs

1//! Editable text: retained editor state keyed by widget `Key`, built on
2//! cosmic-text's `Editor` so cursor motion, selection, and click-to-position
3//! all come from the same shaping truth the rest of the text stack uses.
4//! Edits arrive as data (`InputEvent::Text` / `InputEvent::Key`) routed to
5//! the focused editor; hosts read text back with `Core::edit_text`.
6
7use std::collections::VecDeque;
8
9use cosmic_text::{
10    Action, Attrs, AttrsList, Buffer, Cursor, Edit as _, Editor, FontSystem, Metrics, Motion,
11    Selection, Shaping, Wrap,
12};
13use rustc_hash::FxHashMap;
14
15use crate::color::Color;
16use crate::display::{Clip, ClipId, Quad, QuadKind};
17use crate::geom::{Rect, Size, Vec2};
18use crate::input::{EditKey, Mods};
19use crate::key::Key;
20use crate::resources::Resources;
21use crate::spec::{TextStyle, TextWrap};
22use crate::text::TextSystem;
23use crate::tree::OriginId;
24
25#[derive(Clone, Debug, Default)]
26pub struct EditOptions {
27    pub style: TextStyle,
28    pub multiline: bool,
29    /// A single-line editor that folds to its width, by `style.wrap`, the
30    /// way a document does — instead of taking one line and scrolling it
31    /// under the caret (backlog F44). Its keyboard is still a field's:
32    /// Enter submits, a newline is never admitted, the caret opens at the
33    /// end. In the bindings this is the `wrap` row being declared on the
34    /// element; off, a field does not read `style.wrap` at all. A
35    /// `multiline` editor wraps either way and ignores this.
36    pub wrap: bool,
37    /// Takes keyboard focus on the frame this declaration starts — a new
38    /// editor, one back after a gap, one whose flag just turned on — and
39    /// only while nothing holds focus: never from a focused control, and
40    /// never again after a blur (`docs/adr/0022`, decision 9).
41    pub autofocus: bool,
42    /// Selection highlight color. `None` is the theme's `selection`,
43    /// which is what a field gets unless the caller says otherwise — so a
44    /// selection over a label and one over a field are the same tint on
45    /// both bases (ADR 0019).
46    pub accent: Option<Color>,
47}
48
49pub(crate) struct EditState {
50    editor: Editor<'static>,
51    pub(crate) style: TextStyle,
52    pub(crate) accent: Color,
53    pub(crate) multiline: bool,
54    /// Whether the buffer is laid out to its box's width: a document, or a
55    /// field with `wrap` declared (backlog F44). What `wrapped`,
56    /// `line_offset` and emission's clip decide by — a field that does not
57    /// fold takes one line and scrolls it.
58    folds: bool,
59    pub(crate) origin: OriginId,
60    scale: f32,
61    /// Wrap width (physical) the buffer is laid out at; None = unwrapped.
62    wrap: Option<f32>,
63    /// Bumped on every content change.
64    pub version: u64,
65    /// Bumped whenever the same text is laid out under new metrics (a
66    /// style or a scale change), so neither cache below can answer a
67    /// question about this font with a size measured for the last one.
68    metrics_rev: u32,
69    /// Cached wrapped measurement: (version, metrics, wrap bits, size).
70    measured: Option<(u64, u32, u32, Size)>,
71    /// Cached unwrapped measurement: (version, metrics, size). What the
72    /// fit width reads, and the reason it is cached rather than taken off
73    /// the buffer as it stands: the buffer is still carrying whatever
74    /// width `wrapped` last set on it (backlog F38).
75    natural: Option<(u64, u32, Size)>,
76    /// How far a single-line field has scrolled its text left, in physical
77    /// px. A field keeps the caret inside its box by moving the text under
78    /// it, the way a native field does, rather than by wrapping (backlog
79    /// F41); an editor that folds (`folds`) wraps and this stays 0.
80    offset_x: f32,
81    /// In-progress IME composition: a marked, uncommitted range living
82    /// inside the buffer (so the text around it reflows as it grows).
83    preedit: Option<Preedit>,
84    /// Edit history, oldest first. The widget owns its buffer, so it owns
85    /// undo too — hosts with their own text model (on_key sinks) bring
86    /// their own history and never touch this.
87    undo: VecDeque<EditOp>,
88    redo: VecDeque<EditOp>,
89    /// What the top undo op can still absorb (typing bursts, delete runs).
90    coalesce: Option<Coalesce>,
91    /// Whether the last declaration carried `autofocus`: with
92    /// `last_declared`, what makes the next one an edge or a repeat.
93    autofocus: bool,
94    /// The frame this key was last declared in. Only the budget reads it
95    /// (see [`MAX_UNDECLARED_EDITS`]); a state declared every frame never
96    /// looks at it again.
97    last_declared: u64,
98}
99
100/// How many *undeclared* editors the store keeps before the longest
101/// undeclared one is dropped (backlog F26). A declared editor is never
102/// evicted, however many there are: retention across absence is what the
103/// `<edit>` row promises, so this is a ceiling, not a prune.
104///
105/// Why 256: one `EditState` is an `Editor` over a shaped `Buffer`, and a
106/// counting allocator measured a fresh one at 3.4 KB empty, 4.9 KB holding
107/// `"hello"`, and 22 KB holding a 39-character line (the shaped glyphs are
108/// most of it). 256 of the worst of those is ~5.6 MB, and ~1.2 MB at a
109/// short field — a bound an app can afford, and one no ordinary view comes
110/// near: 256 fields no longer on screen is already an app generating keys.
111pub const MAX_UNDECLARED_EDITS: usize = 256;
112
113const UNDO_CAP: usize = 1000;
114/// Max bytes one coalesced op absorbs before a new unit starts.
115const COALESCE_MAX: usize = 64;
116
117/// One reversible edit: `deleted` was removed at `at` and `inserted` put in
118/// its place. Operational, not a snapshot — undo cost tracks the edit size,
119/// never the document size.
120struct EditOp {
121    at: Cursor,
122    deleted: String,
123    inserted: String,
124    /// Caret restore points for undo / redo.
125    cursor_before: Cursor,
126    cursor_after: Cursor,
127}
128
129#[derive(Clone, Copy, PartialEq)]
130enum Coalesce {
131    /// Plain typing: appends to the top op's `inserted`.
132    Insert,
133    /// Backspace runs walking left: prepends to `deleted`.
134    Backspace,
135    /// Forward-delete runs at a fixed spot: appends to `deleted`.
136    Delete,
137}
138
139/// Where a cursor lands after inserting `s` at `at`.
140fn end_cursor(at: Cursor, s: &str) -> Cursor {
141    match s.rsplit_once('\n') {
142        None => Cursor::new(at.line, at.index + s.len()),
143        Some((head, tail)) => Cursor::new(at.line + head.matches('\n').count() + 1, tail.len()),
144    }
145}
146
147/// Position equality, ignoring affinity (which editor cursors carry but
148/// computed ones don't).
149fn same_pos(a: Cursor, b: Cursor) -> bool {
150    a.line == b.line && a.index == b.index
151}
152
153/// IME composition state. The text is *in* the buffer starting at `start`
154/// — inserted without touching the undo history, replaced on every update,
155/// removed on commit or cancel — so the paragraph wraps and the following
156/// text shifts exactly as if it had been typed. The editor's own caret is
157/// parked at the IME-reported offset inside it.
158struct Preedit {
159    start: Cursor,
160    text: String,
161}
162
163/// Byte offset of `c` in the buffer's full text (lines joined by '\n').
164fn abs_offset(b: &Buffer, c: Cursor) -> usize {
165    b.lines
166        .iter()
167        .take(c.line)
168        .map(|l| l.text().len() + 1)
169        .sum::<usize>()
170        + c.index
171}
172
173impl EditState {
174    /// Both measurement caches, dropped together: the text under them
175    /// changed, so neither the wrapped size nor the natural one still
176    /// describes it.
177    fn invalidate_measurements(&mut self) {
178        self.measured = None;
179        self.natural = None;
180    }
181
182    /// Pushes a fresh op (clearing redo), merging into the top op when the
183    /// declared coalesce kind matches and the edits are adjacent.
184    fn record(&mut self, op: EditOp, kind: Option<Coalesce>) {
185        self.redo.clear();
186        if let (Some(k), Some(last)) = (kind, self.undo.back_mut())
187            && self.coalesce == Some(k)
188            && last.deleted.len() + last.inserted.len() + op.deleted.len() + op.inserted.len()
189                <= COALESCE_MAX
190        {
191            let merged = match k {
192                Coalesce::Insert => {
193                    same_pos(op.at, end_cursor(last.at, &last.inserted)) && {
194                        last.inserted.push_str(&op.inserted);
195                        true
196                    }
197                }
198                Coalesce::Backspace => {
199                    same_pos(end_cursor(op.at, &op.deleted), last.at) && {
200                        last.at = op.at;
201                        last.deleted.insert_str(0, &op.deleted);
202                        true
203                    }
204                }
205                Coalesce::Delete => {
206                    same_pos(op.at, last.at) && {
207                        last.deleted.push_str(&op.deleted);
208                        true
209                    }
210                }
211            };
212            if merged {
213                last.cursor_after = op.cursor_after;
214                return;
215            }
216        }
217        if self.undo.len() >= UNDO_CAP {
218            self.undo.pop_front();
219        }
220        self.undo.push_back(op);
221        self.coalesce = kind;
222    }
223
224    /// Caret motion, clicks, blur: the next edit starts a new undo unit.
225    fn break_coalesce(&mut self) {
226        self.coalesce = None;
227    }
228
229    /// Replaces `[at .. at+remove]` with `insert`, no recording — the raw
230    /// mechanism undo/redo replay through.
231    fn splice(&mut self, at: Cursor, remove: &str, insert: &str) {
232        if remove.is_empty() {
233            self.editor.set_selection(Selection::None);
234            self.editor.set_cursor(at);
235        } else {
236            self.editor.set_selection(Selection::Normal(at));
237            self.editor.set_cursor(end_cursor(at, remove));
238            self.editor.delete_selection();
239        }
240        if !insert.is_empty() {
241            self.editor.insert_string(insert, None);
242        }
243    }
244
245    /// Replaces the selection (if any) with `text`, as one recorded op.
246    fn insert_recorded(&mut self, text: &str) {
247        let cursor_before = self.editor.cursor();
248        let deleted = self.editor.copy_selection().unwrap_or_default();
249        self.editor.delete_selection();
250        let at = self.editor.cursor();
251        self.editor.insert_string(text, None);
252        let kind = (deleted.is_empty() && !text.contains('\n')).then_some(Coalesce::Insert);
253        self.record(
254            EditOp {
255                at,
256                deleted,
257                inserted: text.to_string(),
258                cursor_before,
259                cursor_after: self.editor.cursor(),
260            },
261            kind,
262        );
263    }
264
265    /// Deletes the selection as one recorded op; false when there is none.
266    fn delete_selection_recorded(&mut self) -> bool {
267        let cursor_before = self.editor.cursor();
268        let Some(deleted) = self.editor.copy_selection() else {
269            return false;
270        };
271        if !self.editor.delete_selection() {
272            return false;
273        }
274        let at = self.editor.cursor();
275        self.record(
276            EditOp {
277                at,
278                deleted,
279                inserted: String::new(),
280                cursor_before,
281                cursor_after: at,
282            },
283            None,
284        );
285        true
286    }
287
288    /// Backspace/Delete (plain or word): selects via `motion`, deletes as a
289    /// recorded op. False at the buffer boundary (nothing to delete).
290    fn delete_motion_recorded(
291        &mut self,
292        motion: Motion,
293        kind: Coalesce,
294        fs: &mut FontSystem,
295    ) -> bool {
296        let cursor_before = self.editor.cursor();
297        self.editor.set_selection(Selection::Normal(cursor_before));
298        self.editor.action(fs, Action::Motion(motion));
299        let deleted = self.editor.copy_selection().unwrap_or_default();
300        if deleted.is_empty() {
301            self.editor.set_selection(Selection::None);
302            return false;
303        }
304        self.editor.delete_selection();
305        let at = self.editor.cursor();
306        self.record(
307            EditOp {
308                at,
309                deleted,
310                inserted: String::new(),
311                cursor_before,
312                cursor_after: at,
313            },
314            Some(kind),
315        );
316        true
317    }
318
319    /// Removes an in-progress composition from the buffer (cancel, blur,
320    /// or anything that must act on committed text only), leaving the
321    /// caret where the composition began. Unrecorded, like its insertion.
322    fn abandon_preedit(&mut self) -> bool {
323        let Some(pre) = self.preedit.take() else {
324            return false;
325        };
326        self.splice(pre.start, &pre.text, "");
327        self.editor.set_cursor(pre.start);
328        self.editor.set_selection(Selection::None);
329        true
330    }
331
332    fn undo_one(&mut self) -> bool {
333        let Some(op) = self.undo.pop_back() else {
334            return false;
335        };
336        self.splice(op.at, &op.inserted, &op.deleted);
337        self.editor.set_cursor(op.cursor_before);
338        self.editor.set_selection(Selection::None);
339        self.redo.push_back(op);
340        self.coalesce = None;
341        true
342    }
343
344    fn redo_one(&mut self) -> bool {
345        let Some(op) = self.redo.pop_back() else {
346            return false;
347        };
348        self.splice(op.at, &op.deleted, &op.inserted);
349        self.editor.set_cursor(op.cursor_after);
350        self.editor.set_selection(Selection::None);
351        self.undo.push_back(op);
352        self.coalesce = None;
353        true
354    }
355}
356
357/// A held `set_edit_text` the frame after it did not claim, in whichever
358/// spelling the call used — the two name the same mistake and raise the
359/// same code, but a warning that says "key" for a call that said "label"
360/// sends its reader looking in the wrong place.
361pub(crate) enum Unclaimed {
362    Key(Key),
363    Label(String),
364}
365
366pub struct EditStore {
367    states: FxHashMap<Key, EditState>,
368    /// Text set for a key nothing has declared yet: `set_text` holds it
369    /// here and the next `declare` under that key seeds with it instead of
370    /// with `initial`. An `update` that opens an editor and sets its text
371    /// in the same turn runs a frame ahead of the view that declares it,
372    /// so without this the call lands on nothing and the app sees the
373    /// editor open with `initial` (backlog F25). What the frame after it
374    /// does not claim is dropped by `finish_frame`, with a warning.
375    pending: FxHashMap<Key, String>,
376    /// The same seed named by label instead of by key, for the app that
377    /// has no key to give: the hex key comes from an event the node
378    /// fired, and an editor a rename is opening for the first time has
379    /// fired none (backlog F32). Held until `text_edit` declares an
380    /// editor under the label and claims it — which is also where a
381    /// *retained* editor is reached, since a key kept off screen (F20,
382    /// F26) has a state `declare` would not reseed and a label
383    /// `Core::key_of` cannot resolve while it goes undeclared.
384    pending_labels: FxHashMap<String, String>,
385    pub(crate) focused: Option<Key>,
386    /// Edit node being drag-selected (with its content origin, logical).
387    pub(crate) dragging: Option<(Key, Vec2)>,
388    /// Edit whose caret moved since the last frame; `finish_frame` scrolls
389    /// the nearest scrollable ancestor to keep the caret visible, then clears.
390    pub(crate) caret_moved: Option<Key>,
391    /// Bumped on anything that should restart the caret blink cycle (edits,
392    /// motion, clicks, focus changes). Frame drivers watch it to re-arm
393    /// their blink timer — the core itself stays clock-free.
394    caret_stamp: u64,
395    /// Whether the caret is currently drawn; toggled by the frame driver's
396    /// blink timer. Headless drivers never touch it, so the caret is solid.
397    blink_visible: bool,
398    /// The frame being built, stamped onto every state `declare` touches.
399    frame_no: u64,
400}
401
402impl Default for EditStore {
403    fn default() -> Self {
404        Self {
405            states: FxHashMap::default(),
406            pending: FxHashMap::default(),
407            pending_labels: FxHashMap::default(),
408            focused: None,
409            dragging: None,
410            caret_moved: None,
411            caret_stamp: 0,
412            blink_visible: true,
413            frame_no: 0,
414        }
415    }
416}
417
418/// What an editor's buffer may hold: a field is one line, so a newline that
419/// arrived in a seed or a `set_text` is dropped rather than drawn below a
420/// box measured for one line (F41). The same rule `apply_text` applies to
421/// typing and pasting, so the two doors agree.
422fn admitted(text: &str, multiline: bool) -> std::borrow::Cow<'_, str> {
423    if multiline || !text.contains(['\n', '\r']) {
424        return std::borrow::Cow::Borrowed(text);
425    }
426    std::borrow::Cow::Owned(text.chars().filter(|c| *c != '\n' && *c != '\r').collect())
427}
428
429/// At the family's regular weight, as text draws it (F100).
430fn attrs_for<'a>(style: &TextStyle, res: &'a Resources) -> Attrs<'a> {
431    res.weights_of(style.family).apply(
432        Attrs::new()
433            .family(res.family_of(style.family))
434            .font_features(crate::text::cosmic_features(&style.features)),
435        false,
436    )
437}
438
439impl EditStore {
440    /// Gives every editor's text the weights its family is asked at now
441    /// (RG59): a face of a registered family came or went. The text,
442    /// caret and history stay; every line shapes again, and every
443    /// measurement of it is of the old weights.
444    pub(crate) fn reweigh(&mut self, res: &Resources) {
445        for s in self.states.values_mut() {
446            let a = AttrsList::new(&attrs_for(&s.style, res));
447            let mut moved = false;
448            s.editor.with_buffer_mut(|b| {
449                for line in &mut b.lines {
450                    moved |= line.set_attrs_list(a.clone());
451                }
452            });
453            if moved {
454                s.editor.set_redraw(true);
455                s.wrap = None;
456                s.metrics_rev = s.metrics_rev.wrapping_add(1);
457                s.invalidate_measurements();
458            }
459        }
460    }
461
462    pub fn focused(&self) -> Option<Key> {
463        self.focused
464    }
465
466    pub fn set_focus(&mut self, key: Option<Key>) {
467        if self.focused != key {
468            self.caret_stamp += 1;
469            // A blurred editor abandons any in-progress composition.
470            if let Some(old) = self.focused
471                && let Some(s) = self.states.get_mut(&old)
472                && s.abandon_preedit()
473            {
474                s.version += 1;
475                s.invalidate_measurements();
476            }
477        }
478        self.focused = key;
479    }
480
481    /// See `caret_stamp` field: compare across frames to restart blink.
482    pub fn caret_stamp(&self) -> u64 {
483        self.caret_stamp
484    }
485
486    /// Blink-phase toggle for frame drivers; `true` draws the caret.
487    pub fn set_blink_visible(&mut self, visible: bool) {
488        self.blink_visible = visible;
489    }
490
491    /// The phase as last set; `true` draws the caret.
492    pub fn blink_visible(&self) -> bool {
493        self.blink_visible
494    }
495
496    fn touch_caret(&mut self, key: Key) {
497        self.caret_moved = Some(key);
498        self.caret_stamp += 1;
499    }
500
501    /// How many states are retained — declared and undeclared together.
502    /// What a test watches the budget through (backlog F26).
503    pub fn len(&self) -> usize {
504        self.states.len()
505    }
506
507    pub fn is_empty(&self) -> bool {
508        self.states.is_empty()
509    }
510
511    /// Stamps the frame being built and, if the map has grown past the
512    /// budget, drops the longest-undeclared states
513    /// ([`MAX_UNDECLARED_EDITS`]). A state the frame that just ended
514    /// declared is never evicted, and neither is the focused or the
515    /// drag-selected one — the store still points at those.
516    ///
517    /// The length test is what an ordinary frame pays: a store inside the
518    /// budget never walks itself, which is why this can run every frame
519    /// rather than every 240th like the anim store's cutoff sweep.
520    pub(crate) fn begin_frame(&mut self, frame_no: u64) {
521        self.frame_no = frame_no;
522        if self.states.len() > MAX_UNDECLARED_EDITS {
523            self.evict(frame_no.saturating_sub(1));
524        }
525    }
526
527    /// Drops the oldest undeclared states down to the budget. `declared_at`
528    /// is the frame that just ended: a state stamped with it is declared.
529    fn evict(&mut self, declared_at: u64) {
530        let focused = self.focused;
531        let dragging = self.dragging.map(|(k, _)| k);
532        let caret_moved = &mut self.caret_moved;
533        crate::retain::evict_undeclared(
534            &mut self.states,
535            MAX_UNDECLARED_EDITS,
536            declared_at,
537            |s| s.last_declared,
538            |k| Some(k) == focused || Some(k) == dragging,
539            |k| {
540                if *caret_moved == Some(k) {
541                    *caret_moved = None;
542                }
543            },
544        );
545    }
546
547    /// Ensures state exists for `key`, seeding `initial` on first creation
548    /// — or, if a `set_text` for this key arrived before anything declared
549    /// it, that text instead (see [`EditStore::pending`]). Returns whether
550    /// this declaration is an *autofocus edge*: the key was not declared
551    /// with `autofocus` on the frame before this one — a new editor, one
552    /// back after a gap, or one whose `autofocus` just turned on — which
553    /// is the one frame the flag may act on (`docs/adr/0022`, decision 9).
554    #[allow(clippy::too_many_arguments)]
555    pub(crate) fn declare(
556        &mut self,
557        key: Key,
558        initial: &str,
559        opts: &EditOptions,
560        origin: OriginId,
561        scale: f32,
562        fs: &mut FontSystem,
563        res: &Resources,
564    ) -> bool {
565        let frame_no = self.frame_no;
566        let edge = self
567            .states
568            .get(&key)
569            .is_none_or(|s| s.last_declared + 1 != frame_no || !s.autofocus);
570        // Only a creation consumes the seed: a key already declared has
571        // no pending text (a `set_text` with state behind it is applied
572        // where it is called), and taking one here would drop it.
573        let seed = if self.states.contains_key(&key) {
574            None
575        } else {
576            self.pending.remove(&key)
577        };
578        let seeded = seed.is_some();
579        let initial = seed.as_deref().unwrap_or(initial);
580        let state = self.states.entry(key).or_insert_with(|| {
581            let metrics = Metrics::new(opts.style.size * scale, opts.style.line_height * scale);
582            let mut buffer = Buffer::new(fs, metrics);
583            buffer.set_size(None, None);
584            buffer.set_text(
585                &admitted(initial, opts.multiline),
586                &attrs_for(&opts.style, res),
587                Shaping::Advanced,
588                None,
589            );
590            let mut editor = Editor::new(buffer);
591            // A single-line field opens with the caret after its seeded
592            // text — what a native field does with a prefilled value, and
593            // what a rename wants, since typing into a name meant to be
594            // extended otherwise prepends to it (backlog F20). A multiline
595            // editor is a document and opens at its top, as native text
596            // views do. Placed, not moved: no `touch_caret`, so nothing
597            // scrolls to reveal it before the user has touched it.
598            //
599            // A held `set_text` is not `initial`: it is that call arriving
600            // where it can land, so it leaves the caret where the call
601            // does — at the end, document or not.
602            if seeded || !opts.multiline {
603                let end = editor.with_buffer(|b| {
604                    let line = b.lines.len().saturating_sub(1);
605                    Cursor::new(line, b.lines.get(line).map_or(0, |l| l.text().len()))
606                });
607                editor.set_cursor(end);
608            }
609            EditState {
610                editor,
611                style: opts.style,
612                accent: opts.accent.unwrap_or(crate::select::TINT),
613                multiline: opts.multiline,
614                folds: false,
615                origin,
616                scale,
617                wrap: None,
618                version: 0,
619                metrics_rev: 0,
620                measured: None,
621                natural: None,
622                offset_x: 0.0,
623                preedit: None,
624                undo: VecDeque::new(),
625                redo: VecDeque::new(),
626                coalesce: None,
627                autofocus: false,
628                last_declared: frame_no,
629            }
630        });
631        state.last_declared = frame_no;
632        state.autofocus = opts.autofocus;
633        state.origin = origin;
634        state.multiline = opts.multiline;
635        state.accent = opts.accent.unwrap_or(crate::select::TINT);
636        // A document wraps between words whatever its style says, as it
637        // always has; a field folds only when asked, and then by the mode
638        // the `wrap` row picked — so a rename field breaks where the label
639        // it renames breaks (backlog F44). `wrap="none"` on a field is the
640        // field: one line, scrolled.
641        let folds = opts.multiline || (opts.wrap && opts.style.wrap != TextWrap::None);
642        let mode = match (opts.multiline, opts.style.wrap) {
643            (false, TextWrap::Glyph) => Wrap::Glyph,
644            _ => Wrap::WordOrGlyph,
645        };
646        if state.folds != folds || state.editor.with_buffer(|b| b.wrap()) != mode {
647            state.folds = folds;
648            state.editor.with_buffer_mut(|b| b.set_wrap(mode));
649            state.invalidate_measurements();
650        }
651        // Style/scale changes re-metric the buffer (text and cursor survive).
652        // The text is the same, so `version` does not move — but every
653        // measurement of it is now of the wrong font, which is what
654        // `metrics_rev` is for: without it a cache keyed on the text alone
655        // answers the new frame with the old size.
656        if state.scale != scale || state.style != opts.style {
657            state.style = opts.style;
658            state.scale = scale;
659            let metrics = Metrics::new(opts.style.size * scale, opts.style.line_height * scale);
660            state.editor.with_buffer_mut(|b| b.set_metrics(metrics));
661            state.wrap = None;
662            state.metrics_rev = state.metrics_rev.wrapping_add(1);
663            state.invalidate_measurements();
664        }
665        // `autofocus` is the core's decision (it owns the one focus).
666        edge
667    }
668
669    pub fn contains(&self, key: Key) -> bool {
670        self.states.contains_key(&key)
671    }
672
673    pub fn origin_of(&self, key: Key) -> Option<OriginId> {
674        self.states.get(&key).map(|s| s.origin)
675    }
676
677    /// The committed text: an in-progress composition is not part of it.
678    pub fn text(&self, key: Key) -> Option<String> {
679        let s = self.states.get(&key)?;
680        Some(s.editor.with_buffer(|b| {
681            let mut text = buffer_text(b);
682            if let Some(pre) = &s.preedit {
683                let at = abs_offset(b, pre.start);
684                if text.is_char_boundary(at) && at + pre.text.len() <= text.len() {
685                    text.replace_range(at..at + pre.text.len(), "");
686                }
687            }
688            text
689        }))
690    }
691
692    /// Returns whether the text landed in an editor: `false` says it was
693    /// held for the frame that declares the key, so nothing on screen
694    /// changed yet and the frame that will change it is the app's — a
695    /// driver that redraws on every write would re-lower the tree that
696    /// declares no editor and drop the seed to the warning (backlog F42).
697    pub fn set_text(&mut self, key: Key, text: &str, fs: &mut FontSystem, res: &Resources) -> bool {
698        let Some(s) = self.states.get_mut(&key) else {
699            // Nothing has declared this key yet. The call is not wrong —
700            // the `update` that opens an editor runs before the view that
701            // declares it — so hold the text for the frame that does
702            // (backlog F25) rather than falling through silently.
703            self.pending.insert(key, text.to_string());
704            return false;
705        };
706        s.preedit = None;
707        let a = attrs_for(&s.style, res);
708        let text = admitted(text, s.multiline);
709        s.editor
710            .with_buffer_mut(|b| b.set_text(&text, &a, Shaping::Advanced, None));
711        s.editor.set_selection(Selection::None);
712        s.editor.action(fs, Action::Motion(Motion::BufferEnd));
713        s.version += 1;
714        s.invalidate_measurements();
715        s.wrap = None;
716        // A wholesale replacement invalidates the recorded deltas.
717        s.undo.clear();
718        s.redo.clear();
719        s.coalesce = None;
720        self.touch_caret(key);
721        true
722    }
723
724    /// Holds `text` for the next editor declared under `label`, for a
725    /// `set_edit_text` by a name nothing has declared yet (backlog F32).
726    /// The key path is [`EditStore::pending`]; this one is claimed by
727    /// [`EditStore::claim_label`] from inside the build, where the label
728    /// and the key it resolves to are both in hand.
729    pub(crate) fn hold_label(&mut self, label: &str, text: &str) {
730        self.pending_labels
731            .insert(label.to_string(), text.to_string());
732    }
733
734    /// A declaring editor takes the seed held for its label, if there is
735    /// one. Two shapes, and the second is the one a seed by key cannot
736    /// have: a *new* editor takes it as `declare` takes a pending key,
737    /// over `initial`; an editor that already has a state — retained
738    /// while its key was off screen — takes it as `set_text`, since
739    /// `declare` reseeds nothing that exists and the draft would
740    /// otherwise come back over the model's text.
741    pub(crate) fn claim_label(
742        &mut self,
743        key: Key,
744        label: &str,
745        fs: &mut FontSystem,
746        res: &Resources,
747    ) {
748        if self.pending_labels.is_empty() {
749            return;
750        }
751        let Some(text) = self.pending_labels.remove(label) else {
752            return;
753        };
754        if self.states.contains_key(&key) {
755            self.set_text(key, &text, fs, res);
756        } else {
757            self.pending.insert(key, text);
758        }
759    }
760
761    /// The seeds no frame claimed, dropped: `finish_frame` drains this
762    /// after the build and raises [`crate::diag::EDIT_TEXT_WITHOUT_EDITOR`]
763    /// for each, so a `set_edit_text` on a key — or a label — the view
764    /// never declares is a line rather than nothing at all. Sorted within
765    /// each spelling, so the order two unclaimed seeds are reported in
766    /// does not depend on a hash seed.
767    pub(crate) fn take_unclaimed_seeds(&mut self) -> Vec<Unclaimed> {
768        let mut out: Vec<Unclaimed> = Vec::new();
769        if !self.pending.is_empty() {
770            let mut keys: Vec<Key> = self.pending.drain().map(|(k, _)| k).collect();
771            keys.sort_unstable();
772            out.extend(keys.into_iter().map(Unclaimed::Key));
773        }
774        if !self.pending_labels.is_empty() {
775            let mut labels: Vec<String> = self.pending_labels.drain().map(|(l, _)| l).collect();
776            labels.sort_unstable();
777            out.extend(labels.into_iter().map(Unclaimed::Label));
778        }
779        out
780    }
781
782    pub fn version(&self, key: Key) -> u64 {
783        self.states.get(&key).map_or(0, |s| s.version)
784    }
785
786    /// The caret as a byte offset into the committed text, and the
787    /// non-empty selection as a byte range, for the access tree.
788    pub fn caret_and_selection(&self, key: Key) -> Option<(usize, Option<(usize, usize)>)> {
789        let s = self.states.get(&key)?;
790        Some(s.editor.with_buffer(|b| {
791            let caret = abs_offset(b, s.editor.cursor());
792            let selection = s
793                .editor
794                .selection_bounds()
795                .map(|(a, z)| (abs_offset(b, a), abs_offset(b, z)))
796                .filter(|(a, z)| a != z);
797            (caret, selection)
798        }))
799    }
800
801    /// The selection's anchor and the caret as (line, byte index) pairs
802    /// (equal without a selection), for the access tree.
803    pub fn selection_cursors(&self, key: Key) -> Option<((usize, usize), (usize, usize))> {
804        let s = self.states.get(&key)?;
805        let caret = s.editor.cursor();
806        let anchor = match s.editor.selection() {
807            Selection::Normal(c) | Selection::Line(c) | Selection::Word(c) => c,
808            Selection::None => caret,
809        };
810        Some(((anchor.line, anchor.index), (caret.line, caret.index)))
811    }
812
813    /// The editor's laid-out lines as access runs (see
814    /// [`crate::access::AccessRun`]); `origin` is where the content box
815    /// starts, logical px.
816    pub(crate) fn runs(
817        &self,
818        key: Key,
819        node: Key,
820        origin: Vec2,
821        scale: f32,
822    ) -> Vec<crate::access::AccessRun> {
823        let Some(s) = self.states.get(&key) else {
824            return Vec::new();
825        };
826        let mut out = Vec::new();
827        let mut n = 0;
828        // Where the glyphs are, not where they would be unscrolled: a
829        // screen reader's character rects have to land on the pixels
830        // emission drew (a scrolled field, F41).
831        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
832        s.editor.with_buffer(|b| {
833            crate::access::runs_of_buffer(
834                b,
835                crate::access::RunSource {
836                    key: node,
837                    line: 0,
838                    byte_base: 0,
839                    origin,
840                    scale,
841                    newline_after_last: false,
842                },
843                &mut n,
844                &mut out,
845            )
846        });
847        out
848    }
849
850    /// Moves the caret to `focus` and the selection's other end to
851    /// `anchor`, both (line, byte index) pairs clamped into the text; equal
852    /// pairs clear the selection. What a screen reader's "select from here
853    /// to there" becomes.
854    pub fn set_selection(&mut self, key: Key, anchor: (usize, usize), focus: (usize, usize)) {
855        let Some(s) = self.states.get_mut(&key) else {
856            return;
857        };
858        s.abandon_preedit();
859        let clamp = |b: &Buffer, (line, index): (usize, usize)| {
860            let line = line.min(b.lines.len().saturating_sub(1));
861            let text = b.lines[line].text();
862            let mut index = index.min(text.len());
863            while !text.is_char_boundary(index) {
864                index -= 1;
865            }
866            Cursor::new(line, index)
867        };
868        let (a, f) = s
869            .editor
870            .with_buffer(|b| (clamp(b, anchor), clamp(b, focus)));
871        s.editor.set_cursor(f);
872        s.editor.set_selection(if a == f {
873            Selection::None
874        } else {
875            Selection::Normal(a)
876        });
877        s.break_coalesce();
878        self.touch_caret(key);
879    }
880
881    /// Types `text` over the selection (or at the caret), as one undo
882    /// step; true when the content changed.
883    pub fn replace_selection(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
884        let Some(s) = self.states.get_mut(&key) else {
885            return false;
886        };
887        s.abandon_preedit();
888        if text.is_empty() && s.editor.selection_bounds().is_none() {
889            return false;
890        }
891        s.insert_recorded(text);
892        s.editor.shape_as_needed(fs, false);
893        s.version += 1;
894        s.invalidate_measurements();
895        self.touch_caret(key);
896        true
897    }
898
899    pub fn copy_selection(&self, key: Key) -> Option<String> {
900        self.states.get(&key)?.editor.copy_selection()
901    }
902
903    /// Whether the editor `key` has a non-empty selection — what
904    /// `copy_selection` would answer, without building the string. A
905    /// menu asking whether Copy applies asks this every time it opens.
906    pub fn has_selection(&self, key: Key) -> bool {
907        self.states
908            .get(&key)
909            .and_then(|s| s.editor.selection_bounds())
910            .is_some_and(|(a, b)| a != b)
911    }
912
913    /// Whether the editor `key` has an edit to undo, and one to redo.
914    pub fn history(&self, key: Key) -> (bool, bool) {
915        self.states
916            .get(&key)
917            .map_or((false, false), |s| (!s.undo.is_empty(), !s.redo.is_empty()))
918    }
919
920    /// Deletes the selection; returns true if anything was deleted.
921    pub fn delete_selection(&mut self, key: Key, fs: &mut FontSystem) -> bool {
922        let Some(s) = self.states.get_mut(&key) else {
923            return false;
924        };
925        let _ = fs;
926        s.abandon_preedit();
927        if s.delete_selection_recorded() {
928            s.version += 1;
929            s.invalidate_measurements();
930            self.touch_caret(key);
931            true
932        } else {
933            false
934        }
935    }
936
937    // -- Input application (focused editor). Returns true if content changed.
938
939    pub(crate) fn apply_text(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
940        let Some(s) = self.states.get_mut(&key) else {
941            return false;
942        };
943        // A commit ends the composition (winit also clears preedit first);
944        // the committed text goes in where the composition began.
945        s.abandon_preedit();
946        let filtered: String = text
947            .chars()
948            .filter(|c| !c.is_control() || (*c == '\n' && s.multiline) || *c == '\t')
949            .collect();
950        if filtered.is_empty() {
951            return false;
952        }
953        s.insert_recorded(&filtered);
954        s.editor.shape_as_needed(fs, false);
955        s.version += 1;
956        s.invalidate_measurements();
957        self.touch_caret(key);
958        true
959    }
960
961    /// Returns (content_changed, submit) — submit is Enter in single-line.
962    pub(crate) fn apply_key(
963        &mut self,
964        key: Key,
965        ek: EditKey,
966        mods: Mods,
967        fs: &mut FontSystem,
968    ) -> (bool, bool) {
969        let Some(s) = self.states.get_mut(&key) else {
970            return (false, false);
971        };
972        // Keys reaching the editor mid-composition mean the IME let them
973        // through; act on committed text only.
974        let mut changed = s.abandon_preedit();
975        let mut submit = false;
976        match ek {
977            EditKey::Left
978            | EditKey::Right
979            | EditKey::Up
980            | EditKey::Down
981            | EditKey::Home
982            | EditKey::End
983            | EditKey::PageUp
984            | EditKey::PageDown => {
985                let motion = match (ek, mods.word, mods.doc) {
986                    (EditKey::Left, true, _) => Motion::LeftWord,
987                    (EditKey::Right, true, _) => Motion::RightWord,
988                    (EditKey::Left, _, true) => Motion::Home,
989                    (EditKey::Right, _, true) => Motion::End,
990                    (EditKey::Up, _, true) => Motion::BufferStart,
991                    (EditKey::Down, _, true) => Motion::BufferEnd,
992                    (EditKey::Left, ..) => Motion::Left,
993                    (EditKey::Right, ..) => Motion::Right,
994                    (EditKey::Up, ..) => Motion::Up,
995                    (EditKey::Down, ..) => Motion::Down,
996                    (EditKey::Home, _, true) => Motion::BufferStart,
997                    (EditKey::End, _, true) => Motion::BufferEnd,
998                    (EditKey::Home, ..) => Motion::Home,
999                    (EditKey::End, ..) => Motion::End,
1000                    (EditKey::PageUp, ..) => Motion::PageUp,
1001                    (EditKey::PageDown, ..) => Motion::PageDown,
1002                    _ => unreachable!(),
1003                };
1004                if mods.shift {
1005                    if s.editor.selection() == Selection::None {
1006                        s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1007                    }
1008                } else {
1009                    s.editor.set_selection(Selection::None);
1010                }
1011                s.editor.action(fs, Action::Motion(motion));
1012                s.break_coalesce();
1013            }
1014            EditKey::Backspace => {
1015                changed |= s.delete_selection_recorded()
1016                    || s.delete_motion_recorded(
1017                        if mods.word {
1018                            Motion::LeftWord
1019                        } else {
1020                            Motion::Left
1021                        },
1022                        Coalesce::Backspace,
1023                        fs,
1024                    );
1025            }
1026            EditKey::Delete => {
1027                changed |= s.delete_selection_recorded()
1028                    || s.delete_motion_recorded(
1029                        if mods.word {
1030                            Motion::RightWord
1031                        } else {
1032                            Motion::Right
1033                        },
1034                        Coalesce::Delete,
1035                        fs,
1036                    );
1037            }
1038            EditKey::Enter => {
1039                if s.multiline {
1040                    s.insert_recorded("\n");
1041                    changed = true;
1042                } else {
1043                    submit = true;
1044                }
1045            }
1046            EditKey::Tab => {
1047                if s.multiline {
1048                    s.insert_recorded("    ");
1049                    changed = true;
1050                }
1051            }
1052            EditKey::Undo => changed |= s.undo_one(),
1053            EditKey::Redo => changed |= s.redo_one(),
1054            EditKey::SelectAll => {
1055                s.editor.action(fs, Action::Motion(Motion::BufferStart));
1056                s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1057                s.editor.action(fs, Action::Motion(Motion::BufferEnd));
1058                s.break_coalesce();
1059            }
1060            EditKey::Escape => {
1061                s.editor.action(fs, Action::Escape);
1062                s.break_coalesce();
1063            }
1064        }
1065        s.editor.shape_as_needed(fs, false);
1066        if changed {
1067            s.version += 1;
1068            s.invalidate_measurements();
1069        }
1070        self.touch_caret(key);
1071        (changed, submit)
1072    }
1073
1074    /// Mouse press inside the edit at content-local logical position.
1075    /// `clicks` is the driver-counted multi-click: 2 selects the word,
1076    /// 3 the line (cosmic-text's double/triple click actions). With
1077    /// `extend` — a Shift-press — the caret moves there keeping the
1078    /// selection's anchor, seeding one at the caret when there is none
1079    /// (cosmic-text's `Drag` does both; ADR 0029, decision 3), and the
1080    /// click count says nothing. Either way it is a click: a live
1081    /// composition is abandoned and the next edit starts an undo unit.
1082    pub(crate) fn click(
1083        &mut self,
1084        key: Key,
1085        local: Vec2,
1086        clicks: u8,
1087        extend: bool,
1088        fs: &mut FontSystem,
1089    ) {
1090        if let Some(s) = self.states.get_mut(&key) {
1091            if s.abandon_preedit() {
1092                s.version += 1;
1093                s.invalidate_measurements();
1094            }
1095            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1096            let action = match (extend, clicks) {
1097                (true, _) => Action::Drag { x, y },
1098                (false, 0 | 1) => Action::Click { x, y },
1099                (false, 2) => Action::DoubleClick { x, y },
1100                (false, _) => Action::TripleClick { x, y },
1101            };
1102            s.editor.action(fs, action);
1103            s.editor.shape_as_needed(fs, false);
1104            s.break_coalesce();
1105            self.caret_stamp += 1;
1106        }
1107    }
1108
1109    /// Mouse motion with the button held: moves the caret to the point
1110    /// and keeps the selection's anchor (seeding one at the caret when
1111    /// there is none — cosmic-text's `Drag` does both). The caret moved,
1112    /// so it is marked like a keyboard motion's: a field scrolls its own
1113    /// text toward the drag at once, and a scroller above a document
1114    /// reveals the caret on the release — not under the held pointer,
1115    /// where the drag's own rate is what moves it (ADR 0029, decision 2;
1116    /// `scroll_caret_into_view`).
1117    pub(crate) fn drag(&mut self, key: Key, local: Vec2, fs: &mut FontSystem) {
1118        if let Some(s) = self.states.get_mut(&key) {
1119            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1120            let before = s.editor.cursor();
1121            s.editor.action(fs, Action::Drag { x, y });
1122            if s.editor.cursor() != before {
1123                self.touch_caret(key);
1124            } else {
1125                self.caret_stamp += 1;
1126            }
1127        }
1128    }
1129
1130    /// Replaces the focused editor's IME composition. The text lives in
1131    /// the buffer from the moment it appears — following text shifts and
1132    /// the paragraph rewraps — but never in the undo history; the caret
1133    /// sits at the IME-reported offset inside it. Empty text cancels
1134    /// (winit sends that before every commit). Returns true if anything
1135    /// visible changed.
1136    pub(crate) fn set_preedit(
1137        &mut self,
1138        key: Key,
1139        text: &str,
1140        cursor: Option<(usize, usize)>,
1141        fs: &mut FontSystem,
1142    ) -> bool {
1143        let Some(s) = self.states.get_mut(&key) else {
1144            return false;
1145        };
1146        if text.is_empty() {
1147            if !s.abandon_preedit() {
1148                return false;
1149            }
1150        } else {
1151            let start = match s.preedit.take() {
1152                Some(pre) => {
1153                    s.splice(pre.start, &pre.text, text);
1154                    pre.start
1155                }
1156                None => {
1157                    // Composing over a selection replaces it, as typing
1158                    // would — that part is a real, recorded edit.
1159                    s.delete_selection_recorded();
1160                    let at = s.editor.cursor();
1161                    s.editor.insert_string(text, None);
1162                    at
1163                }
1164            };
1165            // Caret at the IME's offset inside the composition (its end
1166            // when unreported), clamped to a char boundary.
1167            let mut at = cursor.map_or(text.len(), |(c, _)| c.min(text.len()));
1168            while at > 0 && !text.is_char_boundary(at) {
1169                at -= 1;
1170            }
1171            s.editor.set_cursor(end_cursor(start, &text[..at]));
1172            s.editor.set_selection(Selection::None);
1173            s.preedit = Some(Preedit {
1174                start,
1175                text: text.to_string(),
1176            });
1177            s.break_coalesce();
1178        }
1179        s.editor.shape_as_needed(fs, false);
1180        s.version += 1;
1181        s.invalidate_measurements();
1182        self.touch_caret(key);
1183        true
1184    }
1185
1186    /// Drops the selection of one editor, leaving the caret where the
1187    /// selection's live end was. What a selection started elsewhere in
1188    /// the window calls, so no window ever shows two selections
1189    /// (`docs/adr/0017-selection-as-a-scope.md`).
1190    pub(crate) fn collapse_selection(&mut self, key: Key) -> bool {
1191        let Some(s) = self.states.get_mut(&key) else {
1192            return false;
1193        };
1194        if matches!(s.editor.selection(), Selection::None) {
1195            return false;
1196        }
1197        s.editor.set_selection(Selection::None);
1198        self.caret_stamp += 1;
1199        true
1200    }
1201
1202    /// The active composition text, if any (for tests and hosts).
1203    pub fn preedit(&self, key: Key) -> Option<&str> {
1204        self.states
1205            .get(&key)?
1206            .preedit
1207            .as_ref()
1208            .map(|p| p.text.as_str())
1209    }
1210
1211    pub fn is_multiline(&self, key: Key) -> bool {
1212        self.states.get(&key).is_some_and(|s| s.multiline)
1213    }
1214
1215    /// Whether `key` lays its text out to its box's width: a document, or
1216    /// a field with `wrap` declared (backlog F44). What emission asks
1217    /// before narrowing a field's clip — an editor that folds never
1218    /// scrolls, so it keeps the node's.
1219    pub(crate) fn folds(&self, key: Key) -> bool {
1220        self.states.get(&key).is_some_and(|s| s.folds)
1221    }
1222
1223    /// Caret rect in physical px, relative to the edit's content origin.
1224    /// None when the caret isn't laid out (e.g. no state for `key`).
1225    pub(crate) fn caret_rect(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
1226        let s = self.states.get_mut(&key)?;
1227        s.editor.shape_as_needed(fs, false);
1228        let (x, y) = s.editor.cursor_position()?;
1229        let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
1230        Some(Rect::new(
1231            x as f32 - s.offset_x,
1232            y as f32,
1233            (2.0 * s.scale).max(2.0),
1234            line_height,
1235        ))
1236    }
1237
1238    /// How far a single-line field's text is scrolled left, in physical
1239    /// px, for a content box `inner_w` wide (physical too). 0 for an
1240    /// editor that folds — a document, or a field with `wrap` — which
1241    /// wraps instead.
1242    ///
1243    /// A field does not wrap ([`EditStore::wrapped`]), so a value that
1244    /// outgrows its box is moved under the caret rather than folded onto a
1245    /// second line — which is what a native field does, and what an app
1246    /// otherwise has to fake by declaring the box wider than the text it
1247    /// is about to hold (backlog F41). Recomputed where the box is known,
1248    /// so a field that grows or shrinks between frames re-anchors with it.
1249    pub(crate) fn line_offset(&mut self, key: Key, inner_w: f32, fs: &mut FontSystem) -> f32 {
1250        let focused = self.focused == Some(key);
1251        let Some(s) = self.states.get_mut(&key) else {
1252            return 0.0;
1253        };
1254        if s.folds {
1255            return 0.0;
1256        }
1257        // Unfocused, a field shows its value from the start: what it says
1258        // is what a reader wants, not where its caret was left.
1259        if !focused {
1260            s.offset_x = 0.0;
1261            return 0.0;
1262        }
1263        s.editor.shape_as_needed(fs, false);
1264        let caret_w = (2.0 * s.scale).max(2.0);
1265        let text_w = s
1266            .editor
1267            .with_buffer(|b| b.layout_runs().map(|r| r.line_w).fold(0.0f32, f32::max));
1268        if let Some((x, _)) = s.editor.cursor_position() {
1269            let x = x as f32;
1270            // Two ends, one rule: keep the caret inside the box, moving
1271            // the text by the least that does it.
1272            if x - s.offset_x > inner_w - caret_w {
1273                s.offset_x = x - inner_w + caret_w;
1274            }
1275            if x < s.offset_x {
1276                s.offset_x = x;
1277            }
1278        }
1279        // Never past the end of the text (a field that shrank, or one
1280        // whose value was replaced by a shorter one, scrolls back).
1281        s.offset_x = s.offset_x.clamp(0.0, (text_w + caret_w - inner_w).max(0.0));
1282        s.offset_x
1283    }
1284
1285    // -- Layout measurement (logical units)
1286
1287    /// What the text wants on its own: the width a `Fit` editor takes, and
1288    /// the floor under a `Min::FIT` one.
1289    ///
1290    /// Measured with the wrap taken *off*, and cached against the text and
1291    /// its metrics rather than read off the buffer as it stands. The
1292    /// buffer is still carrying whatever width `wrapped` last set on it,
1293    /// and a fit width measured under that is a width that feeds back on
1294    /// itself: the box takes the widest wrapped line, the next frame wraps
1295    /// to that, and a field declared to hug its text ratchets down to one
1296    /// character with every keystroke on its own line (backlog F38).
1297    pub(crate) fn intrinsic(&mut self, key: Key, fs: &mut FontSystem) -> Size {
1298        let Some(s) = self.states.get_mut(&key) else {
1299            return Size::ZERO;
1300        };
1301        if let Some((v, m, size)) = s.natural
1302            && (v, m) == (s.version, s.metrics_rev)
1303        {
1304            return size;
1305        }
1306        // Taken off and left off: `wrapped` runs after this in the same
1307        // pass, and putting the frame's width back is its job. Off
1308        // unconditionally rather than when `wrap` says it is on — a style
1309        // change clears that flag without touching the buffer, and
1310        // measuring the wrapped buffer is the whole bug.
1311        s.editor.with_buffer_mut(|b| b.set_size(None, None));
1312        s.wrap = None;
1313        s.editor.shape_as_needed(fs, false);
1314        // The same measurement a text node takes of itself, except that an
1315        // empty editor is still one line tall.
1316        let (w, h) = s.editor.with_buffer(|b| {
1317            let (size, lines) = crate::text::measure_buffer(b, 0);
1318            (size.w, lines.max(1) as f32 * b.metrics().line_height)
1319        });
1320        // Caret margin so the cursor at line end isn't clipped.
1321        let size = Size::new((w + 2.0 * s.scale) / s.scale, h / s.scale);
1322        s.natural = Some((s.version, s.metrics_rev, size));
1323        size
1324    }
1325
1326    /// The editor's height at its final content width — and, for a
1327    /// single-line field, the width it is *not* wrapped to.
1328    ///
1329    /// `multiline: false` is a field, not a short document: it lays out on
1330    /// one line whatever it is given and scrolls that line under the caret
1331    /// (see [`EditStore::line_offset`]), which is what a native field does
1332    /// and what the `<edit>` row has always said it is. Wrapping one was
1333    /// how a name that outgrew its box came to be drawn two lines tall
1334    /// inside a box measured for one (backlog F41). The exception is a
1335    /// field that asked to fold (`EditOptions::wrap`, backlog F44): it
1336    /// wraps to its width exactly as a document does, and keeps a field's
1337    /// keyboard.
1338    /// The first line's baseline of editor `key` as `wrapped` last laid it
1339    /// out, logical px below the top of its text (backlog C13): what a
1340    /// field beside its label lines up by. An empty editor is one line of
1341    /// its own metrics. `NaN` for a key no editor holds.
1342    pub(crate) fn baseline(&self, key: Key) -> f32 {
1343        let Some(s) = self.states.get(&key) else {
1344            return f32::NAN;
1345        };
1346        let b = s.editor.with_buffer(|b| {
1347            b.layout_runs()
1348                .next()
1349                .map_or(b.metrics().line_height * 0.8, |r| r.line_y.round())
1350        });
1351        b / s.scale
1352    }
1353
1354    pub(crate) fn wrapped(&mut self, key: Key, max_w: f32, fs: &mut FontSystem) -> Size {
1355        let Some(s) = self.states.get_mut(&key) else {
1356            return Size::ZERO;
1357        };
1358        if !s.folds {
1359            // The buffer stays unwrapped (an editor that folded last frame
1360            // may be carrying a width), and the box is one line tall
1361            // whatever the text has in it — a `\n` that reached a field
1362            // through `set_text` does not make it two.
1363            if s.wrap.is_some() {
1364                s.editor.with_buffer_mut(|b| b.set_size(None, None));
1365                s.wrap = None;
1366                s.invalidate_measurements();
1367            }
1368            s.editor.shape_as_needed(fs, false);
1369            let line = s.editor.with_buffer(|b| b.metrics().line_height);
1370            return Size::new(max_w, line / s.scale);
1371        }
1372        let target = (max_w * s.scale).max(1.0);
1373        if crate::text::wrap_differs(s.wrap, Some(target)) {
1374            s.editor.with_buffer_mut(|b| b.set_size(Some(target), None));
1375            s.wrap = Some(target);
1376        }
1377        let stamp = (s.version, s.metrics_rev, target.to_bits());
1378        if let Some((v, m, w, size)) = s.measured
1379            && (v, m, w) == stamp
1380        {
1381            return size;
1382        }
1383        s.editor.shape_as_needed(fs, false);
1384        let h = s.editor.with_buffer(|b| {
1385            let (_, lines) = crate::text::measure_buffer(b, 0);
1386            lines.max(1) as f32 * b.metrics().line_height
1387        });
1388        let size = Size::new(max_w, h / s.scale);
1389        s.measured = Some((stamp.0, stamp.1, stamp.2, size));
1390        size
1391    }
1392
1393    // -- Emission
1394
1395    /// Draws selection, glyphs, and caret. `origin` is the content box origin
1396    /// in physical px; everything emitted is clipped by `clip`.
1397    #[allow(clippy::too_many_arguments)]
1398    pub(crate) fn emit(
1399        &mut self,
1400        key: Key,
1401        origin: Vec2,
1402        focused: bool,
1403        clip: Clip,
1404        clip_id: ClipId,
1405        fs: &mut FontSystem,
1406        text_system: &mut TextSystem,
1407        atlas: &mut crate::atlas::GlyphAtlas,
1408        out: &mut Vec<Quad>,
1409    ) {
1410        let blink_visible = self.blink_visible;
1411        let Some(s) = self.states.get_mut(&key) else {
1412            return;
1413        };
1414        let raster = text_system.raster_mut();
1415        s.editor.shape_as_needed(fs, false);
1416        let color = s.style.color_or_default();
1417        let accent = s.accent;
1418        let scale = s.scale;
1419        let selection = s.editor.selection_bounds();
1420        // The composition range, marked like a selection but drawn as a
1421        // tint plus underline; the caret stays solid while composing so
1422        // the IME's offset inside the text is never hidden by a blink.
1423        let composing = focused && s.preedit.is_some();
1424        let preedit = s
1425            .preedit
1426            .as_ref()
1427            .map(|p| (p.start, end_cursor(p.start, &p.text)));
1428        let cursor_pos = if focused && (blink_visible || composing) {
1429            s.editor.cursor_position()
1430        } else {
1431            None
1432        };
1433        // A single-line field scrolls its text under the caret; the box it
1434        // scrolls inside is the clip emission was handed (F41).
1435        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
1436
1437        s.editor.with_buffer(|b| {
1438            let line_height = b.metrics().line_height;
1439            // Runs come in line order: skip everything above the clip and
1440            // stop at the first run past its bottom — a 100k-line document
1441            // emits only the visible screenful of quads.
1442            let runs = b
1443                .layout_runs()
1444                .filter(|run| origin.y + run.line_top + line_height >= clip.rect.y)
1445                .take_while(|run| origin.y + run.line_top <= clip.rect.y + clip.rect.h);
1446            for run in runs {
1447                // Selection highlight for this run (mixed BiDi runs can
1448                // yield several disjoint spans). `highlight` is only valid
1449                // for runs on lines inside the selection span — outside it
1450                // marks the whole run selected.
1451                if let Some((start, end)) = selection
1452                    && run.line_i >= start.line
1453                    && run.line_i <= end.line
1454                {
1455                    let mut any = false;
1456                    for (x, w) in run.highlight(start, end) {
1457                        any = true;
1458                        out.push(Quad {
1459                            rect: Rect::new(
1460                                origin.x + x,
1461                                origin.y + run.line_top,
1462                                w.max(2.0),
1463                                line_height,
1464                            ),
1465                            color: accent,
1466                            border_color: Color::TRANSPARENT,
1467                            radius: [0.0; 4],
1468                            border_w: 0.0,
1469                            blur: 0.0,
1470                            kind: QuadKind::Solid,
1471                            uv: [0; 4],
1472                            clip: clip_id,
1473                        });
1474                    }
1475                    // Empty line inside the selection: a stub for the
1476                    // selected newline keeps the highlight continuous.
1477                    if !any && run.glyphs.is_empty() && end.line > run.line_i {
1478                        out.push(Quad {
1479                            rect: Rect::new(origin.x, origin.y + run.line_top, 2.0, line_height),
1480                            color: accent,
1481                            border_color: Color::TRANSPARENT,
1482                            radius: [0.0; 4],
1483                            border_w: 0.0,
1484                            blur: 0.0,
1485                            kind: QuadKind::Solid,
1486                            uv: [0; 4],
1487                            clip: clip_id,
1488                        });
1489                    }
1490                }
1491                // Composition backdrop + underline under its glyphs.
1492                if let Some((ps, pe)) = preedit
1493                    && run.line_i >= ps.line
1494                    && run.line_i <= pe.line
1495                {
1496                    let underline_h = scale.max(1.0);
1497                    for (x, w) in run.highlight(ps, pe) {
1498                        let solid = |rect: Rect, color: Color| Quad {
1499                            rect,
1500                            color,
1501                            border_color: Color::TRANSPARENT,
1502                            radius: [0.0; 4],
1503                            border_w: 0.0,
1504                            blur: 0.0,
1505                            kind: QuadKind::Solid,
1506                            uv: [0; 4],
1507                            clip: clip_id,
1508                        };
1509                        out.push(solid(
1510                            Rect::new(origin.x + x, origin.y + run.line_top, w, line_height),
1511                            Color { a: 0.3, ..accent },
1512                        ));
1513                        out.push(solid(
1514                            Rect::new(
1515                                origin.x + x,
1516                                origin.y + run.line_top + line_height - underline_h,
1517                                w,
1518                                underline_h,
1519                            ),
1520                            color,
1521                        ));
1522                    }
1523                }
1524                // Glyphs.
1525                for glyph in run.glyphs.iter() {
1526                    let physical = glyph.physical((0.0, 0.0), 1.0);
1527                    let Some(slot) =
1528                        crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
1529                    else {
1530                        continue;
1531                    };
1532                    let x = origin.x + physical.x as f32 + slot.left as f32;
1533                    let y = origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32;
1534                    let glyph_color = glyph
1535                        .color_opt
1536                        .map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a()))
1537                        .unwrap_or(color);
1538                    out.push(Quad {
1539                        rect: Rect::new(x, y, slot.w as f32, slot.h as f32),
1540                        color: glyph_color,
1541                        border_color: Color::TRANSPARENT,
1542                        radius: [0.0; 4],
1543                        border_w: 0.0,
1544                        blur: 0.0,
1545                        kind: crate::text::glyph_kind(&slot),
1546                        uv: [slot.x, slot.y, slot.w, slot.h],
1547                        clip: clip_id,
1548                    });
1549                }
1550            }
1551            // Caret.
1552            if let Some((cx, cy)) = cursor_pos {
1553                out.push(Quad {
1554                    rect: Rect::new(
1555                        origin.x + cx as f32,
1556                        origin.y + cy as f32,
1557                        (2.0 * scale).max(2.0),
1558                        line_height,
1559                    ),
1560                    color,
1561                    border_color: Color::TRANSPARENT,
1562                    radius: [0.0; 4],
1563                    border_w: 0.0,
1564                    blur: 0.0,
1565                    kind: QuadKind::Solid,
1566                    uv: [0; 4],
1567                    clip: clip_id,
1568                });
1569            }
1570        });
1571    }
1572}
1573
1574fn buffer_text(b: &Buffer) -> String {
1575    let mut out = String::new();
1576    for (i, line) in b.lines.iter().enumerate() {
1577        if i > 0 {
1578            out.push('\n');
1579        }
1580        out.push_str(line.text());
1581    }
1582    out
1583}