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