Skip to main content

freya_components/
input.rs

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 state.
88    #[default]
89    Idle,
90    /// Pointer is hovering the input.
91    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/// Small box to write some text.
119///
120/// ## **Normal**
121///
122/// ```rust
123/// # use freya::prelude::*;
124/// fn app() -> impl IntoElement {
125///     let value = use_state(String::new);
126///     Input::new(value).placeholder("Type here")
127/// }
128/// # use freya_testing::prelude::*;
129/// # launch_doc(|| {
130/// #   rect().center().expanded().child(app())
131/// # }, "./images/gallery_input.png").render();
132/// ```
133/// ## **Filled**
134///
135/// ```rust
136/// # use freya::prelude::*;
137/// fn app() -> impl IntoElement {
138///     let value = use_state(String::new);
139///     Input::new(value).placeholder("Type here").filled()
140/// }
141/// # use freya_testing::prelude::*;
142/// # launch_doc(|| {
143/// #   rect().center().expanded().child(app())
144/// # }, "./images/gallery_filled_input.png").render();
145/// ```
146/// ## **Flat**
147///
148/// ```rust
149/// # use freya::prelude::*;
150/// fn app() -> impl IntoElement {
151///     let value = use_state(String::new);
152///     Input::new(value).placeholder("Type here").flat()
153/// }
154/// # use freya_testing::prelude::*;
155/// # launch_doc(|| {
156/// #   rect().center().expanded().child(app())
157/// # }, "./images/gallery_flat_input.png").render();
158/// ```
159///
160/// # Preview
161/// ![Input Preview][input]
162/// ![Filled Input Preview][filled_input]
163/// ![Flat Input Preview][flat_input]
164#[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    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Filled].
292    pub fn filled(self) -> Self {
293        self.style_variant(InputStyleVariant::Filled)
294    }
295
296    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Flat].
297    pub fn flat(self) -> Self {
298        self.style_variant(InputStyleVariant::Flat)
299    }
300
301    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Compact].
302    pub fn compact(self) -> Self {
303        self.layout_variant(InputLayoutVariant::Compact)
304    }
305
306    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Expanded].
307    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    /// Optional element rendered before the text input.
317    pub fn leading(mut self, leading: impl Into<Element>) -> Self {
318        self.leading = Some(leading.into());
319        self
320    }
321
322    /// Optional element rendered after the text input.
323    pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
324        self.trailing = Some(trailing.into());
325        self
326    }
327
328    /// Sets a pre-handler called for each key event. Return `true` to let the input process it,
329    /// `false` to skip. The callback may call `stop_propagation()` / `prevent_default()` directly.
330    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                // On submit
439                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                // On unfocus
446                Key::Named(NamedKey::Escape) => {
447                    a11y_id.request_unfocus();
448                    Cursor::set(CursorIcon::default());
449                }
450                // On change
451                _ => {
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                    // Require a second press to enabling dragging on Android
493                    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                    // Require a second press to enabling dragging on Android
518                    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                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
577                    is_dragging.set(false);
578                } else {
579                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
580                    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}