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