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