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