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