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        hover_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 focus = use_hook(|| Focus::new_for_id(self.a11y_id.unwrap_or_else(Focus::new_id)));
326        let focus_status = use_focus_status(focus);
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_status() != FocusStatus::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                    focus.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                        movement_timeout.reset();
417                        editable.process_event(EditableEvent::KeyDown {
418                            key: &e.key,
419                            modifiers: e.modifiers,
420                        });
421                        let text = editable.editor().read().committed_text();
422
423                        let apply_change = match &on_validate {
424                            Some(on_validate) => {
425                                let mut editor = editable.editor_mut().write();
426                                let validator = InputValidator::new(text.clone());
427                                on_validate.call(validator.clone());
428                                if !validator.is_valid() {
429                                    if let Some(selection) = editor.undo() {
430                                        *editor.selection_mut() = selection;
431                                    }
432                                    editor.editor_history().clear_redos();
433                                }
434                                validator.is_valid()
435                            }
436                            None => true,
437                        };
438
439                        if apply_change {
440                            *value.write() = text;
441                        }
442                    }
443                }
444            }
445        };
446
447        let on_key_up = move |e: Event<KeyboardEventData>| {
448            e.stop_propagation();
449            editable.process_event(EditableEvent::KeyUp { key: &e.key });
450        };
451
452        let on_input_pointer_down = move |e: Event<PointerEventData>| {
453            e.stop_propagation();
454            e.prevent_default();
455            is_dragging.set(true);
456            movement_timeout.reset();
457            if !display_placeholder {
458                let area = area.read().to_f64();
459                let global_location = e.global_location().clamp(area.min(), area.max());
460                let location = (global_location - area.min()).to_point();
461                editable.process_event(EditableEvent::Down {
462                    location,
463                    editor_line: EditorLine::SingleParagraph,
464                    holder: &holder.read(),
465                });
466            }
467            focus.request_focus();
468        };
469
470        let on_pointer_down = move |e: Event<PointerEventData>| {
471            e.stop_propagation();
472            e.prevent_default();
473            is_dragging.set(true);
474            movement_timeout.reset();
475            if !display_placeholder {
476                editable.process_event(EditableEvent::Down {
477                    location: e.element_location(),
478                    editor_line: EditorLine::SingleParagraph,
479                    holder: &holder.read(),
480                });
481            }
482            focus.request_focus();
483        };
484
485        let on_global_pointer_move = move |e: Event<PointerEventData>| {
486            if focus.is_focused() && *is_dragging.read() {
487                let mut location = e.global_location();
488                location.x -= area.read().min_x() as f64;
489                location.y -= area.read().min_y() as f64;
490                editable.process_event(EditableEvent::Move {
491                    location,
492                    editor_line: EditorLine::SingleParagraph,
493                    holder: &holder.read(),
494                });
495            }
496        };
497
498        let on_pointer_enter = move |_| {
499            *status.write() = InputStatus::Hovering;
500            if enabled() {
501                Cursor::set(CursorIcon::Text);
502            } else {
503                Cursor::set(CursorIcon::NotAllowed);
504            }
505        };
506
507        let on_pointer_leave = move |_| {
508            if status() == InputStatus::Hovering {
509                Cursor::set(CursorIcon::default());
510                *status.write() = InputStatus::default();
511            }
512        };
513
514        let on_global_pointer_press = move |_: Event<PointerEventData>| {
515            match *status.read() {
516                InputStatus::Idle if focus.is_focused() => {
517                    editable.process_event(EditableEvent::Release);
518                }
519                InputStatus::Hovering => {
520                    editable.process_event(EditableEvent::Release);
521                }
522                _ => {}
523            };
524
525            if focus.is_focused() {
526                if *is_dragging.read() {
527                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
528                    is_dragging.set(false);
529                } else {
530                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
531                    focus.request_unfocus();
532                }
533            }
534        };
535
536        let on_pointer_press = move |e: Event<PointerEventData>| {
537            e.stop_propagation();
538            e.prevent_default();
539            match *status.read() {
540                InputStatus::Idle if focus.is_focused() => {
541                    editable.process_event(EditableEvent::Release);
542                }
543                InputStatus::Hovering => {
544                    editable.process_event(EditableEvent::Release);
545                }
546                _ => {}
547            };
548
549            if focus.is_focused() {
550                is_dragging.set_if_modified(false);
551            }
552        };
553
554        let a11y_id = focus.a11y_id();
555
556        let (background, cursor_index, text_selection) =
557            if enabled() && focus_status() != FocusStatus::Not {
558                (
559                    theme_colors.hover_background,
560                    Some(editable.editor().read().cursor_pos()),
561                    editable
562                        .editor()
563                        .read()
564                        .get_visible_selection(EditorLine::SingleParagraph),
565                )
566            } else {
567                (theme_colors.background, None, None)
568            };
569
570        let border = if focus_status().is_focused() {
571            Border::new()
572                .fill(theme_colors.focus_border_fill)
573                .width(2.)
574                .alignment(BorderAlignment::Inner)
575        } else {
576            Border::new()
577                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
578                .width(1.)
579                .alignment(BorderAlignment::Inner)
580        };
581
582        let color = if display_placeholder {
583            theme_colors.placeholder_color
584        } else {
585            theme_colors.color
586        };
587
588        let value = self.value.read();
589        let a11y_text: Cow<str> = match (self.mode.clone(), &self.placeholder) {
590            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
591            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
592            (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
593        };
594
595        let a11_role = match self.mode {
596            InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
597            _ => AccessibilityRole::TextInput,
598        };
599
600        rect()
601            .a11y_id(a11y_id)
602            .a11y_focusable(self.enabled)
603            .a11y_auto_focus(self.auto_focus)
604            .a11y_alt(a11y_text)
605            .a11y_role(a11_role)
606            .maybe(self.enabled, |el| {
607                el.on_key_up(on_key_up)
608                    .on_key_down(on_key_down)
609                    .on_pointer_down(on_input_pointer_down)
610                    .on_ime_preedit(on_ime_preedit)
611                    .on_pointer_press(on_pointer_press)
612                    .on_global_pointer_press(on_global_pointer_press)
613                    .on_global_pointer_move(on_global_pointer_move)
614            })
615            .on_pointer_enter(on_pointer_enter)
616            .on_pointer_leave(on_pointer_leave)
617            .width(self.width.clone())
618            .background(background.mul_if(!self.enabled, 0.85))
619            .border(border)
620            .corner_radius(theme_layout.corner_radius)
621            .content(Content::Flex)
622            .direction(Direction::Horizontal)
623            .cross_align(Alignment::center())
624            .maybe_child(
625                self.leading
626                    .clone()
627                    .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
628            )
629            .child(
630                ScrollView::new()
631                    .width(Size::flex(1.))
632                    .height(Size::Inner)
633                    .direction(Direction::Horizontal)
634                    .show_scrollbar(false)
635                    .child(
636                        paragraph()
637                            .holder(holder.read().clone())
638                            .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
639                            .min_width(Size::func(move |context| {
640                                Some(context.parent + theme_layout.inner_margin.horizontal())
641                            }))
642                            .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
643                            .margin(theme_layout.inner_margin)
644                            .cursor_index(cursor_index)
645                            .cursor_color(cursor_color)
646                            .color(color)
647                            .text_align(self.text_align)
648                            .max_lines(1)
649                            .highlights(text_selection.map(|h| vec![h]))
650                            .maybe(display_placeholder, |el| {
651                                el.span(self.placeholder.as_ref().unwrap().to_string())
652                            })
653                            .maybe(!display_placeholder, |el| {
654                                let editor = editable.editor().read();
655                                if editor.has_preedit() {
656                                    let (b, p, a) = editor.preedit_text_segments();
657                                    let (b, p, a) = match self.mode.clone() {
658                                        InputMode::Hidden(ch) => {
659                                            let ch = ch.to_string();
660                                            (
661                                                ch.repeat(b.chars().count()),
662                                                ch.repeat(p.chars().count()),
663                                                ch.repeat(a.chars().count()),
664                                            )
665                                        }
666                                        InputMode::Shown => (b, p, a),
667                                    };
668                                    el.span(b)
669                                        .span(
670                                            Span::new(p).text_decoration(TextDecoration::Underline),
671                                        )
672                                        .span(a)
673                                } else {
674                                    let text = match self.mode.clone() {
675                                        InputMode::Hidden(ch) => {
676                                            ch.to_string().repeat(editor.rope().len_chars())
677                                        }
678                                        InputMode::Shown => editor.rope().to_string(),
679                                    };
680                                    el.span(text)
681                                }
682                            }),
683                    ),
684            )
685            .maybe_child(
686                self.trailing
687                    .clone()
688                    .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
689            )
690    }
691
692    fn render_key(&self) -> DiffKey {
693        self.key.clone().or(self.default_key())
694    }
695}