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