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<T>;
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<T>;
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
118pub trait InputType: Clone + PartialEq + 'static {
120 fn as_text(&self) -> Cow<'_, str>;
122
123 fn from_text(text: &str) -> Option<Self>;
125}
126
127impl InputType for String {
128 fn as_text(&self) -> Cow<'_, str> {
129 Cow::Borrowed(self)
130 }
131
132 fn from_text(text: &str) -> Option<Self> {
133 Some(text.to_owned())
134 }
135}
136
137macro_rules! impl_input_type_parseable {
138 ($($ty:ty),*) => {
139 $(
140 impl InputType for $ty {
141 fn as_text(&self) -> Cow<'_, str> {
142 Cow::Owned(self.to_string())
143 }
144
145 fn from_text(text: &str) -> Option<Self> {
146 text.parse().ok()
147 }
148 }
149 )*
150 };
151}
152
153impl_input_type_parseable!(
154 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64
155);
156
157#[cfg_attr(feature = "docs",
216 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
217 doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
218 doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
219)]
220#[derive(Clone, PartialEq)]
221pub struct Input<T: 'static = String> {
222 pub(crate) theme_colors: Option<InputColorsThemePartial>,
223 pub(crate) theme_layout: Option<InputLayoutThemePartial>,
224 value: Writable<T>,
225 placeholder: Option<Cow<'static, str>>,
226 on_validate: Option<EventHandler<InputValidator>>,
227 on_submit: Option<EventHandler<T>>,
228 mode: InputMode,
229 auto_focus: bool,
230 width: Size,
231 enabled: bool,
232 key: DiffKey,
233 style_variant: InputStyleVariant,
234 layout_variant: InputLayoutVariant,
235 text_align: TextAlign,
236 a11y_id: Option<AccessibilityId>,
237 leading: Option<Element>,
238 trailing: Option<Element>,
239 on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
240}
241
242impl<T: 'static> KeyExt for Input<T> {
243 fn write_key(&mut self) -> &mut DiffKey {
244 &mut self.key
245 }
246}
247
248impl<T: InputType> Input<T> {
249 pub fn new(value: impl Into<Writable<T>>) -> Self {
250 Input {
251 theme_colors: None,
252 theme_layout: None,
253 value: value.into(),
254 placeholder: None,
255 on_validate: None,
256 on_submit: None,
257 mode: InputMode::default(),
258 auto_focus: false,
259 width: Size::px(150.),
260 enabled: true,
261 key: DiffKey::default(),
262 style_variant: InputStyleVariant::Normal,
263 layout_variant: InputLayoutVariant::Normal,
264 text_align: TextAlign::default(),
265 a11y_id: None,
266 leading: None,
267 trailing: None,
268 on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| match &e.key {
269 Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => true,
270 Key::Named(NamedKey::Tab) => false,
271 _ => {
272 e.stop_propagation();
273 e.prevent_default();
274 true
275 }
276 }),
277 }
278 }
279
280 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
281 self.enabled = enabled.into();
282 self
283 }
284
285 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
286 self.placeholder = Some(placeholder.into());
287 self
288 }
289
290 pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
291 self.on_validate = Some(on_validate.into());
292 self
293 }
294
295 pub fn on_submit(mut self, on_submit: impl Into<EventHandler<T>>) -> Self {
296 self.on_submit = Some(on_submit.into());
297 self
298 }
299
300 pub fn mode(mut self, mode: InputMode) -> Self {
301 self.mode = mode;
302 self
303 }
304
305 pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
306 self.auto_focus = auto_focus.into();
307 self
308 }
309
310 pub fn width(mut self, width: impl Into<Size>) -> Self {
311 self.width = width.into();
312 self
313 }
314
315 pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
316 self.theme_colors = Some(theme);
317 self
318 }
319
320 pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
321 self.theme_layout = Some(theme);
322 self
323 }
324
325 pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
326 self.text_align = text_align.into();
327 self
328 }
329
330 pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
331 self.style_variant = style_variant.into();
332 self
333 }
334
335 pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
336 self.layout_variant = layout_variant.into();
337 self
338 }
339
340 pub fn filled(self) -> Self {
342 self.style_variant(InputStyleVariant::Filled)
343 }
344
345 pub fn flat(self) -> Self {
347 self.style_variant(InputStyleVariant::Flat)
348 }
349
350 pub fn compact(self) -> Self {
352 self.layout_variant(InputLayoutVariant::Compact)
353 }
354
355 pub fn expanded(self) -> Self {
357 self.layout_variant(InputLayoutVariant::Expanded)
358 }
359
360 pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
361 self.a11y_id = Some(a11y_id.into());
362 self
363 }
364
365 pub fn leading(mut self, leading: impl Into<Element>) -> Self {
367 self.leading = Some(leading.into());
368 self
369 }
370
371 pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
373 self.trailing = Some(trailing.into());
374 self
375 }
376
377 pub fn on_pre_key_down(
380 mut self,
381 on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
382 ) -> Self {
383 self.on_pre_key_down = on_pre_key_down.into();
384 self
385 }
386}
387
388impl<T: 'static> CornerRadiusExt for Input<T> {
389 fn with_corner_radius(self, corner_radius: f32) -> Self {
390 self.corner_radius(corner_radius)
391 }
392}
393
394impl<T: InputType> Component for Input<T> {
395 fn render(&self) -> impl IntoElement {
396 let a11y_id = use_hook(|| self.a11y_id.unwrap_or_else(AccessibilityId::new_unique));
397 let focus = use_focus(a11y_id);
398 let holder = use_state(ParagraphHolder::default);
399 let mut area = use_state(Area::default);
400 let mut status = use_state(InputStatus::default);
401 let allow_write_clipboard = !matches!(self.mode, InputMode::Hidden(_));
402 let mut editable = use_editable(
403 || self.value.read().as_text().into_owned(),
404 move || EditableConfig::new().with_allow_write_clipboard(allow_write_clipboard),
405 );
406 let mut is_dragging = use_state(|| false);
407 let mut value = self.value.clone();
408
409 let theme_colors = match self.style_variant {
410 InputStyleVariant::Normal => {
411 get_theme!(&self.theme_colors, InputColorsThemePreference, "input")
412 }
413 InputStyleVariant::Filled => get_theme!(
414 &self.theme_colors,
415 InputColorsThemePreference,
416 "filled_input"
417 ),
418 InputStyleVariant::Flat => {
419 get_theme!(&self.theme_colors, InputColorsThemePreference, "flat_input")
420 }
421 };
422 let theme_layout = match self.layout_variant {
423 InputLayoutVariant::Normal => get_theme!(
424 &self.theme_layout,
425 InputLayoutThemePreference,
426 "input_layout"
427 ),
428 InputLayoutVariant::Compact => get_theme!(
429 &self.theme_layout,
430 InputLayoutThemePreference,
431 "compact_input_layout"
432 ),
433 InputLayoutVariant::Expanded => get_theme!(
434 &self.theme_layout,
435 InputLayoutThemePreference,
436 "expanded_input_layout"
437 ),
438 };
439
440 let (mut movement_timeout, cursor_color) =
441 use_cursor_blink(focus() != Focus::Not, theme_colors.color);
442
443 let enabled = use_reactive(&self.enabled);
444 use_drop(move || {
445 if status() == InputStatus::Hovering && enabled() {
446 Cursor::set(CursorIcon::default());
447 }
448 });
449
450 let on_validate = self.on_validate.clone();
451 let on_submit = self.on_submit.clone();
452
453 let editor_value = T::from_text(&editable.editor().read().committed_text());
454 if editor_value.as_ref() != Some(&*self.value.read()) {
455 let mut editor = editable.editor_mut().write();
456 editor.clear_preedit();
457 editor.set(&self.value.read().as_text());
458 editor.editor_history().clear();
459 editor.clear_selection();
460 }
461
462 let display_placeholder = {
463 let editor = editable.editor().read();
464 editor.rope().len_chars() == 0 && self.placeholder.is_some() && !editor.has_preedit()
465 };
466
467 let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
468 let mut editor = editable.editor_mut().write();
469 if e.data().text.is_empty() {
470 editor.clear_preedit();
471 } else {
472 editor.set_preedit(&e.data().text);
473 }
474 };
475
476 let on_pre_key_down = self.on_pre_key_down.clone();
477 let on_key_down = move |e: Event<KeyboardEventData>| {
478 let key = e.key.clone();
479 let modifiers = e.modifiers;
480
481 if !on_pre_key_down.call(e) {
482 return;
483 }
484
485 match &key {
486 Key::Named(NamedKey::Enter) => {
488 if let Some(on_submit) = &on_submit {
489 on_submit.call(value.peek().clone());
490 }
491 }
492 Key::Named(NamedKey::Escape) => {
494 a11y_id.request_unfocus();
495 Cursor::set(CursorIcon::default());
496 }
497 _ => {
499 movement_timeout.reset();
500 editable.process_event(EditableEvent::KeyDown {
501 key: &key,
502 modifiers,
503 });
504 let text = editable.editor().read().committed_text();
505
506 let is_valid = on_validate.as_ref().is_none_or(|on_validate| {
507 let validator = InputValidator::new(text.clone());
508 on_validate.call(validator.clone());
509 validator.is_valid()
510 });
511
512 if let Some(new_value) = is_valid.then(|| T::from_text(&text)).flatten() {
513 *value.write() = new_value;
514 } else {
515 let mut editor = editable.editor_mut().write();
516 if let Some(selection) = editor.undo() {
517 *editor.selection_mut() = selection;
518 }
519 editor.editor_history().clear_redos();
520 }
521 }
522 }
523 };
524
525 let on_key_up = move |e: Event<KeyboardEventData>| {
526 e.stop_propagation();
527 editable.process_event(EditableEvent::KeyUp { key: &e.key });
528 };
529
530 let on_input_focus_press = move |e: Event<FocusPressEventData>| {
531 e.stop_propagation();
532 e.prevent_default();
533 if cfg!(target_os = "android") {
534 if a11y_id.is_focused() {
535 is_dragging.set_if_modified(true);
537 }
538 } else {
539 is_dragging.set_if_modified(true);
540 }
541 movement_timeout.reset();
542 if !display_placeholder {
543 let area = area.read().to_f64();
544 let global_location = e.global_location().clamp(area.min(), area.max());
545 let location = (global_location - area.min()).to_point();
546 editable.process_event(EditableEvent::Down {
547 location,
548 editor_line: EditorLine::SingleParagraph,
549 holder: &holder.read(),
550 });
551 }
552 a11y_id.request_focus();
553 };
554
555 let on_focus_press = move |e: Event<FocusPressEventData>| {
556 e.stop_propagation();
557 e.prevent_default();
558 if cfg!(target_os = "android") {
559 if a11y_id.is_focused() {
560 is_dragging.set_if_modified(true);
562 }
563 } else {
564 is_dragging.set_if_modified(true);
565 }
566 movement_timeout.reset();
567 if !display_placeholder {
568 editable.process_event(EditableEvent::Down {
569 location: e.element_location(),
570 editor_line: EditorLine::SingleParagraph,
571 holder: &holder.read(),
572 });
573 }
574 a11y_id.request_focus();
575 };
576
577 let on_global_pointer_move = move |e: Event<PointerEventData>| {
578 if a11y_id.is_focused() && *is_dragging.read() {
579 let mut location = e.global_location();
580 location.x -= area.read().min_x() as f64;
581 location.y -= area.read().min_y() as f64;
582 editable.process_event(EditableEvent::Move {
583 location,
584 editor_line: EditorLine::SingleParagraph,
585 holder: &holder.read(),
586 });
587 }
588 };
589
590 let on_pointer_enter = move |_| {
591 *status.write() = InputStatus::Hovering;
592 if enabled() {
593 Cursor::set(CursorIcon::Text);
594 } else {
595 Cursor::set(CursorIcon::NotAllowed);
596 }
597 };
598
599 let on_pointer_leave = move |_| {
600 if status() == InputStatus::Hovering {
601 Cursor::set(CursorIcon::default());
602 *status.write() = InputStatus::default();
603 }
604 };
605
606 let on_global_pointer_press = move |_: Event<PointerEventData>| {
607 match *status.read() {
608 InputStatus::Idle if a11y_id.is_focused() => {
609 editable.process_event(EditableEvent::Release);
610 }
611 InputStatus::Hovering => {
612 editable.process_event(EditableEvent::Release);
613 }
614 _ => {}
615 };
616
617 if a11y_id.is_focused() {
618 if *is_dragging.read() {
619 is_dragging.set(false);
621 } else {
622 a11y_id.request_unfocus();
624 }
625 }
626 };
627
628 let on_pointer_press = move |e: Event<PointerEventData>| {
629 e.stop_propagation();
630 e.prevent_default();
631 match *status.read() {
632 InputStatus::Idle if a11y_id.is_focused() => {
633 editable.process_event(EditableEvent::Release);
634 }
635 InputStatus::Hovering => {
636 editable.process_event(EditableEvent::Release);
637 }
638 _ => {}
639 };
640
641 if a11y_id.is_focused() {
642 is_dragging.set_if_modified(false);
643 }
644 };
645
646 let (background, cursor_index, text_selection) = if enabled() && focus() != Focus::Not {
647 (
648 theme_colors.focus_background,
649 Some(editable.editor().read().cursor_pos()),
650 editable
651 .editor()
652 .read()
653 .get_visible_selection(EditorLine::SingleParagraph),
654 )
655 } else {
656 (theme_colors.background, None, None)
657 };
658
659 let border = if focus().is_focused() {
660 Border::new()
661 .fill(theme_colors.focus_border_fill)
662 .width(2.)
663 .alignment(BorderAlignment::Inner)
664 } else {
665 Border::new()
666 .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
667 .width(1.)
668 .alignment(BorderAlignment::Inner)
669 };
670
671 let color = if display_placeholder {
672 theme_colors.placeholder_color
673 } else {
674 theme_colors.color
675 };
676
677 let committed_text = editable.editor().read().committed_text();
678 let a11y_text: Cow<str> = match (self.mode.clone(), &self.placeholder) {
679 (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
680 (InputMode::Hidden(ch), _) => {
681 Cow::Owned(ch.to_string().repeat(committed_text.chars().count()))
682 }
683 (InputMode::Shown, _) => Cow::Owned(committed_text),
684 };
685
686 let a11_role = match self.mode {
687 InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
688 _ => AccessibilityRole::TextInput,
689 };
690
691 rect()
692 .a11y_id(a11y_id)
693 .a11y_focusable(self.enabled)
694 .a11y_auto_focus(self.auto_focus)
695 .a11y_alt(a11y_text)
696 .a11y_role(a11_role)
697 .maybe(self.enabled, |el| {
698 el.on_key_up(on_key_up)
699 .on_key_down(on_key_down)
700 .on_focus_press(on_input_focus_press)
701 .on_ime_preedit(on_ime_preedit)
702 .on_pointer_press(on_pointer_press)
703 .on_global_pointer_press(on_global_pointer_press)
704 .on_global_pointer_move(on_global_pointer_move)
705 })
706 .on_pointer_enter(on_pointer_enter)
707 .on_pointer_leave(on_pointer_leave)
708 .width(self.width.clone())
709 .background(background.mul_if(!self.enabled, 0.85))
710 .border(border)
711 .corner_radius(theme_layout.corner_radius)
712 .content(Content::Flex)
713 .direction(Direction::Horizontal)
714 .cross_align(Alignment::center())
715 .maybe_child(
716 self.leading
717 .clone()
718 .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
719 )
720 .child(
721 ScrollView::new()
722 .width(Size::flex(1.))
723 .height(Size::Inner)
724 .direction(Direction::Horizontal)
725 .show_scrollbar(false)
726 .child(
727 paragraph()
728 .holder(holder.read().clone())
729 .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
730 .min_width(Size::func(move |context| {
731 Some(context.parent - theme_layout.inner_margin.horizontal())
732 }))
733 .maybe(self.enabled, |el| el.on_focus_press(on_focus_press))
734 .margin(theme_layout.inner_margin)
735 .cursor_index(cursor_index)
736 .cursor_color(cursor_color)
737 .color(color)
738 .text_align(self.text_align)
739 .max_lines(1)
740 .highlights(text_selection.map(|h| vec![h]))
741 .maybe(display_placeholder, |el| {
742 el.span(self.placeholder.as_ref().unwrap().to_string())
743 })
744 .maybe(!display_placeholder, |el| {
745 let editor = editable.editor().read();
746 if editor.has_preedit() {
747 let (b, p, a) = editor.preedit_text_segments();
748 let (b, p, a) = match self.mode.clone() {
749 InputMode::Hidden(ch) => {
750 let ch = ch.to_string();
751 (
752 ch.repeat(b.chars().count()),
753 ch.repeat(p.chars().count()),
754 ch.repeat(a.chars().count()),
755 )
756 }
757 InputMode::Shown => (b, p, a),
758 };
759 el.span(b)
760 .span(
761 Span::new(p).text_decoration(TextDecoration::Underline),
762 )
763 .span(a)
764 } else {
765 let text = match self.mode.clone() {
766 InputMode::Hidden(ch) => {
767 ch.to_string().repeat(editor.rope().len_chars())
768 }
769 InputMode::Shown => editor.rope().to_string(),
770 };
771 el.span(text)
772 }
773 }),
774 ),
775 )
776 .maybe_child(
777 self.trailing
778 .clone()
779 .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
780 )
781 }
782
783 fn render_key(&self) -> DiffKey {
784 self.key.clone().or(self.default_key())
785 }
786}