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 {
22 value: RwSignal<String>,
23 caret: RwSignal<usize>,
26 style: Rc<dyn Fn() -> TextStyle>,
27 id: FocusId,
28 leaf: LayoutLeaf,
29 on_submit: Option<Box<dyn Fn()>>,
30 placeholder: String,
33 mask: Option<char>,
36}
37
38impl Input {
39 pub fn new(
40 value: RwSignal<String>,
41 layout_style: LayoutStyle,
42 style_fn: impl Fn() -> TextStyle + 'static,
43 ) -> Result<Self, LayoutError> {
44 let leaf = LayoutLeaf::register(layout_style)?;
45 let caret = value.with(|s| s.len());
46 let id = focus::next_id();
47 focus::register_at(id, focus::FocusKind::TextEntry, leaf.node);
50 Ok(Self {
51 value,
52 caret: signal(caret),
53 style: Rc::new(style_fn),
54 id,
55 leaf,
56 on_submit: None,
57 placeholder: String::new(),
58 mask: None,
59 })
60 }
61
62 pub fn masked(mut self, bullet: char) -> Self {
68 self.mask = Some(bullet);
69 self
70 }
71
72 pub fn secret(self) -> Self {
74 self.masked('•')
75 }
76
77 fn shown(&self, text: &str) -> String {
79 match self.mask {
80 Some(bullet) => text.chars().map(|_| bullet).collect(),
81 None => text.to_string(),
82 }
83 }
84
85 pub fn on_submit(mut self, f: impl Fn() + 'static) -> Self {
87 self.on_submit = Some(Box::new(f));
88 self
89 }
90
91 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
94 self.placeholder = p.into();
95 self
96 }
97
98 pub fn autofocus(self) -> Self {
106 focus::request(self.id);
107 self
108 }
109
110 fn caret_at(&self, text: &str) -> usize {
112 let mut c = self.caret.get().min(text.len());
113 while c > 0 && !text.is_char_boundary(c) {
114 c -= 1;
115 }
116 c
117 }
118
119 fn edit(&mut self, key: &Key, mods: &ModifiersState) -> EventResult {
122 let mut text = self.value.get();
123 let mut caret = self.caret_at(&text);
124 match key {
125 Key::Char(_) if mods.is_ctrl || mods.is_meta => return EventResult::Ignored,
127 Key::Char(c) if !c.is_control() => {
128 text.insert(caret, *c);
129 caret += c.len_utf8();
130 }
131 Key::Named(NamedKey::Space) => {
132 text.insert(caret, ' ');
133 caret += 1;
134 }
135 Key::Named(NamedKey::Backspace) => {
136 if caret == 0 {
137 return EventResult::Ignored;
138 }
139 let prev = prev_boundary(&text, caret);
140 text.replace_range(prev..caret, "");
141 caret = prev;
142 }
143 Key::Named(NamedKey::Delete) => {
144 if caret >= text.len() {
145 return EventResult::Ignored;
146 }
147 let next = next_boundary(&text, caret);
148 text.replace_range(caret..next, "");
149 }
150 Key::Named(NamedKey::ArrowLeft) => caret = prev_boundary(&text, caret),
151 Key::Named(NamedKey::ArrowRight) => caret = next_boundary(&text, caret),
152 Key::Named(NamedKey::Home) => caret = 0,
153 Key::Named(NamedKey::End) => caret = text.len(),
154 Key::Named(NamedKey::Enter) => {
155 if let Some(cb) = &self.on_submit {
156 cb();
157 }
158 return EventResult::Handled;
159 }
160 Key::Named(NamedKey::Escape) => {
161 focus::release(self.id);
162 return EventResult::Handled;
163 }
164 Key::Named(NamedKey::Tab) => {
166 if mods.is_shift {
167 focus::focus_prev();
168 } else {
169 focus::focus_next();
170 }
171 return EventResult::Handled;
172 }
173 _ => return EventResult::Ignored,
174 }
175 if self.value.with(|s| s != &text) {
177 self.value.set(text);
178 }
179 self.caret.set(caret);
180 EventResult::Handled
181 }
182}
183
184impl Component for Input {
185 fn view(&self) -> RenderNode {
186 let r = self.leaf.rect.get();
187 let text = self.value.get();
188 let style = (self.style)();
189 let full = Rect {
190 x: 0.0,
191 y: 0.0,
192 width: r.width,
193 height: r.height,
194 };
195 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
198 let muted = match style.paint {
199 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
200 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
201 };
202 let mut ph_style = style;
203 ph_style.paint = muted;
204 RenderNode::text(self.placeholder.clone(), full, ph_style)
205 } else {
206 RenderNode::text(self.shown(&text), full, style)
207 };
208
209 if focus::is_focused(self.id) {
211 let caret = self.caret_at(&text);
212 let prefix = self.shown(&text[..caret]);
215 let (prefix_w, _) = crate::text_metrics::measure_text(&prefix, 1.0e6, &style);
216 let line_h = crate::text_metrics::line_height(style.font_size);
217 let caret_rect = Rect {
218 x: prefix_w,
219 y: 0.0,
220 width: CARET_WIDTH,
221 height: line_h,
222 };
223 let caret_node =
224 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
225 self.leaf
226 .at_layout_position(RenderNode::group([text_node, caret_node]))
227 } else {
228 self.leaf.at_layout_position(text_node)
229 }
230 }
231
232 fn on_event(&mut self, event: &Event) -> EventResult {
233 let rect = self.leaf.rect.get();
234 match event {
235 Event::PointerPressed {
236 x,
237 y,
238 button: PointerButton::Primary,
239 ..
240 } => {
241 if rect.contains(*x as f32, *y as f32) {
242 focus::request_from_pointer(self.id);
243 self.caret.set(self.value.with(|s| s.len()));
245 EventResult::Handled
246 } else {
247 EventResult::Ignored
248 }
249 }
250 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
251 self.edit(key, modifiers)
252 }
253 _ => EventResult::Ignored,
254 }
255 }
256
257 fn debug_name(&self) -> &'static str {
258 "Input"
259 }
260}
261
262impl Drop for Input {
263 fn drop(&mut self) {
264 focus::unregister(self.id);
266 }
267}
268
269impl_leaf_widget!(Input);
270
271fn prev_boundary(s: &str, i: usize) -> usize {
273 let mut j = i.min(s.len());
274 if j == 0 {
275 return 0;
276 }
277 j -= 1;
278 while j > 0 && !s.is_char_boundary(j) {
279 j -= 1;
280 }
281 j
282}
283
284fn next_boundary(s: &str, i: usize) -> usize {
286 let mut j = (i + 1).min(s.len());
287 while j < s.len() && !s.is_char_boundary(j) {
288 j += 1;
289 }
290 j
291}
292
293#[cfg(test)]
294mod tests {
295 use crate::context::reset_layout_runtime;
296 use layout_core::AvailableSpace;
297 use platform_core::PointerSource;
298 use renderer_core::Color;
299
300 use super::*;
301 use crate::context::{compute_layout, new_container};
302 use crate::layout_item::LayoutItem;
303
304 fn key(k: Key) -> Event {
305 Event::KeyPressed {
306 key: k,
307 modifiers: ModifiersState::default(),
308 }
309 }
310
311 fn focused_input(initial: &str) -> (Input, RwSignal<String>) {
313 reset_layout_runtime();
314 let value = signal(initial.to_string());
315 let input = Input::new(
316 value.clone(),
317 LayoutStyle::new().width(200.0).height(20.0),
318 || TextStyle::new(14.0, Color::BLACK),
319 )
320 .unwrap();
321 let root = new_container(
322 LayoutStyle::new().flex_column().width(200.0).height(100.0),
323 &[input.layout_node()],
324 )
325 .unwrap();
326 compute_layout(
327 root,
328 AvailableSpace::Definite(200.0),
329 AvailableSpace::Definite(100.0),
330 )
331 .unwrap();
332 focus::request(input.id);
333 (input, value)
334 }
335
336 #[test]
340 fn the_shortcut_guard_covers_every_key_this_field_edits() {
341 let plain = ModifiersState::default();
342 let named = [
343 NamedKey::Space,
344 NamedKey::Backspace,
345 NamedKey::Delete,
346 NamedKey::ArrowLeft,
347 NamedKey::ArrowRight,
348 NamedKey::ArrowUp,
349 NamedKey::ArrowDown,
350 NamedKey::Home,
351 NamedKey::End,
352 NamedKey::Enter,
353 NamedKey::Escape,
354 NamedKey::Tab,
355 NamedKey::PageUp,
356 NamedKey::PageDown,
357 NamedKey::F5,
358 NamedKey::Insert,
359 ];
360 let keys: Vec<Key> = std::iter::once(Key::Char('3'))
361 .chain(std::iter::once(Key::Char('s')))
362 .chain(named.into_iter().map(Key::Named))
363 .collect();
364 for k in keys {
365 let (mut input, _value) = focused_input("hello");
367 input.caret.set(2);
368 let guarded = focus::text_entry_takes_key(&k, plain);
371 let edited = input.edit(&k, &plain) == EventResult::Handled;
372 assert!(
373 !edited || guarded,
374 "{k:?} is edited by the field but the shortcut guard lets it through"
375 );
376 focus::clear();
377 }
378 }
379
380 #[test]
381 fn autofocus_makes_a_field_typable_without_a_tap() {
382 reset_layout_runtime();
383 focus::clear();
384 let value = signal(String::new());
385 let mut input = Input::new(
386 value.clone(),
387 LayoutStyle::new().width(200.0).height(20.0),
388 || TextStyle::new(14.0, Color::BLACK),
389 )
390 .unwrap()
391 .autofocus();
392 assert!(
393 focus::is_focused(input.id),
394 "the field holds focus from construction"
395 );
396 input.on_event(&key(Key::Char('x')));
397 assert_eq!(value.get(), "x", "and the very first keystroke is text");
398
399 reset_layout_runtime();
401 focus::clear();
402 let untouched = signal(String::new());
403 let mut plain = Input::new(
404 untouched.clone(),
405 LayoutStyle::new().width(200.0).height(20.0),
406 || TextStyle::new(14.0, Color::BLACK),
407 )
408 .unwrap();
409 assert!(!focus::is_focused(plain.id));
410 plain.on_event(&key(Key::Char('x')));
411 assert_eq!(untouched.get(), "");
412 }
413
414 #[test]
415 fn typing_inserts_at_caret() {
416 let (mut input, value) = focused_input("");
417 for c in "hi".chars() {
418 input.on_event(&key(Key::Char(c)));
419 }
420 assert_eq!(value.get(), "hi");
421 assert_eq!(input.caret.get(), 2);
422 }
423
424 #[test]
425 fn backspace_and_arrows_edit_mid_string() {
426 let (mut input, value) = focused_input("abc");
427 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
429 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
430 assert_eq!(input.caret.get(), 1);
431 input.on_event(&key(Key::Named(NamedKey::Backspace)));
433 assert_eq!(value.get(), "bc");
434 assert_eq!(input.caret.get(), 0);
435 input.on_event(&key(Key::Char('X')));
437 assert_eq!(value.get(), "Xbc");
438 }
439
440 #[test]
441 fn keys_ignored_when_not_focused() {
442 let (mut input, value) = focused_input("a");
443 focus::clear();
444 let r = input.on_event(&key(Key::Char('z')));
445 assert_eq!(r, EventResult::Ignored);
446 assert_eq!(value.get(), "a", "an unfocused input must not edit");
447 }
448
449 #[test]
450 fn a_masked_field_hides_the_text_without_changing_it() {
451 let (mut input, value) = focused_input("");
452 input = input.secret();
453 for c in ['h', 'u', 'n', 't', 'e', 'r'] {
454 input.on_event(&key(Key::Char(c)));
455 }
456 assert_eq!(
457 value.get(),
458 "hunter",
459 "the bound signal keeps the real text, which is what a submit handler reads"
460 );
461 assert_eq!(
462 input.shown(&value.get()),
463 "••••••",
464 "and the screen does not"
465 );
466
467 assert_eq!(input.shown("mañana"), "••••••");
470 assert_eq!(input.shown(""), "");
471 }
472
473 #[test]
474 fn an_unmasked_field_is_unchanged() {
475 let (input, value) = focused_input("plain");
476 assert_eq!(input.shown(&value.get()), "plain");
477 }
478
479 #[test]
480 fn tap_focuses_and_ctrl_chord_is_ignored() {
481 let (mut input, value) = focused_input("hi");
482 focus::clear();
483 let r = input.on_event(&Event::PointerPressed {
484 x: 10.0,
485 y: 5.0,
486 button: PointerButton::Primary,
487 source: PointerSource::Mouse,
488 });
489 assert_eq!(r, EventResult::Handled);
490 assert!(
491 focus::is_focused(input.id),
492 "a tap inside focuses the input"
493 );
494 let paste = Event::KeyPressed {
496 key: Key::Char('v'),
497 modifiers: ModifiersState {
498 is_ctrl: true,
499 ..Default::default()
500 },
501 };
502 assert_eq!(input.on_event(&paste), EventResult::Ignored);
503 assert_eq!(value.get(), "hi");
504 }
505}