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