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    get_theme,
26    scrollviews::ScrollView,
27    theming::component_themes::{
28        InputColorsThemePartial,
29        InputLayoutThemePartial,
30        InputLayoutThemePartialExt,
31    },
32};
33
34#[derive(Clone, PartialEq)]
35pub enum InputStyleVariant {
36    Normal,
37    Filled,
38    Flat,
39}
40
41#[derive(Clone, PartialEq)]
42pub enum InputLayoutVariant {
43    Normal,
44    Compact,
45    Expanded,
46}
47
48#[derive(Default, Clone, PartialEq)]
49pub enum InputMode {
50    #[default]
51    Shown,
52    Hidden(char),
53}
54
55impl InputMode {
56    pub fn new_password() -> Self {
57        Self::Hidden('*')
58    }
59}
60
61#[derive(Debug, Default, PartialEq, Clone, Copy)]
62pub enum InputStatus {
63    /// Default state.
64    #[default]
65    Idle,
66    /// Pointer is hovering the input.
67    Hovering,
68}
69
70#[derive(Clone)]
71pub struct InputValidator {
72    valid: Rc<RefCell<bool>>,
73    text: Rc<RefCell<String>>,
74}
75
76impl InputValidator {
77    pub fn new(text: String) -> Self {
78        Self {
79            valid: Rc::new(RefCell::new(true)),
80            text: Rc::new(RefCell::new(text)),
81        }
82    }
83    pub fn text(&'_ self) -> Ref<'_, String> {
84        self.text.borrow()
85    }
86    pub fn set_valid(&self, is_valid: bool) {
87        *self.valid.borrow_mut() = is_valid;
88    }
89    pub fn is_valid(&self) -> bool {
90        *self.valid.borrow()
91    }
92}
93
94/// Small box to write some text.
95///
96/// ## **Normal**
97///
98/// ```rust
99/// # use freya::prelude::*;
100/// fn app() -> impl IntoElement {
101///     let value = use_state(String::new);
102///     Input::new(value).placeholder("Type here")
103/// }
104/// # use freya_testing::prelude::*;
105/// # launch_doc(|| {
106/// #   rect().center().expanded().child(app())
107/// # }, "./images/gallery_input.png").render();
108/// ```
109/// ## **Filled**
110///
111/// ```rust
112/// # use freya::prelude::*;
113/// fn app() -> impl IntoElement {
114///     let value = use_state(String::new);
115///     Input::new(value).placeholder("Type here").filled()
116/// }
117/// # use freya_testing::prelude::*;
118/// # launch_doc(|| {
119/// #   rect().center().expanded().child(app())
120/// # }, "./images/gallery_filled_input.png").render();
121/// ```
122/// ## **Flat**
123///
124/// ```rust
125/// # use freya::prelude::*;
126/// fn app() -> impl IntoElement {
127///     let value = use_state(String::new);
128///     Input::new(value).placeholder("Type here").flat()
129/// }
130/// # use freya_testing::prelude::*;
131/// # launch_doc(|| {
132/// #   rect().center().expanded().child(app())
133/// # }, "./images/gallery_flat_input.png").render();
134/// ```
135///
136/// # Preview
137/// ![Input Preview][input]
138/// ![Filled Input Preview][filled_input]
139/// ![Flat Input Preview][flat_input]
140#[cfg_attr(feature = "docs",
141    doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
142    doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
143    doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
144)]
145#[derive(Clone, PartialEq)]
146pub struct Input {
147    pub(crate) theme_colors: Option<InputColorsThemePartial>,
148    pub(crate) theme_layout: Option<InputLayoutThemePartial>,
149    value: Writable<String>,
150    placeholder: Option<Cow<'static, str>>,
151    on_validate: Option<EventHandler<InputValidator>>,
152    on_submit: Option<EventHandler<String>>,
153    mode: InputMode,
154    auto_focus: bool,
155    width: Size,
156    enabled: bool,
157    key: DiffKey,
158    style_variant: InputStyleVariant,
159    layout_variant: InputLayoutVariant,
160    text_align: TextAlign,
161    a11y_id: Option<AccessibilityId>,
162    leading: Option<Element>,
163    trailing: Option<Element>,
164}
165
166impl KeyExt for Input {
167    fn write_key(&mut self) -> &mut DiffKey {
168        &mut self.key
169    }
170}
171
172impl Input {
173    pub fn new(value: impl Into<Writable<String>>) -> Self {
174        Input {
175            theme_colors: None,
176            theme_layout: None,
177            value: value.into(),
178            placeholder: None,
179            on_validate: None,
180            on_submit: None,
181            mode: InputMode::default(),
182            auto_focus: false,
183            width: Size::px(150.),
184            enabled: true,
185            key: DiffKey::default(),
186            style_variant: InputStyleVariant::Normal,
187            layout_variant: InputLayoutVariant::Normal,
188            text_align: TextAlign::default(),
189            a11y_id: None,
190            leading: None,
191            trailing: None,
192        }
193    }
194
195    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
196        self.enabled = enabled.into();
197        self
198    }
199
200    pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
201        self.placeholder = Some(placeholder.into());
202        self
203    }
204
205    pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
206        self.on_validate = Some(on_validate.into());
207        self
208    }
209
210    pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
211        self.on_submit = Some(on_submit.into());
212        self
213    }
214
215    pub fn mode(mut self, mode: InputMode) -> Self {
216        self.mode = mode;
217        self
218    }
219
220    pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
221        self.auto_focus = auto_focus.into();
222        self
223    }
224
225    pub fn width(mut self, width: impl Into<Size>) -> Self {
226        self.width = width.into();
227        self
228    }
229
230    pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
231        self.theme_colors = Some(theme);
232        self
233    }
234
235    pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
236        self.theme_layout = Some(theme);
237        self
238    }
239
240    pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
241        self.text_align = text_align.into();
242        self
243    }
244
245    pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
246        self.style_variant = style_variant.into();
247        self
248    }
249
250    pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
251        self.layout_variant = layout_variant.into();
252        self
253    }
254
255    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Filled].
256    pub fn filled(self) -> Self {
257        self.style_variant(InputStyleVariant::Filled)
258    }
259
260    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Flat].
261    pub fn flat(self) -> Self {
262        self.style_variant(InputStyleVariant::Flat)
263    }
264
265    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Compact].
266    pub fn compact(self) -> Self {
267        self.layout_variant(InputLayoutVariant::Compact)
268    }
269
270    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Expanded].
271    pub fn expanded(self) -> Self {
272        self.layout_variant(InputLayoutVariant::Expanded)
273    }
274
275    pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
276        self.a11y_id = Some(a11y_id.into());
277        self
278    }
279
280    /// Optional element rendered before the text input.
281    pub fn leading(mut self, leading: impl Into<Element>) -> Self {
282        self.leading = Some(leading.into());
283        self
284    }
285
286    /// Optional element rendered after the text input.
287    pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
288        self.trailing = Some(trailing.into());
289        self
290    }
291}
292
293impl CornerRadiusExt for Input {
294    fn with_corner_radius(self, corner_radius: f32) -> Self {
295        self.corner_radius(corner_radius)
296    }
297}
298
299impl Component for Input {
300    fn render(&self) -> impl IntoElement {
301        let focus = use_hook(|| Focus::new_for_id(self.a11y_id.unwrap_or_else(Focus::new_id)));
302        let focus_status = use_focus_status(focus);
303        let holder = use_state(ParagraphHolder::default);
304        let mut area = use_state(Area::default);
305        let mut status = use_state(InputStatus::default);
306        let mut editable = use_editable(|| self.value.read().to_string(), EditableConfig::new);
307        let mut is_dragging = use_state(|| false);
308        let mut value = self.value.clone();
309
310        let theme_colors = match self.style_variant {
311            InputStyleVariant::Normal => get_theme!(&self.theme_colors, input),
312            InputStyleVariant::Filled => get_theme!(&self.theme_colors, filled_input),
313            InputStyleVariant::Flat => get_theme!(&self.theme_colors, flat_input),
314        };
315        let theme_layout = match self.layout_variant {
316            InputLayoutVariant::Normal => get_theme!(&self.theme_layout, input_layout),
317            InputLayoutVariant::Compact => get_theme!(&self.theme_layout, compact_input_layout),
318            InputLayoutVariant::Expanded => get_theme!(&self.theme_layout, expanded_input_layout),
319        };
320
321        let (mut movement_timeout, cursor_color) =
322            use_cursor_blink(focus_status() != FocusStatus::Not, theme_colors.color);
323
324        let enabled = use_reactive(&self.enabled);
325        use_drop(move || {
326            if status() == InputStatus::Hovering && enabled() {
327                Cursor::set(CursorIcon::default());
328            }
329        });
330
331        let display_placeholder = value.read().is_empty()
332            && self.placeholder.is_some()
333            && !editable.editor().read().has_preedit();
334        let on_validate = self.on_validate.clone();
335        let on_submit = self.on_submit.clone();
336
337        if *value.read() != editable.editor().read().committed_text() {
338            let mut editor = editable.editor_mut().write();
339            editor.clear_preedit();
340            editor.set(&value.read());
341            editor.editor_history().clear();
342            editor.clear_selection();
343        }
344
345        let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
346            let mut editor = editable.editor_mut().write();
347            if e.data().text.is_empty() {
348                editor.clear_preedit();
349            } else {
350                editor.set_preedit(&e.data().text);
351            }
352        };
353
354        let on_key_down = move |e: Event<KeyboardEventData>| {
355            match &e.key {
356                // On submit
357                Key::Named(NamedKey::Enter) => {
358                    if let Some(on_submit) = &on_submit {
359                        let text = editable.editor().peek().committed_text();
360                        on_submit.call(text);
361                    }
362                }
363                // On unfocus
364                Key::Named(NamedKey::Escape) => {
365                    focus.request_unfocus();
366                    Cursor::set(CursorIcon::default());
367                }
368                // On change
369                key => {
370                    if *key != Key::Named(NamedKey::Tab) {
371                        e.stop_propagation();
372                        movement_timeout.reset();
373                        editable.process_event(EditableEvent::KeyDown {
374                            key: &e.key,
375                            modifiers: e.modifiers,
376                        });
377                        let text = editable.editor().read().committed_text();
378
379                        let apply_change = match &on_validate {
380                            Some(on_validate) => {
381                                let mut editor = editable.editor_mut().write();
382                                let validator = InputValidator::new(text.clone());
383                                on_validate.call(validator.clone());
384                                if !validator.is_valid() {
385                                    if let Some(selection) = editor.undo() {
386                                        *editor.selection_mut() = selection;
387                                    }
388                                    editor.editor_history().clear_redos();
389                                }
390                                validator.is_valid()
391                            }
392                            None => true,
393                        };
394
395                        if apply_change {
396                            *value.write() = text;
397                        }
398                    }
399                }
400            }
401        };
402
403        let on_key_up = move |e: Event<KeyboardEventData>| {
404            e.stop_propagation();
405            editable.process_event(EditableEvent::KeyUp { key: &e.key });
406        };
407
408        let on_input_pointer_down = move |e: Event<PointerEventData>| {
409            e.stop_propagation();
410            is_dragging.set(true);
411            movement_timeout.reset();
412            if !display_placeholder {
413                let area = area.read().to_f64();
414                let global_location = e.global_location().clamp(area.min(), area.max());
415                let location = (global_location - area.min()).to_point();
416                editable.process_event(EditableEvent::Down {
417                    location,
418                    editor_line: EditorLine::SingleParagraph,
419                    holder: &holder.read(),
420                });
421            }
422            focus.request_focus();
423        };
424
425        let on_pointer_down = move |e: Event<PointerEventData>| {
426            e.stop_propagation();
427            is_dragging.set(true);
428            movement_timeout.reset();
429            if !display_placeholder {
430                editable.process_event(EditableEvent::Down {
431                    location: e.element_location(),
432                    editor_line: EditorLine::SingleParagraph,
433                    holder: &holder.read(),
434                });
435            }
436            focus.request_focus();
437        };
438
439        let on_global_pointer_move = move |e: Event<PointerEventData>| {
440            if focus.is_focused() && *is_dragging.read() {
441                let mut location = e.global_location();
442                location.x -= area.read().min_x() as f64;
443                location.y -= area.read().min_y() as f64;
444                editable.process_event(EditableEvent::Move {
445                    location,
446                    editor_line: EditorLine::SingleParagraph,
447                    holder: &holder.read(),
448                });
449            }
450        };
451
452        let on_pointer_enter = move |_| {
453            *status.write() = InputStatus::Hovering;
454            if enabled() {
455                Cursor::set(CursorIcon::Text);
456            } else {
457                Cursor::set(CursorIcon::NotAllowed);
458            }
459        };
460
461        let on_pointer_leave = move |_| {
462            if status() == InputStatus::Hovering {
463                Cursor::set(CursorIcon::default());
464                *status.write() = InputStatus::default();
465            }
466        };
467
468        let on_global_pointer_press = move |_: Event<PointerEventData>| {
469            match *status.read() {
470                InputStatus::Idle if focus.is_focused() => {
471                    editable.process_event(EditableEvent::Release);
472                }
473                InputStatus::Hovering => {
474                    editable.process_event(EditableEvent::Release);
475                }
476                _ => {}
477            };
478
479            if focus.is_focused() {
480                if *is_dragging.read() {
481                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
482                    is_dragging.set(false);
483                } else {
484                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
485                    focus.request_unfocus();
486                }
487            }
488        };
489
490        let on_pointer_press = move |e: Event<PointerEventData>| {
491            e.stop_propagation();
492            e.prevent_default();
493            match *status.read() {
494                InputStatus::Idle if focus.is_focused() => {
495                    editable.process_event(EditableEvent::Release);
496                }
497                InputStatus::Hovering => {
498                    editable.process_event(EditableEvent::Release);
499                }
500                _ => {}
501            };
502
503            if focus.is_focused() {
504                is_dragging.set_if_modified(false);
505            }
506        };
507
508        let a11y_id = focus.a11y_id();
509
510        let (background, cursor_index, text_selection) =
511            if enabled() && focus_status() != FocusStatus::Not {
512                (
513                    theme_colors.hover_background,
514                    Some(editable.editor().read().cursor_pos()),
515                    editable
516                        .editor()
517                        .read()
518                        .get_visible_selection(EditorLine::SingleParagraph),
519                )
520            } else {
521                (theme_colors.background, None, None)
522            };
523
524        let border = if focus_status().is_focused() {
525            Border::new()
526                .fill(theme_colors.focus_border_fill)
527                .width(2.)
528                .alignment(BorderAlignment::Inner)
529        } else {
530            Border::new()
531                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
532                .width(1.)
533                .alignment(BorderAlignment::Inner)
534        };
535
536        let color = if display_placeholder {
537            theme_colors.placeholder_color
538        } else {
539            theme_colors.color
540        };
541
542        let value = self.value.read();
543        let a11y_text: Cow<str> = match (self.mode.clone(), &self.placeholder) {
544            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
545            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
546            (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
547        };
548
549        let a11_role = match self.mode {
550            InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
551            _ => AccessibilityRole::TextInput,
552        };
553
554        rect()
555            .a11y_id(a11y_id)
556            .a11y_focusable(self.enabled)
557            .a11y_auto_focus(self.auto_focus)
558            .a11y_alt(a11y_text)
559            .a11y_role(a11_role)
560            .maybe(self.enabled, |el| {
561                el.on_key_up(on_key_up)
562                    .on_key_down(on_key_down)
563                    .on_pointer_down(on_input_pointer_down)
564                    .on_ime_preedit(on_ime_preedit)
565                    .on_pointer_press(on_pointer_press)
566                    .on_global_pointer_press(on_global_pointer_press)
567                    .on_global_pointer_move(on_global_pointer_move)
568            })
569            .on_pointer_enter(on_pointer_enter)
570            .on_pointer_leave(on_pointer_leave)
571            .width(self.width.clone())
572            .background(background.mul_if(!self.enabled, 0.85))
573            .border(border)
574            .corner_radius(theme_layout.corner_radius)
575            .content(Content::Flex)
576            .direction(Direction::Horizontal)
577            .cross_align(Alignment::center())
578            .maybe_child(
579                self.leading
580                    .clone()
581                    .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
582            )
583            .child(
584                ScrollView::new()
585                    .width(Size::flex(1.))
586                    .height(Size::Inner)
587                    .direction(Direction::Horizontal)
588                    .show_scrollbar(false)
589                    .child(
590                        paragraph()
591                            .holder(holder.read().clone())
592                            .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
593                            .min_width(Size::func(move |context| {
594                                Some(context.parent + theme_layout.inner_margin.horizontal())
595                            }))
596                            .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
597                            .margin(theme_layout.inner_margin)
598                            .cursor_index(cursor_index)
599                            .cursor_color(cursor_color)
600                            .color(color)
601                            .text_align(self.text_align)
602                            .max_lines(1)
603                            .highlights(text_selection.map(|h| vec![h]))
604                            .maybe(display_placeholder, |el| {
605                                el.span(self.placeholder.as_ref().unwrap().to_string())
606                            })
607                            .maybe(!display_placeholder, |el| {
608                                let editor = editable.editor().read();
609                                if editor.has_preedit() {
610                                    let (b, p, a) = editor.preedit_text_segments();
611                                    let (b, p, a) = match self.mode.clone() {
612                                        InputMode::Hidden(ch) => {
613                                            let ch = ch.to_string();
614                                            (
615                                                ch.repeat(b.chars().count()),
616                                                ch.repeat(p.chars().count()),
617                                                ch.repeat(a.chars().count()),
618                                            )
619                                        }
620                                        InputMode::Shown => (b, p, a),
621                                    };
622                                    el.span(b)
623                                        .span(
624                                            Span::new(p).text_decoration(TextDecoration::Underline),
625                                        )
626                                        .span(a)
627                                } else {
628                                    let text = match self.mode.clone() {
629                                        InputMode::Hidden(ch) => {
630                                            ch.to_string().repeat(editor.rope().len_chars())
631                                        }
632                                        InputMode::Shown => editor.rope().to_string(),
633                                    };
634                                    el.span(text)
635                                }
636                            }),
637                    ),
638            )
639            .maybe_child(
640                self.trailing
641                    .clone()
642                    .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
643            )
644    }
645
646    fn render_key(&self) -> DiffKey {
647        self.key.clone().or(self.default_key())
648    }
649}