1use std::{
2 borrow::Cow,
3 cell::{
4 Ref,
5 RefCell,
6 },
7 rc::Rc,
8};
9
10use freya_core::prelude::*;
11use freya_edit::*;
12use torin::{
13 gaps::Gaps,
14 prelude::{
15 Alignment,
16 Area,
17 Content,
18 Direction,
19 },
20 size::Size,
21};
22
23use crate::{
24 cursor_blink::use_cursor_blink,
25 define_theme,
26 get_theme,
27 scrollviews::ScrollView,
28};
29
30define_theme! {
31 for = Input;
32 theme_field = theme_layout;
33
34 %[component]
35 pub InputLayout {
36 %[fields]
37 corner_radius: CornerRadius,
38 inner_margin: Gaps,
39 }
40}
41
42define_theme! {
43 for = Input;
44 theme_field = theme_colors;
45
46 %[component]
47 pub InputColors {
48 %[fields]
49 background: Color,
50 focus_background: Color,
51 border_fill: Color,
52 focus_border_fill: Color,
53 color: Color,
54 placeholder_color: Color,
55 }
56}
57
58#[derive(Clone, PartialEq)]
59pub enum InputStyleVariant {
60 Normal,
61 Filled,
62 Flat,
63}
64
65#[derive(Clone, PartialEq)]
66pub enum InputLayoutVariant {
67 Normal,
68 Compact,
69 Expanded,
70}
71
72#[derive(Default, Clone, PartialEq)]
73pub enum InputMode {
74 #[default]
75 Shown,
76 Hidden(char),
77}
78
79impl InputMode {
80 pub fn new_password() -> Self {
81 Self::Hidden('*')
82 }
83}
84
85#[derive(Debug, Default, PartialEq, Clone, Copy)]
86pub enum InputStatus {
87 #[default]
89 Idle,
90 Hovering,
92}
93
94#[derive(Clone)]
95pub struct InputValidator {
96 valid: Rc<RefCell<bool>>,
97 text: Rc<RefCell<String>>,
98}
99
100impl InputValidator {
101 pub fn new(text: String) -> Self {
102 Self {
103 valid: Rc::new(RefCell::new(true)),
104 text: Rc::new(RefCell::new(text)),
105 }
106 }
107 pub fn text(&'_ self) -> Ref<'_, String> {
108 self.text.borrow()
109 }
110 pub fn set_valid(&self, is_valid: bool) {
111 *self.valid.borrow_mut() = is_valid;
112 }
113 pub fn is_valid(&self) -> bool {
114 *self.valid.borrow()
115 }
116}
117
118#[cfg_attr(feature = "docs",
165 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
166 doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
167 doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
168)]
169#[derive(Clone, PartialEq)]
170pub struct Input {
171 pub(crate) theme_colors: Option<InputColorsThemePartial>,
172 pub(crate) theme_layout: Option<InputLayoutThemePartial>,
173 value: Writable<String>,
174 placeholder: Option<Cow<'static, str>>,
175 on_validate: Option<EventHandler<InputValidator>>,
176 on_submit: Option<EventHandler<String>>,
177 mode: InputMode,
178 auto_focus: bool,
179 width: Size,
180 enabled: bool,
181 key: DiffKey,
182 style_variant: InputStyleVariant,
183 layout_variant: InputLayoutVariant,
184 text_align: TextAlign,
185 a11y_id: Option<AccessibilityId>,
186 leading: Option<Element>,
187 trailing: Option<Element>,
188}
189
190impl KeyExt for Input {
191 fn write_key(&mut self) -> &mut DiffKey {
192 &mut self.key
193 }
194}
195
196impl Input {
197 pub fn new(value: impl Into<Writable<String>>) -> Self {
198 Input {
199 theme_colors: None,
200 theme_layout: None,
201 value: value.into(),
202 placeholder: None,
203 on_validate: None,
204 on_submit: None,
205 mode: InputMode::default(),
206 auto_focus: false,
207 width: Size::px(150.),
208 enabled: true,
209 key: DiffKey::default(),
210 style_variant: InputStyleVariant::Normal,
211 layout_variant: InputLayoutVariant::Normal,
212 text_align: TextAlign::default(),
213 a11y_id: None,
214 leading: None,
215 trailing: None,
216 }
217 }
218
219 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
220 self.enabled = enabled.into();
221 self
222 }
223
224 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
225 self.placeholder = Some(placeholder.into());
226 self
227 }
228
229 pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
230 self.on_validate = Some(on_validate.into());
231 self
232 }
233
234 pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
235 self.on_submit = Some(on_submit.into());
236 self
237 }
238
239 pub fn mode(mut self, mode: InputMode) -> Self {
240 self.mode = mode;
241 self
242 }
243
244 pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
245 self.auto_focus = auto_focus.into();
246 self
247 }
248
249 pub fn width(mut self, width: impl Into<Size>) -> Self {
250 self.width = width.into();
251 self
252 }
253
254 pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
255 self.theme_colors = Some(theme);
256 self
257 }
258
259 pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
260 self.theme_layout = Some(theme);
261 self
262 }
263
264 pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
265 self.text_align = text_align.into();
266 self
267 }
268
269 pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
270 self.style_variant = style_variant.into();
271 self
272 }
273
274 pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
275 self.layout_variant = layout_variant.into();
276 self
277 }
278
279 pub fn filled(self) -> Self {
281 self.style_variant(InputStyleVariant::Filled)
282 }
283
284 pub fn flat(self) -> Self {
286 self.style_variant(InputStyleVariant::Flat)
287 }
288
289 pub fn compact(self) -> Self {
291 self.layout_variant(InputLayoutVariant::Compact)
292 }
293
294 pub fn expanded(self) -> Self {
296 self.layout_variant(InputLayoutVariant::Expanded)
297 }
298
299 pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
300 self.a11y_id = Some(a11y_id.into());
301 self
302 }
303
304 pub fn leading(mut self, leading: impl Into<Element>) -> Self {
306 self.leading = Some(leading.into());
307 self
308 }
309
310 pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
312 self.trailing = Some(trailing.into());
313 self
314 }
315}
316
317impl CornerRadiusExt for Input {
318 fn with_corner_radius(self, corner_radius: f32) -> Self {
319 self.corner_radius(corner_radius)
320 }
321}
322
323impl Component for Input {
324 fn render(&self) -> impl IntoElement {
325 let a11y_id = use_hook(|| self.a11y_id.unwrap_or_else(AccessibilityId::new_unique));
326 let focus = use_focus(a11y_id);
327 let holder = use_state(ParagraphHolder::default);
328 let mut area = use_state(Area::default);
329 let mut status = use_state(InputStatus::default);
330 let mut editable = use_editable(|| self.value.read().to_string(), EditableConfig::new);
331 let mut is_dragging = use_state(|| false);
332 let mut value = self.value.clone();
333
334 let theme_colors = match self.style_variant {
335 InputStyleVariant::Normal => {
336 get_theme!(&self.theme_colors, InputColorsThemePreference, "input")
337 }
338 InputStyleVariant::Filled => get_theme!(
339 &self.theme_colors,
340 InputColorsThemePreference,
341 "filled_input"
342 ),
343 InputStyleVariant::Flat => {
344 get_theme!(&self.theme_colors, InputColorsThemePreference, "flat_input")
345 }
346 };
347 let theme_layout = match self.layout_variant {
348 InputLayoutVariant::Normal => get_theme!(
349 &self.theme_layout,
350 InputLayoutThemePreference,
351 "input_layout"
352 ),
353 InputLayoutVariant::Compact => get_theme!(
354 &self.theme_layout,
355 InputLayoutThemePreference,
356 "compact_input_layout"
357 ),
358 InputLayoutVariant::Expanded => get_theme!(
359 &self.theme_layout,
360 InputLayoutThemePreference,
361 "expanded_input_layout"
362 ),
363 };
364
365 let (mut movement_timeout, cursor_color) =
366 use_cursor_blink(focus() != Focus::Not, theme_colors.color);
367
368 let enabled = use_reactive(&self.enabled);
369 use_drop(move || {
370 if status() == InputStatus::Hovering && enabled() {
371 Cursor::set(CursorIcon::default());
372 }
373 });
374
375 let display_placeholder = value.read().is_empty()
376 && self.placeholder.is_some()
377 && !editable.editor().read().has_preedit();
378 let on_validate = self.on_validate.clone();
379 let on_submit = self.on_submit.clone();
380
381 if *value.read() != editable.editor().read().committed_text() {
382 let mut editor = editable.editor_mut().write();
383 editor.clear_preedit();
384 editor.set(&value.read());
385 editor.editor_history().clear();
386 editor.clear_selection();
387 }
388
389 let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
390 let mut editor = editable.editor_mut().write();
391 if e.data().text.is_empty() {
392 editor.clear_preedit();
393 } else {
394 editor.set_preedit(&e.data().text);
395 }
396 };
397
398 let on_key_down = move |e: Event<KeyboardEventData>| {
399 match &e.key {
400 Key::Named(NamedKey::Enter) => {
402 if let Some(on_submit) = &on_submit {
403 let text = editable.editor().peek().committed_text();
404 on_submit.call(text);
405 }
406 }
407 Key::Named(NamedKey::Escape) => {
409 a11y_id.request_unfocus();
410 Cursor::set(CursorIcon::default());
411 }
412 key => {
414 if *key != Key::Named(NamedKey::Tab) {
415 e.stop_propagation();
416 e.prevent_default();
417 movement_timeout.reset();
418 editable.process_event(EditableEvent::KeyDown {
419 key: &e.key,
420 modifiers: e.modifiers,
421 });
422 let text = editable.editor().read().committed_text();
423
424 let apply_change = match &on_validate {
425 Some(on_validate) => {
426 let mut editor = editable.editor_mut().write();
427 let validator = InputValidator::new(text.clone());
428 on_validate.call(validator.clone());
429 if !validator.is_valid() {
430 if let Some(selection) = editor.undo() {
431 *editor.selection_mut() = selection;
432 }
433 editor.editor_history().clear_redos();
434 }
435 validator.is_valid()
436 }
437 None => true,
438 };
439
440 if apply_change {
441 *value.write() = text;
442 }
443 }
444 }
445 }
446 };
447
448 let on_key_up = move |e: Event<KeyboardEventData>| {
449 e.stop_propagation();
450 editable.process_event(EditableEvent::KeyUp { key: &e.key });
451 };
452
453 let on_input_pointer_down = move |e: Event<PointerEventData>| {
454 e.stop_propagation();
455 e.prevent_default();
456 is_dragging.set(true);
457 movement_timeout.reset();
458 if !display_placeholder {
459 let area = area.read().to_f64();
460 let global_location = e.global_location().clamp(area.min(), area.max());
461 let location = (global_location - area.min()).to_point();
462 editable.process_event(EditableEvent::Down {
463 location,
464 editor_line: EditorLine::SingleParagraph,
465 holder: &holder.read(),
466 });
467 }
468 a11y_id.request_focus();
469 };
470
471 let on_pointer_down = move |e: Event<PointerEventData>| {
472 e.stop_propagation();
473 e.prevent_default();
474 is_dragging.set(true);
475 movement_timeout.reset();
476 if !display_placeholder {
477 editable.process_event(EditableEvent::Down {
478 location: e.element_location(),
479 editor_line: EditorLine::SingleParagraph,
480 holder: &holder.read(),
481 });
482 }
483 a11y_id.request_focus();
484 };
485
486 let on_global_pointer_move = move |e: Event<PointerEventData>| {
487 if a11y_id.is_focused() && *is_dragging.read() {
488 let mut location = e.global_location();
489 location.x -= area.read().min_x() as f64;
490 location.y -= area.read().min_y() as f64;
491 editable.process_event(EditableEvent::Move {
492 location,
493 editor_line: EditorLine::SingleParagraph,
494 holder: &holder.read(),
495 });
496 }
497 };
498
499 let on_pointer_enter = move |_| {
500 *status.write() = InputStatus::Hovering;
501 if enabled() {
502 Cursor::set(CursorIcon::Text);
503 } else {
504 Cursor::set(CursorIcon::NotAllowed);
505 }
506 };
507
508 let on_pointer_leave = move |_| {
509 if status() == InputStatus::Hovering {
510 Cursor::set(CursorIcon::default());
511 *status.write() = InputStatus::default();
512 }
513 };
514
515 let on_global_pointer_press = move |_: Event<PointerEventData>| {
516 match *status.read() {
517 InputStatus::Idle if a11y_id.is_focused() => {
518 editable.process_event(EditableEvent::Release);
519 }
520 InputStatus::Hovering => {
521 editable.process_event(EditableEvent::Release);
522 }
523 _ => {}
524 };
525
526 if a11y_id.is_focused() {
527 if *is_dragging.read() {
528 is_dragging.set(false);
530 } else {
531 a11y_id.request_unfocus();
533 }
534 }
535 };
536
537 let on_pointer_press = move |e: Event<PointerEventData>| {
538 e.stop_propagation();
539 e.prevent_default();
540 match *status.read() {
541 InputStatus::Idle if a11y_id.is_focused() => {
542 editable.process_event(EditableEvent::Release);
543 }
544 InputStatus::Hovering => {
545 editable.process_event(EditableEvent::Release);
546 }
547 _ => {}
548 };
549
550 if a11y_id.is_focused() {
551 is_dragging.set_if_modified(false);
552 }
553 };
554
555 let (background, cursor_index, text_selection) = if enabled() && focus() != Focus::Not {
556 (
557 theme_colors.focus_background,
558 Some(editable.editor().read().cursor_pos()),
559 editable
560 .editor()
561 .read()
562 .get_visible_selection(EditorLine::SingleParagraph),
563 )
564 } else {
565 (theme_colors.background, None, None)
566 };
567
568 let border = if focus().is_focused() {
569 Border::new()
570 .fill(theme_colors.focus_border_fill)
571 .width(2.)
572 .alignment(BorderAlignment::Inner)
573 } else {
574 Border::new()
575 .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
576 .width(1.)
577 .alignment(BorderAlignment::Inner)
578 };
579
580 let color = if display_placeholder {
581 theme_colors.placeholder_color
582 } else {
583 theme_colors.color
584 };
585
586 let value = self.value.read();
587 let a11y_text: Cow<str> = match (self.mode.clone(), &self.placeholder) {
588 (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
589 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
590 (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
591 };
592
593 let a11_role = match self.mode {
594 InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
595 _ => AccessibilityRole::TextInput,
596 };
597
598 rect()
599 .a11y_id(a11y_id)
600 .a11y_focusable(self.enabled)
601 .a11y_auto_focus(self.auto_focus)
602 .a11y_alt(a11y_text)
603 .a11y_role(a11_role)
604 .maybe(self.enabled, |el| {
605 el.on_key_up(on_key_up)
606 .on_key_down(on_key_down)
607 .on_pointer_down(on_input_pointer_down)
608 .on_ime_preedit(on_ime_preedit)
609 .on_pointer_press(on_pointer_press)
610 .on_global_pointer_press(on_global_pointer_press)
611 .on_global_pointer_move(on_global_pointer_move)
612 })
613 .on_pointer_enter(on_pointer_enter)
614 .on_pointer_leave(on_pointer_leave)
615 .width(self.width.clone())
616 .background(background.mul_if(!self.enabled, 0.85))
617 .border(border)
618 .corner_radius(theme_layout.corner_radius)
619 .content(Content::Flex)
620 .direction(Direction::Horizontal)
621 .cross_align(Alignment::center())
622 .maybe_child(
623 self.leading
624 .clone()
625 .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
626 )
627 .child(
628 ScrollView::new()
629 .width(Size::flex(1.))
630 .height(Size::Inner)
631 .direction(Direction::Horizontal)
632 .show_scrollbar(false)
633 .child(
634 paragraph()
635 .holder(holder.read().clone())
636 .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
637 .min_width(Size::func(move |context| {
638 Some(context.parent - theme_layout.inner_margin.horizontal())
639 }))
640 .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
641 .margin(theme_layout.inner_margin)
642 .cursor_index(cursor_index)
643 .cursor_color(cursor_color)
644 .color(color)
645 .text_align(self.text_align)
646 .max_lines(1)
647 .highlights(text_selection.map(|h| vec![h]))
648 .maybe(display_placeholder, |el| {
649 el.span(self.placeholder.as_ref().unwrap().to_string())
650 })
651 .maybe(!display_placeholder, |el| {
652 let editor = editable.editor().read();
653 if editor.has_preedit() {
654 let (b, p, a) = editor.preedit_text_segments();
655 let (b, p, a) = match self.mode.clone() {
656 InputMode::Hidden(ch) => {
657 let ch = ch.to_string();
658 (
659 ch.repeat(b.chars().count()),
660 ch.repeat(p.chars().count()),
661 ch.repeat(a.chars().count()),
662 )
663 }
664 InputMode::Shown => (b, p, a),
665 };
666 el.span(b)
667 .span(
668 Span::new(p).text_decoration(TextDecoration::Underline),
669 )
670 .span(a)
671 } else {
672 let text = match self.mode.clone() {
673 InputMode::Hidden(ch) => {
674 ch.to_string().repeat(editor.rope().len_chars())
675 }
676 InputMode::Shown => editor.rope().to_string(),
677 };
678 el.span(text)
679 }
680 }),
681 ),
682 )
683 .maybe_child(
684 self.trailing
685 .clone()
686 .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
687 )
688 }
689
690 fn render_key(&self) -> DiffKey {
691 self.key.clone().or(self.default_key())
692 }
693}