1use std::{
2 borrow::Cow,
3 cell::{
4 Ref,
5 RefCell,
6 },
7 rc::Rc,
8};
9
10use freya_core::{
11 elements::paragraph::{
12 ParagraphCursorExt,
13 ParagraphHolderInner,
14 },
15 prelude::*,
16};
17use freya_edit::*;
18use torin::{
19 gaps::Gaps,
20 prelude::{
21 Alignment,
22 Area,
23 AreaModel,
24 Content,
25 Direction,
26 },
27 size::Size,
28};
29use tracing::warn;
30
31use crate::{
32 cursor_blink::use_cursor_blink,
33 define_theme,
34 get_theme,
35 scrollviews::{
36 ScrollConfig,
37 ScrollView,
38 use_scroll_controller,
39 },
40};
41
42define_theme! {
43 for = Input;
44 theme_field = theme_layout;
45
46 %[component]
47 pub InputLayout {
48 %[fields]
49 corner_radius: CornerRadius,
50 inner_margin: Gaps,
51 }
52}
53
54define_theme! {
55 for = Input;
56 theme_field = theme_colors;
57
58 %[component]
59 pub InputColors {
60 %[fields]
61 background: Color,
62 focus_background: Color,
63 border_fill: Color,
64 focus_border_fill: Color,
65 color: Color,
66 placeholder_color: Color,
67 }
68}
69
70#[derive(Clone, PartialEq)]
71pub enum InputStyleVariant {
72 Normal,
73 Filled,
74 Flat,
75}
76
77#[derive(Clone, PartialEq)]
78pub enum InputLayoutVariant {
79 Normal,
80 Compact,
81 Expanded,
82}
83
84#[derive(Default, Clone, Copy, PartialEq)]
85pub enum InputMode {
86 #[default]
87 Shown,
88 Hidden(char),
89}
90
91impl InputMode {
92 pub fn new_password() -> Self {
93 Self::Hidden('*')
94 }
95}
96
97#[derive(Debug, Default, PartialEq, Clone, Copy)]
98pub enum InputStatus {
99 #[default]
101 Idle,
102 Hovering,
104}
105
106#[derive(Clone)]
107pub struct InputValidator {
108 valid: Rc<RefCell<bool>>,
109 text: Rc<RefCell<String>>,
110}
111
112impl InputValidator {
113 pub fn new(text: String) -> Self {
114 Self {
115 valid: Rc::new(RefCell::new(true)),
116 text: Rc::new(RefCell::new(text)),
117 }
118 }
119 pub fn text(&'_ self) -> Ref<'_, String> {
120 self.text.borrow()
121 }
122 pub fn set_valid(&self, is_valid: bool) {
123 *self.valid.borrow_mut() = is_valid;
124 }
125 pub fn is_valid(&self) -> bool {
126 *self.valid.borrow()
127 }
128}
129
130#[cfg_attr(feature = "docs",
177 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
178 doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
179 doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
180)]
181#[derive(Clone, PartialEq)]
182pub struct Input {
183 pub(crate) theme_colors: Option<InputColorsThemePartial>,
184 pub(crate) theme_layout: Option<InputLayoutThemePartial>,
185 value: Writable<String>,
186 placeholder: Option<Cow<'static, str>>,
187 on_validate: Option<EventHandler<InputValidator>>,
188 on_submit: Option<EventHandler<String>>,
189 mode: InputMode,
190 auto_focus: bool,
191 width: Size,
192 height: Size,
193 multiline: bool,
194 enabled: bool,
195 key: DiffKey,
196 style_variant: InputStyleVariant,
197 layout_variant: InputLayoutVariant,
198 text_align: TextAlign,
199 a11y_id: Option<AccessibilityId>,
200 leading: Option<Element>,
201 trailing: Option<Element>,
202 on_pre_key_down: Option<Callback<Event<KeyboardEventData>, bool>>,
203}
204
205impl KeyExt for Input {
206 fn write_key(&mut self) -> &mut DiffKey {
207 &mut self.key
208 }
209}
210
211impl Input {
212 pub fn new(value: impl Into<Writable<String>>) -> Self {
213 Input {
214 theme_colors: None,
215 theme_layout: None,
216 value: value.into(),
217 placeholder: None,
218 on_validate: None,
219 on_submit: None,
220 mode: InputMode::default(),
221 auto_focus: false,
222 width: Size::px(150.),
223 height: Size::default(),
224 multiline: false,
225 enabled: true,
226 key: DiffKey::default(),
227 style_variant: InputStyleVariant::Normal,
228 layout_variant: InputLayoutVariant::Normal,
229 text_align: TextAlign::default(),
230 a11y_id: None,
231 leading: None,
232 trailing: None,
233 on_pre_key_down: None,
234 }
235 }
236
237 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
238 self.enabled = enabled.into();
239 self
240 }
241
242 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
243 self.placeholder = Some(placeholder.into());
244 self
245 }
246
247 pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
248 self.on_validate = Some(on_validate.into());
249 self
250 }
251
252 pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
253 self.on_submit = Some(on_submit.into());
254 self
255 }
256
257 pub fn mode(mut self, mode: InputMode) -> Self {
258 self.mode = mode;
259 self
260 }
261
262 pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
263 self.auto_focus = auto_focus.into();
264 self
265 }
266
267 pub fn width(mut self, width: impl Into<Size>) -> Self {
268 self.width = width.into();
269 self
270 }
271
272 pub fn height(mut self, height: impl Into<Size>) -> Self {
273 self.height = height.into();
274 self
275 }
276
277 pub fn multiline(mut self, multiline: impl Into<bool>) -> Self {
280 self.multiline = multiline.into();
281 self
282 }
283
284 pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
285 self.theme_colors = Some(theme);
286 self
287 }
288
289 pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
290 self.theme_layout = Some(theme);
291 self
292 }
293
294 pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
295 self.text_align = text_align.into();
296 self
297 }
298
299 pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
300 self.style_variant = style_variant.into();
301 self
302 }
303
304 pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
305 self.layout_variant = layout_variant.into();
306 self
307 }
308
309 pub fn filled(self) -> Self {
311 self.style_variant(InputStyleVariant::Filled)
312 }
313
314 pub fn flat(self) -> Self {
316 self.style_variant(InputStyleVariant::Flat)
317 }
318
319 pub fn compact(self) -> Self {
321 self.layout_variant(InputLayoutVariant::Compact)
322 }
323
324 pub fn expanded(self) -> Self {
326 self.layout_variant(InputLayoutVariant::Expanded)
327 }
328
329 pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
330 self.a11y_id = Some(a11y_id.into());
331 self
332 }
333
334 pub fn leading(mut self, leading: impl Into<Element>) -> Self {
336 self.leading = Some(leading.into());
337 self
338 }
339
340 pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
342 self.trailing = Some(trailing.into());
343 self
344 }
345
346 pub fn on_pre_key_down(
349 mut self,
350 on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
351 ) -> Self {
352 self.on_pre_key_down = Some(on_pre_key_down.into());
353 self
354 }
355}
356
357impl CornerRadiusExt for Input {
358 fn with_corner_radius(self, corner_radius: f32) -> Self {
359 self.corner_radius(corner_radius)
360 }
361}
362
363impl Component for Input {
364 fn render(&self) -> impl IntoElement {
365 let a11y_id = use_hook(|| self.a11y_id.unwrap_or_else(AccessibilityId::new_unique));
366 let focus = use_focus(a11y_id);
367 let holder = use_state(ParagraphHolder::default);
368 let mut area = use_state(Area::default);
369 let mut viewport_area = use_state(Area::default);
370 let mut scroll_controller = use_scroll_controller(ScrollConfig::default);
371 let mut status = use_state(InputStatus::default);
372 let is_masked = matches!(self.mode, InputMode::Hidden(_));
373 let mut editable = use_editable(
374 || self.value.read().to_string(),
375 move || {
376 EditableConfig::new()
377 .with_allow_write_clipboard(!is_masked)
378 .with_select_all_on_double_click(is_masked)
379 },
380 );
381 let mut is_dragging = use_state(|| false);
382 let mut value = self.value.clone();
383
384 let theme_colors = match self.style_variant {
385 InputStyleVariant::Normal => {
386 get_theme!(&self.theme_colors, InputColorsThemePreference, "input")
387 }
388 InputStyleVariant::Filled => get_theme!(
389 &self.theme_colors,
390 InputColorsThemePreference,
391 "filled_input"
392 ),
393 InputStyleVariant::Flat => {
394 get_theme!(&self.theme_colors, InputColorsThemePreference, "flat_input")
395 }
396 };
397 let theme_layout = match self.layout_variant {
398 InputLayoutVariant::Normal => get_theme!(
399 &self.theme_layout,
400 InputLayoutThemePreference,
401 "input_layout"
402 ),
403 InputLayoutVariant::Compact => get_theme!(
404 &self.theme_layout,
405 InputLayoutThemePreference,
406 "compact_input_layout"
407 ),
408 InputLayoutVariant::Expanded => get_theme!(
409 &self.theme_layout,
410 InputLayoutThemePreference,
411 "expanded_input_layout"
412 ),
413 };
414
415 let (mut movement_timeout, cursor_color) =
416 use_cursor_blink(focus() != Focus::Not, theme_colors.color);
417
418 let enabled = use_reactive(&self.enabled);
419
420 let display_placeholder = value.read().is_empty()
421 && self.placeholder.is_some()
422 && !editable.editor().read().has_preedit();
423 let on_validate = self.on_validate.clone();
424 let on_submit = self.on_submit.clone();
425
426 if *value.read() != editable.editor().read().committed_text() {
427 let mut editor = editable.editor_mut().write();
428 editor.clear_preedit();
429 editor.set(&value.read());
430 editor.editor_history_mut().clear();
431 editor.clear_selection();
432 }
433
434 let mode = self.mode;
435 let text_align = self.text_align;
436 let inner_margin = theme_layout.inner_margin;
437 let multiline = self.multiline;
438 let mut follow_cursor = move || {
439 if !a11y_id.is_focused() || display_placeholder {
440 return;
441 }
442
443 let holder = holder.peek();
444 let holder = holder.0.borrow();
445 let Some(ParagraphHolderInner {
446 paragraph,
447 scale_factor,
448 }) = holder.as_ref()
449 else {
450 warn!("Paragraph should be build by now.");
451 return;
452 };
453
454 let viewport = viewport_area();
455 if viewport.width() == 0. {
456 return;
457 }
458
459 let editor = editable.editor().peek();
461 let text = match mode {
462 InputMode::Hidden(character) => {
463 character.to_string().repeat(editor.rope().len_chars())
464 }
465 InputMode::Shown => editor.rope().to_string(),
466 };
467
468 let cursor_rect = paragraph.cursor_rect(&text, editor.cursor_pos(), text_align);
469 let cursor_x = cursor_rect.left / (*scale_factor as f32);
470
471 let visible_start_x = viewport.min_x() - area.peek().min_x();
473
474 if cursor_x < visible_start_x {
476 scroll_controller.scroll_to_x(-cursor_x as i32);
477 } else if cursor_x + inner_margin.horizontal() > visible_start_x + viewport.width() {
478 scroll_controller
479 .scroll_to_x(-(cursor_x + inner_margin.horizontal() - viewport.width()) as i32);
480 }
481
482 if multiline {
483 let cursor_top = cursor_rect.top / (*scale_factor as f32);
484 let cursor_bottom = cursor_rect.bottom / (*scale_factor as f32);
485 let visible_start_y = viewport.min_y() - area.peek().min_y();
486
487 if cursor_top < visible_start_y {
488 scroll_controller.scroll_to_y(-cursor_top as i32);
489 } else if cursor_bottom + inner_margin.vertical()
490 > visible_start_y + viewport.height()
491 {
492 scroll_controller.scroll_to_y(
493 -(cursor_bottom + inner_margin.vertical() - viewport.height()) as i32,
494 );
495 }
496 }
497 };
498
499 let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
500 let mut editor = editable.editor_mut().write();
501 if e.data().text.is_empty() {
502 editor.clear_preedit();
503 } else {
504 editor.set_preedit(&e.data().text);
505 }
506 };
507
508 let on_pre_key_down = self.on_pre_key_down.clone().unwrap_or_else(|| {
509 Callback::new(move |e: Event<KeyboardEventData>| match &e.key {
510 Key::Named(NamedKey::Enter) if !multiline => true,
511 Key::Named(NamedKey::Escape) | Key::Named(NamedKey::Shift) => true,
512 Key::Named(NamedKey::Tab) => false,
513 _ => {
514 e.stop_propagation();
515 e.prevent_default();
516 true
517 }
518 })
519 });
520 let on_key_down = move |e: Event<KeyboardEventData>| {
521 let key = e.key.clone();
522 let modifiers = e.modifiers;
523
524 if !on_pre_key_down.call(e) {
525 return;
526 }
527
528 match &key {
529 Key::Named(NamedKey::Enter) if !multiline => {
531 if let Some(on_submit) = &on_submit {
532 let text = editable.editor().peek().committed_text();
533 on_submit.call(text);
534 }
535 }
536 Key::Named(NamedKey::Escape) => {
538 a11y_id.request_unfocus();
539 }
540 _ => {
542 movement_timeout.reset();
543 let previous_history_version =
544 editable.editor().peek().editor_history().version;
545 editable.process_event(EditableEvent::KeyDown {
546 key: &key,
547 modifiers,
548 editor_line: Some(EditorLine::SingleParagraph),
549 holder: Some(&holder.read()),
550 });
551 let text = editable.editor().read().committed_text();
552
553 let apply_change = match &on_validate {
554 Some(on_validate) => {
555 let mut editor = editable.editor_mut().write();
556 let validator = InputValidator::new(text.clone());
557 on_validate.call(validator.clone());
558 if !validator.is_valid() {
559 if let Some(selection) = editor.undo() {
560 *editor.selection_mut() = selection;
561 }
562 editor.editor_history_mut().clear_redos();
563 }
564 validator.is_valid()
565 }
566 None => true,
567 };
568
569 if apply_change {
570 *value.write() = text;
571 }
572 if editable.editor().peek().editor_history().version == previous_history_version
573 {
574 follow_cursor();
575 }
576 }
577 }
578 };
579
580 let on_key_up = move |e: Event<KeyboardEventData>| {
581 e.stop_propagation();
582 editable.process_event(EditableEvent::KeyUp { key: &e.key });
583 };
584
585 let on_input_focus_press = move |e: Event<FocusPressEventData>| {
586 e.stop_propagation();
587 e.prevent_default();
588 if cfg!(target_os = "android") {
589 if a11y_id.is_focused() {
590 is_dragging.set_if_modified(true);
592 }
593 } else {
594 is_dragging.set_if_modified(true);
595 }
596 movement_timeout.reset();
597 if !display_placeholder {
598 let text_area = area.read().without_gaps(&inner_margin).to_f64();
599 let global_location = e.global_location().clamp(text_area.min(), text_area.max());
600 let location = (global_location - text_area.min()).to_point();
601 editable.process_event(EditableEvent::Down {
602 location,
603 editor_line: EditorLine::SingleParagraph,
604 holder: &holder.read(),
605 });
606 }
607 a11y_id.request_focus();
608 };
609
610 let on_focus_press = move |e: Event<FocusPressEventData>| {
611 e.stop_propagation();
612 e.prevent_default();
613 if cfg!(target_os = "android") {
614 if a11y_id.is_focused() {
615 is_dragging.set_if_modified(true);
617 }
618 } else {
619 is_dragging.set_if_modified(true);
620 }
621 movement_timeout.reset();
622 if !display_placeholder {
623 editable.process_event(EditableEvent::Down {
624 location: e.element_location(),
625 editor_line: EditorLine::SingleParagraph,
626 holder: &holder.read(),
627 });
628 }
629 a11y_id.request_focus();
630 };
631
632 let on_global_pointer_move = move |e: Event<PointerEventData>| {
633 if a11y_id.is_focused() && *is_dragging.read() {
634 let text_area = area.read().without_gaps(&inner_margin).to_f64();
635 let location = (e.global_location() - text_area.min()).to_point();
636 editable.process_event(EditableEvent::Move {
637 location,
638 editor_line: EditorLine::SingleParagraph,
639 holder: &holder.read(),
640 });
641 follow_cursor();
642 }
643 };
644
645 let on_pointer_enter = move |_| {
646 *status.write() = InputStatus::Hovering;
647 };
648
649 let on_pointer_leave = move |_| {
650 if status() == InputStatus::Hovering {
651 *status.write() = InputStatus::default();
652 }
653 };
654
655 let on_global_pointer_press = move |_: Event<PointerEventData>| {
656 match *status.read() {
657 InputStatus::Idle if a11y_id.is_focused() => {
658 editable.process_event(EditableEvent::Release);
659 }
660 InputStatus::Hovering => {
661 editable.process_event(EditableEvent::Release);
662 }
663 _ => {}
664 };
665
666 if a11y_id.is_focused() {
667 if *is_dragging.read() {
668 is_dragging.set(false);
670 } else {
671 a11y_id.request_unfocus();
673 }
674 }
675 };
676
677 let on_pointer_press = move |e: Event<PointerEventData>| {
678 e.stop_propagation();
679 e.prevent_default();
680 match *status.read() {
681 InputStatus::Idle if a11y_id.is_focused() => {
682 editable.process_event(EditableEvent::Release);
683 }
684 InputStatus::Hovering => {
685 editable.process_event(EditableEvent::Release);
686 }
687 _ => {}
688 };
689
690 if a11y_id.is_focused() {
691 is_dragging.set_if_modified(false);
692 }
693 };
694
695 let on_paragraph_sized = move |e: Event<SizedEventData>| {
696 let text_size_changed = area.peek().size != e.area.size;
697 area.set_if_modified(e.area);
698 if text_size_changed {
699 follow_cursor();
700 }
701 };
702
703 let (background, cursor_index, text_selection) = if enabled() && focus() != Focus::Not {
704 (
705 theme_colors.focus_background,
706 Some(editable.editor().read().cursor_pos()),
707 editable
708 .editor()
709 .read()
710 .get_visible_selection(EditorLine::SingleParagraph),
711 )
712 } else {
713 (theme_colors.background, None, None)
714 };
715
716 let border = if focus().is_focused() {
717 Border::new()
718 .fill(theme_colors.focus_border_fill)
719 .width(2.)
720 .alignment(BorderAlignment::Inner)
721 } else {
722 Border::new()
723 .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
724 .width(1.)
725 .alignment(BorderAlignment::Inner)
726 };
727
728 let color = if display_placeholder {
729 theme_colors.placeholder_color
730 } else {
731 theme_colors.color
732 };
733
734 let value = self.value.read();
735 let a11y_text: Cow<str> = match (self.mode, &self.placeholder) {
736 (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
737 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
738 (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
739 };
740
741 let a11_role = match self.mode {
742 InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
743 _ if self.multiline => AccessibilityRole::MultilineTextInput,
744 _ => AccessibilityRole::TextInput,
745 };
746
747 rect()
748 .a11y_id(a11y_id)
749 .a11y_focusable(self.enabled)
750 .a11y_auto_focus(self.auto_focus)
751 .a11y_alt(a11y_text)
752 .a11y_role(a11_role)
753 .maybe(self.enabled, |el| {
754 el.on_key_up(on_key_up)
755 .on_key_down(on_key_down)
756 .on_focus_press(on_input_focus_press)
757 .on_ime_preedit(on_ime_preedit)
758 .on_pointer_press(on_pointer_press)
759 .on_global_pointer_press(on_global_pointer_press)
760 .on_global_pointer_move(on_global_pointer_move)
761 })
762 .on_pointer_enter(on_pointer_enter)
763 .on_pointer_leave(on_pointer_leave)
764 .cursor(if self.enabled {
765 CursorIcon::Text
766 } else {
767 CursorIcon::NotAllowed
768 })
769 .width(self.width.clone())
770 .height(self.height.clone())
771 .background(background.mul_if(!self.enabled, 0.85))
772 .border(border)
773 .corner_radius(theme_layout.corner_radius)
774 .content(Content::Flex)
775 .direction(Direction::Horizontal)
776 .cross_align(Alignment::center())
777 .maybe_child(
778 self.leading
779 .clone()
780 .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
781 )
782 .child(
783 ScrollView::new_controlled(scroll_controller)
784 .width(Size::flex(1.))
785 .height(if self.multiline && !matches!(self.height, Size::Inner) {
786 Size::fill()
787 } else {
788 Size::Inner
789 })
790 .direction(if self.multiline {
791 Direction::Vertical
792 } else {
793 Direction::Horizontal
794 })
795 .show_scrollbar(self.multiline)
796 .on_sized(move |e: Event<SizedEventData>| viewport_area.set_if_modified(e.area))
797 .child(
798 paragraph()
799 .holder(holder.read().clone())
800 .on_sized(on_paragraph_sized)
801 .min_width(Size::func(move |context| {
802 Some(context.parent - theme_layout.inner_margin.horizontal())
803 }))
804 .maybe(self.multiline, |el| {
805 el.max_width(Size::func(move |context| {
806 Some(context.parent - theme_layout.inner_margin.horizontal())
807 }))
808 })
809 .maybe(self.enabled, |el| el.on_focus_press(on_focus_press))
810 .margin(theme_layout.inner_margin)
811 .cursor_index(cursor_index)
812 .cursor_color(cursor_color)
813 .color(color)
814 .text_align(self.text_align)
815 .maybe(!self.multiline, |el| el.max_lines(1))
816 .highlights(text_selection.map(|h| vec![h]))
817 .maybe(display_placeholder, |el| {
818 el.span(self.placeholder.as_ref().unwrap().to_string())
819 })
820 .maybe(!display_placeholder, |el| {
821 let editor = editable.editor().read();
822 if editor.has_preedit() {
823 let (b, p, a) = editor.preedit_text_segments();
824 let (b, p, a) = match self.mode {
825 InputMode::Hidden(ch) => {
826 let ch = ch.to_string();
827 (
828 ch.repeat(b.chars().count()),
829 ch.repeat(p.chars().count()),
830 ch.repeat(a.chars().count()),
831 )
832 }
833 InputMode::Shown => (b, p, a),
834 };
835 el.span(b)
836 .span(
837 Span::new(p).text_decoration(TextDecoration::Underline),
838 )
839 .span(a)
840 } else {
841 let text = match self.mode {
842 InputMode::Hidden(ch) => {
843 ch.to_string().repeat(editor.rope().len_chars())
844 }
845 InputMode::Shown => editor.rope().to_string(),
846 };
847 el.span(text)
848 }
849 }),
850 ),
851 )
852 .maybe_child(
853 self.trailing
854 .clone()
855 .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
856 )
857 }
858
859 fn render_key(&self) -> DiffKey {
860 self.key.clone().or(self.default_key())
861 }
862}