Skip to main content

kui_core/
edit.rs

1//! Editable text: the retained state of every `text_edit` node, keyed by
2//! node `Key`, built on cosmic-text's editor.
3//!
4//! A view declares an editor with `Ui::text_edit(label, initial, &opts,
5//! spec)`, where `opts` is an [`EditOptions`]. The core keeps the buffer,
6//! caret, selection, IME composition and undo history across frames,
7//! routes `InputEvent::Text` / `InputEvent::Key` to the focused editor and
8//! posts `changed` and `submit` events. An app reads the draft back with
9//! `Core::edit_text` / `Ui::edit_text` and replaces it with
10//! `Core::set_edit_text`; the store itself is not something an app
11//! touches.
12//!
13//! ```rust
14//! use kui_core::{Core, EditOptions, InputEvent, NodeSpec, Size, TextStyle};
15//!
16//! let mut core = Core::new();
17//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
18//! let opts = EditOptions {
19//!     style: TextStyle::new(14.0),
20//!     autofocus: true,
21//!     ..Default::default()
22//! };
23//! let field = ui.text_edit("name", "hello", &opts, NodeSpec::row().grow_width().pad(4.0));
24//! ui.finish();
25//!
26//! // Typing reaches the focused editor; the host reads the draft back.
27//! core.handle_input(InputEvent::Text(" world".into()));
28//! assert_eq!(core.edit_text(field).as_deref(), Some("hello world"));
29//! ```
30
31use std::collections::VecDeque;
32
33use cosmic_text::{
34    Action, Attrs, AttrsList, Buffer, Cursor, Edit as _, Editor, FontSystem, Motion, Selection,
35    Shaping, Wrap,
36};
37use rustc_hash::FxHashMap;
38
39use crate::color::Color;
40use crate::display::{Clip, ClipId, Quad, QuadKind};
41use crate::geom::{Rect, Size, Vec2};
42use crate::input::{EditKey, Mods};
43use crate::key::Key;
44use crate::resources::Resources;
45use crate::spec::{TextStyle, TextWrap};
46use crate::text::TextSystem;
47use crate::tree::OriginId;
48
49/// How a `text_edit` node behaves: its text style, whether it is a
50/// single-line field or a multiline document, and how it takes focus and
51/// paints its selection. Build one with struct update syntax from
52/// `Default`.
53#[derive(Clone, Debug, Default)]
54pub struct EditOptions {
55    pub style: TextStyle,
56    /// A document rather than a field: Enter inserts a newline, the text
57    /// wraps to the box, and the caret opens at the top.
58    pub multiline: bool,
59    /// A single-line editor that folds to its width, by `style.wrap`, the
60    /// way a document does — instead of taking one line and scrolling it
61    /// under the caret. Its keyboard is still a field's:
62    /// Enter submits, a newline is never admitted, the caret opens at the
63    /// end. In the bindings this is the `wrap` row being declared on the
64    /// element; off, a field does not read `style.wrap` at all. A
65    /// `multiline` editor wraps either way and ignores this.
66    pub wrap: bool,
67    /// Takes keyboard focus on the frame this declaration starts — a new
68    /// editor, one back after a gap, one whose flag just turned on — and
69    /// only while nothing holds focus: never from a focused control, and
70    /// never again after a blur.
71    pub autofocus: bool,
72    /// 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. Whether or not a line's
472    /// attributes moved: a new fallback list (`set_fallback_fonts`)
473    /// changes the faces a line is shaped in and none of its attributes
474    /// (RG114).
475    pub(crate) fn reweigh(&mut self, res: &Resources) {
476        for s in self.states.values_mut() {
477            let a = AttrsList::new(&attrs_for(&s.style, res));
478            s.editor.with_buffer_mut(|b| {
479                for line in &mut b.lines {
480                    line.set_attrs_list(a.clone());
481                    line.reset_shaping();
482                }
483            });
484            s.editor.set_redraw(true);
485            s.wrap = None;
486            s.metrics_rev = s.metrics_rev.wrapping_add(1);
487            s.invalidate_measurements();
488        }
489    }
490
491    pub fn focused(&self) -> Option<Key> {
492        self.focused
493    }
494
495    pub fn set_focus(&mut self, key: Option<Key>) {
496        if self.focused != key {
497            self.caret_stamp += 1;
498            // A blurred editor abandons any in-progress composition.
499            if let Some(old) = self.focused
500                && let Some(s) = self.states.get_mut(&old)
501                && s.abandon_preedit()
502            {
503                s.version += 1;
504                s.invalidate_measurements();
505            }
506        }
507        self.focused = key;
508    }
509
510    /// See `caret_stamp` field: compare across frames to restart blink.
511    pub fn caret_stamp(&self) -> u64 {
512        self.caret_stamp
513    }
514
515    /// Blink-phase toggle for frame drivers; `true` draws the caret.
516    pub fn set_blink_visible(&mut self, visible: bool) {
517        self.blink_visible = visible;
518    }
519
520    /// The phase as last set; `true` draws the caret.
521    pub fn blink_visible(&self) -> bool {
522        self.blink_visible
523    }
524
525    fn touch_caret(&mut self, key: Key) {
526        self.caret_moved = Some(key);
527        self.caret_stamp += 1;
528    }
529
530    /// How many states are retained — declared and undeclared together.
531    /// What a test watches the budget through.
532    pub fn len(&self) -> usize {
533        self.states.len()
534    }
535
536    pub fn is_empty(&self) -> bool {
537        self.states.is_empty()
538    }
539
540    /// Stamps the frame being built and, if the map has grown past the
541    /// budget, drops the longest-undeclared states
542    /// ([`MAX_UNDECLARED_EDITS`]). A state the frame that just ended
543    /// declared is never evicted, and neither is the focused or the
544    /// drag-selected one — the store still points at those.
545    ///
546    /// The length test is what an ordinary frame pays: a store inside the
547    /// budget never walks itself, which is why this can run every frame
548    /// rather than every 240th like the anim store's cutoff sweep.
549    pub(crate) fn begin_frame(&mut self, frame_no: u64) {
550        self.frame_no = frame_no;
551        if self.states.len() > MAX_UNDECLARED_EDITS {
552            self.evict(frame_no.saturating_sub(1));
553        }
554    }
555
556    /// Drops the oldest undeclared states down to the budget. `declared_at`
557    /// is the frame that just ended: a state stamped with it is declared.
558    fn evict(&mut self, declared_at: u64) {
559        let focused = self.focused;
560        let dragging = self.dragging.map(|(k, _)| k);
561        let caret_moved = &mut self.caret_moved;
562        crate::retain::evict_undeclared(
563            &mut self.states,
564            MAX_UNDECLARED_EDITS,
565            declared_at,
566            |s| s.last_declared,
567            |k| Some(k) == focused || Some(k) == dragging,
568            |k| {
569                if *caret_moved == Some(k) {
570                    *caret_moved = None;
571                }
572            },
573        );
574    }
575
576    /// Ensures state exists for `key`, seeding `initial` on first creation
577    /// — or, if a `set_text` for this key arrived before anything declared
578    /// it, that text instead (see [`EditStore::pending`]). Returns whether
579    /// this declaration is an *autofocus edge*: the key was not declared
580    /// with `autofocus` on the frame before this one — a new editor, one
581    /// back after a gap, or one whose `autofocus` just turned on — which
582    /// is the one frame the flag may act on.
583    #[allow(clippy::too_many_arguments)]
584    pub(crate) fn declare(
585        &mut self,
586        key: Key,
587        initial: &str,
588        opts: &EditOptions,
589        origin: OriginId,
590        scale: f32,
591        fs: &mut FontSystem,
592        res: &Resources,
593    ) -> bool {
594        let frame_no = self.frame_no;
595        let edge = self
596            .states
597            .get(&key)
598            .is_none_or(|s| s.last_declared + 1 != frame_no || !s.autofocus);
599        // Only a creation consumes the seed: a key already declared has
600        // no pending text (a `set_text` with state behind it is applied
601        // where it is called), and taking one here would drop it.
602        let seed = if self.states.contains_key(&key) {
603            None
604        } else {
605            self.pending.remove(&key)
606        };
607        let seeded = seed.is_some();
608        let initial = seed.as_deref().unwrap_or(initial);
609        let state = self.states.entry(key).or_insert_with(|| {
610            let metrics = crate::text::shaper_metrics(
611                opts.style.size * scale,
612                opts.style.line_height * scale,
613            );
614            let mut buffer = Buffer::new(fs, metrics);
615            buffer.set_size(None, None);
616            buffer.set_text(
617                &admitted(initial, opts.multiline),
618                &attrs_for(&opts.style, res),
619                Shaping::Advanced,
620                None,
621            );
622            let mut editor = Editor::new(buffer);
623            // A single-line field opens with the caret after its seeded
624            // text — what a native field does with a prefilled value, and
625            // what a rename wants, since typing into a name meant to be
626            // extended otherwise prepends to it. A multiline
627            // editor is a document and opens at its top, as native text
628            // views do. Placed, not moved: no `touch_caret`, so nothing
629            // scrolls to reveal it before the user has touched it.
630            //
631            // A held `set_text` is not `initial`: it is that call arriving
632            // where it can land, so it leaves the caret where the call
633            // does — at the end, document or not.
634            if seeded || !opts.multiline {
635                let end = editor.with_buffer(|b| {
636                    let line = b.lines.len().saturating_sub(1);
637                    Cursor::new(line, b.lines.get(line).map_or(0, |l| l.text().len()))
638                });
639                editor.set_cursor(end);
640            }
641            EditState {
642                editor,
643                style: opts.style,
644                accent: opts.accent.unwrap_or(crate::select::TINT),
645                multiline: opts.multiline,
646                folds: false,
647                origin,
648                scale,
649                wrap: None,
650                version: 0,
651                metrics_rev: 0,
652                measured: None,
653                natural: None,
654                offset_x: 0.0,
655                preedit: None,
656                undo: VecDeque::new(),
657                redo: VecDeque::new(),
658                coalesce: None,
659                autofocus: false,
660                last_declared: frame_no,
661            }
662        });
663        state.last_declared = frame_no;
664        state.autofocus = opts.autofocus;
665        state.origin = origin;
666        state.multiline = opts.multiline;
667        state.accent = opts.accent.unwrap_or(crate::select::TINT);
668        // A document wraps between words whatever its style says, as it
669        // always has; a field folds only when asked, and then by the mode
670        // the `wrap` row picked — so a rename field breaks where the label
671        // it renames breaks. `wrap="none"` on a field is the
672        // field: one line, scrolled.
673        let folds = opts.multiline || (opts.wrap && opts.style.wrap != TextWrap::None);
674        let mode = match (opts.multiline, opts.style.wrap) {
675            (false, TextWrap::Glyph) => Wrap::Glyph,
676            _ => Wrap::WordOrGlyph,
677        };
678        if state.folds != folds || state.editor.with_buffer(|b| b.wrap()) != mode {
679            state.folds = folds;
680            state.editor.with_buffer_mut(|b| b.set_wrap(mode));
681            state.invalidate_measurements();
682        }
683        // Style/scale changes re-metric the buffer (text and cursor survive).
684        // The text is the same, so `version` does not move — but every
685        // measurement of it is now of the wrong font, which is what
686        // `metrics_rev` is for: without it a cache keyed on the text alone
687        // answers the new frame with the old size.
688        if state.scale != scale || state.style != opts.style {
689            state.style = opts.style;
690            state.scale = scale;
691            let metrics = crate::text::shaper_metrics(
692                opts.style.size * scale,
693                opts.style.line_height * scale,
694            );
695            state.editor.with_buffer_mut(|b| b.set_metrics(metrics));
696            state.wrap = None;
697            state.metrics_rev = state.metrics_rev.wrapping_add(1);
698            state.invalidate_measurements();
699        }
700        // `autofocus` is the core's decision (it owns the one focus).
701        edge
702    }
703
704    pub fn contains(&self, key: Key) -> bool {
705        self.states.contains_key(&key)
706    }
707
708    pub fn origin_of(&self, key: Key) -> Option<OriginId> {
709        self.states.get(&key).map(|s| s.origin)
710    }
711
712    /// The committed text: an in-progress composition is not part of it.
713    pub fn text(&self, key: Key) -> Option<String> {
714        let s = self.states.get(&key)?;
715        Some(s.editor.with_buffer(|b| {
716            let mut text = buffer_text(b);
717            if let Some(pre) = &s.preedit {
718                let at = abs_offset(b, pre.start);
719                if text.is_char_boundary(at) && at + pre.text.len() <= text.len() {
720                    text.replace_range(at..at + pre.text.len(), "");
721                }
722            }
723            text
724        }))
725    }
726
727    /// Returns whether the text landed in an editor: `false` says it was
728    /// held for the frame that declares the key, so nothing on screen
729    /// changed yet and the frame that will change it is the app's — a
730    /// driver that redraws on every write would re-lower the tree that
731    /// declares no editor and drop the seed to the warning.
732    pub fn set_text(&mut self, key: Key, text: &str, fs: &mut FontSystem, res: &Resources) -> bool {
733        let Some(s) = self.states.get_mut(&key) else {
734            // Nothing has declared this key yet. The call is not wrong —
735            // the `update` that opens an editor runs before the view that
736            // declares it — so hold the text for the frame that does
737            // rather than falling through silently.
738            self.pending.insert(key, text.to_string());
739            return false;
740        };
741        s.preedit = None;
742        let a = attrs_for(&s.style, res);
743        let text = admitted(text, s.multiline);
744        s.editor
745            .with_buffer_mut(|b| b.set_text(&text, &a, Shaping::Advanced, None));
746        s.editor.set_selection(Selection::None);
747        s.editor.action(fs, Action::Motion(Motion::BufferEnd));
748        s.version += 1;
749        s.invalidate_measurements();
750        s.wrap = None;
751        // A wholesale replacement invalidates the recorded deltas.
752        s.undo.clear();
753        s.redo.clear();
754        s.coalesce = None;
755        self.touch_caret(key);
756        true
757    }
758
759    /// Holds `text` for the next editor declared under `label`, for a
760    /// `set_edit_text` by a name nothing has declared yet.
761    /// The key path is [`EditStore::pending`]; this one is claimed by
762    /// [`EditStore::claim_label`] from inside the build, where the label
763    /// and the key it resolves to are both in hand.
764    pub(crate) fn hold_label(&mut self, label: &str, text: &str) {
765        self.pending_labels
766            .insert(label.to_string(), text.to_string());
767    }
768
769    /// A declaring editor takes the seed held for its label, if there is
770    /// one. Two shapes, and the second is the one a seed by key cannot
771    /// have: a *new* editor takes it as `declare` takes a pending key,
772    /// over `initial`; an editor that already has a state — retained
773    /// while its key was off screen — takes it as `set_text`, since
774    /// `declare` reseeds nothing that exists and the draft would
775    /// otherwise come back over the model's text.
776    pub(crate) fn claim_label(
777        &mut self,
778        key: Key,
779        label: &str,
780        fs: &mut FontSystem,
781        res: &Resources,
782    ) {
783        if self.pending_labels.is_empty() {
784            return;
785        }
786        let Some(text) = self.pending_labels.remove(label) else {
787            return;
788        };
789        if self.states.contains_key(&key) {
790            self.set_text(key, &text, fs, res);
791        } else {
792            self.pending.insert(key, text);
793        }
794    }
795
796    /// The seeds no frame claimed, dropped: `finish_frame` drains this
797    /// after the build and raises [`crate::diag::EDIT_TEXT_WITHOUT_EDITOR`]
798    /// for each, so a `set_edit_text` on a key — or a label — the view
799    /// never declares is a line rather than nothing at all. Sorted within
800    /// each spelling, so the order two unclaimed seeds are reported in
801    /// does not depend on a hash seed.
802    pub(crate) fn take_unclaimed_seeds(&mut self) -> Vec<Unclaimed> {
803        let mut out: Vec<Unclaimed> = Vec::new();
804        if !self.pending.is_empty() {
805            let mut keys: Vec<Key> = self.pending.drain().map(|(k, _)| k).collect();
806            keys.sort_unstable();
807            out.extend(keys.into_iter().map(Unclaimed::Key));
808        }
809        if !self.pending_labels.is_empty() {
810            let mut labels: Vec<String> = self.pending_labels.drain().map(|(l, _)| l).collect();
811            labels.sort_unstable();
812            out.extend(labels.into_iter().map(Unclaimed::Label));
813        }
814        out
815    }
816
817    pub fn version(&self, key: Key) -> u64 {
818        self.states.get(&key).map_or(0, |s| s.version)
819    }
820
821    /// The caret as a byte offset into the committed text, and the
822    /// non-empty selection as a byte range, for the access tree.
823    pub fn caret_and_selection(&self, key: Key) -> Option<(usize, Option<(usize, usize)>)> {
824        let s = self.states.get(&key)?;
825        Some(s.editor.with_buffer(|b| {
826            let caret = abs_offset(b, s.editor.cursor());
827            let selection = s
828                .editor
829                .selection_bounds()
830                .map(|(a, z)| (abs_offset(b, a), abs_offset(b, z)))
831                .filter(|(a, z)| a != z);
832            (caret, selection)
833        }))
834    }
835
836    /// The selection's anchor and the caret as (line, byte index) pairs
837    /// (equal without a selection), for the access tree.
838    pub fn selection_cursors(&self, key: Key) -> Option<((usize, usize), (usize, usize))> {
839        let s = self.states.get(&key)?;
840        let caret = s.editor.cursor();
841        let anchor = match s.editor.selection() {
842            Selection::Normal(c) | Selection::Line(c) | Selection::Word(c) => c,
843            Selection::None => caret,
844        };
845        Some(((anchor.line, anchor.index), (caret.line, caret.index)))
846    }
847
848    /// The editor's laid-out lines as access runs (see
849    /// [`crate::access::AccessRun`]); `origin` is where the content box
850    /// starts, logical px.
851    pub(crate) fn runs(
852        &self,
853        key: Key,
854        node: Key,
855        origin: Vec2,
856        scale: f32,
857    ) -> Vec<crate::access::AccessRun> {
858        let Some(s) = self.states.get(&key) else {
859            return Vec::new();
860        };
861        let mut out = Vec::new();
862        let mut n = 0;
863        // Where the glyphs are, not where they would be unscrolled: a
864        // screen reader's character rects have to land on the pixels
865        // emission drew (a scrolled field, F41).
866        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
867        s.editor.with_buffer(|b| {
868            crate::access::runs_of_buffer(
869                b,
870                crate::access::RunSource {
871                    key: node,
872                    line: 0,
873                    byte_base: 0,
874                    origin,
875                    scale,
876                    newline_after_last: false,
877                },
878                &mut n,
879                &mut out,
880            )
881        });
882        out
883    }
884
885    /// Moves the caret to `focus` and the selection's other end to
886    /// `anchor`, both (line, byte index) pairs clamped into the text; equal
887    /// pairs clear the selection. What a screen reader's "select from here
888    /// to there" becomes.
889    pub fn set_selection(&mut self, key: Key, anchor: (usize, usize), focus: (usize, usize)) {
890        let Some(s) = self.states.get_mut(&key) else {
891            return;
892        };
893        s.abandon_preedit();
894        let clamp = |b: &Buffer, (line, index): (usize, usize)| {
895            let line = line.min(b.lines.len().saturating_sub(1));
896            let text = b.lines[line].text();
897            let mut index = index.min(text.len());
898            while !text.is_char_boundary(index) {
899                index -= 1;
900            }
901            Cursor::new(line, index)
902        };
903        let (a, f) = s
904            .editor
905            .with_buffer(|b| (clamp(b, anchor), clamp(b, focus)));
906        s.editor.set_cursor(f);
907        s.editor.set_selection(if a == f {
908            Selection::None
909        } else {
910            Selection::Normal(a)
911        });
912        s.break_coalesce();
913        self.touch_caret(key);
914    }
915
916    /// Types `text` over the selection (or at the caret), as one undo
917    /// step; true when the content changed.
918    pub fn replace_selection(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
919        let Some(s) = self.states.get_mut(&key) else {
920            return false;
921        };
922        s.abandon_preedit();
923        if text.is_empty() && s.editor.selection_bounds().is_none() {
924            return false;
925        }
926        s.insert_recorded(text);
927        s.editor.shape_as_needed(fs, false);
928        s.version += 1;
929        s.invalidate_measurements();
930        self.touch_caret(key);
931        true
932    }
933
934    pub fn copy_selection(&self, key: Key) -> Option<String> {
935        self.states.get(&key)?.editor.copy_selection()
936    }
937
938    /// Whether the editor `key` has a non-empty selection — what
939    /// `copy_selection` would answer, without building the string. A
940    /// menu asking whether Copy applies asks this every time it opens.
941    pub fn has_selection(&self, key: Key) -> bool {
942        self.states
943            .get(&key)
944            .and_then(|s| s.editor.selection_bounds())
945            .is_some_and(|(a, b)| a != b)
946    }
947
948    /// Whether the editor `key` has an edit to undo, and one to redo.
949    pub fn history(&self, key: Key) -> (bool, bool) {
950        self.states
951            .get(&key)
952            .map_or((false, false), |s| (!s.undo.is_empty(), !s.redo.is_empty()))
953    }
954
955    /// Deletes the selection; returns true if anything was deleted.
956    pub fn delete_selection(&mut self, key: Key, fs: &mut FontSystem) -> bool {
957        let Some(s) = self.states.get_mut(&key) else {
958            return false;
959        };
960        let _ = fs;
961        s.abandon_preedit();
962        if s.delete_selection_recorded() {
963            s.version += 1;
964            s.invalidate_measurements();
965            self.touch_caret(key);
966            true
967        } else {
968            false
969        }
970    }
971
972    // -- Input application (focused editor). Returns true if content changed.
973
974    pub(crate) fn apply_text(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
975        let Some(s) = self.states.get_mut(&key) else {
976            return false;
977        };
978        // A commit ends the composition (winit also clears preedit first);
979        // the committed text goes in where the composition began.
980        s.abandon_preedit();
981        let filtered: String = text
982            .chars()
983            .filter(|c| !c.is_control() || (*c == '\n' && s.multiline) || *c == '\t')
984            .collect();
985        if filtered.is_empty() {
986            return false;
987        }
988        s.insert_recorded(&filtered);
989        s.editor.shape_as_needed(fs, false);
990        s.version += 1;
991        s.invalidate_measurements();
992        self.touch_caret(key);
993        true
994    }
995
996    /// Returns (content_changed, submit) — submit is Enter in single-line.
997    pub(crate) fn apply_key(
998        &mut self,
999        key: Key,
1000        ek: EditKey,
1001        mods: Mods,
1002        fs: &mut FontSystem,
1003    ) -> (bool, bool) {
1004        let Some(s) = self.states.get_mut(&key) else {
1005            return (false, false);
1006        };
1007        // Keys reaching the editor mid-composition mean the IME let them
1008        // through; act on committed text only.
1009        let mut changed = s.abandon_preedit();
1010        let mut submit = false;
1011        match ek {
1012            EditKey::Left
1013            | EditKey::Right
1014            | EditKey::Up
1015            | EditKey::Down
1016            | EditKey::Home
1017            | EditKey::End
1018            | EditKey::PageUp
1019            | EditKey::PageDown => {
1020                let motion = match (ek, mods.word, mods.doc) {
1021                    (EditKey::Left, true, _) => Motion::LeftWord,
1022                    (EditKey::Right, true, _) => Motion::RightWord,
1023                    (EditKey::Left, _, true) => Motion::Home,
1024                    (EditKey::Right, _, true) => Motion::End,
1025                    (EditKey::Up, _, true) => Motion::BufferStart,
1026                    (EditKey::Down, _, true) => Motion::BufferEnd,
1027                    (EditKey::Left, ..) => Motion::Left,
1028                    (EditKey::Right, ..) => Motion::Right,
1029                    (EditKey::Up, ..) => Motion::Up,
1030                    (EditKey::Down, ..) => Motion::Down,
1031                    (EditKey::Home, _, true) => Motion::BufferStart,
1032                    (EditKey::End, _, true) => Motion::BufferEnd,
1033                    (EditKey::Home, ..) => Motion::Home,
1034                    (EditKey::End, ..) => Motion::End,
1035                    (EditKey::PageUp, ..) => Motion::PageUp,
1036                    (EditKey::PageDown, ..) => Motion::PageDown,
1037                    _ => unreachable!(),
1038                };
1039                if mods.shift {
1040                    if s.editor.selection() == Selection::None {
1041                        s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1042                    }
1043                } else {
1044                    s.editor.set_selection(Selection::None);
1045                }
1046                s.editor.action(fs, Action::Motion(motion));
1047                s.break_coalesce();
1048            }
1049            EditKey::Backspace => {
1050                changed |= s.delete_selection_recorded()
1051                    || s.delete_motion_recorded(
1052                        if mods.word {
1053                            Motion::LeftWord
1054                        } else {
1055                            Motion::Left
1056                        },
1057                        Coalesce::Backspace,
1058                        fs,
1059                    );
1060            }
1061            EditKey::Delete => {
1062                changed |= s.delete_selection_recorded()
1063                    || s.delete_motion_recorded(
1064                        if mods.word {
1065                            Motion::RightWord
1066                        } else {
1067                            Motion::Right
1068                        },
1069                        Coalesce::Delete,
1070                        fs,
1071                    );
1072            }
1073            EditKey::Enter => {
1074                if s.multiline {
1075                    s.insert_recorded("\n");
1076                    changed = true;
1077                } else {
1078                    submit = true;
1079                }
1080            }
1081            EditKey::Tab => {
1082                if s.multiline {
1083                    s.insert_recorded("    ");
1084                    changed = true;
1085                }
1086            }
1087            EditKey::Undo => changed |= s.undo_one(),
1088            EditKey::Redo => changed |= s.redo_one(),
1089            EditKey::SelectAll => {
1090                s.editor.action(fs, Action::Motion(Motion::BufferStart));
1091                s.editor.set_selection(Selection::Normal(s.editor.cursor()));
1092                s.editor.action(fs, Action::Motion(Motion::BufferEnd));
1093                s.break_coalesce();
1094            }
1095            EditKey::Escape => {
1096                s.editor.action(fs, Action::Escape);
1097                s.break_coalesce();
1098            }
1099        }
1100        s.editor.shape_as_needed(fs, false);
1101        if changed {
1102            s.version += 1;
1103            s.invalidate_measurements();
1104        }
1105        self.touch_caret(key);
1106        (changed, submit)
1107    }
1108
1109    /// Mouse press inside the edit at content-local logical position.
1110    /// `clicks` is the driver-counted multi-click: 2 selects the word,
1111    /// 3 the line (cosmic-text's double/triple click actions). With
1112    /// `extend` — a Shift-press — the caret moves there keeping the
1113    /// selection's anchor, seeding one at the caret when there is none
1114    /// (cosmic-text's `Drag` does both), and the
1115    /// click count says nothing. Either way it is a click: a live
1116    /// composition is abandoned and the next edit starts an undo unit.
1117    pub(crate) fn click(
1118        &mut self,
1119        key: Key,
1120        local: Vec2,
1121        clicks: u8,
1122        extend: bool,
1123        fs: &mut FontSystem,
1124    ) {
1125        if let Some(s) = self.states.get_mut(&key) {
1126            if s.abandon_preedit() {
1127                s.version += 1;
1128                s.invalidate_measurements();
1129            }
1130            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1131            let action = match (extend, clicks) {
1132                (true, _) => Action::Drag { x, y },
1133                (false, 0 | 1) => Action::Click { x, y },
1134                (false, 2) => Action::DoubleClick { x, y },
1135                (false, _) => Action::TripleClick { x, y },
1136            };
1137            s.editor.action(fs, action);
1138            s.editor.shape_as_needed(fs, false);
1139            s.break_coalesce();
1140            self.caret_stamp += 1;
1141        }
1142    }
1143
1144    /// Mouse motion with the button held: moves the caret to the point
1145    /// and keeps the selection's anchor (seeding one at the caret when
1146    /// there is none — cosmic-text's `Drag` does both). The caret moved,
1147    /// so it is marked like a keyboard motion's: a field scrolls its own
1148    /// text toward the drag at once, and a scroller above a document
1149    /// reveals the caret on the release — not under the held pointer,
1150    /// where the drag's own rate is what moves it
1151    /// (`scroll_caret_into_view`).
1152    pub(crate) fn drag(&mut self, key: Key, local: Vec2, fs: &mut FontSystem) {
1153        if let Some(s) = self.states.get_mut(&key) {
1154            let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
1155            let before = s.editor.cursor();
1156            s.editor.action(fs, Action::Drag { x, y });
1157            if s.editor.cursor() != before {
1158                self.touch_caret(key);
1159            } else {
1160                self.caret_stamp += 1;
1161            }
1162        }
1163    }
1164
1165    /// Replaces the focused editor's IME composition. The text lives in
1166    /// the buffer from the moment it appears — following text shifts and
1167    /// the paragraph rewraps — but never in the undo history; the caret
1168    /// sits at the IME-reported offset inside it. Empty text cancels
1169    /// (winit sends that before every commit). Returns true if anything
1170    /// visible changed.
1171    pub(crate) fn set_preedit(
1172        &mut self,
1173        key: Key,
1174        text: &str,
1175        cursor: Option<(usize, usize)>,
1176        fs: &mut FontSystem,
1177    ) -> bool {
1178        let Some(s) = self.states.get_mut(&key) else {
1179            return false;
1180        };
1181        if text.is_empty() {
1182            if !s.abandon_preedit() {
1183                return false;
1184            }
1185        } else {
1186            let start = match s.preedit.take() {
1187                Some(pre) => {
1188                    s.splice(pre.start, &pre.text, text);
1189                    pre.start
1190                }
1191                None => {
1192                    // Composing over a selection replaces it, as typing
1193                    // would — that part is a real, recorded edit.
1194                    s.delete_selection_recorded();
1195                    let at = s.editor.cursor();
1196                    s.editor.insert_string(text, None);
1197                    at
1198                }
1199            };
1200            // Caret at the IME's offset inside the composition (its end
1201            // when unreported), clamped to a char boundary.
1202            let mut at = cursor.map_or(text.len(), |(c, _)| c.min(text.len()));
1203            while at > 0 && !text.is_char_boundary(at) {
1204                at -= 1;
1205            }
1206            s.editor.set_cursor(end_cursor(start, &text[..at]));
1207            s.editor.set_selection(Selection::None);
1208            s.preedit = Some(Preedit {
1209                start,
1210                text: text.to_string(),
1211            });
1212            s.break_coalesce();
1213        }
1214        s.editor.shape_as_needed(fs, false);
1215        s.version += 1;
1216        s.invalidate_measurements();
1217        self.touch_caret(key);
1218        true
1219    }
1220
1221    /// Drops the selection of one editor, leaving the caret where the
1222    /// selection's live end was. What a selection started elsewhere in
1223    /// the window calls, so no window ever shows two selections.
1224    pub(crate) fn collapse_selection(&mut self, key: Key) -> bool {
1225        let Some(s) = self.states.get_mut(&key) else {
1226            return false;
1227        };
1228        if matches!(s.editor.selection(), Selection::None) {
1229            return false;
1230        }
1231        s.editor.set_selection(Selection::None);
1232        self.caret_stamp += 1;
1233        true
1234    }
1235
1236    /// The active composition text, if any (for tests and hosts).
1237    pub fn preedit(&self, key: Key) -> Option<&str> {
1238        self.states
1239            .get(&key)?
1240            .preedit
1241            .as_ref()
1242            .map(|p| p.text.as_str())
1243    }
1244
1245    pub fn is_multiline(&self, key: Key) -> bool {
1246        self.states.get(&key).is_some_and(|s| s.multiline)
1247    }
1248
1249    /// Whether `key` lays its text out to its box's width: a document, or
1250    /// a field with `wrap` declared. What emission asks
1251    /// before narrowing a field's clip — an editor that folds never
1252    /// scrolls, so it keeps the node's.
1253    pub(crate) fn folds(&self, key: Key) -> bool {
1254        self.states.get(&key).is_some_and(|s| s.folds)
1255    }
1256
1257    /// Caret rect in physical px, relative to the edit's content origin.
1258    /// None when the caret isn't laid out (e.g. no state for `key`).
1259    pub(crate) fn caret_rect(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
1260        let s = self.states.get_mut(&key)?;
1261        s.editor.shape_as_needed(fs, false);
1262        let (x, y) = s.editor.cursor_position()?;
1263        let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
1264        Some(Rect::new(
1265            x as f32 - s.offset_x,
1266            y as f32,
1267            (2.0 * s.scale).max(2.0),
1268            line_height,
1269        ))
1270    }
1271
1272    /// How far a single-line field's text is scrolled left, in physical
1273    /// px, for a content box `inner_w` wide (physical too). 0 for an
1274    /// editor that folds — a document, or a field with `wrap` — which
1275    /// wraps instead.
1276    ///
1277    /// A field does not wrap ([`EditStore::wrapped`]), so a value that
1278    /// outgrows its box is moved under the caret rather than folded onto a
1279    /// second line — which is what a native field does, and what an app
1280    /// otherwise has to fake by declaring the box wider than the text it
1281    /// is about to hold. Recomputed where the box is known,
1282    /// so a field that grows or shrinks between frames re-anchors with it.
1283    pub(crate) fn line_offset(&mut self, key: Key, inner_w: f32, fs: &mut FontSystem) -> f32 {
1284        let focused = self.focused == Some(key);
1285        let Some(s) = self.states.get_mut(&key) else {
1286            return 0.0;
1287        };
1288        if s.folds {
1289            return 0.0;
1290        }
1291        // Unfocused, a field shows its value from the start: what it says
1292        // is what a reader wants, not where its caret was left.
1293        if !focused {
1294            s.offset_x = 0.0;
1295            return 0.0;
1296        }
1297        s.editor.shape_as_needed(fs, false);
1298        let caret_w = (2.0 * s.scale).max(2.0);
1299        let text_w = s
1300            .editor
1301            .with_buffer(|b| b.layout_runs().map(|r| r.line_w).fold(0.0f32, f32::max));
1302        if let Some((x, _)) = s.editor.cursor_position() {
1303            let x = x as f32;
1304            // Two ends, one rule: keep the caret inside the box, moving
1305            // the text by the least that does it.
1306            if x - s.offset_x > inner_w - caret_w {
1307                s.offset_x = x - inner_w + caret_w;
1308            }
1309            if x < s.offset_x {
1310                s.offset_x = x;
1311            }
1312        }
1313        // Never past the end of the text (a field that shrank, or one
1314        // whose value was replaced by a shorter one, scrolls back).
1315        s.offset_x = s.offset_x.clamp(0.0, (text_w + caret_w - inner_w).max(0.0));
1316        s.offset_x
1317    }
1318
1319    // -- Layout measurement (logical units)
1320
1321    /// What the text wants on its own: the width a `Fit` editor takes, and
1322    /// the floor under a `Min::FIT` one.
1323    ///
1324    /// Measured with the wrap taken *off*, and cached against the text and
1325    /// its metrics rather than read off the buffer as it stands. The
1326    /// buffer is still carrying whatever width `wrapped` last set on it,
1327    /// and a fit width measured under that is a width that feeds back on
1328    /// itself: the box takes the widest wrapped line, the next frame wraps
1329    /// to that, and a field declared to hug its text ratchets down to one
1330    /// character with every keystroke on its own line.
1331    pub(crate) fn intrinsic(&mut self, key: Key, fs: &mut FontSystem) -> Size {
1332        let Some(s) = self.states.get_mut(&key) else {
1333            return Size::ZERO;
1334        };
1335        if let Some((v, m, size)) = s.natural
1336            && (v, m) == (s.version, s.metrics_rev)
1337        {
1338            return size;
1339        }
1340        // Taken off and left off: `wrapped` runs after this in the same
1341        // pass, and putting the frame's width back is its job. Off
1342        // unconditionally rather than when `wrap` says it is on — a style
1343        // change clears that flag without touching the buffer, and
1344        // measuring the wrapped buffer is the whole bug.
1345        s.editor.with_buffer_mut(|b| b.set_size(None, None));
1346        s.wrap = None;
1347        s.editor.shape_as_needed(fs, false);
1348        // The same measurement a text node takes of itself, except that an
1349        // empty editor is still one line tall.
1350        let (w, h) = s.editor.with_buffer(|b| {
1351            let (size, lines) = crate::text::measure_buffer(b, 0);
1352            (size.w, lines.max(1) as f32 * b.metrics().line_height)
1353        });
1354        // Caret margin so the cursor at line end isn't clipped.
1355        let size = Size::new((w + 2.0 * s.scale) / s.scale, h / s.scale);
1356        s.natural = Some((s.version, s.metrics_rev, size));
1357        size
1358    }
1359
1360    /// The editor's height at its final content width — and, for a
1361    /// single-line field, the width it is *not* wrapped to.
1362    ///
1363    /// `multiline: false` is a field, not a short document: it lays out on
1364    /// one line whatever it is given and scrolls that line under the caret
1365    /// (see [`EditStore::line_offset`]), which is what a native field does
1366    /// and what the `<edit>` row has always said it is. Wrapping one was
1367    /// how a name that outgrew its box came to be drawn two lines tall
1368    /// inside a box measured for one. The exception is a
1369    /// field that asked to fold (`EditOptions::wrap`): it
1370    /// wraps to its width exactly as a document does, and keeps a field's
1371    /// keyboard.
1372    /// The first line's baseline of editor `key` as `wrapped` last laid it
1373    /// out, logical px below the top of its text: what a
1374    /// field beside its label lines up by. An empty editor is one line of
1375    /// its own metrics. `NaN` for a key no editor holds.
1376    pub(crate) fn baseline(&self, key: Key) -> f32 {
1377        let Some(s) = self.states.get(&key) else {
1378            return f32::NAN;
1379        };
1380        let b = s.editor.with_buffer(|b| {
1381            b.layout_runs()
1382                .next()
1383                .map_or(b.metrics().line_height * 0.8, |r| r.line_y.round())
1384        });
1385        b / s.scale
1386    }
1387
1388    pub(crate) fn wrapped(&mut self, key: Key, max_w: f32, fs: &mut FontSystem) -> Size {
1389        let Some(s) = self.states.get_mut(&key) else {
1390            return Size::ZERO;
1391        };
1392        if !s.folds {
1393            // The buffer stays unwrapped (an editor that folded last frame
1394            // may be carrying a width), and the box is one line tall
1395            // whatever the text has in it — a `\n` that reached a field
1396            // through `set_text` does not make it two.
1397            if s.wrap.is_some() {
1398                s.editor.with_buffer_mut(|b| b.set_size(None, None));
1399                s.wrap = None;
1400                s.invalidate_measurements();
1401            }
1402            s.editor.shape_as_needed(fs, false);
1403            let line = s.editor.with_buffer(|b| b.metrics().line_height);
1404            return Size::new(max_w, line / s.scale);
1405        }
1406        let target = (max_w * s.scale).max(1.0);
1407        if crate::text::wrap_differs(s.wrap, Some(target)) {
1408            s.editor.with_buffer_mut(|b| b.set_size(Some(target), None));
1409            s.wrap = Some(target);
1410        }
1411        let stamp = (s.version, s.metrics_rev, target.to_bits());
1412        if let Some((v, m, w, size)) = s.measured
1413            && (v, m, w) == stamp
1414        {
1415            return size;
1416        }
1417        s.editor.shape_as_needed(fs, false);
1418        let h = s.editor.with_buffer(|b| {
1419            let (_, lines) = crate::text::measure_buffer(b, 0);
1420            lines.max(1) as f32 * b.metrics().line_height
1421        });
1422        let size = Size::new(max_w, h / s.scale);
1423        s.measured = Some((stamp.0, stamp.1, stamp.2, size));
1424        size
1425    }
1426
1427    // -- Emission
1428
1429    /// Draws selection, glyphs, and caret. `origin` is the content box origin
1430    /// in physical px; everything emitted is clipped by `clip`.
1431    #[allow(clippy::too_many_arguments)]
1432    pub(crate) fn emit(
1433        &mut self,
1434        key: Key,
1435        origin: Vec2,
1436        focused: bool,
1437        clip: Clip,
1438        clip_id: ClipId,
1439        fs: &mut FontSystem,
1440        text_system: &mut TextSystem,
1441        atlas: &mut crate::atlas::GlyphAtlas,
1442        out: &mut Vec<Quad>,
1443    ) {
1444        let blink_visible = self.blink_visible;
1445        let Some(s) = self.states.get_mut(&key) else {
1446            return;
1447        };
1448        let raster = text_system.raster_mut();
1449        s.editor.shape_as_needed(fs, false);
1450        let color = s.style.color_or_default();
1451        let accent = s.accent;
1452        let scale = s.scale;
1453        let selection = s.editor.selection_bounds();
1454        // The composition range, marked like a selection but drawn as a
1455        // tint plus underline; the caret stays solid while composing so
1456        // the IME's offset inside the text is never hidden by a blink.
1457        let composing = focused && s.preedit.is_some();
1458        let preedit = s
1459            .preedit
1460            .as_ref()
1461            .map(|p| (p.start, end_cursor(p.start, &p.text)));
1462        let cursor_pos = if focused && (blink_visible || composing) {
1463            s.editor.cursor_position()
1464        } else {
1465            None
1466        };
1467        // A single-line field scrolls its text under the caret; the box it
1468        // scrolls inside is the clip emission was handed.
1469        let origin = Vec2::new(origin.x - s.offset_x, origin.y);
1470
1471        s.editor.with_buffer(|b| {
1472            let line_height = b.metrics().line_height;
1473            // Runs come in line order: skip everything above the clip and
1474            // stop at the first run past its bottom — a 100k-line document
1475            // emits only the visible screenful of quads.
1476            let runs = b
1477                .layout_runs()
1478                .filter(|run| origin.y + run.line_top + line_height >= clip.rect.y)
1479                .take_while(|run| origin.y + run.line_top <= clip.rect.y + clip.rect.h);
1480            for run in runs {
1481                // Selection highlight for this run (mixed BiDi runs can
1482                // yield several disjoint spans). `highlight` is only valid
1483                // for runs on lines inside the selection span — outside it
1484                // marks the whole run selected.
1485                if let Some((start, end)) = selection
1486                    && run.line_i >= start.line
1487                    && run.line_i <= end.line
1488                {
1489                    let mut any = false;
1490                    for (x, w) in run.highlight(start, end) {
1491                        any = true;
1492                        out.push(Quad {
1493                            rect: Rect::new(
1494                                origin.x + x,
1495                                origin.y + run.line_top,
1496                                w.max(2.0),
1497                                line_height,
1498                            ),
1499                            color: accent,
1500                            border_color: Color::TRANSPARENT,
1501                            radius: [0.0; 4],
1502                            border_w: 0.0,
1503                            blur: 0.0,
1504                            kind: QuadKind::Solid,
1505                            uv: [0; 4],
1506                            clip: clip_id,
1507                        });
1508                    }
1509                    // Empty line inside the selection: a stub for the
1510                    // selected newline keeps the highlight continuous.
1511                    if !any && run.glyphs.is_empty() && end.line > run.line_i {
1512                        out.push(Quad {
1513                            rect: Rect::new(origin.x, origin.y + run.line_top, 2.0, line_height),
1514                            color: accent,
1515                            border_color: Color::TRANSPARENT,
1516                            radius: [0.0; 4],
1517                            border_w: 0.0,
1518                            blur: 0.0,
1519                            kind: QuadKind::Solid,
1520                            uv: [0; 4],
1521                            clip: clip_id,
1522                        });
1523                    }
1524                }
1525                // Composition backdrop + underline under its glyphs.
1526                if let Some((ps, pe)) = preedit
1527                    && run.line_i >= ps.line
1528                    && run.line_i <= pe.line
1529                {
1530                    let underline_h = scale.max(1.0);
1531                    for (x, w) in run.highlight(ps, pe) {
1532                        let solid = |rect: Rect, color: Color| Quad {
1533                            rect,
1534                            color,
1535                            border_color: Color::TRANSPARENT,
1536                            radius: [0.0; 4],
1537                            border_w: 0.0,
1538                            blur: 0.0,
1539                            kind: QuadKind::Solid,
1540                            uv: [0; 4],
1541                            clip: clip_id,
1542                        };
1543                        out.push(solid(
1544                            Rect::new(origin.x + x, origin.y + run.line_top, w, line_height),
1545                            Color { a: 0.3, ..accent },
1546                        ));
1547                        out.push(solid(
1548                            Rect::new(
1549                                origin.x + x,
1550                                origin.y + run.line_top + line_height - underline_h,
1551                                w,
1552                                underline_h,
1553                            ),
1554                            color,
1555                        ));
1556                    }
1557                }
1558                // Glyphs.
1559                for glyph in run.glyphs.iter() {
1560                    let physical = glyph.physical((0.0, 0.0), 1.0);
1561                    let Some(slot) =
1562                        crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
1563                    else {
1564                        continue;
1565                    };
1566                    let x = origin.x + physical.x as f32 + slot.left as f32;
1567                    let y = origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32;
1568                    let glyph_color = glyph
1569                        .color_opt
1570                        .map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a()))
1571                        .unwrap_or(color);
1572                    out.push(Quad {
1573                        rect: Rect::new(x, y, slot.w as f32, slot.h as f32),
1574                        color: glyph_color,
1575                        border_color: Color::TRANSPARENT,
1576                        radius: [0.0; 4],
1577                        border_w: 0.0,
1578                        blur: 0.0,
1579                        kind: crate::text::glyph_kind(&slot),
1580                        uv: [slot.x, slot.y, slot.w, slot.h],
1581                        clip: clip_id,
1582                    });
1583                }
1584            }
1585            // Caret.
1586            if let Some((cx, cy)) = cursor_pos {
1587                out.push(Quad {
1588                    rect: Rect::new(
1589                        origin.x + cx as f32,
1590                        origin.y + cy as f32,
1591                        (2.0 * scale).max(2.0),
1592                        line_height,
1593                    ),
1594                    color,
1595                    border_color: Color::TRANSPARENT,
1596                    radius: [0.0; 4],
1597                    border_w: 0.0,
1598                    blur: 0.0,
1599                    kind: QuadKind::Solid,
1600                    uv: [0; 4],
1601                    clip: clip_id,
1602                });
1603            }
1604        });
1605    }
1606}
1607
1608fn buffer_text(b: &Buffer) -> String {
1609    let mut out = String::new();
1610    for (i, line) in b.lines.iter().enumerate() {
1611        if i > 0 {
1612            out.push('\n');
1613        }
1614        out.push_str(line.text());
1615    }
1616    out
1617}