Skip to main content

kui_core/
edit.rs

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