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