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(id);
49 Ok(Self {
50 value,
51 caret: signal(caret),
52 style: Rc::new(style_fn),
53 id,
54 leaf,
55 on_submit: None,
56 placeholder: String::new(),
57 mask: None,
58 })
59 }
60
61 pub fn masked(mut self, bullet: char) -> Self {
67 self.mask = Some(bullet);
68 self
69 }
70
71 pub fn secret(self) -> Self {
73 self.masked('•')
74 }
75
76 fn shown(&self, text: &str) -> String {
78 match self.mask {
79 Some(bullet) => text.chars().map(|_| bullet).collect(),
80 None => text.to_string(),
81 }
82 }
83
84 pub fn on_submit(mut self, f: impl Fn() + 'static) -> Self {
86 self.on_submit = Some(Box::new(f));
87 self
88 }
89
90 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
93 self.placeholder = p.into();
94 self
95 }
96
97 pub fn autofocus(self) -> Self {
105 focus::request(self.id);
106 self
107 }
108
109 pub fn request_focus(&self) {
112 focus::request(self.id);
113 }
114
115 pub fn focused(&self) -> bool {
117 focus::is_focused(self.id)
118 }
119
120 pub fn focus_handle(&self) -> focus::FocusHandle {
123 focus::handle(self.id)
124 }
125
126 fn caret_at(&self, text: &str) -> usize {
128 let mut c = self.caret.get().min(text.len());
129 while c > 0 && !text.is_char_boundary(c) {
130 c -= 1;
131 }
132 c
133 }
134
135 fn edit(&mut self, key: &Key, mods: &ModifiersState) -> EventResult {
138 let mut text = self.value.get();
139 let mut caret = self.caret_at(&text);
140 match key {
141 Key::Char(_) if mods.is_ctrl || mods.is_meta => return EventResult::Ignored,
143 Key::Char(c) if !c.is_control() => {
144 text.insert(caret, *c);
145 caret += c.len_utf8();
146 }
147 Key::Named(NamedKey::Space) => {
148 text.insert(caret, ' ');
149 caret += 1;
150 }
151 Key::Named(NamedKey::Backspace) => {
152 if caret == 0 {
153 return EventResult::Ignored;
154 }
155 let prev = prev_boundary(&text, caret);
156 text.replace_range(prev..caret, "");
157 caret = prev;
158 }
159 Key::Named(NamedKey::Delete) => {
160 if caret >= text.len() {
161 return EventResult::Ignored;
162 }
163 let next = next_boundary(&text, caret);
164 text.replace_range(caret..next, "");
165 }
166 Key::Named(NamedKey::ArrowLeft) => caret = prev_boundary(&text, caret),
167 Key::Named(NamedKey::ArrowRight) => caret = next_boundary(&text, caret),
168 Key::Named(NamedKey::Home) => caret = 0,
169 Key::Named(NamedKey::End) => caret = text.len(),
170 Key::Named(NamedKey::Enter) => {
171 if let Some(cb) = &self.on_submit {
172 cb();
173 }
174 return EventResult::Handled;
175 }
176 Key::Named(NamedKey::Escape) => {
177 focus::release(self.id);
178 return EventResult::Handled;
179 }
180 Key::Named(NamedKey::Tab) => {
182 if mods.is_shift {
183 focus::focus_prev();
184 } else {
185 focus::focus_next();
186 }
187 return EventResult::Handled;
188 }
189 _ => return EventResult::Ignored,
190 }
191 if self.value.with(|s| s != &text) {
193 self.value.set(text);
194 }
195 self.caret.set(caret);
196 EventResult::Handled
197 }
198}
199
200impl Component for Input {
201 fn view(&self) -> RenderNode {
202 let r = self.leaf.rect.get();
203 let text = self.value.get();
204 let style = (self.style)();
205 let full = Rect {
206 x: 0.0,
207 y: 0.0,
208 width: r.width,
209 height: r.height,
210 };
211 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
214 let muted = match style.paint {
215 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
216 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
217 };
218 let mut ph_style = style;
219 ph_style.paint = muted;
220 RenderNode::text(self.placeholder.clone(), full, ph_style)
221 } else {
222 RenderNode::text(self.shown(&text), full, style)
223 };
224
225 if focus::is_focused(self.id) {
227 let caret = self.caret_at(&text);
228 let prefix = self.shown(&text[..caret]);
231 let (prefix_w, _) = renderer_text::measure_text(&prefix, 1.0e6, &style);
232 let line_h = style.font_size * renderer_text::LINE_HEIGHT_FACTOR;
233 let caret_rect = Rect {
234 x: prefix_w,
235 y: 0.0,
236 width: CARET_WIDTH,
237 height: line_h,
238 };
239 let caret_node =
240 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
241 self.leaf
242 .at_layout_position(RenderNode::group([text_node, caret_node]))
243 } else {
244 self.leaf.at_layout_position(text_node)
245 }
246 }
247
248 fn on_event(&mut self, event: &Event) -> EventResult {
249 let rect = self.leaf.rect.get();
250 match event {
251 Event::PointerPressed {
252 x,
253 y,
254 button: PointerButton::Primary,
255 ..
256 } => {
257 if rect.contains(*x as f32, *y as f32) {
258 focus::request(self.id);
259 self.caret.set(self.value.with(|s| s.len()));
261 EventResult::Handled
262 } else {
263 EventResult::Ignored
264 }
265 }
266 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
267 self.edit(key, modifiers)
268 }
269 _ => EventResult::Ignored,
270 }
271 }
272
273 fn debug_name(&self) -> &'static str {
274 "Input"
275 }
276}
277
278impl Drop for Input {
279 fn drop(&mut self) {
280 focus::unregister(self.id);
282 }
283}
284
285impl_leaf_widget!(Input);
286
287fn prev_boundary(s: &str, i: usize) -> usize {
289 let mut j = i.min(s.len());
290 if j == 0 {
291 return 0;
292 }
293 j -= 1;
294 while j > 0 && !s.is_char_boundary(j) {
295 j -= 1;
296 }
297 j
298}
299
300fn next_boundary(s: &str, i: usize) -> usize {
302 let mut j = (i + 1).min(s.len());
303 while j < s.len() && !s.is_char_boundary(j) {
304 j += 1;
305 }
306 j
307}
308
309#[cfg(test)]
310mod tests {
311 use crate::context::reset_layout_runtime;
312 use layout_core::AvailableSpace;
313 use platform_core::PointerSource;
314 use renderer_core::Color;
315
316 use super::*;
317 use crate::context::{compute_layout, new_container};
318 use crate::layout_item::LayoutItem;
319
320 fn key(k: Key) -> Event {
321 Event::KeyPressed {
322 key: k,
323 modifiers: ModifiersState::default(),
324 }
325 }
326
327 fn focused_input(initial: &str) -> (Input, RwSignal<String>) {
329 reset_layout_runtime();
330 let value = signal(initial.to_string());
331 let input = Input::new(
332 value.clone(),
333 LayoutStyle::new().width(200.0).height(20.0),
334 || TextStyle::new(14.0, Color::BLACK),
335 )
336 .unwrap();
337 let root = new_container(
338 LayoutStyle::new().flex_column().width(200.0).height(100.0),
339 &[input.layout_node()],
340 )
341 .unwrap();
342 compute_layout(
343 root,
344 AvailableSpace::Definite(200.0),
345 AvailableSpace::Definite(100.0),
346 )
347 .unwrap();
348 focus::request(input.id);
349 (input, value)
350 }
351
352 #[test]
353 fn autofocus_makes_a_field_typable_without_a_tap() {
354 reset_layout_runtime();
355 focus::clear();
356 let value = signal(String::new());
357 let mut input = Input::new(
358 value.clone(),
359 LayoutStyle::new().width(200.0).height(20.0),
360 || TextStyle::new(14.0, Color::BLACK),
361 )
362 .unwrap()
363 .autofocus();
364 assert!(input.focused(), "the field holds focus from construction");
365 input.on_event(&key(Key::Char('x')));
366 assert_eq!(value.get(), "x", "and the very first keystroke is text");
367
368 reset_layout_runtime();
370 focus::clear();
371 let untouched = signal(String::new());
372 let mut plain = Input::new(
373 untouched.clone(),
374 LayoutStyle::new().width(200.0).height(20.0),
375 || TextStyle::new(14.0, Color::BLACK),
376 )
377 .unwrap();
378 assert!(!plain.focused());
379 plain.on_event(&key(Key::Char('x')));
380 assert_eq!(untouched.get(), "");
381 }
382
383 #[test]
384 fn typing_inserts_at_caret() {
385 let (mut input, value) = focused_input("");
386 for c in "hi".chars() {
387 input.on_event(&key(Key::Char(c)));
388 }
389 assert_eq!(value.get(), "hi");
390 assert_eq!(input.caret.get(), 2);
391 }
392
393 #[test]
394 fn backspace_and_arrows_edit_mid_string() {
395 let (mut input, value) = focused_input("abc");
396 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
398 input.on_event(&key(Key::Named(NamedKey::ArrowLeft)));
399 assert_eq!(input.caret.get(), 1);
400 input.on_event(&key(Key::Named(NamedKey::Backspace)));
402 assert_eq!(value.get(), "bc");
403 assert_eq!(input.caret.get(), 0);
404 input.on_event(&key(Key::Char('X')));
406 assert_eq!(value.get(), "Xbc");
407 }
408
409 #[test]
410 fn keys_ignored_when_not_focused() {
411 let (mut input, value) = focused_input("a");
412 focus::clear();
413 let r = input.on_event(&key(Key::Char('z')));
414 assert_eq!(r, EventResult::Ignored);
415 assert_eq!(value.get(), "a", "an unfocused input must not edit");
416 }
417
418 #[test]
419 fn a_masked_field_hides_the_text_without_changing_it() {
420 let (mut input, value) = focused_input("");
421 input = input.secret();
422 for c in ['h', 'u', 'n', 't', 'e', 'r'] {
423 input.on_event(&key(Key::Char(c)));
424 }
425 assert_eq!(
426 value.get(),
427 "hunter",
428 "the bound signal keeps the real text, which is what a submit handler reads"
429 );
430 assert_eq!(
431 input.shown(&value.get()),
432 "••••••",
433 "and the screen does not"
434 );
435
436 assert_eq!(input.shown("mañana"), "••••••");
439 assert_eq!(input.shown(""), "");
440 }
441
442 #[test]
443 fn an_unmasked_field_is_unchanged() {
444 let (input, value) = focused_input("plain");
445 assert_eq!(input.shown(&value.get()), "plain");
446 }
447
448 #[test]
449 fn tap_focuses_and_ctrl_chord_is_ignored() {
450 let (mut input, value) = focused_input("hi");
451 focus::clear();
452 let r = input.on_event(&Event::PointerPressed {
453 x: 10.0,
454 y: 5.0,
455 button: PointerButton::Primary,
456 source: PointerSource::Mouse,
457 });
458 assert_eq!(r, EventResult::Handled);
459 assert!(
460 focus::is_focused(input.id),
461 "a tap inside focuses the input"
462 );
463 let paste = Event::KeyPressed {
465 key: Key::Char('v'),
466 modifiers: ModifiersState {
467 is_ctrl: true,
468 ..Default::default()
469 },
470 };
471 assert_eq!(input.on_event(&paste), EventResult::Ignored);
472 assert_eq!(value.get(), "hi");
473 }
474}