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