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