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