1use std::rc::Rc;
2
3use geometry_core::Rect;
4use layout_core::{LayoutError, LayoutStyle};
5use platform_core::{Event, Key, ModifiersState, NamedKey, PointerButton};
6use reactive_core::{RwSignal, signal};
7use renderer_core::{Color, Paint, RectStyle, ShapeStyle, TextStyle};
8use ui_tree::{Component, EventResult, RenderNode};
9
10use crate::focus::{self, FocusId};
11use crate::impl_leaf_widget;
12use crate::layout_leaf::LayoutLeaf;
13
14const CARET_WIDTH: f32 = 1.5;
16
17pub struct Input {
23 value: RwSignal<String>,
24 caret: RwSignal<usize>,
27 anchor: RwSignal<Option<usize>>,
30 style: Rc<dyn Fn() -> TextStyle>,
31 id: FocusId,
32 leaf: LayoutLeaf,
33 on_submit: Option<Box<dyn Fn()>>,
34 placeholder: String,
37 mask: Option<char>,
40}
41
42impl Input {
43 pub fn new(
44 value: RwSignal<String>,
45 layout_style: LayoutStyle,
46 style_fn: impl Fn() -> TextStyle + 'static,
47 ) -> Result<Self, LayoutError> {
48 let leaf = LayoutLeaf::register(layout_style)?;
49 let caret = value.with(|s| s.len());
50 let id = focus::next_id();
51 focus::register_at(id, focus::FocusKind::TextEntry, leaf.node);
54 Ok(Self {
55 value,
56 caret: signal(caret),
57 anchor: signal(None),
58 style: Rc::new(style_fn),
59 id,
60 leaf,
61 on_submit: None,
62 placeholder: String::new(),
63 mask: None,
64 })
65 }
66
67 pub fn masked(mut self, bullet: char) -> Self {
73 self.mask = Some(bullet);
74 self
75 }
76
77 pub fn secret(self) -> Self {
79 self.masked('•')
80 }
81
82 fn shown(&self, text: &str) -> String {
84 match self.mask {
85 Some(bullet) => text.chars().map(|_| bullet).collect(),
86 None => text.to_string(),
87 }
88 }
89
90 pub fn on_submit(mut self, f: impl Fn() + 'static) -> Self {
92 self.on_submit = Some(Box::new(f));
93 self
94 }
95
96 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
99 self.placeholder = p.into();
100 self
101 }
102
103 pub fn autofocus(self) -> Self {
111 focus::request(self.id);
112 self
113 }
114
115 fn caret_at(&self, text: &str) -> usize {
117 let mut c = self.caret.get().min(text.len());
118 while c > 0 && !text.is_char_boundary(c) {
119 c -= 1;
120 }
121 c
122 }
123
124 fn selection(&self, text: &str) -> Option<(usize, usize)> {
127 let caret = self.caret_at(text);
128 let mut anchor = self.anchor.get()?.min(text.len());
129 while anchor > 0 && !text.is_char_boundary(anchor) {
130 anchor -= 1;
131 }
132 (anchor != caret).then(|| (anchor.min(caret), anchor.max(caret)))
133 }
134
135 fn selected_text(&self, text: &str) -> Option<String> {
137 self.selection(text)
138 .map(|(from, to)| text[from..to].to_string())
139 }
140
141 fn take_selection(&self, text: &mut String) -> Option<usize> {
145 let (from, to) = self.selection(text)?;
146 text.replace_range(from..to, "");
147 Some(from)
148 }
149
150 fn edit(&mut self, key: &Key, mods: &ModifiersState) -> EventResult {
153 let mut text = self.value.get();
154 let mut caret = self.caret_at(&text);
155 let chord = mods.is_ctrl || mods.is_meta;
156 let mut anchor = if mods.is_shift {
159 Some(self.anchor.get().unwrap_or(caret))
160 } else {
161 None
162 };
163 match key {
164 Key::Char('a') | Key::Char('A') if chord => {
165 anchor = Some(0);
166 caret = text.len();
167 }
168 Key::Char('c') | Key::Char('C') if chord => {
169 let Some(selected) = self.selected_text(&text) else {
170 return EventResult::Ignored;
171 };
172 services_core::set_clipboard_text(&selected);
173 return EventResult::Handled;
175 }
176 Key::Char('x') | Key::Char('X') if chord => {
177 let Some(selected) = self.selected_text(&text) else {
178 return EventResult::Ignored;
179 };
180 services_core::set_clipboard_text(&selected);
181 caret = self.take_selection(&mut text).unwrap_or(caret);
182 }
183 Key::Char('v') | Key::Char('V') if chord => {
184 let Some(pasted) = services_core::clipboard_text() else {
185 return EventResult::Ignored;
186 };
187 let pasted = pasted.lines().next().unwrap_or_default().to_string();
190 let had_selection = self.take_selection(&mut text);
191 if pasted.is_empty() && had_selection.is_none() {
192 return EventResult::Ignored;
193 }
194 caret = had_selection.unwrap_or(caret);
195 text.insert_str(caret, &pasted);
196 caret += pasted.len();
197 }
198 Key::Char(_) if chord => return EventResult::Ignored,
200 Key::Char(c) if !c.is_control() => {
201 caret = self.take_selection(&mut text).unwrap_or(caret);
202 text.insert(caret, *c);
203 caret += c.len_utf8();
204 }
205 Key::Named(NamedKey::Space) => {
206 caret = self.take_selection(&mut text).unwrap_or(caret);
207 text.insert(caret, ' ');
208 caret += 1;
209 }
210 Key::Named(NamedKey::Backspace) => {
212 if let Some(at) = self.take_selection(&mut text) {
213 caret = at;
214 } else {
215 if caret == 0 {
216 return EventResult::Ignored;
217 }
218 let prev = prev_boundary(&text, caret);
219 text.replace_range(prev..caret, "");
220 caret = prev;
221 }
222 }
223 Key::Named(NamedKey::Delete) => {
224 if let Some(at) = self.take_selection(&mut text) {
225 caret = at;
226 } else {
227 if caret >= text.len() {
228 return EventResult::Ignored;
229 }
230 let next = next_boundary(&text, caret);
231 text.replace_range(caret..next, "");
232 }
233 }
234 Key::Named(NamedKey::ArrowLeft) => {
237 caret = match self.selection(&text) {
238 Some((from, _)) if !mods.is_shift => from,
239 _ => prev_boundary(&text, caret),
240 }
241 }
242 Key::Named(NamedKey::ArrowRight) => {
243 caret = match self.selection(&text) {
244 Some((_, to)) if !mods.is_shift => to,
245 _ => next_boundary(&text, caret),
246 }
247 }
248 Key::Named(NamedKey::Home) => caret = 0,
249 Key::Named(NamedKey::End) => caret = text.len(),
250 Key::Named(NamedKey::Enter) => {
251 if let Some(cb) = &self.on_submit {
252 cb();
253 }
254 return EventResult::Handled;
255 }
256 Key::Named(NamedKey::Escape) => {
257 focus::release(self.id);
258 return EventResult::Handled;
259 }
260 Key::Named(NamedKey::Tab) => {
262 if mods.is_shift {
263 focus::focus_prev();
264 } else {
265 focus::focus_next();
266 }
267 return EventResult::Handled;
268 }
269 _ => return EventResult::Ignored,
270 }
271 if self.value.with(|s| s != &text) {
273 self.value.set(text);
274 }
275 self.caret.set(caret);
276 self.anchor.set(anchor.filter(|a| *a != caret));
279 EventResult::Handled
280 }
281}
282
283impl Component for Input {
284 fn view(&self) -> RenderNode {
285 let r = self.leaf.rect.get();
286 let text = self.value.get();
287 let style = (self.style)();
288 let full = Rect {
289 x: 0.0,
290 y: 0.0,
291 width: r.width,
292 height: r.height,
293 };
294 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
297 let muted = match style.paint {
298 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
299 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
300 };
301 let mut ph_style = style;
302 ph_style.paint = muted;
303 RenderNode::text(self.placeholder.clone(), full, ph_style)
304 } else {
305 RenderNode::text(self.shown(&text), full, style)
306 };
307
308 if focus::is_focused(self.id) {
310 let caret = self.caret_at(&text);
311 let highlight = self.selection(&text).map(|(from, to)| {
315 let measure = |upto: usize| {
316 crate::text_metrics::measure_text(&self.shown(&text[..upto]), 1.0e6, &style).0
317 };
318 let (start, end) = (measure(from), measure(to));
319 let fill = match style.paint {
320 Paint::Solid(c) => c.with_alpha(0.25),
321 _ => Color::rgba(0.4, 0.6, 0.9, 0.3),
322 };
323 RenderNode::rect(
324 Rect {
325 x: start,
326 y: 0.0,
327 width: (end - start).max(1.0),
328 height: crate::text_metrics::line_height(style.font_size),
329 },
330 RectStyle::default().with_fill(Paint::Solid(fill)),
331 )
332 });
333 let prefix = self.shown(&text[..caret]);
336 let (prefix_w, _) = crate::text_metrics::measure_text(&prefix, 1.0e6, &style);
337 let line_h = crate::text_metrics::line_height(style.font_size);
338 let caret_rect = Rect {
339 x: prefix_w,
340 y: 0.0,
341 width: CARET_WIDTH,
342 height: line_h,
343 };
344 let caret_node =
345 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
346 let layers = match highlight {
347 Some(highlight) => vec![highlight, text_node, caret_node],
348 None => vec![text_node, caret_node],
349 };
350 self.leaf.at_layout_position(RenderNode::group(layers))
351 } else {
352 self.leaf.at_layout_position(text_node)
353 }
354 }
355
356 fn on_event(&mut self, event: &Event) -> EventResult {
357 let rect = self.leaf.rect.get();
358 match event {
359 Event::PointerPressed {
360 x,
361 y,
362 button: PointerButton::Primary,
363 ..
364 } => {
365 if rect.contains(*x as f32, *y as f32) {
366 focus::request_from_pointer(self.id);
367 self.caret.set(self.value.with(|s| s.len()));
370 self.anchor.set(None);
371 EventResult::Handled
372 } else {
373 EventResult::Ignored
374 }
375 }
376 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
377 self.edit(key, modifiers)
378 }
379 _ => EventResult::Ignored,
380 }
381 }
382
383 fn debug_name(&self) -> &'static str {
384 "Input"
385 }
386}
387
388impl Drop for Input {
389 fn drop(&mut self) {
390 focus::unregister(self.id);
392 }
393}
394
395impl_leaf_widget!(Input);
396
397fn prev_boundary(s: &str, i: usize) -> usize {
399 let mut j = i.min(s.len());
400 if j == 0 {
401 return 0;
402 }
403 j -= 1;
404 while j > 0 && !s.is_char_boundary(j) {
405 j -= 1;
406 }
407 j
408}
409
410fn next_boundary(s: &str, i: usize) -> usize {
412 let mut j = (i + 1).min(s.len());
413 while j < s.len() && !s.is_char_boundary(j) {
414 j += 1;
415 }
416 j
417}
418
419#[cfg(test)]
420mod tests {
421 use crate::context::reset_layout_runtime;
422 use layout_core::AvailableSpace;
423 use platform_core::PointerSource;
424 use renderer_core::Color;
425
426 use super::*;
427 use crate::context::{compute_layout, new_container};
428 use crate::layout_item::LayoutItem;
429
430 fn key(k: Key) -> Event {
431 Event::KeyPressed {
432 key: k,
433 modifiers: ModifiersState::default(),
434 }
435 }
436
437 fn chord(k: Key) -> Event {
438 Event::KeyPressed {
439 key: k,
440 modifiers: ModifiersState {
441 is_ctrl: true,
442 ..ModifiersState::default()
443 },
444 }
445 }
446
447 fn shifted(k: Key) -> Event {
448 Event::KeyPressed {
449 key: k,
450 modifiers: ModifiersState {
451 is_shift: true,
452 ..ModifiersState::default()
453 },
454 }
455 }
456
457 #[test]
458 fn shift_arrows_grow_a_selection_and_a_plain_one_drops_it() {
459 let (mut input, _) = focused_input("hello");
460 input.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
461 input.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
462 assert_eq!(input.selection("hello"), Some((3, 5)), "two chars selected");
463 input.on_event(&key(Key::Named(NamedKey::ArrowRight)));
464 assert_eq!(input.selection("hello"), None, "a plain arrow drops it");
465 }
466
467 #[test]
469 fn typing_over_a_selection_replaces_it() {
470 let (mut input, value) = focused_input("hello");
471 input.on_event(&chord(Key::Char('a')));
472 input.on_event(&key(Key::Char('x')));
473 assert_eq!(value.get(), "x");
474 }
475
476 #[test]
477 fn backspace_takes_the_selection_rather_than_one_character() {
478 let (mut input, value) = focused_input("hello");
479 input.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
480 input.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
481 input.on_event(&key(Key::Named(NamedKey::Backspace)));
482 assert_eq!(value.get(), "hel");
483 }
484
485 #[test]
488 fn a_plain_arrow_collapses_to_the_selection_edge() {
489 let (mut input, _) = focused_input("hello");
490 input.on_event(&chord(Key::Char('a')));
491 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
492 assert_eq!(input.caret.get(), 0, "collapsed to the low edge");
493 }
494
495 #[test]
496 fn cut_removes_the_selection_and_copy_leaves_it() {
497 let (mut input, value) = focused_input("hello");
498 input.on_event(&chord(Key::Char('a')));
499 input.on_event(&chord(Key::Char('c')));
500 assert_eq!(value.get(), "hello", "copy leaves the text alone");
501 assert_eq!(input.selection("hello"), Some((0, 5)), "and the selection");
502 input.on_event(&chord(Key::Char('x')));
503 assert_eq!(value.get(), "", "cut takes it");
504 }
505
506 #[test]
509 fn copy_without_a_selection_is_not_consumed() {
510 let (mut input, _) = focused_input("hello");
511 assert_eq!(input.on_event(&chord(Key::Char('c'))), EventResult::Ignored);
512 }
513 fn focused_input(initial: &str) -> (Input, RwSignal<String>) {
515 reset_layout_runtime();
516 let value = signal(initial.to_string());
517 let input = Input::new(
518 value.clone(),
519 LayoutStyle::new().width(200.0).height(20.0),
520 || TextStyle::new(14.0, Color::BLACK),
521 )
522 .unwrap();
523 let root = new_container(
524 LayoutStyle::new().flex_column().width(200.0).height(100.0),
525 &[input.layout_node()],
526 )
527 .unwrap();
528 compute_layout(
529 root,
530 AvailableSpace::Definite(200.0),
531 AvailableSpace::Definite(100.0),
532 )
533 .unwrap();
534 focus::request(input.id);
535 (input, value)
536 }
537
538 #[test]
542 fn the_shortcut_guard_covers_every_key_this_field_edits() {
543 let plain = ModifiersState::default();
544 let named = [
545 NamedKey::Space,
546 NamedKey::Backspace,
547 NamedKey::Delete,
548 NamedKey::ArrowLeft,
549 NamedKey::ArrowRight,
550 NamedKey::ArrowUp,
551 NamedKey::ArrowDown,
552 NamedKey::Home,
553 NamedKey::End,
554 NamedKey::Enter,
555 NamedKey::Escape,
556 NamedKey::Tab,
557 NamedKey::PageUp,
558 NamedKey::PageDown,
559 NamedKey::F5,
560 NamedKey::Insert,
561 ];
562 let keys: Vec<Key> = std::iter::once(Key::Char('3'))
563 .chain(std::iter::once(Key::Char('s')))
564 .chain(named.into_iter().map(Key::Named))
565 .collect();
566 for k in keys {
567 let (mut input, _value) = focused_input("hello");
569 input.caret.set(2);
570 let guarded = focus::text_entry_takes_key(&k, plain);
573 let edited = input.edit(&k, &plain) == EventResult::Handled;
574 assert!(
575 !edited || guarded,
576 "{k:?} is edited by the field but the shortcut guard lets it through"
577 );
578 focus::clear();
579 }
580 }
581
582 #[test]
583 fn autofocus_makes_a_field_typable_without_a_tap() {
584 reset_layout_runtime();
585 focus::clear();
586 let value = signal(String::new());
587 let mut input = Input::new(
588 value.clone(),
589 LayoutStyle::new().width(200.0).height(20.0),
590 || TextStyle::new(14.0, Color::BLACK),
591 )
592 .unwrap()
593 .autofocus();
594 assert!(
595 focus::is_focused(input.id),
596 "the field holds focus from construction"
597 );
598 input.on_event(&key(Key::Char('x')));
599 assert_eq!(value.get(), "x", "and the very first keystroke is text");
600
601 reset_layout_runtime();
603 focus::clear();
604 let untouched = signal(String::new());
605 let mut plain = Input::new(
606 untouched.clone(),
607 LayoutStyle::new().width(200.0).height(20.0),
608 || TextStyle::new(14.0, Color::BLACK),
609 )
610 .unwrap();
611 assert!(!focus::is_focused(plain.id));
612 plain.on_event(&key(Key::Char('x')));
613 assert_eq!(untouched.get(), "");
614 }
615
616 #[test]
617 fn typing_inserts_at_caret() {
618 let (mut input, value) = focused_input("");
619 for c in "hi".chars() {
620 input.on_event(&key(Key::Char(c)));
621 }
622 assert_eq!(value.get(), "hi");
623 assert_eq!(input.caret.get(), 2);
624 }
625
626 #[test]
627 fn backspace_and_arrows_edit_mid_string() {
628 let (mut input, value) = focused_input("abc");
629 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
631 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
632 assert_eq!(input.caret.get(), 1);
633 input.on_event(&key(Key::Named(NamedKey::Backspace)));
635 assert_eq!(value.get(), "bc");
636 assert_eq!(input.caret.get(), 0);
637 input.on_event(&key(Key::Char('X')));
639 assert_eq!(value.get(), "Xbc");
640 }
641
642 #[test]
643 fn keys_ignored_when_not_focused() {
644 let (mut input, value) = focused_input("a");
645 focus::clear();
646 let r = input.on_event(&key(Key::Char('z')));
647 assert_eq!(r, EventResult::Ignored);
648 assert_eq!(value.get(), "a", "an unfocused input must not edit");
649 }
650
651 #[test]
652 fn a_masked_field_hides_the_text_without_changing_it() {
653 let (mut input, value) = focused_input("");
654 input = input.secret();
655 for c in ['h', 'u', 'n', 't', 'e', 'r'] {
656 input.on_event(&key(Key::Char(c)));
657 }
658 assert_eq!(
659 value.get(),
660 "hunter",
661 "the bound signal keeps the real text, which is what a submit handler reads"
662 );
663 assert_eq!(
664 input.shown(&value.get()),
665 "••••••",
666 "and the screen does not"
667 );
668
669 assert_eq!(input.shown("mañana"), "••••••");
672 assert_eq!(input.shown(""), "");
673 }
674
675 #[test]
676 fn an_unmasked_field_is_unchanged() {
677 let (input, value) = focused_input("plain");
678 assert_eq!(input.shown(&value.get()), "plain");
679 }
680
681 #[test]
682 fn tap_focuses_and_ctrl_chord_is_ignored() {
683 let (mut input, value) = focused_input("hi");
684 focus::clear();
685 let r = input.on_event(&Event::PointerPressed {
686 x: 10.0,
687 y: 5.0,
688 button: PointerButton::Primary,
689 source: PointerSource::Mouse,
690 });
691 assert_eq!(r, EventResult::Handled);
692 assert!(
693 focus::is_focused(input.id),
694 "a tap inside focuses the input"
695 );
696 let paste = Event::KeyPressed {
698 key: Key::Char('v'),
699 modifiers: ModifiersState {
700 is_ctrl: true,
701 ..Default::default()
702 },
703 };
704 assert_eq!(input.on_event(&paste), EventResult::Ignored);
705 assert_eq!(value.get(), "hi");
706 }
707}