Skip to main content

denise_ui/widgets/
text_area.rs

1//! A multi-line text editor that edits through a document it does not own.
2//!
3//! # Why the widget does not hold the text
4//!
5//! A [`TextInput`](super::TextInput) owns a `String`, and for a name or a
6//! setpoint that is right. An editor cannot: the file it is showing may be
7//! gigabytes, and a widget that held it would have decided, on behalf of every
8//! application, that a file is loaded before it is shown. So `TextArea` asks a
9//! [`TextDocument`] for the lines it is about to draw and nothing else, and
10//! tells it where to insert and what to delete. The one that ships,
11//! [`TextBuffer`], is a `Vec<String>` and is what a form gets; an application
12//! with a file too big for that implements the trait over whatever it has.
13//!
14//! The document's line count is an `Option`, which is the one place a big
15//! file's shape leaks into the trait: a document still counting its lines
16//! answers `None`, the widget numbers and scrolls the lines it has been told
17//! are known, and the total arrives when it arrives.
18//!
19//! # What it does not do
20//!
21//! No wrapping: a line is as wide as it is, and the view scrolls sideways to
22//! follow the caret — as far as the widest line the widget has drawn and a
23//! caret past its end, and no further. A document that is not all in memory
24//! cannot say how wide the file is, only how wide what has been read is, so
25//! that is what the sideways scroll and the bar along the bottom are measured
26//! against. No clipboard of its own — this crate has no system to ask — so
27//! copy, cut and paste are *messages*: the widget hands the application the
28//! text it copied, or asks for the text to paste, and the application answers
29//! through [`TextArea::insert_text`]. No Tab: the tree owns Tab for focus
30//! stepping, and an editor that took it would trap the keyboard.
31
32use alloc::borrow::Cow;
33use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35use core::cell::{Cell, RefCell, RefMut};
36use core::ops::Range;
37
38use denise::Pen;
39use denise::{
40    Color, ElementState, InputEvent, KeyCode, Modifiers, Point, PointerButton, Rect, Role,
41};
42use denise_text::{TextEngine, TextStyle};
43
44use crate::motion::Wake;
45use crate::widget::{
46    Animation, Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
47};
48use crate::widgets::describe::{
49    Describe, DynDescribe, Group, Mismatch, Property, PropertyKind, Value,
50};
51use crate::widgets::style::{CARET_BLINKS_FOR_MS, DOUBLE_CLICK_MS, muted};
52
53/// Half-period of the caret blink, in milliseconds.
54const BLINK_MS: u64 = 500;
55
56/// A place in a document: a line, and a byte offset into it.
57///
58/// Bytes rather than characters, because that is what a `&str` is sliced by,
59/// and a column that counted characters would cost a walk of the line to use.
60/// Every position the widget makes lands on a character boundary.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct Pos {
63    /// 0-based line.
64    pub line: usize,
65    /// Byte offset into the line.
66    pub col: usize,
67}
68
69impl Pos {
70    /// The start of the document.
71    pub const ZERO: Self = Self { line: 0, col: 0 };
72
73    /// A position.
74    #[must_use]
75    pub const fn new(line: usize, col: usize) -> Self {
76        Self { line, col }
77    }
78}
79
80/// A coloured run of one line, for syntax highlighting.
81///
82/// A colour rather than a theme role, because a highlighting scheme has its
83/// own palette and the document is the one that knows it.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct Span {
86    /// Byte offset the run starts at.
87    pub start: usize,
88    /// Byte offset it ends at, exclusive.
89    pub end: usize,
90    /// What to draw it in.
91    pub color: Color,
92}
93
94/// What a [`TextArea`] edits.
95///
96/// Every method takes `&mut self`, because a document over a file may have
97/// to read to answer and may cache what it read. The widget keeps the document
98/// in a [`RefCell`] so painting, which is `&self`, can ask too.
99pub trait TextDocument: 'static {
100    /// How many lines there are, or `None` while that is still being counted.
101    ///
102    /// A document always has at least one line: a trailing newline ends a
103    /// line and starts an empty last one.
104    fn line_count(&mut self) -> Option<usize>;
105
106    /// Lines the document can hand over now. Equal to [`line_count`] once it
107    /// is known, and growing until then.
108    ///
109    /// [`line_count`]: TextDocument::line_count
110    fn known_lines(&mut self) -> usize;
111
112    /// Line `n` without its newline, or `None` if it is not (yet) known.
113    fn line(&mut self, n: usize) -> Option<Cow<'_, str>>;
114
115    /// Inserts `text`, which may hold newlines, at `at`.
116    fn insert(&mut self, at: Pos, text: &str);
117
118    /// Deletes `[from, to)`. `from <= to`; a range across lines joins them.
119    fn delete(&mut self, from: Pos, to: Pos);
120
121    /// Takes back the last edit. `false` if there was none.
122    fn undo(&mut self) -> bool {
123        false
124    }
125
126    /// Redoes the last edit undone. `false` if there was none.
127    fn redo(&mut self) -> bool {
128        false
129    }
130
131    /// The coloured runs of line `n`, appended to `out` in ascending order
132    /// without overlaps. Text between runs is drawn in the theme's content
133    /// colour. The default highlights nothing.
134    fn spans(&mut self, n: usize, out: &mut Vec<Span>) {
135        let _ = (n, out);
136    }
137
138    /// Byte ranges of line `n` to mark behind the text — every match of a
139    /// search, say — appended to `out` in ascending order without overlaps.
140    /// `line` is the line's text as the widget already has it, so marking
141    /// costs no second read. A range out of order, past the end or not on a
142    /// character boundary is skipped; the selection is drawn over the marks.
143    /// The default marks nothing.
144    fn highlights(&mut self, n: usize, line: &str, out: &mut Vec<Range<usize>>) {
145        let _ = (n, line, out);
146    }
147}
148
149/// Where `text` inserted at `at` ends.
150#[must_use]
151pub fn end_of(at: Pos, text: &str) -> Pos {
152    match text.rfind('\n') {
153        None => Pos::new(at.line, at.col + text.len()),
154        Some(last) => {
155            let newlines = text.bytes().filter(|&b| b == b'\n').count();
156            Pos::new(at.line + newlines, text.len() - last - 1)
157        }
158    }
159}
160
161/// One edit, kept so it can be taken back.
162#[derive(Clone, Debug)]
163struct Edit {
164    at: Pos,
165    text: String,
166    inserted: bool,
167}
168
169/// A [`TextDocument`] held in memory as a line per `String`.
170///
171/// What a form gets, and what an application with an ordinary amount of text
172/// wants. Undo is a journal of the edits, so it costs what was typed rather
173/// than a copy of the text per keystroke; consecutive typing on one line is
174/// one entry.
175#[derive(Clone, Debug)]
176pub struct TextBuffer {
177    lines: Vec<String>,
178    undo: Vec<Edit>,
179    redo: Vec<Edit>,
180}
181
182impl TextBuffer {
183    /// An empty buffer: one empty line.
184    #[must_use]
185    pub fn new() -> Self {
186        Self::from_text("")
187    }
188
189    /// A buffer holding `text`.
190    #[must_use]
191    pub fn from_text(text: &str) -> Self {
192        Self {
193            lines: text.split('\n').map(String::from).collect(),
194            undo: Vec::new(),
195            redo: Vec::new(),
196        }
197    }
198
199    /// The whole text, lines joined by `\n`.
200    #[must_use]
201    pub fn text(&self) -> String {
202        self.lines.join("\n")
203    }
204
205    /// The lines.
206    #[must_use]
207    pub fn lines(&self) -> &[String] {
208        &self.lines
209    }
210
211    /// Inserts without recording, and returns where the text ends.
212    fn splice_in(&mut self, at: Pos, text: &str) -> Pos {
213        let line = &mut self.lines[at.line];
214        let tail = line.split_off(at.col);
215        let mut parts = text.split('\n');
216        line.push_str(parts.next().unwrap_or(""));
217        let mut cursor = at.line;
218        for part in parts {
219            cursor += 1;
220            self.lines.insert(cursor, String::from(part));
221        }
222        let end = Pos::new(cursor, self.lines[cursor].len());
223        self.lines[cursor].push_str(&tail);
224        end
225    }
226
227    /// Deletes without recording, and returns what was there.
228    fn cut_out(&mut self, from: Pos, to: Pos) -> String {
229        if from.line == to.line {
230            return self.lines[from.line].drain(from.col..to.col).collect();
231        }
232        let mut out = self.lines[from.line].split_off(from.col);
233        let kept = self.lines[to.line].split_off(to.col);
234        for line in self.lines.drain(from.line + 1..=to.line) {
235            out.push('\n');
236            out.push_str(&line);
237        }
238        self.lines[from.line].push_str(&kept);
239        out
240    }
241}
242
243impl Default for TextBuffer {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl TextDocument for TextBuffer {
250    fn line_count(&mut self) -> Option<usize> {
251        Some(self.lines.len())
252    }
253
254    fn known_lines(&mut self) -> usize {
255        self.lines.len()
256    }
257
258    fn line(&mut self, n: usize) -> Option<Cow<'_, str>> {
259        self.lines.get(n).map(|l| Cow::Borrowed(l.as_str()))
260    }
261
262    fn insert(&mut self, at: Pos, text: &str) {
263        if text.is_empty() {
264            return;
265        }
266        self.splice_in(at, text);
267        self.redo.clear();
268        // Typing along one line extends the last entry rather than making one
269        // per key, so undo takes back a word rather than a letter.
270        if let Some(last) = self.undo.last_mut()
271            && last.inserted
272            && !last.text.contains('\n')
273            && !text.contains('\n')
274            && end_of(last.at, &last.text) == at
275        {
276            last.text.push_str(text);
277            return;
278        }
279        self.undo.push(Edit {
280            at,
281            text: text.to_string(),
282            inserted: true,
283        });
284    }
285
286    fn delete(&mut self, from: Pos, to: Pos) {
287        if from >= to {
288            return;
289        }
290        let text = self.cut_out(from, to);
291        self.redo.clear();
292        self.undo.push(Edit {
293            at: from,
294            text,
295            inserted: false,
296        });
297    }
298
299    fn undo(&mut self) -> bool {
300        let Some(edit) = self.undo.pop() else {
301            return false;
302        };
303        if edit.inserted {
304            self.cut_out(edit.at, end_of(edit.at, &edit.text));
305        } else {
306            self.splice_in(edit.at, &edit.text);
307        }
308        self.redo.push(edit);
309        true
310    }
311
312    fn redo(&mut self) -> bool {
313        let Some(edit) = self.redo.pop() else {
314            return false;
315        };
316        if edit.inserted {
317            self.splice_in(edit.at, &edit.text);
318        } else {
319            self.cut_out(edit.at, end_of(edit.at, &edit.text));
320        }
321        self.undo.push(edit);
322        true
323    }
324}
325
326/// What the widget asks of the application's clipboard.
327///
328/// The widget has no clipboard to reach: this crate runs on panels with no
329/// window system as well as desktops with one. So it says what it wants and
330/// the application, which knows what it is running on, does it.
331#[derive(Clone, Debug, PartialEq, Eq)]
332pub enum ClipboardRequest {
333    /// The selection, to put on the clipboard.
334    Copy(String),
335    /// The selection, already deleted from the document, to put on the
336    /// clipboard.
337    Cut(String),
338    /// The clipboard's text is wanted: answer with [`TextArea::insert_text`].
339    Paste,
340}
341
342/// Lines of text somebody edits, drawn from a [`TextDocument`].
343///
344/// # Blinking
345///
346/// As [`TextInput`](super::TextInput): the caret blinks only while the widget
347/// has focus, and an unfocused editor asks for no frames.
348///
349/// # Focus
350///
351/// No focus ring. The caret is the sign that the keyboard goes here, and a
352/// ring around a widget the size of a window would frame the whole window.
353///
354/// # Moving by word
355///
356/// Ctrl and an arrow move by word, and so does Option on a Mac; Command and an
357/// arrow go to the start or end of the line, and with Home or End to the start
358/// or end of the document. Shift extends with all of them.
359///
360/// # Selecting
361///
362/// A press places the caret, a second on the same spot takes the word under it,
363/// and a third takes the line — its text, not the newline after it, so deleting
364/// a line taken that way leaves an empty one rather than pulling the next one
365/// up. A fourth press starts the count again. Dragging extends from where the
366/// press landed, and Shift extends with the arrows, Home, End and a click. The
367/// word rule is [`TextInput`](super::TextInput)'s, shared so that a
368/// double-click takes the same run of characters in a field as in an editor.
369pub struct TextArea<M, D = TextBuffer> {
370    doc: RefCell<D>,
371    caret: Pos,
372    /// Where the selection started, if there is one. The selection is
373    /// `anchor..caret` in whichever order they fall.
374    anchor: Option<Pos>,
375    /// The x the caret is trying to keep while moving up and down, so a walk
376    /// through short lines comes back out at the column it went in at.
377    goal_x: Option<i32>,
378    /// First line drawn.
379    top: usize,
380    /// Pixels of the first line scrolled up out of view, less than a row.
381    /// Always 0 unless `smooth_scroll` is on, and 0 again whenever the view
382    /// moves by whole lines — a jump, a page, the thumb, the caret.
383    top_px: i32,
384    /// The wheel moves the text a pixel at a time rather than a line.
385    smooth_scroll: bool,
386    /// Pixels the text is scrolled sideways. A `Cell`, because a range
387    /// selected from outside an event — [`select_range`](Self::select_range)
388    /// — can only be measured, and so scrolled to, by the next paint.
389    scroll_x: Cell<i32>,
390    /// The widest line the widget has measured, in pixels: how far the text
391    /// scrolls sideways, and what the bar along the bottom is a share of.
392    /// Lines are measured as they are drawn, so this is the width of the text
393    /// that has been seen rather than of the file — which a document reading
394    /// a file line by line could not be asked for anyway. Only ever grows,
395    /// until the document, the font or the tab width is replaced.
396    content_w: Cell<i32>,
397    /// Paint is to scroll the caret's range into view sideways.
398    reveal_pending: Cell<bool>,
399    /// Wheel pixels not yet worth a whole line.
400    wheel_rest: i32,
401    style: TextStyle,
402    gutter: bool,
403    /// Columns from one tab stop to the next, a column being a space wide.
404    tab_width: u8,
405    read_only: bool,
406    on_change: Option<M>,
407    on_clipboard: Option<fn(ClipboardRequest) -> M>,
408    dragging: bool,
409    /// The scrollbar's thumb is held: the pointer's y when it was taken, and
410    /// the top line then.
411    thumb_drag: Option<(i32, usize)>,
412    /// The bottom scrollbar's thumb is held: the pointer's x when it was
413    /// taken, and the sideways scroll then.
414    h_thumb_drag: Option<(i32, i32)>,
415    /// Whole rows the last paint had room for, so a jump made from outside
416    /// an event — [`go_to`](Self::go_to) — can centre its line.
417    rows_seen: Cell<usize>,
418    /// Where the last press landed, when, and how many have stacked up on that
419    /// spot: one places the caret, two take the word, three take the line.
420    last_click: Option<(Pos, u64, u8)>,
421    blink_epoch: u64,
422    caret_on: bool,
423    has_focus: bool,
424}
425
426/// Where a scrollbar's thumb sits on a track `track` long, as
427/// `(offset, length)`: as long a share of the track as the `shown` part is of
428/// the `total`, never under `min`, and as far along as `at` is through the
429/// range of positions that keep the end in view.
430///
431/// Both bars use it: down the side `shown` and `total` are rows and lines,
432/// along the bottom they are pixels of width.
433fn thumb_span(track: i32, shown: usize, total: usize, at: usize, min: i32) -> (i32, i32) {
434    let total = total.max(1) as i64;
435    let shown = shown.max(1) as i64;
436    let len = ((track as i64 * shown / total) as i32)
437        .max(min)
438        .min(track.max(1));
439    let max_at = (total - shown).max(0);
440    let off = if max_at == 0 {
441        0
442    } else {
443        ((track - len) as i64 * (at as i64).min(max_at) / max_at) as i32
444    };
445    (off, len)
446}
447
448impl<M> TextArea<M, TextBuffer> {
449    /// An editor over a buffer holding `text`.
450    #[must_use]
451    pub fn from_text(text: &str) -> Self {
452        Self::new(TextBuffer::from_text(text))
453    }
454
455    /// The whole text of the buffer.
456    #[must_use]
457    pub fn text(&self) -> String {
458        self.doc.borrow().text()
459    }
460
461    /// Replaces the text, putting the caret at the start. Silent, and the
462    /// undo history goes with the old text.
463    pub fn set_text(&mut self, text: &str) {
464        *self.doc.get_mut() = TextBuffer::from_text(text);
465        self.caret = Pos::ZERO;
466        self.anchor = None;
467        self.top = 0;
468        self.top_px = 0;
469        self.scroll_x.set(0);
470        self.content_w.set(0);
471    }
472}
473
474impl<M> Default for TextArea<M, TextBuffer> {
475    fn default() -> Self {
476        Self::from_text("")
477    }
478}
479
480impl<M, D: TextDocument> TextArea<M, D> {
481    /// An editor over `doc`.
482    #[must_use]
483    pub fn new(doc: D) -> Self {
484        Self {
485            doc: RefCell::new(doc),
486            caret: Pos::ZERO,
487            anchor: None,
488            goal_x: None,
489            top: 0,
490            top_px: 0,
491            smooth_scroll: false,
492            scroll_x: Cell::new(0),
493            content_w: Cell::new(0),
494            reveal_pending: Cell::new(false),
495            wheel_rest: 0,
496            style: TextStyle::built_in(16),
497            gutter: true,
498            tab_width: 4,
499            read_only: false,
500            on_change: None,
501            on_clipboard: None,
502            dragging: false,
503            thumb_drag: None,
504            h_thumb_drag: None,
505            rows_seen: Cell::new(0),
506            last_click: None,
507            blink_epoch: 0,
508            caret_on: true,
509            has_focus: false,
510        }
511    }
512
513    /// Sets the message emitted after every edit a person makes.
514    #[must_use]
515    pub fn with_change(mut self, message: M) -> Self {
516        self.on_change = Some(message);
517        self
518    }
519
520    /// Sets how clipboard requests reach the application. Without it, copy,
521    /// cut and paste do nothing.
522    #[must_use]
523    pub fn with_clipboard(mut self, message: fn(ClipboardRequest) -> M) -> Self {
524        self.on_clipboard = Some(message);
525        self
526    }
527
528    /// Sets the font and size.
529    #[must_use]
530    pub fn with_style(mut self, style: TextStyle) -> Self {
531        self.style = style;
532        self
533    }
534
535    /// Sets the size, keeping the font.
536    #[must_use]
537    pub fn with_size(mut self, size_px: u16) -> Self {
538        self.style.size_px = size_px;
539        self
540    }
541
542    /// Whether to number the lines down the left.
543    #[must_use]
544    pub fn with_gutter(mut self, gutter: bool) -> Self {
545        self.gutter = gutter;
546        self
547    }
548
549    /// Sets how many columns apart the tab stops are; four unless told.
550    /// A column is a space wide, and at least one.
551    #[must_use]
552    pub fn with_tab_width(mut self, columns: u8) -> Self {
553        self.tab_width = columns.max(1);
554        self
555    }
556
557    /// Shows the text and places a caret in it, but changes nothing.
558    #[must_use]
559    pub fn with_read_only(mut self, read_only: bool) -> Self {
560        self.read_only = read_only;
561        self
562    }
563
564    /// Whether the wheel moves the text a pixel at a time, as a trackpad
565    /// reports it, rather than a whole line once a line's worth has built
566    /// up. Off unless told. A fast wheel goes as far either way.
567    #[must_use]
568    pub fn with_smooth_scroll(mut self, smooth: bool) -> Self {
569        self.set_smooth_scroll(smooth);
570        self
571    }
572
573    /// The document.
574    pub fn document(&self) -> core::cell::Ref<'_, D> {
575        self.doc.borrow()
576    }
577
578    /// The document, to change. Silent; the caret is clamped to whatever the
579    /// document says next time it is used.
580    pub fn document_mut(&mut self) -> &mut D {
581        self.doc.get_mut()
582    }
583
584    /// Replaces the document, putting the caret at the start.
585    pub fn set_document(&mut self, doc: D) {
586        *self.doc.get_mut() = doc;
587        self.caret = Pos::ZERO;
588        self.anchor = None;
589        self.top = 0;
590        self.top_px = 0;
591        self.scroll_x.set(0);
592        self.content_w.set(0);
593    }
594
595    /// The font and size.
596    #[inline]
597    pub const fn style(&self) -> TextStyle {
598        self.style
599    }
600
601    /// Replaces the font and size. The measured width of the text goes with
602    /// the old font, and is taken again as the lines are drawn.
603    pub fn set_style(&mut self, style: TextStyle) {
604        self.style = style;
605        self.top_px = 0;
606        self.content_w.set(0);
607    }
608
609    /// Whether the lines are numbered.
610    #[inline]
611    pub const fn gutter(&self) -> bool {
612        self.gutter
613    }
614
615    /// Numbers the lines, or stops.
616    pub fn set_gutter(&mut self, gutter: bool) {
617        self.gutter = gutter;
618    }
619
620    /// How many columns apart the tab stops are.
621    #[inline]
622    pub const fn tab_width(&self) -> u8 {
623        self.tab_width
624    }
625
626    /// Moves the tab stops `columns` apart, at least one. Tabs are part of
627    /// how wide a line is, so that is measured again as the lines are drawn.
628    pub fn set_tab_width(&mut self, columns: u8) {
629        self.tab_width = columns.max(1);
630        self.content_w.set(0);
631    }
632
633    /// Whether editing is off.
634    #[inline]
635    pub const fn is_read_only(&self) -> bool {
636        self.read_only
637    }
638
639    /// Turns editing off or on.
640    pub fn set_read_only(&mut self, read_only: bool) {
641        self.read_only = read_only;
642    }
643
644    /// Whether the wheel scrolls a pixel at a time.
645    #[inline]
646    pub const fn smooth_scroll(&self) -> bool {
647        self.smooth_scroll
648    }
649
650    /// Scrolls by the pixel, or by the line. Turned off, the view settles on
651    /// the line it was part way through.
652    pub fn set_smooth_scroll(&mut self, smooth: bool) {
653        self.smooth_scroll = smooth;
654        if !smooth {
655            self.top_px = 0;
656            self.wheel_rest = 0;
657        }
658    }
659
660    /// Where the caret is.
661    #[inline]
662    pub const fn caret(&self) -> Pos {
663        self.caret
664    }
665
666    /// Puts the caret at `pos`, clamped to the document, and clears the
667    /// selection. The view follows on the next event or paint.
668    pub fn set_caret(&mut self, pos: Pos) {
669        self.caret = self.clamp(pos);
670        self.anchor = None;
671        self.goal_x = None;
672    }
673
674    /// The selection as `(from, to)`, or `None` if nothing is selected.
675    pub fn selection(&self) -> Option<(Pos, Pos)> {
676        let anchor = self.anchor.filter(|a| *a != self.caret)?;
677        Some((anchor.min(self.caret), anchor.max(self.caret)))
678    }
679
680    /// Selects everything.
681    pub fn select_all(&mut self) {
682        self.anchor = Some(Pos::ZERO);
683        self.caret = self.last_pos();
684        self.goal_x = None;
685    }
686
687    /// The selected text, lines joined by `\n`, or `None` if nothing is
688    /// selected.
689    pub fn selected_text(&self) -> Option<String> {
690        let (from, to) = self.selection()?;
691        let mut doc = self.doc();
692        let mut out = String::new();
693        for n in from.line..=to.line {
694            let Some(line) = doc.line(n) else { break };
695            let start = if n == from.line { from.col } else { 0 };
696            let end = if n == to.line { to.col } else { line.len() };
697            if n != from.line {
698                out.push('\n');
699            }
700            out.push_str(&line[start.min(end)..end]);
701        }
702        Some(out)
703    }
704
705    /// Replaces the selection, or inserts at the caret, and leaves the caret
706    /// after the text. Silent: this is how an application answers a
707    /// [`ClipboardRequest::Paste`], and it knows it did.
708    pub fn insert_text(&mut self, text: &str) {
709        self.replace_selection(text);
710    }
711
712    /// First line drawn.
713    #[inline]
714    pub const fn top(&self) -> usize {
715        self.top
716    }
717
718    /// Scrolls so `line` is the first drawn, clamped to the lines known.
719    pub fn set_top(&mut self, line: usize) {
720        let last = self.known_lines() - 1;
721        self.top = line.min(last);
722        self.top_px = 0;
723    }
724
725    /// Pixels of the first line drawn that are scrolled up out of view: 0
726    /// unless [`smooth_scroll`](Self::smooth_scroll) is on, and less than a
727    /// row.
728    #[inline]
729    pub const fn top_px(&self) -> i32 {
730        self.top_px
731    }
732
733    /// Whole rows the last paint had room for; 0 before the first.
734    pub fn visible_rows(&self) -> usize {
735        self.rows_seen.get()
736    }
737
738    /// Puts the caret at the start of 0-based `line`, clamped to the lines
739    /// known, and scrolls so the line sits in the middle of the view — as
740    /// far as the last paint's row count can say where the middle is.
741    pub fn go_to(&mut self, line: usize) {
742        self.set_caret(Pos::new(line, 0));
743        let rows = self.rows_seen.get();
744        let top = self.caret.line.saturating_sub(rows / 2);
745        self.top = top.min(self.max_top(rows));
746        self.top_px = 0;
747        self.scroll_x.set(0);
748    }
749
750    /// Selects `[from, to)` with the caret at `to`, both clamped to the
751    /// document, and scrolls the selection into view: centred on its first
752    /// line if that was off screen, and sideways on the next paint, which has
753    /// the fonts to measure it with. What a find lands on. Silent.
754    pub fn select_range(&mut self, from: Pos, to: Pos) {
755        let (from, to) = (self.clamp(from), self.clamp(to));
756        self.anchor = Some(from);
757        self.caret = to;
758        self.goal_x = None;
759        let rows = self.rows_seen.get();
760        let shown = rows > 0 && from.line >= self.top && to.line < self.top + rows;
761        if !shown {
762            let top = from.line.saturating_sub(rows / 2);
763            self.top = top.min(self.max_top(rows));
764            self.top_px = 0;
765        }
766        self.reveal_pending.set(true);
767    }
768
769    /// Pixels the text is scrolled sideways.
770    pub fn scroll_x(&self) -> i32 {
771        self.scroll_x.get()
772    }
773
774    /// The largest top that still fills `rows` rows, or the last line when
775    /// there are fewer lines than that.
776    fn max_top(&self, rows: usize) -> usize {
777        self.known_lines().saturating_sub(rows.max(1))
778    }
779
780    fn doc(&self) -> RefMut<'_, D> {
781        self.doc.borrow_mut()
782    }
783
784    /// Line `n` as its own `String`, so nothing holds the document while the
785    /// text is used.
786    fn line_text(&self, n: usize) -> Option<String> {
787        self.doc().line(n).map(Cow::into_owned)
788    }
789
790    fn line_len(&self, n: usize) -> usize {
791        self.doc().line(n).map_or(0, |l| l.len())
792    }
793
794    fn known_lines(&self) -> usize {
795        self.doc().known_lines().max(1)
796    }
797
798    /// The end of the last known line.
799    fn last_pos(&self) -> Pos {
800        let line = self.known_lines() - 1;
801        Pos::new(line, self.line_len(line))
802    }
803
804    /// `pos` moved onto a line and a character boundary that exist.
805    fn clamp(&self, pos: Pos) -> Pos {
806        let line = pos.line.min(self.known_lines() - 1);
807        let Some(text) = self.line_text(line) else {
808            return Pos::new(line, 0);
809        };
810        let mut col = pos.col.min(text.len());
811        while !text.is_char_boundary(col) {
812            col -= 1;
813        }
814        Pos::new(line, col)
815    }
816
817    // ---- geometry ---------------------------------------------------------
818
819    /// Padding between the gutter and the text, and inside the gutter.
820    #[inline]
821    const fn pad(&self) -> i32 {
822        self.style.size_px as i32 / 3
823    }
824
825    fn row_height(&self, engine: &TextEngine) -> i32 {
826        engine.line_height(self.style).max(1)
827    }
828
829    /// How far the first line is scrolled up, kept under a row of `row_h`
830    /// — which a size set through [`Describe`] can shrink under it.
831    fn offset_in(&self, row_h: i32) -> i32 {
832        self.top_px.clamp(0, row_h - 1)
833    }
834
835    /// Width of the gutter, numbers or not.
836    fn gutter_width(&self, engine: &mut TextEngine) -> i32 {
837        if !self.gutter {
838            return self.pad();
839        }
840        let mut doc = self.doc();
841        let count = doc.line_count().unwrap_or_else(|| doc.known_lines()).max(1);
842        drop(doc);
843        let digits = count.to_string().len().max(3) as i32;
844        engine.measure_line(self.style, "0").max(1) * digits + self.pad() * 2
845    }
846
847    /// Width of the scrollbar strip down the right.
848    #[inline]
849    fn bar_width(&self) -> i32 {
850        (self.style.size_px as i32 * 5 / 8).max(6)
851    }
852
853    /// The scrollbar's strip, stopping above the bottom one when that is
854    /// there, so the two do not meet in the corner.
855    fn bar_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
856        let w = self.bar_width().min(bounds.width.max(0));
857        let h = (bounds.height - self.h_bar_height(engine, bounds)).max(0);
858        Rect::new(bounds.right() - w, bounds.y, w, h)
859    }
860
861    /// The thumb within the strip, or `None` when everything fits.
862    fn thumb_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Option<Rect> {
863        let rows = self.rows(engine, bounds);
864        let total = self.known_lines();
865        if total <= rows {
866            return None;
867        }
868        let bar = self.bar_rect(engine, bounds);
869        // A pixel of strip either side of the thumb, and two above and
870        // below: wide enough to find and grab, with the strip still showing
871        // as its track.
872        let (side, end) = (1, 2);
873        let track_h = (bar.height - end * 2).max(1);
874        let (y, h) = thumb_span(track_h, rows, total, self.top, self.bar_width() * 2);
875        Some(Rect::new(
876            bar.x + side,
877            bar.y + end + y,
878            (bar.width - side * 2).max(1),
879            h,
880        ))
881    }
882
883    /// The strip along the bottom, under the text and beside the gutter. As
884    /// tall as the side strip is wide, and nothing at all when the text fits.
885    fn h_bar_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
886        let h = self.h_bar_height(engine, bounds);
887        let gutter = self.gutter_width(engine);
888        Rect::new(
889            bounds.x + gutter,
890            bounds.bottom() - h,
891            self.text_width(engine, bounds),
892            h,
893        )
894    }
895
896    /// The thumb within the bottom strip, or `None` when the text fits.
897    fn h_thumb_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Option<Rect> {
898        let bar = self.h_bar_rect(engine, bounds);
899        if bar.height == 0 || bar.width <= 0 {
900            return None;
901        }
902        // The track is the width on screen; the whole of it is that and
903        // whatever is off to the right.
904        let span = bar.width + self.max_scroll_x(engine, bounds);
905        let (side, end) = (1, 2);
906        let track_w = (bar.width - end * 2).max(1);
907        let (x, w) = thumb_span(
908            track_w,
909            bar.width as usize,
910            span as usize,
911            self.scroll_x.get().max(0) as usize,
912            self.bar_width() * 2,
913        );
914        Some(Rect::new(
915            bar.x + end + x,
916            bar.y + side,
917            w,
918            (bar.height - side * 2).max(1),
919        ))
920    }
921
922    /// How tall the bottom strip is: nothing unless the text is wider than
923    /// there is room for it.
924    fn h_bar_height(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
925        let bar = self.bar_width();
926        if self.max_scroll_x(engine, bounds) > 0 && bounds.height > bar {
927            bar
928        } else {
929            0
930        }
931    }
932
933    /// Room kept past the end of the longest line: the caret standing after
934    /// its last character, and about a character of daylight after that, so
935    /// the view stops a little beyond the text rather than exactly at it.
936    fn trail(&self) -> i32 {
937        self.caret_width() + self.pad() * 2
938    }
939
940    /// The furthest the text scrolls sideways: what of the widest line
941    /// measured so far, and the room after it, is past the right edge — and
942    /// nothing at all when the line and its caret fit, which is what keeps a
943    /// document of short lines from scrolling sideways by the trail alone.
944    fn max_scroll_x(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
945        let width = self.text_width(engine, bounds);
946        let content = self.content_w.get();
947        if content + self.caret_width() > width {
948            (content + self.trail() - width).max(0)
949        } else {
950            0
951        }
952    }
953
954    /// Takes `width` into the width of the text, if it is the widest yet.
955    fn saw_width(&self, width: i32) {
956        if width > self.content_w.get() {
957            self.content_w.set(width);
958        }
959    }
960
961    /// Pulls the sideways scroll back inside what there is to scroll — after
962    /// a resize, or an edit that took the long line away.
963    fn clamp_scroll_x(&self, engine: &mut TextEngine, bounds: Rect) {
964        let max = self.max_scroll_x(engine, bounds);
965        if self.scroll_x.get() > max {
966            self.scroll_x.set(max);
967        }
968    }
969
970    /// How wide the text is drawn: between the gutter and the side bar.
971    fn text_width(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
972        let gutter = self.gutter_width(engine);
973        (bounds.width - gutter - self.bar_width()).max(0)
974    }
975
976    /// The rectangle the text is drawn in: between the gutter and the
977    /// scrollbar, and above the bottom one when that is there.
978    fn text_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
979        let gutter = self.gutter_width(engine);
980        let width = self.text_width(engine, bounds);
981        let height = (bounds.height - self.h_bar_height(engine, bounds)).max(0);
982        Rect::new(bounds.x + gutter, bounds.y, width, height)
983    }
984
985    /// Whole rows the text has room for.
986    fn rows(&self, engine: &mut TextEngine, bounds: Rect) -> usize {
987        let height = self.text_rect(engine, bounds).height;
988        (height / self.row_height(engine)).max(1) as usize
989    }
990
991    /// Horizontal offset of `col` within `line`, unscrolled, with every tab
992    /// before it reaching its stop.
993    ///
994    /// Everything that turns a column into a position goes through here or
995    /// through [`advance`](Self::advance) — the caret, a click, the
996    /// selection, the marks, scrolling to the caret — so a tab is as wide to
997    /// all of them as it is drawn.
998    fn x_of(&self, engine: &mut TextEngine, line: &str, col: usize) -> i32 {
999        self.advance(engine, 0, &line[..col.min(line.len())])
1000    }
1001
1002    /// Where `text` ends when it starts `x` pixels into its line: its width
1003    /// added, except that a tab jumps to the next stop. Stops are counted
1004    /// from the start of the line, so `x` must be too.
1005    fn advance(&self, engine: &mut TextEngine, mut x: i32, text: &str) -> i32 {
1006        for (i, piece) in text.split('\t').enumerate() {
1007            if i > 0 {
1008                x = self.next_stop(engine, x);
1009            }
1010            if !piece.is_empty() {
1011                x += engine.measure_line(self.style, piece);
1012            }
1013        }
1014        x
1015    }
1016
1017    /// The first tab stop past `x`: a tab at a stop goes on to the next one,
1018    /// as it does in every terminal.
1019    fn next_stop(&self, engine: &mut TextEngine, x: i32) -> i32 {
1020        let stop = engine.measure_line(self.style, " ").max(1) * i32::from(self.tab_width.max(1));
1021        (x.max(0) / stop + 1) * stop
1022    }
1023
1024    /// Draws `text` starting `x` pixels into the line that begins at
1025    /// `origin`, tabs reaching their stops, and returns where it ends.
1026    fn draw_run(
1027        &self,
1028        engine: &mut TextEngine,
1029        canvas: &mut Pen<'_>,
1030        origin: Point,
1031        mut x: i32,
1032        text: &str,
1033        color: Color,
1034    ) -> i32 {
1035        for (i, piece) in text.split('\t').enumerate() {
1036            if i > 0 {
1037                x = self.next_stop(engine, x);
1038            }
1039            if !piece.is_empty() {
1040                let at = Point::new(origin.x + x, origin.y);
1041                x += engine.draw(canvas, self.style, at, piece, color).width as i32;
1042            }
1043        }
1044        x
1045    }
1046
1047    /// The character boundary of `line` nearest to `x`.
1048    fn col_at_x(&self, engine: &mut TextEngine, line: &str, x: i32) -> usize {
1049        if x <= 0 || line.is_empty() {
1050            return 0;
1051        }
1052        let bounds: Vec<usize> = line
1053            .char_indices()
1054            .map(|(i, _)| i)
1055            .chain(core::iter::once(line.len()))
1056            .collect();
1057        // The last boundary whose prefix fits, found by bisection: text only
1058        // gets wider as it gets longer.
1059        let (mut lo, mut hi) = (0, bounds.len() - 1);
1060        while lo < hi {
1061            let mid = (lo + hi).div_ceil(2);
1062            if self.x_of(engine, line, bounds[mid]) <= x {
1063                lo = mid;
1064            } else {
1065                hi = mid - 1;
1066            }
1067        }
1068        // Past the middle of the character is the far side of it.
1069        if lo + 1 < bounds.len() {
1070            let here = self.x_of(engine, line, bounds[lo]);
1071            let next = self.x_of(engine, line, bounds[lo + 1]);
1072            if x - here > next - x {
1073                return bounds[lo + 1];
1074            }
1075        }
1076        bounds[lo]
1077    }
1078
1079    /// The position under `point`.
1080    fn pos_at(&self, engine: &mut TextEngine, bounds: Rect, point: Point) -> Pos {
1081        let row_h = self.row_height(engine);
1082        let y = point.y - bounds.y + self.offset_in(row_h);
1083        let row = (y.max(0) / row_h) as usize;
1084        let line = (self.top + row).min(self.known_lines() - 1);
1085        let text = self.line_text(line).unwrap_or_default();
1086        let x = point.x - self.text_rect(engine, bounds).x + self.scroll_x.get();
1087        Pos::new(line, self.col_at_x(engine, &text, x))
1088    }
1089
1090    /// Scrolls so the caret is on screen.
1091    fn reveal_caret(&mut self, engine: &mut TextEngine, bounds: Rect) {
1092        let rows = self.rows(engine, bounds);
1093        let row_h = self.row_height(engine);
1094        let height = self.text_rect(engine, bounds).height;
1095        let offset = self.offset_in(row_h);
1096        // Part of the first line hidden above hides a caret on it, as a line
1097        // below the last whole row does.
1098        let below = (self.caret.line.saturating_sub(self.top) + 1) as i64 * row_h as i64
1099            - offset as i64
1100            > height.max(row_h) as i64;
1101        if self.caret.line < self.top || (self.caret.line == self.top && offset > 0) {
1102            self.top = self.caret.line;
1103            self.top_px = 0;
1104        } else if below {
1105            self.top = self.caret.line + 1 - rows;
1106            self.top_px = 0;
1107        }
1108        self.reveal_caret_x(engine, bounds);
1109    }
1110
1111    /// Scrolls sideways so the caret is on screen.
1112    fn reveal_caret_x(&self, engine: &mut TextEngine, bounds: Rect) {
1113        let area = self.text_rect(engine, bounds);
1114        if area.width <= 0 {
1115            return;
1116        }
1117        let Some(line) = self.line_text(self.caret.line) else {
1118            return;
1119        };
1120        let x = self.x_of(engine, &line, self.caret.col);
1121        // The caret is on this line, so the line is as wide as the caret at
1122        // least: a caret taken to the end of a line longer than any drawn so
1123        // far can be scrolled to, because the width grows with it.
1124        self.saw_width(x);
1125        let scroll = self.scroll_x.get();
1126        if x < scroll {
1127            self.scroll_x.set(x);
1128        } else if x + self.trail() > scroll + area.width {
1129            self.scroll_x.set(x + self.trail() - area.width);
1130        }
1131        self.clamp_scroll_x(engine, bounds);
1132    }
1133
1134    /// Scrolls sideways so the caret is on screen and, when the selection is
1135    /// on one line and fits, the start of it too: a match at the far end of a
1136    /// long line is shown whole rather than cut at the left edge.
1137    fn reveal_range_x(&self, engine: &mut TextEngine, bounds: Rect) {
1138        self.reveal_caret_x(engine, bounds);
1139        let Some(anchor) = self.anchor else { return };
1140        if anchor.line != self.caret.line || anchor.col >= self.caret.col {
1141            return;
1142        }
1143        let Some(line) = self.line_text(self.caret.line) else {
1144            return;
1145        };
1146        let width = self.text_rect(engine, bounds).width;
1147        let start = self.x_of(engine, &line, anchor.col);
1148        let end = self.x_of(engine, &line, self.caret.col) + self.caret_width() + self.pad();
1149        if start < self.scroll_x.get() && end - start <= width {
1150            self.scroll_x.set(start);
1151        }
1152    }
1153
1154    #[inline]
1155    fn caret_width(&self) -> i32 {
1156        (i32::from(self.style.size_px) / 10).max(1)
1157    }
1158
1159    // ---- moving -----------------------------------------------------------
1160
1161    /// Moves the caret, extending the selection or dropping it.
1162    fn move_to(&mut self, to: Pos, extend: bool) {
1163        if extend {
1164            self.anchor.get_or_insert(self.caret);
1165        } else {
1166            self.anchor = None;
1167        }
1168        self.caret = to;
1169    }
1170
1171    fn left_of(&self, pos: Pos) -> Pos {
1172        if pos.col > 0 {
1173            let line = self.line_text(pos.line).unwrap_or_default();
1174            Pos::new(pos.line, prev_boundary(&line, pos.col))
1175        } else if pos.line > 0 {
1176            Pos::new(pos.line - 1, self.line_len(pos.line - 1))
1177        } else {
1178            pos
1179        }
1180    }
1181
1182    /// The start of the word to the left of `pos`, crossing a line end the way
1183    /// a plain arrow does when there is no word left on this line.
1184    fn word_left(&self, pos: Pos) -> Pos {
1185        if pos.col == 0 {
1186            return self.left_of(pos);
1187        }
1188        let line = self.line_text(pos.line).unwrap_or_default();
1189        let mut col = pos.col;
1190        while col > 0 && line[..col].chars().next_back().is_some_and(|c| !is_word(c)) {
1191            col = prev_boundary(&line, col);
1192        }
1193        while col > 0 && line[..col].chars().next_back().is_some_and(is_word) {
1194            col = prev_boundary(&line, col);
1195        }
1196        Pos::new(pos.line, col)
1197    }
1198
1199    /// The end of the word to the right of `pos`, by the mirror of that rule.
1200    fn word_right(&self, pos: Pos) -> Pos {
1201        let line = self.line_text(pos.line).unwrap_or_default();
1202        if pos.col >= line.len() {
1203            return self.right_of(pos);
1204        }
1205        let mut col = pos.col;
1206        while col < line.len() && line[col..].chars().next().is_some_and(|c| !is_word(c)) {
1207            col = next_boundary(&line, col);
1208        }
1209        while col < line.len() && line[col..].chars().next().is_some_and(is_word) {
1210            col = next_boundary(&line, col);
1211        }
1212        Pos::new(pos.line, col)
1213    }
1214
1215    fn right_of(&self, pos: Pos) -> Pos {
1216        let line = self.line_text(pos.line).unwrap_or_default();
1217        if pos.col < line.len() {
1218            Pos::new(pos.line, next_boundary(&line, pos.col))
1219        } else if pos.line + 1 < self.known_lines() {
1220            Pos::new(pos.line + 1, 0)
1221        } else {
1222            pos
1223        }
1224    }
1225
1226    /// The caret `by` lines away, at the x it is trying to keep.
1227    fn vertical(&mut self, engine: &mut TextEngine, by: isize) -> Pos {
1228        let goal = match self.goal_x {
1229            Some(x) => x,
1230            None => {
1231                let line = self.line_text(self.caret.line).unwrap_or_default();
1232                let x = self.x_of(engine, &line, self.caret.col);
1233                self.goal_x = Some(x);
1234                x
1235            }
1236        };
1237        let last = self.known_lines() - 1;
1238        let line = self.caret.line.saturating_add_signed(by).min(last);
1239        let text = self.line_text(line).unwrap_or_default();
1240        Pos::new(line, self.col_at_x(engine, &text, goal))
1241    }
1242
1243    // ---- editing ----------------------------------------------------------
1244
1245    /// Deletes the selection if there is one and inserts `text` at the caret.
1246    fn replace_selection(&mut self, text: &str) {
1247        if let Some((from, to)) = self.selection() {
1248            self.doc().delete(from, to);
1249            self.caret = from;
1250        }
1251        self.anchor = None;
1252        self.goal_x = None;
1253        if !text.is_empty() {
1254            self.doc().insert(self.caret, text);
1255            self.caret = end_of(self.caret, text);
1256        }
1257    }
1258
1259    fn backspace(&mut self) -> bool {
1260        if self.selection().is_some() {
1261            self.replace_selection("");
1262            return true;
1263        }
1264        let from = self.left_of(self.caret);
1265        if from == self.caret {
1266            return false;
1267        }
1268        self.doc().delete(from, self.caret);
1269        self.caret = from;
1270        self.goal_x = None;
1271        true
1272    }
1273
1274    fn delete_forward(&mut self) -> bool {
1275        if self.selection().is_some() {
1276            self.replace_selection("");
1277            return true;
1278        }
1279        let to = self.right_of(self.caret);
1280        if to == self.caret {
1281            return false;
1282        }
1283        self.doc().delete(self.caret, to);
1284        self.goal_x = None;
1285        true
1286    }
1287
1288    /// Restarts the blink so the caret is solid while it is being moved, and
1289    /// asks to animate again, since a caret that blinked its fill has stopped.
1290    fn wake_caret(&mut self, ctx: &mut EventCtx<'_, M>) {
1291        self.blink_epoch = ctx.now_ms;
1292        self.caret_on = true;
1293        ctx.request_animation();
1294    }
1295
1296    /// What every caret move ends with.
1297    fn moved(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
1298        self.wake_caret(ctx);
1299        let bounds = ctx.bounds;
1300        self.reveal_caret(ctx.text, bounds);
1301        Handled::Yes
1302    }
1303
1304    fn key(&mut self, code: KeyCode, modifiers: Modifiers, ctx: &mut EventCtx<'_, M>) -> Handled
1305    where
1306        M: Clone,
1307    {
1308        let shift = modifiers.contains(Modifiers::SHIFT);
1309        // Ctrl on the desktops that use it, Command on the one that does not;
1310        // a panel with a bare keyboard has neither and needs neither.
1311        let primary = modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::SUPER);
1312        // Ctrl on Windows and Linux, Option on a Mac: the two spellings of "by
1313        // word", taken together because the widget cannot ask which keyboard it
1314        // is in front of. Command and an arrow is the Mac's "to the end of the
1315        // line", which is what Home and End do here.
1316        let by_word = modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::ALT);
1317        let to_line_end = modifiers.contains(Modifiers::SUPER);
1318        let edited = match code {
1319            KeyCode::ArrowLeft => {
1320                let to = if to_line_end {
1321                    Pos::new(self.caret.line, 0)
1322                } else if by_word {
1323                    self.word_left(self.caret)
1324                } else {
1325                    match self.selection() {
1326                        Some((from, _)) if !shift => from,
1327                        _ => self.left_of(self.caret),
1328                    }
1329                };
1330                self.move_to(to, shift);
1331                self.goal_x = None;
1332                return self.moved(ctx);
1333            }
1334            KeyCode::ArrowRight => {
1335                let to = if to_line_end {
1336                    Pos::new(self.caret.line, self.line_len(self.caret.line))
1337                } else if by_word {
1338                    self.word_right(self.caret)
1339                } else {
1340                    match self.selection() {
1341                        Some((_, to)) if !shift => to,
1342                        _ => self.right_of(self.caret),
1343                    }
1344                };
1345                self.move_to(to, shift);
1346                self.goal_x = None;
1347                return self.moved(ctx);
1348            }
1349            KeyCode::ArrowUp | KeyCode::ArrowDown => {
1350                let by = if code == KeyCode::ArrowUp { -1 } else { 1 };
1351                let to = self.vertical(ctx.text, by);
1352                self.move_to(to, shift);
1353                return self.moved(ctx);
1354            }
1355            KeyCode::PageUp | KeyCode::PageDown => {
1356                let rows = self.rows(ctx.text, ctx.bounds) as isize;
1357                let by = if code == KeyCode::PageUp { -rows } else { rows };
1358                let to = self.vertical(ctx.text, by);
1359                self.move_to(to, shift);
1360                // The view pages with the caret rather than merely following
1361                // it, so the caret keeps its row on screen.
1362                let max_top = self.max_top(rows as usize);
1363                self.top = self.top.saturating_add_signed(by).min(max_top);
1364                self.top_px = 0;
1365                return self.moved(ctx);
1366            }
1367            KeyCode::Home => {
1368                let to = if primary {
1369                    Pos::ZERO
1370                } else {
1371                    Pos::new(self.caret.line, 0)
1372                };
1373                self.move_to(to, shift);
1374                self.goal_x = None;
1375                return self.moved(ctx);
1376            }
1377            KeyCode::End => {
1378                let to = if primary {
1379                    self.last_pos()
1380                } else {
1381                    Pos::new(self.caret.line, self.line_len(self.caret.line))
1382                };
1383                self.move_to(to, shift);
1384                self.goal_x = None;
1385                return self.moved(ctx);
1386            }
1387            KeyCode::Escape => {
1388                if self.anchor.take().is_none() {
1389                    return Handled::No;
1390                }
1391                return Handled::Yes;
1392            }
1393            KeyCode::A if primary => {
1394                self.select_all();
1395                return self.moved(ctx);
1396            }
1397            KeyCode::C if primary => {
1398                return self.clipboard(ctx, false);
1399            }
1400            KeyCode::X if primary => {
1401                return self.clipboard(ctx, true);
1402            }
1403            KeyCode::V if primary => {
1404                if self.read_only {
1405                    return Handled::No;
1406                }
1407                let Some(request) = self.on_clipboard else {
1408                    return Handled::No;
1409                };
1410                ctx.emit(request(ClipboardRequest::Paste));
1411                return Handled::Yes;
1412            }
1413            KeyCode::Z if primary && !self.read_only => {
1414                let done = if shift {
1415                    self.doc().redo()
1416                } else {
1417                    self.doc().undo()
1418                };
1419                if !done {
1420                    return Handled::No;
1421                }
1422                // The document moved under the caret and only it knows where
1423                // to; the nearest place that still exists is the honest guess.
1424                self.caret = self.clamp(self.caret);
1425                self.anchor = None;
1426                self.goal_x = None;
1427                true
1428            }
1429            KeyCode::Y if primary && !self.read_only => {
1430                if !self.doc().redo() {
1431                    return Handled::No;
1432                }
1433                self.caret = self.clamp(self.caret);
1434                self.anchor = None;
1435                self.goal_x = None;
1436                true
1437            }
1438            KeyCode::Backspace if !self.read_only => self.backspace(),
1439            KeyCode::Delete if !self.read_only => self.delete_forward(),
1440            KeyCode::Enter | KeyCode::NumpadEnter if !self.read_only => {
1441                self.replace_selection("\n");
1442                true
1443            }
1444            _ => return Handled::No,
1445        };
1446        if edited && let Some(message) = self.on_change.clone() {
1447            ctx.emit(message);
1448        }
1449        self.moved(ctx)
1450    }
1451
1452    /// Copy, or cut, through the application.
1453    fn clipboard(&mut self, ctx: &mut EventCtx<'_, M>, cut: bool) -> Handled
1454    where
1455        M: Clone,
1456    {
1457        let Some(request) = self.on_clipboard else {
1458            return Handled::No;
1459        };
1460        let Some(text) = self.selected_text() else {
1461            return Handled::No;
1462        };
1463        if cut && !self.read_only {
1464            self.replace_selection("");
1465            ctx.emit(request(ClipboardRequest::Cut(text)));
1466            if let Some(message) = self.on_change.clone() {
1467                ctx.emit(message);
1468            }
1469            return self.moved(ctx);
1470        }
1471        ctx.emit(request(ClipboardRequest::Copy(text)));
1472        Handled::Yes
1473    }
1474
1475    fn press(
1476        &mut self,
1477        position: Point,
1478        modifiers: Modifiers,
1479        ctx: &mut EventCtx<'_, M>,
1480    ) -> Handled {
1481        let bounds = ctx.bounds;
1482        if self.bar_rect(ctx.text, bounds).contains(position) {
1483            return self.press_bar(position, ctx);
1484        }
1485        if self.h_bar_rect(ctx.text, bounds).contains(position) {
1486            return self.press_h_bar(position, ctx);
1487        }
1488        let pos = self.pos_at(ctx.text, bounds, position);
1489        let now = ctx.now_ms;
1490        // One press places the caret, a second on the same spot takes the word
1491        // under it, a third takes the line, and a fourth starts the count over
1492        // rather than sticking on the line.
1493        let count = match self.last_click {
1494            Some((at, when, count)) if at == pos && now.saturating_sub(when) <= DOUBLE_CLICK_MS => {
1495                count % 3 + 1
1496            }
1497            _ => 1,
1498        };
1499        self.last_click = Some((pos, now, count));
1500        match count {
1501            2 => {
1502                let line = self.line_text(pos.line).unwrap_or_default();
1503                let (start, end) = word_at(&line, pos.col);
1504                self.anchor = Some(Pos::new(pos.line, start));
1505                self.caret = Pos::new(pos.line, end);
1506                self.dragging = false;
1507            }
1508            3 => {
1509                // The line's text and not the newline after it, so Backspace on
1510                // a line taken this way leaves an empty line where it was rather
1511                // than pulling the next one up onto the line above.
1512                self.anchor = Some(Pos::new(pos.line, 0));
1513                self.caret = Pos::new(pos.line, self.line_len(pos.line));
1514                self.dragging = false;
1515            }
1516            _ => {
1517                self.move_to(pos, modifiers.contains(Modifiers::SHIFT));
1518                self.dragging = true;
1519            }
1520        }
1521        self.goal_x = None;
1522        self.moved(ctx)
1523    }
1524
1525    /// A press in the scrollbar: takes the thumb, or pages towards the press.
1526    fn press_bar(&mut self, position: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
1527        let bounds = ctx.bounds;
1528        let Some(thumb) = self.thumb_rect(ctx.text, bounds) else {
1529            return Handled::No;
1530        };
1531        let rows = self.rows(ctx.text, bounds);
1532        if thumb.contains(position) {
1533            self.thumb_drag = Some((position.y, self.top));
1534        } else if position.y < thumb.y {
1535            self.top = self.top.saturating_sub(rows);
1536            self.top_px = 0;
1537        } else {
1538            self.top = (self.top + rows).min(self.max_top(rows));
1539            self.top_px = 0;
1540        }
1541        Handled::Yes
1542    }
1543
1544    /// A press in the bottom scrollbar: takes the thumb, or pages sideways
1545    /// towards the press.
1546    fn press_h_bar(&mut self, position: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
1547        let bounds = ctx.bounds;
1548        let Some(thumb) = self.h_thumb_rect(ctx.text, bounds) else {
1549            return Handled::No;
1550        };
1551        if thumb.contains(position) {
1552            self.h_thumb_drag = Some((position.x, self.scroll_x.get()));
1553            return Handled::Yes;
1554        }
1555        let page = self.text_width(ctx.text, bounds);
1556        let max = self.max_scroll_x(ctx.text, bounds);
1557        let to = if position.x < thumb.x {
1558            self.scroll_x.get() - page
1559        } else {
1560            self.scroll_x.get() + page
1561        };
1562        self.scroll_x.set(to.clamp(0, max));
1563        Handled::Yes
1564    }
1565
1566    /// The bottom thumb, held, followed the pointer to `x`.
1567    fn drag_h_thumb(&mut self, engine: &mut TextEngine, bounds: Rect, x: i32) -> Handled {
1568        let Some((from_x, from_scroll)) = self.h_thumb_drag else {
1569            return Handled::No;
1570        };
1571        let Some(thumb) = self.h_thumb_rect(engine, bounds) else {
1572            return Handled::No;
1573        };
1574        let max = self.max_scroll_x(engine, bounds);
1575        let travel = (self.h_bar_rect(engine, bounds).width - 4 - thumb.width).max(1) as i64;
1576        let moved = (x - from_x) as i64 * max as i64 / travel;
1577        let scroll = (from_scroll as i64 + moved).clamp(0, max as i64) as i32;
1578        if scroll == self.scroll_x.get() {
1579            return Handled::No;
1580        }
1581        self.scroll_x.set(scroll);
1582        Handled::Yes
1583    }
1584
1585    /// The thumb, held, followed the pointer to `y`.
1586    fn drag_thumb(&mut self, engine: &mut TextEngine, bounds: Rect, y: i32) -> Handled {
1587        let Some((from_y, from_top)) = self.thumb_drag else {
1588            return Handled::No;
1589        };
1590        let Some(thumb) = self.thumb_rect(engine, bounds) else {
1591            return Handled::No;
1592        };
1593        let rows = self.rows(engine, bounds);
1594        let max_top = self.max_top(rows);
1595        let travel = (self.bar_rect(engine, bounds).height - 4 - thumb.height).max(1) as i64;
1596        let moved = (y - from_y) as i64 * max_top as i64 / travel;
1597        let top = (from_top as i64 + moved).clamp(0, max_top as i64) as usize;
1598        if top == self.top && self.top_px == 0 {
1599            return Handled::No;
1600        }
1601        self.top = top;
1602        self.top_px = 0;
1603        Handled::Yes
1604    }
1605
1606    fn wheel(
1607        &mut self,
1608        engine: &mut TextEngine,
1609        bounds: Rect,
1610        delta_x: f32,
1611        delta_y: f32,
1612    ) -> Handled {
1613        let row_h = self.row_height(engine);
1614        let max_top = self.max_top(self.rows(engine, bounds));
1615        let (top, top_px) = if self.smooth_scroll {
1616            // The view as one distance in pixels, moved and clamped as one,
1617            // and split back into a line and the part of it above the view.
1618            // The furthest it goes is the last line's end at a whole row, as
1619            // it is scrolling by lines.
1620            let row = row_h as i64;
1621            let at = self.top as i64 * row + self.offset_in(row_h) as i64 + delta_y as i64;
1622            let at = at.clamp(0, max_top as i64 * row);
1623            ((at / row) as usize, (at % row) as i32)
1624        } else {
1625            self.wheel_rest += delta_y as i32;
1626            let lines = self.wheel_rest / row_h;
1627            self.wheel_rest -= lines * row_h;
1628            let top = self.top.saturating_add_signed(lines as isize).min(max_top);
1629            (top, 0)
1630        };
1631        let scroll_x =
1632            (self.scroll_x.get() + delta_x as i32).clamp(0, self.max_scroll_x(engine, bounds));
1633        if top == self.top && top_px == self.top_px && scroll_x == self.scroll_x.get() {
1634            return Handled::No;
1635        }
1636        self.top = top;
1637        self.top_px = top_px;
1638        self.scroll_x.set(scroll_x);
1639        Handled::Yes
1640    }
1641}
1642
1643/// The boundary before `col` in `line`.
1644fn prev_boundary(line: &str, col: usize) -> usize {
1645    line[..col]
1646        .chars()
1647        .next_back()
1648        .map_or(0, |c| col - c.len_utf8())
1649}
1650
1651/// The boundary after `col` in `line`.
1652fn next_boundary(line: &str, col: usize) -> usize {
1653    line[col..]
1654        .chars()
1655        .next()
1656        .map_or(col, |c| col + c.len_utf8())
1657}
1658
1659/// Whether `c` is part of a word, for double-click selection.
1660///
1661/// Shared with [`TextInput`](super::TextInput) so that a double-click takes the
1662/// same run of characters in a field as it does in an editor. One rule, because
1663/// two would drift and nobody would notice which one was wrong.
1664pub(crate) fn is_word(c: char) -> bool {
1665    c.is_alphanumeric() || c == '_'
1666}
1667
1668/// The run of word characters — or of non-word ones — around `col`.
1669fn word_at(line: &str, col: usize) -> (usize, usize) {
1670    // The word before wins at its end, so a double-click just past the last
1671    // letter takes the word rather than the space after it.
1672    let before = line[..col].chars().next_back();
1673    let after = line[col..].chars().next();
1674    let class = match (before, after) {
1675        (Some(b), _) if is_word(b) => true,
1676        (_, Some(a)) => is_word(a),
1677        (Some(b), None) => is_word(b),
1678        (None, None) => return (col, col),
1679    };
1680    let start = line[..col]
1681        .char_indices()
1682        .rev()
1683        .take_while(|(_, c)| is_word(*c) == class)
1684        .last()
1685        .map_or(col, |(i, _)| i);
1686    let end = line[col..]
1687        .char_indices()
1688        .find(|(_, c)| is_word(*c) != class)
1689        .map_or(line.len(), |(i, _)| col + i);
1690    (start, end)
1691}
1692
1693impl<M: Clone + 'static, D: TextDocument> Widget<M> for TextArea<M, D> {
1694    fn describe(&self) -> Option<&dyn DynDescribe> {
1695        Some(self)
1696    }
1697
1698    fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
1699        Some(self)
1700    }
1701
1702    fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
1703        // An editor is as big as you make it; the one thing it can say is
1704        // that fewer than three lines is not an editor.
1705        Measured::tall(ctx.text.line_height(self.style).max(1) * 3)
1706    }
1707
1708    fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
1709        let bounds = ctx.bounds;
1710        let theme = ctx.theme;
1711        let disabled = ctx.state.contains(VisualState::DISABLED);
1712        let focused = ctx.state.contains(VisualState::FOCUSED);
1713        canvas.fill_rect(bounds, theme.color(Role::Base100));
1714
1715        let row_h = self.row_height(ctx.text);
1716        let gutter_w = self.gutter_width(ctx.text);
1717        if self.reveal_pending.take() {
1718            self.reveal_range_x(ctx.text, bounds);
1719        }
1720        // A window made wider, or an edit that took the longest line away,
1721        // can leave the view scrolled past the end of the text.
1722        self.clamp_scroll_x(ctx.text, bounds);
1723        let area = self.text_rect(ctx.text, bounds);
1724        let text_x = area.x - self.scroll_x.get();
1725        let content = if disabled {
1726            theme.color(Role::Base300)
1727        } else {
1728            theme.color(Role::BaseContent)
1729        };
1730        let dim = muted(theme.color(Role::Base100), content);
1731        let selected = theme.color(Role::Accent).with_alpha(60);
1732        let marked = theme.color(Role::Warning).with_alpha(90);
1733        let space_w = ctx.text.measure_line(self.style, " ").max(1);
1734        let selection = self.selection();
1735
1736        if self.gutter {
1737            let gutter = Rect::new(bounds.x, bounds.y, gutter_w, bounds.height);
1738            canvas.fill_rect(gutter, theme.color(Role::Base200));
1739        }
1740
1741        self.rows_seen.set(self.rows(ctx.text, bounds));
1742        let mut spans = Vec::new();
1743        let mut marks: Vec<Range<usize>> = Vec::new();
1744        // The first line starts above the view by as much of it as is
1745        // scrolled away, and a row more is drawn for the room that frees.
1746        let offset = self.offset_in(row_h);
1747        let rows = ((area.height + offset) / row_h + 1).max(1) as usize;
1748        for row in 0..rows {
1749            let n = self.top + row;
1750            let Some(line) = self.line_text(n) else { break };
1751            let y = bounds.y + row as i32 * row_h - offset;
1752            if y >= area.bottom() {
1753                break;
1754            }
1755            let strip = Rect::new(bounds.x, y, bounds.width, row_h);
1756            if !strip.intersects(&canvas.clip()) {
1757                continue;
1758            }
1759
1760            if self.gutter {
1761                let number = (n + 1).to_string();
1762                let w = ctx.text.measure_line(self.style, &number);
1763                let x = bounds.x + gutter_w - self.pad() - w;
1764                let color = if n == self.caret.line { content } else { dim };
1765                // Clipped at the top, where a number part scrolled away
1766                // would otherwise draw over whatever is above the editor.
1767                let gutter = Rect::new(bounds.x, bounds.y, gutter_w, bounds.height);
1768                let mut numbers = canvas.with_clip(gutter);
1769                ctx.text
1770                    .draw(&mut numbers, self.style, Point::new(x, y), &number, color);
1771            }
1772
1773            let mut clipped = canvas.with_clip(area);
1774            marks.clear();
1775            if !disabled {
1776                self.doc().highlights(n, &line, &mut marks);
1777            }
1778            // Measured a stretch at a time from the last mark's end, rather
1779            // than each from the start of the line: a one-letter search on a
1780            // long line is thousands of marks, and measuring every prefix
1781            // would be quadratic in the line. Marks past the right edge are
1782            // not measured at all.
1783            // Positions are kept from the start of the line, where tab stops
1784            // are counted from, and moved by the scroll only to be drawn.
1785            let (mut col, mut x) = (0, 0);
1786            for mark in &marks {
1787                if mark.start < col
1788                    || mark.start >= mark.end
1789                    || mark.end > line.len()
1790                    || !line.is_char_boundary(mark.start)
1791                    || !line.is_char_boundary(mark.end)
1792                {
1793                    continue;
1794                }
1795                let start = self.advance(ctx.text, x, &line[col..mark.start]);
1796                if text_x + start >= area.right() {
1797                    break;
1798                }
1799                let end = self.advance(ctx.text, start, &line[mark.start..mark.end]);
1800                if text_x + end > area.x {
1801                    clipped.fill_rect(Rect::new(text_x + start, y, end - start, row_h), marked);
1802                }
1803                (col, x) = (mark.end, end);
1804            }
1805
1806            if let Some((from, to)) = selection
1807                && n >= from.line
1808                && n <= to.line
1809            {
1810                let start = if n == from.line { from.col } else { 0 };
1811                let end = if n == to.line { to.col } else { line.len() };
1812                let x0 = text_x + self.x_of(ctx.text, &line, start);
1813                let mut x1 = text_x + self.x_of(ctx.text, &line, end);
1814                if n < to.line {
1815                    // The newline is selected too, and is a space wide.
1816                    x1 += space_w;
1817                }
1818                clipped.fill_rect(Rect::new(x0, y, x1 - x0, row_h), selected);
1819            }
1820
1821            spans.clear();
1822            if !disabled {
1823                self.doc().spans(n, &mut spans);
1824            }
1825            let origin = Point::new(text_x, y);
1826            let mut x = 0;
1827            let mut at = 0;
1828            for span in spans
1829                .iter()
1830                .filter(|s| s.start < s.end && s.end <= line.len())
1831            {
1832                if span.start > at {
1833                    let run = &line[at..span.start];
1834                    x = self.draw_run(ctx.text, &mut clipped, origin, x, run, content);
1835                }
1836                let run = &line[span.start..span.end];
1837                x = self.draw_run(ctx.text, &mut clipped, origin, x, run, span.color);
1838                at = span.end;
1839            }
1840            if at < line.len() {
1841                x = self.draw_run(ctx.text, &mut clipped, origin, x, &line[at..], content);
1842            }
1843            // Drawing the line has measured it, so the width comes free.
1844            self.saw_width(x);
1845
1846            if n == self.caret.line && focused && self.caret_on && !disabled {
1847                let x = text_x + self.x_of(ctx.text, &line, self.caret.col);
1848                clipped.fill_rect(
1849                    Rect::new(x, y, self.caret_width(), row_h),
1850                    theme.color(Role::Accent),
1851                );
1852            }
1853        }
1854
1855        // The scrollbar: a strip down the right, a thumb only when there is
1856        // more than fits. Its share of the strip is the rows' share of the
1857        // lines, so a file still being counted shows a thumb that shrinks as
1858        // the count climbs.
1859        if let Some(thumb) = self.thumb_rect(ctx.text, bounds) {
1860            let bar = self.bar_rect(ctx.text, bounds);
1861            let radius = bar.width / 3;
1862            canvas.fill_rect(bar, theme.color(Role::Base200));
1863            let alpha = if self.thumb_drag.is_some() { 170 } else { 110 };
1864            canvas.fill_rounded_rect(thumb, radius, content.with_alpha(alpha));
1865        }
1866
1867        // And the same along the bottom, of the width of the lines drawn so
1868        // far rather than of the lines themselves — there when the text is
1869        // wider than the room for it, and not there at all when it fits.
1870        if let Some(thumb) = self.h_thumb_rect(ctx.text, bounds) {
1871            let bar = self.h_bar_rect(ctx.text, bounds);
1872            let radius = bar.height / 3;
1873            canvas.fill_rect(bar, theme.color(Role::Base200));
1874            let alpha = if self.h_thumb_drag.is_some() {
1875                170
1876            } else {
1877                110
1878            };
1879            canvas.fill_rounded_rect(thumb, radius, content.with_alpha(alpha));
1880        }
1881    }
1882
1883    fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
1884        match event {
1885            Event::FocusGained => {
1886                self.has_focus = true;
1887                self.wake_caret(ctx);
1888                Handled::No
1889            }
1890            Event::FocusLost => {
1891                self.has_focus = false;
1892                self.dragging = false;
1893                self.thumb_drag = None;
1894                self.h_thumb_drag = None;
1895                self.wake_caret(ctx);
1896                Handled::No
1897            }
1898            Event::PressCancelled => {
1899                self.dragging = false;
1900                self.thumb_drag = None;
1901                self.h_thumb_drag = None;
1902                Handled::No
1903            }
1904            Event::Input(InputEvent::Text { ch }) if !ch.is_control() => {
1905                if self.read_only {
1906                    return Handled::No;
1907                }
1908                let mut buf = [0; 4];
1909                self.replace_selection(ch.encode_utf8(&mut buf));
1910                if let Some(message) = self.on_change.clone() {
1911                    ctx.emit(message);
1912                }
1913                self.moved(ctx)
1914            }
1915            Event::Input(InputEvent::Key {
1916                code,
1917                state: ElementState::Down,
1918                modifiers,
1919                ..
1920            }) => self.key(*code, *modifiers, ctx),
1921            Event::Input(InputEvent::PointerButton {
1922                button: PointerButton::Left,
1923                state: ElementState::Down,
1924                position,
1925                modifiers,
1926            }) => self.press(*position, *modifiers, ctx),
1927            Event::Input(InputEvent::PointerButton {
1928                button: PointerButton::Left,
1929                state: ElementState::Up,
1930                ..
1931            }) => {
1932                self.dragging = false;
1933                let held = self.thumb_drag.take().is_some() | self.h_thumb_drag.take().is_some();
1934                // A thumb draws differently while held.
1935                if held { Handled::Yes } else { Handled::No }
1936            }
1937            Event::Input(InputEvent::PointerMoved { position }) if self.thumb_drag.is_some() => {
1938                let bounds = ctx.bounds;
1939                self.drag_thumb(ctx.text, bounds, position.y)
1940            }
1941            Event::Input(InputEvent::PointerMoved { position }) if self.h_thumb_drag.is_some() => {
1942                let bounds = ctx.bounds;
1943                self.drag_h_thumb(ctx.text, bounds, position.x)
1944            }
1945            Event::Input(InputEvent::PointerMoved { position }) if self.dragging => {
1946                let bounds = ctx.bounds;
1947                let pos = self.pos_at(ctx.text, bounds, *position);
1948                if pos == self.caret {
1949                    return Handled::No;
1950                }
1951                self.move_to(pos, true);
1952                self.goal_x = None;
1953                self.moved(ctx)
1954            }
1955            Event::Input(InputEvent::PointerScroll {
1956                delta_x, delta_y, ..
1957            }) => {
1958                let bounds = ctx.bounds;
1959                self.wheel(ctx.text, bounds, *delta_x, *delta_y)
1960            }
1961            _ => Handled::No,
1962        }
1963    }
1964
1965    fn accepts_pointer(&self) -> bool {
1966        true
1967    }
1968
1969    fn focusable(&self) -> bool {
1970        true
1971    }
1972
1973    fn animate(&mut self, now_ms: u64) -> Animation {
1974        if !self.has_focus {
1975            return Animation::NONE;
1976        }
1977        let elapsed = now_ms.saturating_sub(self.blink_epoch);
1978        if elapsed >= CARET_BLINKS_FOR_MS {
1979            // Lit, and asleep until the caret is next moved.
1980            let repaint = !self.caret_on;
1981            self.caret_on = true;
1982            return Animation {
1983                repaint,
1984                next: Wake::Never,
1985            };
1986        }
1987        let on = (elapsed / BLINK_MS).is_multiple_of(2);
1988        let repaint = on != self.caret_on;
1989        self.caret_on = on;
1990        Animation {
1991            repaint,
1992            next: Wake::At(
1993                self.blink_epoch.saturating_add(
1994                    (elapsed / BLINK_MS)
1995                        .saturating_add(1)
1996                        .saturating_mul(BLINK_MS),
1997                ),
1998            ),
1999        }
2000    }
2001
2002    /// Blinking is a schedule, not a motion; see [`TextInput`](super::TextInput).
2003    fn snap(&mut self, now_ms: u64) -> Animation {
2004        Widget::<M>::animate(self, now_ms)
2005    }
2006}
2007
2008impl<M, D: TextDocument> Describe for TextArea<M, D> {
2009    const KIND: &'static str = "text-area";
2010    const DOC: &'static str = "Lines of text somebody edits.";
2011    const GROUP: Group = Group::Input;
2012    const ICON: &'static denise::icon::Icon = &super::icons::TEXT_AREA;
2013
2014    const PROPERTIES: &'static [Property] = &[
2015        Property::new(
2016            "gutter",
2017            PropertyKind::Bool,
2018            "Number the lines down the left.",
2019        ),
2020        Property::new(
2021            "read-only",
2022            PropertyKind::Bool,
2023            "Show the text and place a caret in it, but change nothing.",
2024        ),
2025        Property::new(
2026            "size",
2027            PropertyKind::Int { min: 6, max: 96 },
2028            "Text size in logical pixels.",
2029        )
2030        .in_pixels(),
2031        Property::new(
2032            "smooth-scroll",
2033            PropertyKind::Bool,
2034            "Scroll a pixel at a time on the wheel, not a line.",
2035        ),
2036        Property::new(
2037            "tab-width",
2038            PropertyKind::Int { min: 1, max: 16 },
2039            "Columns from one tab stop to the next.",
2040        ),
2041    ];
2042
2043    fn get(&self, name: &str) -> Option<Value> {
2044        Some(match name {
2045            "gutter" => Value::Bool(self.gutter),
2046            "read-only" => Value::Bool(self.read_only),
2047            "size" => Value::Int(i32::from(self.style.size_px)),
2048            "smooth-scroll" => Value::Bool(self.smooth_scroll),
2049            "tab-width" => Value::Int(i32::from(self.tab_width)),
2050            _ => return None,
2051        })
2052    }
2053
2054    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
2055        match name {
2056            "gutter" => self.gutter = value.as_bool()?,
2057            "read-only" => self.read_only = value.as_bool()?,
2058            "size" => self.style.size_px = value.as_size()?,
2059            "smooth-scroll" => self.set_smooth_scroll(value.as_bool()?),
2060            "tab-width" => self.tab_width = value.as_index()?.clamp(1, 16) as u8,
2061            _ => return Err(Mismatch::Unknown),
2062        }
2063        Ok(())
2064    }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069    use super::*;
2070
2071    #[test]
2072    fn end_of_counts_lines_and_the_last_column() {
2073        assert_eq!(end_of(Pos::new(2, 3), "ab"), Pos::new(2, 5));
2074        assert_eq!(end_of(Pos::new(2, 3), "a\nbc"), Pos::new(3, 2));
2075        assert_eq!(end_of(Pos::new(2, 3), "\n\n"), Pos::new(4, 0));
2076    }
2077
2078    #[test]
2079    fn a_buffer_inserts_and_deletes_across_lines() {
2080        let mut b = TextBuffer::from_text("one\ntwo");
2081        b.insert(Pos::new(0, 3), " and\na half");
2082        assert_eq!(b.text(), "one and\na half\ntwo");
2083        b.delete(Pos::new(0, 3), Pos::new(2, 1));
2084        assert_eq!(b.text(), "onewo");
2085        assert_eq!(b.line_count(), Some(1));
2086        assert_eq!(b.line(0).as_deref(), Some("onewo"));
2087        assert_eq!(b.line(1), None);
2088    }
2089
2090    #[test]
2091    fn undo_takes_back_a_run_of_typing_at_once() {
2092        let mut b = TextBuffer::new();
2093        for ch in ["h", "i", "\n", "t", "here"] {
2094            let at = end_of(Pos::ZERO, &b.text());
2095            b.insert(at, ch);
2096        }
2097        assert_eq!(b.text(), "hi\nthere");
2098        assert!(b.undo());
2099        assert_eq!(b.text(), "hi\n", "the second line was one run");
2100        assert!(b.undo());
2101        assert_eq!(b.text(), "hi");
2102        assert!(b.undo());
2103        assert_eq!(b.text(), "");
2104        assert!(!b.undo());
2105        assert!(b.redo());
2106        assert!(b.redo());
2107        assert_eq!(b.text(), "hi\n");
2108        b.insert(Pos::new(1, 0), "x");
2109        assert!(!b.redo(), "a new edit drops the redo history");
2110        b.delete(Pos::new(0, 1), Pos::new(1, 1));
2111        assert_eq!(b.text(), "h");
2112        assert!(b.undo());
2113        assert_eq!(b.text(), "hi\nx");
2114    }
2115
2116    #[test]
2117    fn words_are_runs_of_one_class() {
2118        assert_eq!(word_at("let x_1 = f(a)", 5), (4, 7));
2119        assert_eq!(
2120            word_at("let x_1 = f(a)", 7),
2121            (4, 7),
2122            "at the end of a word is in it"
2123        );
2124        assert_eq!(
2125            word_at("let x_1 = f(a)", 11),
2126            (10, 11),
2127            "after `f` is still `f`"
2128        );
2129        assert_eq!(word_at("let x_1 = f(a)", 3), (0, 3));
2130        assert_eq!(word_at("a (b", 2), (1, 3), "a run of non-word characters");
2131        assert_eq!(word_at("", 0), (0, 0));
2132        assert_eq!(word_at("æøå bc", 0), (0, 6));
2133    }
2134
2135    #[test]
2136    fn the_thumb_is_the_rows_share_of_the_lines_and_never_a_sliver() {
2137        // Ten rows of a hundred lines: a tenth of the track.
2138        assert_eq!(thumb_span(1000, 10, 100, 0, 20), (0, 100));
2139        // At the last top the thumb touches the bottom.
2140        assert_eq!(thumb_span(1000, 10, 100, 90, 20), (900, 100));
2141        // Halfway through the tops is halfway down the travel.
2142        assert_eq!(thumb_span(1000, 10, 100, 45, 20), (450, 100));
2143        // A million lines would make a sub-pixel thumb; the floor holds.
2144        assert_eq!(thumb_span(1000, 10, 1_000_000, 0, 20), (0, 20));
2145        // Everything fits: the thumb is the whole track and does not move.
2146        assert_eq!(thumb_span(1000, 50, 20, 0, 20), (0, 1000));
2147        assert_eq!(
2148            thumb_span(0, 10, 100, 5, 20),
2149            (0, 1),
2150            "a track with no height"
2151        );
2152    }
2153
2154    #[test]
2155    fn boundaries_step_over_whole_characters() {
2156        let line = "aæb";
2157        assert_eq!(next_boundary(line, 1), 3);
2158        assert_eq!(prev_boundary(line, 3), 1);
2159        assert_eq!(prev_boundary(line, 0), 0);
2160        assert_eq!(next_boundary(line, 4), 4);
2161    }
2162}