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