Skip to main content

denise_ui/widgets/
text_input.rs

1//! A single-line editable text field.
2
3use alloc::string::String;
4
5use denise::Pen;
6use denise::{ElementState, InputEvent, KeyCode, Modifiers, Point, Radius, Rect, Role};
7use denise_text::{TextEngine, TextStyle};
8
9use crate::motion::Wake;
10use crate::widget::{
11    Animation, Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
12};
13use crate::widgets::describe::{
14    Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, Value,
15};
16use crate::widgets::style::{
17    Align, CARET_BLINKS_FOR_MS, DOUBLE_CLICK_MS, focus_ring, interactive_pair,
18};
19use crate::widgets::text_area::{ClipboardRequest, is_word};
20
21/// Half-period of the caret blink, in milliseconds.
22const BLINK_MS: u64 = 500;
23
24/// A single-line text field with a caret.
25///
26/// # Selecting
27///
28/// Taking focus selects everything, which is what makes Tab-and-type replace a
29/// setpoint rather than append to it. A press places the caret, a second on the
30/// same spot takes the word under it, a third takes the whole field, and
31/// dragging — with a mouse or a finger — extends from where the press landed.
32/// Shift extends with the arrows, Home and End; ⌘A or Ctrl+A takes everything.
33/// Typing, Backspace and Delete replace what is selected.
34///
35/// # The clipboard
36///
37/// ⌘C, ⌘X and ⌘V — or Ctrl — when the application has said where they go with
38/// [`with_clipboard`](Self::with_clipboard). The widget has no clipboard to
39/// reach, so it asks: copy and cut hand over the text, paste emits
40/// [`ClipboardRequest::Paste`] and the application answers with
41/// [`insert_text`](Self::insert_text). Without that wiring the three keys do
42/// nothing at all, which is what a panel with no clipboard wants.
43///
44/// A **password field refuses copy and cut**. Its whole job is that what is on
45/// the screen cannot be read, and handing the value to the system clipboard
46/// would undo that for one keystroke. Pasting into one is still allowed.
47///
48/// # Moving by word
49///
50/// Ctrl and an arrow move by word, and so does Option on a Mac — the widget
51/// cannot ask which keyboard it is in front of, so it takes both spellings.
52/// Command and an arrow go to the start or the end, which is what Command does
53/// on the machine that has it and what Home and End do everywhere. Shift
54/// extends with all of them.
55///
56/// # What it still does not do
57///
58/// No undo. A kiosk field takes a name, a PIN or a setpoint;
59/// [`TextArea`](super::TextArea) is where an editor's machinery lives.
60///
61/// # Blinking
62///
63/// The caret blinks only while the field has focus: taking focus requests
64/// animation, and losing it makes [`Widget::animate`] answer `None`, which is
65/// how a widget hands the CPU back. An unfocused panel therefore has nothing
66/// running on a timer at all — the difference between a device that idles and
67/// one that keeps a core awake for its whole service life. Typing resets the
68/// phase so the caret stays solid while it is moving.
69///
70/// A blink damages the whole field rather than the caret, because
71/// [`Widget::animate`] reports *that* something changed, not *where*. On a Pi 3
72/// that is 26 kpx twice a second — 58 µs, or 0.35% of one 60 Hz frame — against
73/// the 32 px the caret actually occupies. The 800× coarseness is real and the
74/// cost of removing it is a wider trait; the measurement is why it has not been
75/// paid.
76#[derive(Clone, Debug)]
77pub struct TextInput<M> {
78    text: String,
79    placeholder: String,
80    /// Caret position as a **character** index, not a byte offset.
81    caret: usize,
82    /// First character drawn, for fields wider than their box.
83    first_visible: usize,
84    max_chars: usize,
85    style: TextStyle,
86    radius: Radius,
87    submit: Option<M>,
88    password: bool,
89    blink_epoch: u64,
90    caret_on: bool,
91    /// Whether the field currently has focus, mirrored from the focus events.
92    /// `animate` has no context to ask the tree, and this is what lets it stop
93    /// asking for frames the moment focus moves away.
94    has_focus: bool,
95    /// The other end of the selection, as a character index, or `None` when
96    /// there is only a caret: `anchor..caret` in whichever order they fall.
97    anchor: Option<usize>,
98    /// A press is still down in the field, so moving extends the selection.
99    dragging: bool,
100    /// Where the last press landed, when, and how many have stacked up on that
101    /// spot: one places the caret, two take the word, three take everything.
102    clicks: Option<(usize, u64, u8)>,
103    /// Where copy, cut and paste go, when the application has said.
104    on_clipboard: Option<fn(ClipboardRequest) -> M>,
105}
106
107impl<M> TextInput<M> {
108    /// An empty field.
109    pub fn new() -> Self {
110        Self {
111            text: String::new(),
112            placeholder: String::new(),
113            caret: 0,
114            first_visible: 0,
115            max_chars: 256,
116            style: TextStyle::built_in(16),
117            radius: Radius::Field,
118            submit: None,
119            password: false,
120            blink_epoch: 0,
121            caret_on: true,
122            has_focus: false,
123            anchor: None,
124            dragging: false,
125            clicks: None,
126            on_clipboard: None,
127        }
128    }
129
130    /// Sets the text shown when the field is empty.
131    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
132        self.placeholder = placeholder.into();
133        self
134    }
135
136    /// Sets the message emitted when Enter is pressed.
137    pub fn with_submit(mut self, message: M) -> Self {
138        self.submit = Some(message);
139        self
140    }
141
142    /// Wires copy, cut and paste to the application, which owns the clipboard.
143    ///
144    /// Without this the three keys do nothing: a panel with no window system
145    /// has nowhere to copy to, and a widget that pretended otherwise would be
146    /// lying about where the text went.
147    pub fn with_clipboard(mut self, message: fn(ClipboardRequest) -> M) -> Self {
148        self.on_clipboard = Some(message);
149        self
150    }
151
152    /// Caps the number of characters the field will hold.
153    pub fn with_max_chars(mut self, max: usize) -> Self {
154        self.max_chars = max;
155        self
156    }
157
158    /// Sets the font and size.
159    pub fn with_style(mut self, style: TextStyle) -> Self {
160        self.style = style;
161        self
162    }
163
164    /// Sets the size, keeping the font.
165    pub fn with_size(mut self, size_px: u16) -> Self {
166        self.style.size_px = size_px;
167        self
168    }
169
170    /// The font and size this field draws in.
171    #[inline]
172    pub const fn style(&self) -> TextStyle {
173        self.style
174    }
175
176    /// Draws every character as `*`. The text is still stored in the clear —
177    /// this hides a PIN from someone standing behind the panel, and nothing more.
178    pub fn with_password(mut self, password: bool) -> Self {
179        self.password = password;
180        self
181    }
182
183    /// The current contents.
184    #[inline]
185    pub fn text(&self) -> &str {
186        &self.text
187    }
188
189    /// Replaces the contents, putting the caret at the end.
190    pub fn set_text(&mut self, text: impl Into<String>) {
191        self.text = text.into();
192        self.caret = self.len_chars();
193        self.first_visible = 0;
194        // The anchor indexed text that is no longer there.
195        self.anchor = None;
196    }
197
198    /// Inserts `text` at the caret, replacing the selection: the answer to
199    /// [`ClipboardRequest::Paste`].
200    ///
201    /// **A field is one line**, so this takes what it is given up to the first
202    /// line break and drops the rest, along with any other control characters.
203    /// Pasting three lines into a setpoint has no meaning a widget could guess
204    /// at, and joining them into one would invent a value nobody copied.
205    ///
206    /// What is left is truncated to fit `max_chars` rather than refused: a
207    /// paste one character too long is still mostly what somebody wanted.
208    pub fn insert_text(&mut self, text: &str) {
209        self.delete_selection();
210        let line = text.split(['\n', '\r']).next().unwrap_or_default();
211        let room = self.max_chars.saturating_sub(self.len_chars());
212        let mut at = self.byte_of(self.caret);
213        let mut added = 0;
214        for ch in line.chars().filter(|c| !c.is_control()) {
215            if added == room {
216                break;
217            }
218            self.text.insert(at, ch);
219            at += ch.len_utf8();
220            added += 1;
221        }
222        self.caret += added;
223        self.anchor = None;
224    }
225
226    /// Replaces the font and size.
227    pub fn set_style(&mut self, style: TextStyle) {
228        self.style = style;
229    }
230
231    /// Empties the field.
232    pub fn clear(&mut self) {
233        self.set_text(String::new());
234    }
235
236    /// Caret position, as a character index.
237    #[inline]
238    pub const fn caret(&self) -> usize {
239        self.caret
240    }
241
242    /// The selection as character indices, low end first, or `None` when there
243    /// is only a caret.
244    pub fn selection(&self) -> Option<(usize, usize)> {
245        let anchor = self.anchor.filter(|a| *a != self.caret)?;
246        Some((anchor.min(self.caret), anchor.max(self.caret)))
247    }
248
249    /// The selected text, or `None` when nothing is selected.
250    pub fn selected_text(&self) -> Option<&str> {
251        let (from, to) = self.selection()?;
252        Some(&self.text[self.byte_of(from)..self.byte_of(to)])
253    }
254
255    /// Selects everything, with the caret at the end.
256    pub fn select_all(&mut self) {
257        self.anchor = Some(0);
258        self.caret = self.len_chars();
259    }
260
261    /// Selects `from..to`, clamped to the text, with the caret at `to`.
262    ///
263    /// The pair may be given either way round: the caret lands on `to`, which is
264    /// the end a further Shift-arrow moves.
265    pub fn select_range(&mut self, from: usize, to: usize) {
266        let len = self.len_chars();
267        self.anchor = Some(from.min(len));
268        self.caret = to.min(len);
269    }
270
271    /// Drops the selection, leaving the caret where it is.
272    pub fn clear_selection(&mut self) {
273        self.anchor = None;
274    }
275
276    #[inline]
277    fn len_chars(&self) -> usize {
278        self.text.chars().count()
279    }
280
281    /// Byte offset of character `index`, or the end of the string.
282    fn byte_of(&self, index: usize) -> usize {
283        self.text
284            .char_indices()
285            .nth(index)
286            .map_or(self.text.len(), |(offset, _)| offset)
287    }
288
289    /// Horizontal padding inside the field's bounds.
290    #[inline]
291    const fn pad(&self) -> i32 {
292        self.style.size_px as i32 / 3
293    }
294
295    /// The field's inner rectangle, inside the padding.
296    fn inner(&self, bounds: Rect) -> Rect {
297        Rect::from_edges(
298            bounds.x + self.pad(),
299            bounds.y,
300            bounds.right() - self.pad(),
301            bounds.bottom(),
302        )
303    }
304
305    /// Width of characters `from..to` as they are displayed.
306    ///
307    /// Measured rather than counted. With a proportional font a caret placed by
308    /// multiplying an index by an advance is wrong everywhere except after the
309    /// first character, and wrong in a way that looks like a rendering glitch
310    /// rather than an arithmetic mistake.
311    fn run_width(&self, engine: &mut TextEngine, from: usize, to: usize) -> i32 {
312        if from >= to {
313            return 0;
314        }
315        if self.password {
316            return engine.measure_line(self.style, "*") * (to - from) as i32;
317        }
318        let (start, end) = (self.byte_of(from), self.byte_of(to));
319        engine.measure_line(self.style, &self.text[start..end])
320    }
321
322    /// First character to draw, given where the caret is and how wide the box is.
323    fn window_start(&self, engine: &mut TextEngine, bounds: Rect) -> usize {
324        let available = self.inner(bounds).width;
325        let mut first = self.first_visible.min(self.caret);
326        // Walks rather than bisects: a kiosk field holds a name or a setpoint, and
327        // the loop runs once per character that scrolled off since last frame,
328        // which is almost always one.
329        while first < self.caret && self.run_width(engine, first, self.caret) > available {
330            first += 1;
331        }
332        first
333    }
334
335    /// Horizontal offset of the caret from the field's left edge.
336    ///
337    /// Measured through the engine rather than counted as characters times a
338    /// width, which is the only thing that works with a proportional font.
339    pub fn caret_x(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
340        let first = self.window_start(engine, bounds);
341        self.pad() + self.run_width(engine, first, self.caret)
342    }
343
344    fn scroll_to_caret(&mut self, engine: &mut TextEngine, bounds: Rect) {
345        self.first_visible = self.window_start(engine, bounds);
346    }
347
348    /// Restarts the blink so the caret is solid while it is being moved, and
349    /// asks to animate again, since a caret that blinked its fill has stopped.
350    fn wake_caret(&mut self, ctx: &mut EventCtx<'_, M>) {
351        self.blink_epoch = ctx.now_ms;
352        self.caret_on = true;
353        ctx.request_animation();
354    }
355
356    fn insert(&mut self, ch: char) -> bool {
357        if self.len_chars() >= self.max_chars {
358            return false;
359        }
360        let at = self.byte_of(self.caret);
361        self.text.insert(at, ch);
362        self.caret += 1;
363        // An edit collapses the selection. Leaving the anchor where it was made
364        // the character just typed look selected, so the next keystroke
365        // replaced it — a field that kept only its last letter.
366        self.anchor = None;
367        true
368    }
369
370    fn delete_before(&mut self) -> bool {
371        if self.caret == 0 {
372            return false;
373        }
374        let at = self.byte_of(self.caret - 1);
375        self.text.remove(at);
376        self.caret -= 1;
377        self.anchor = None;
378        true
379    }
380
381    fn delete_after(&mut self) -> bool {
382        if self.caret >= self.len_chars() {
383            return false;
384        }
385        let at = self.byte_of(self.caret);
386        self.text.remove(at);
387        self.anchor = None;
388        true
389    }
390
391    /// Removes the selection, reporting whether there was one. The caret lands
392    /// where the selection started, which is where typing continues from.
393    fn delete_selection(&mut self) -> bool {
394        let Some((from, to)) = self.selection() else {
395            return false;
396        };
397        let (start, end) = (self.byte_of(from), self.byte_of(to));
398        self.text.replace_range(start..end, "");
399        self.caret = from;
400        self.anchor = None;
401        true
402    }
403
404    /// Moves the caret, extending the selection or dropping it.
405    fn move_to(&mut self, to: usize, extend: bool) {
406        if extend {
407            self.anchor.get_or_insert(self.caret);
408        } else {
409            self.anchor = None;
410        }
411        self.caret = to.min(self.len_chars());
412    }
413
414    /// The character index nearest `x`, which is where a press puts the caret.
415    ///
416    /// Nearest, not "the character containing it": a press in the right half of
417    /// a letter belongs after it. Measured through the engine one character at a
418    /// time, the same way [`caret_x`](Self::caret_x) measures, so the caret
419    /// lands exactly where the press was drawn to be.
420    fn index_at(&self, engine: &mut TextEngine, bounds: Rect, x: i32) -> usize {
421        let inner = self.inner(bounds);
422        let first = self.window_start(engine, bounds);
423        let target = x - inner.x;
424        if target <= 0 {
425            return first;
426        }
427        let len = self.len_chars();
428        let mut index = first;
429        let mut before = 0;
430        while index < len {
431            let after = self.run_width(engine, first, index + 1);
432            if target < (before + after) / 2 {
433                break;
434            }
435            before = after;
436            index += 1;
437        }
438        index
439    }
440
441    /// The run of word characters — or of non-word ones — around `index`.
442    ///
443    /// The word before wins at its end, so a double-click just past the last
444    /// letter takes the word rather than the space after it. Same rule as
445    /// [`TextArea`](super::TextArea), because it is the same gesture.
446    fn word_bounds(&self, index: usize) -> (usize, usize) {
447        let len = self.len_chars();
448        let index = index.min(len);
449        let at = |i: usize| self.text.chars().nth(i);
450        let class = match (index.checked_sub(1).and_then(&at), at(index)) {
451            (Some(b), _) if is_word(b) => true,
452            (_, Some(a)) => is_word(a),
453            (Some(b), None) => is_word(b),
454            (None, None) => return (index, index),
455        };
456        let mut start = index;
457        while start > 0 && at(start - 1).is_some_and(|c| is_word(c) == class) {
458            start -= 1;
459        }
460        let mut end = index;
461        while end < len && at(end).is_some_and(|c| is_word(c) == class) {
462            end += 1;
463        }
464        (start, end)
465    }
466
467    /// The start of the word to the left of `from`.
468    ///
469    /// Skips what is not a word and then the word itself, so the caret lands
470    /// where the word begins and pressing again walks to the one before it.
471    fn word_left(&self, from: usize) -> usize {
472        let at = |i: usize| self.text.chars().nth(i);
473        let mut index = from;
474        while index > 0 && at(index - 1).is_some_and(|c| !is_word(c)) {
475            index -= 1;
476        }
477        while index > 0 && at(index - 1).is_some_and(is_word) {
478            index -= 1;
479        }
480        index
481    }
482
483    /// The end of the word to the right of `from`, by the mirror of that rule.
484    fn word_right(&self, from: usize) -> usize {
485        let len = self.len_chars();
486        let at = |i: usize| self.text.chars().nth(i);
487        let mut index = from;
488        while index < len && at(index).is_some_and(|c| !is_word(c)) {
489            index += 1;
490        }
491        while index < len && at(index).is_some_and(is_word) {
492            index += 1;
493        }
494        index
495    }
496
497    /// What a press at `index` means, given what came before it.
498    ///
499    /// One places the caret, two takes the word, three takes everything, and a
500    /// fourth starts the count again — so holding a finger down and tapping
501    /// cycles rather than sticking on "everything". The spot has to match: a
502    /// second press a few characters away is a new first press, not a double.
503    fn click_count(&mut self, index: usize, now_ms: u64) -> u8 {
504        let count = match self.clicks {
505            Some((at, when, count))
506                if at == index && now_ms.saturating_sub(when) <= DOUBLE_CLICK_MS =>
507            {
508                count % 3 + 1
509            }
510            _ => 1,
511        };
512        self.clicks = Some((index, now_ms, count));
513        count
514    }
515
516    /// A press or a tap: places the caret, takes a word, or takes everything.
517    fn press(&mut self, position: Point, extend: bool, ctx: &mut EventCtx<'_, M>) -> Handled {
518        let bounds = ctx.bounds;
519        let index = self.index_at(ctx.text, bounds, position.x);
520        match self.click_count(index, ctx.now_ms) {
521            2 => {
522                let (start, end) = self.word_bounds(index);
523                self.anchor = Some(start);
524                self.caret = end;
525                self.dragging = false;
526            }
527            3 => {
528                self.select_all();
529                self.dragging = false;
530            }
531            _ => {
532                self.move_to(index, extend);
533                self.dragging = true;
534            }
535        }
536        self.wake_caret(ctx);
537        self.scroll_to_caret(ctx.text, bounds);
538        Handled::Yes
539    }
540
541    /// Copy or cut the selection, when there is one and the application asked
542    /// to be told.
543    fn clipboard(&mut self, ctx: &mut EventCtx<'_, M>, cut: bool) -> Handled {
544        let Some(request) = self.on_clipboard else {
545            return Handled::No;
546        };
547        // See the type's docs: a password field does not hand its value over.
548        if self.password {
549            return Handled::No;
550        }
551        let Some(text) = self.selected_text().map(String::from) else {
552            return Handled::No;
553        };
554        if cut {
555            self.delete_selection();
556            ctx.emit(request(ClipboardRequest::Cut(text)));
557            self.wake_caret(ctx);
558            let bounds = ctx.bounds;
559            self.scroll_to_caret(ctx.text, bounds);
560            return Handled::Yes;
561        }
562        ctx.emit(request(ClipboardRequest::Copy(text)));
563        Handled::Yes
564    }
565
566    /// The pointer moved with the press still down: the selection follows it.
567    fn drag(&mut self, position: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
568        let bounds = ctx.bounds;
569        let index = self.index_at(ctx.text, bounds, position.x);
570        if index == self.caret {
571            return Handled::No;
572        }
573        self.move_to(index, true);
574        self.wake_caret(ctx);
575        self.scroll_to_caret(ctx.text, bounds);
576        Handled::Yes
577    }
578}
579
580impl<M> Default for TextInput<M> {
581    fn default() -> Self {
582        Self::new()
583    }
584}
585
586impl<M: Clone + 'static> Widget<M> for TextInput<M> {
587    fn describe(&self) -> Option<&dyn DynDescribe> {
588        Some(self)
589    }
590
591    fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
592        Some(self)
593    }
594    fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
595        // A field is as wide as you make it — that is what a field is — but its
596        // height is one line of its own text in a field-sized box.
597        let line = ctx.text.line_height(self.style);
598        Measured::tall(line.max(ctx.theme.metrics.size_field).max(1))
599    }
600
601    fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
602        let radius = ctx.theme.radius(self.radius);
603        let disabled = ctx.state.contains(VisualState::DISABLED);
604        let focused = ctx.state.contains(VisualState::FOCUSED);
605        let (background, _) = interactive_pair(ctx.theme, Role::Base100, ctx.state);
606        canvas.fill_rounded_rect(ctx.bounds, radius, background);
607        canvas.stroke_rounded_rect(ctx.bounds, radius, 1, ctx.theme.color(Role::Base300));
608        if focused {
609            focus_ring(ctx.theme, ctx.bounds, radius, canvas);
610        }
611
612        let inner = self.inner(ctx.bounds);
613        let line_height = ctx.text.line_height(self.style);
614        let top = inner.y + Align::Center.offset(inner.height, line_height);
615        // Text is clipped to the inner box, so a value longer than the field
616        // scrolls under the border rather than over it.
617        let mut clipped = canvas.with_clip(inner);
618
619        // Under the text, and only while the field has focus: a highlight on a
620        // field nobody is typing into reads as a field that still has the
621        // keyboard. The selection itself is kept, so focus coming back shows it.
622        if focused
623            && !disabled
624            && let Some((from, to)) = self.selection()
625        {
626            let first = self.window_start(ctx.text, ctx.bounds);
627            let start = from.max(first);
628            if to > start {
629                let x0 = inner.x + self.run_width(ctx.text, first, start);
630                let x1 = inner.x + self.run_width(ctx.text, first, to);
631                clipped.fill_rect(
632                    Rect::new(x0, top, x1 - x0, line_height),
633                    ctx.theme.color(Role::Accent).with_alpha(60),
634                );
635            }
636        }
637
638        if self.text.is_empty() {
639            if !self.placeholder.is_empty() {
640                let hint = ctx
641                    .theme
642                    .color(Role::Base300)
643                    .mix(ctx.theme.color(Role::BaseContent), 128);
644                ctx.text.draw(
645                    &mut clipped,
646                    self.style,
647                    Point::new(inner.x, top),
648                    &self.placeholder,
649                    hint,
650                );
651            }
652        } else {
653            let content = if disabled {
654                ctx.theme.color(Role::Base300)
655            } else {
656                ctx.theme.color(Role::BaseContent)
657            };
658            let first = self.window_start(ctx.text, ctx.bounds);
659            if self.password {
660                // Drawn one at a time rather than by building a string of stars,
661                // because a paint path that allocates is a paint path that can
662                // fail on a device with no memory left.
663                let advance = ctx.text.measure_line(self.style, "*");
664                let count = self.len_chars().saturating_sub(first);
665                for i in 0..count {
666                    let x = inner.x + advance * i as i32;
667                    if x > inner.right() {
668                        break;
669                    }
670                    ctx.text
671                        .draw(&mut clipped, self.style, Point::new(x, top), "*", content);
672                }
673            } else {
674                let start = self.byte_of(first);
675                ctx.text.draw(
676                    &mut clipped,
677                    self.style,
678                    Point::new(inner.x, top),
679                    &self.text[start..],
680                    content,
681                );
682            }
683        }
684
685        if focused && self.caret_on && !disabled {
686            let first = self.window_start(ctx.text, ctx.bounds);
687            let x = inner.x + self.run_width(ctx.text, first, self.caret);
688            let width = (i32::from(self.style.size_px) / 10).max(1);
689            clipped.fill_rect(
690                Rect::new(x, top, width, line_height),
691                ctx.theme.color(Role::Accent),
692            );
693        }
694    }
695
696    fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
697        match event {
698            Event::FocusGained => {
699                self.has_focus = true;
700                // Tabbing into a field offers its contents for replacement,
701                // which is what makes Tab-and-type work on a setpoint. A press
702                // is delivered *after* the focus it caused, so clicking into a
703                // field still collapses this to a caret where the finger landed.
704                self.select_all();
705                self.wake_caret(ctx);
706                // Not `Handled`: nothing was consumed. The tree already repaints
707                // on a focus change, so the caret appearing is covered.
708                Handled::No
709            }
710            Event::FocusLost => {
711                self.has_focus = false;
712                self.wake_caret(ctx);
713                Handled::No
714            }
715            // A finger is a pointer here: the same three counts, because a
716            // panel with an on-screen keyboard has no other way to take a word.
717            Event::Input(InputEvent::PointerButton {
718                state: ElementState::Down,
719                position,
720                modifiers,
721                ..
722            }) => self.press(*position, modifiers.contains(Modifiers::SHIFT), ctx),
723            Event::Input(InputEvent::TouchDown { position, .. }) => {
724                self.press(*position, false, ctx)
725            }
726            Event::Input(InputEvent::PointerMoved { position })
727            | Event::Input(InputEvent::TouchMoved { position, .. })
728                if self.dragging =>
729            {
730                self.drag(*position, ctx)
731            }
732            // The release ends the drag, and so does the tree taking the press
733            // away — a scene pushed over the field, or its node hidden. That
734            // arrives as `PressCancelled` and as no pointer event at all, and a
735            // drag left armed would follow the next hover across the field.
736            Event::Input(InputEvent::PointerButton {
737                state: ElementState::Up,
738                ..
739            })
740            | Event::Input(InputEvent::TouchUp { .. })
741            | Event::PressCancelled => {
742                self.dragging = false;
743                Handled::No
744            }
745            Event::Input(InputEvent::Text { ch }) if !ch.is_control() => {
746                // What is selected is what typing replaces.
747                let replaced = self.delete_selection();
748                let inserted = self.insert(*ch);
749                if replaced || inserted {
750                    self.wake_caret(ctx);
751                    let bounds = ctx.bounds;
752                    self.scroll_to_caret(ctx.text, bounds);
753                    Handled::Yes
754                } else {
755                    Handled::No
756                }
757            }
758            Event::Input(InputEvent::Key {
759                code,
760                state: ElementState::Down,
761                modifiers,
762                ..
763            }) => {
764                let extend = modifiers.contains(Modifiers::SHIFT);
765                // Ctrl on the desktops that use it, Command on the one that does
766                // not; a panel with a bare keyboard has neither and needs
767                // neither. Named as `TextArea` names it.
768                let primary =
769                    modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::SUPER);
770                // Ctrl on Windows and Linux, Option on a Mac: the two spellings
771                // of "by word", taken together because the widget cannot ask
772                // which keyboard it is in front of. Command is the Mac's "to the
773                // end", and a field is one line, so that is Home and End.
774                let by_word =
775                    modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::ALT);
776                let to_end = modifiers.contains(Modifiers::SUPER);
777                match code {
778                    KeyCode::A if primary => self.select_all(),
779                    KeyCode::C if primary => return self.clipboard(ctx, false),
780                    KeyCode::X if primary => return self.clipboard(ctx, true),
781                    KeyCode::V if primary => {
782                        let Some(request) = self.on_clipboard else {
783                            return Handled::No;
784                        };
785                        ctx.emit(request(ClipboardRequest::Paste));
786                        return Handled::Yes;
787                    }
788                    // A selection is what Backspace and Delete take first; only
789                    // an empty one falls through to the character either side.
790                    KeyCode::Backspace => {
791                        if !self.delete_selection() {
792                            self.delete_before();
793                        }
794                    }
795                    KeyCode::Delete => {
796                        if !self.delete_selection() {
797                            self.delete_after();
798                        }
799                    }
800                    // An arrow with a selection and no Shift collapses to that
801                    // end rather than moving from the caret, which is what puts
802                    // the caret back where a person is looking.
803                    KeyCode::ArrowLeft if to_end => self.move_to(0, extend),
804                    KeyCode::ArrowRight if to_end => {
805                        let end = self.len_chars();
806                        self.move_to(end, extend);
807                    }
808                    KeyCode::ArrowLeft if by_word => {
809                        let to = self.word_left(self.caret);
810                        self.move_to(to, extend);
811                    }
812                    KeyCode::ArrowRight if by_word => {
813                        let to = self.word_right(self.caret);
814                        self.move_to(to, extend);
815                    }
816                    KeyCode::ArrowLeft => match self.selection() {
817                        Some((from, _)) if !extend => {
818                            self.caret = from;
819                            self.anchor = None;
820                        }
821                        _ => {
822                            let to = self.caret.saturating_sub(1);
823                            self.move_to(to, extend);
824                        }
825                    },
826                    KeyCode::ArrowRight => match self.selection() {
827                        Some((_, to)) if !extend => {
828                            self.caret = to;
829                            self.anchor = None;
830                        }
831                        _ => {
832                            let to = self.caret.saturating_add(1);
833                            self.move_to(to, extend);
834                        }
835                    },
836                    KeyCode::Home => self.move_to(0, extend),
837                    KeyCode::End => {
838                        let end = self.len_chars();
839                        self.move_to(end, extend);
840                    }
841                    KeyCode::Enter | KeyCode::NumpadEnter => {
842                        if let Some(message) = self.submit.clone() {
843                            ctx.emit(message);
844                        }
845                        // Consumed either way: Enter in a field must not fall
846                        // through and activate something else.
847                        return Handled::Yes;
848                    }
849                    _ => return Handled::No,
850                }
851                self.wake_caret(ctx);
852                let bounds = ctx.bounds;
853                self.scroll_to_caret(ctx.text, bounds);
854                // Even a caret move that changed nothing must repaint, because the
855                // caret itself is pixels.
856                Handled::Yes
857            }
858            _ => Handled::No,
859        }
860    }
861
862    fn accepts_pointer(&self) -> bool {
863        true
864    }
865
866    fn focusable(&self) -> bool {
867        true
868    }
869
870    fn animate(&mut self, now_ms: u64) -> Animation {
871        if !self.has_focus {
872            // Blinking is for the field being typed into. Answering `None` is
873            // what takes this widget out of the animating set — the caret is
874            // not drawn without focus, so there is nothing left to repaint.
875            return Animation::NONE;
876        }
877        let elapsed = now_ms.saturating_sub(self.blink_epoch);
878        if elapsed >= CARET_BLINKS_FOR_MS {
879            // Lit, and asleep until the caret is next moved.
880            let repaint = !self.caret_on;
881            self.caret_on = true;
882            return Animation {
883                repaint,
884                next: Wake::Never,
885            };
886        }
887        let on = (elapsed / BLINK_MS).is_multiple_of(2);
888        let repaint = on != self.caret_on;
889        self.caret_on = on;
890        Animation {
891            repaint,
892            // A deadline, not a frame rate: the caret flips at the end of each
893            // blink and wants exactly one wake to do it. Halving the tree's
894            // animation rate must not halve the blink, and turning motion off
895            // must not stop it — a caret gone out mid-blink is a field that
896            // looks like it has lost focus. (Resting after a while of nobody
897            // typing is different: it rests lit.)
898            //
899            // Saturating, because `now_ms` is the application's clock and this
900            // widget does not get to assume anything about it. A host that
901            // counts from the Unix epoch, or a fuzzer that passes `u64::MAX`,
902            // must not be able to panic a panel through the caret blink.
903            next: Wake::At(
904                self.blink_epoch.saturating_add(
905                    (elapsed / BLINK_MS)
906                        .saturating_add(1)
907                        .saturating_mul(BLINK_MS),
908                ),
909            ),
910        }
911    }
912
913    /// Blinking is a schedule, so it survives [`Motion::None`](crate::Motion)
914    /// unchanged — there is nothing to land, and stopping it would be a
915    /// regression dressed up as a preference.
916    fn snap(&mut self, now_ms: u64) -> Animation {
917        Widget::<M>::animate(self, now_ms)
918    }
919}
920
921impl<M> Describe for TextInput<M> {
922    const KIND: &'static str = "text-input";
923    const DOC: &'static str = "A line of text somebody types into.";
924    const GROUP: Group = Group::Input;
925    const ICON: &'static denise::icon::Icon = &super::icons::TEXT_INPUT;
926
927    const PROPERTIES: &'static [Property] = &[
928        Property::new("text", PropertyKind::Text, "Initial contents."),
929        Property::new(
930            "placeholder",
931            PropertyKind::Text,
932            "Shown while the field is empty.",
933        ),
934        Property::new(
935            "on-submit",
936            PropertyKind::Message(Payload::None),
937            "The message emitted on Enter.",
938        ),
939        Property::new(
940            "max-chars",
941            PropertyKind::Int { min: 1, max: 4096 },
942            "How many characters the field will hold.",
943        ),
944        Property::new(
945            "password",
946            PropertyKind::Bool,
947            "Draw every character as `*`. The text is still stored in the clear.",
948        ),
949        Property::new(
950            "size",
951            PropertyKind::Int { min: 6, max: 96 },
952            "Text size in logical pixels.",
953        )
954        .in_pixels(),
955    ];
956
957    fn get(&self, name: &str) -> Option<Value> {
958        Some(match name {
959            "text" => Value::text(self.text.as_str()),
960            "placeholder" => Value::text(self.placeholder.as_str()),
961            // The message is the application's, and this crate has never seen
962            // its type. See the `describe` module docs.
963            "on-submit" => return None,
964            "max-chars" => Value::Int(i32::try_from(self.max_chars).unwrap_or(i32::MAX)),
965            "password" => Value::Bool(self.password),
966            "size" => Value::Int(i32::from(self.style.size_px)),
967            _ => return None,
968        })
969    }
970
971    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
972        match name {
973            // Through the setter, which puts the caret at the end and resets the
974            // window: assigning the field would leave a caret pointing into text
975            // that is no longer there.
976            "text" => self.set_text(value.as_text()?),
977            "placeholder" => self.placeholder = value.as_text()?,
978            "on-submit" => return Err(Mismatch::Supplied),
979            "max-chars" => self.max_chars = value.as_index()?,
980            "password" => self.password = value.as_bool()?,
981            "size" => self.style.size_px = value.as_size()?,
982            _ => return Err(Mismatch::Unknown),
983        }
984        Ok(())
985    }
986}