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}
157
158impl KeyExt for Input {
159    fn write_key(&mut self) -> &mut DiffKey {
160        &mut self.key
161    }
162}
163
164impl Default for Input {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl Input {
171    pub fn new() -> Self {
172        Input {
173            theme_colors: None,
174            theme_layout: None,
175            value: ReadState::Owned(String::new()),
176            placeholder: None,
177            on_change: None,
178            on_validate: None,
179            on_submit: None,
180            mode: InputMode::default(),
181            auto_focus: false,
182            width: Size::px(150.),
183            enabled: true,
184            key: DiffKey::default(),
185            style_variant: InputStyleVariant::Normal,
186            layout_variant: InputLayoutVariant::Normal,
187        }
188    }
189
190    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
191        self.enabled = enabled.into();
192        self
193    }
194
195    pub fn value(mut self, value: impl Into<ReadState<String>>) -> Self {
196        self.value = value.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_change(mut self, on_change: impl Into<EventHandler<String>>) -> Self {
206        self.on_change = Some(on_change.into());
207        self
208    }
209
210    pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
211        self.on_validate = Some(on_validate.into());
212        self
213    }
214
215    pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
216        self.on_submit = Some(on_submit.into());
217        self
218    }
219
220    pub fn mode(mut self, mode: InputMode) -> Self {
221        self.mode = mode;
222        self
223    }
224
225    pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
226        self.auto_focus = auto_focus.into();
227        self
228    }
229
230    pub fn width(mut self, width: impl Into<Size>) -> Self {
231        self.width = width.into();
232        self
233    }
234
235    pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
236        self.theme_colors = Some(theme);
237        self
238    }
239
240    pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
241        self.theme_layout = Some(theme);
242        self
243    }
244
245    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
246        self.key = key.into();
247        self
248    }
249
250    pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
251        self.style_variant = style_variant.into();
252        self
253    }
254
255    pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
256        self.layout_variant = layout_variant.into();
257        self
258    }
259
260    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Filled].
261    pub fn filled(self) -> Self {
262        self.style_variant(InputStyleVariant::Filled)
263    }
264
265    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Flat].
266    pub fn flat(self) -> Self {
267        self.style_variant(InputStyleVariant::Flat)
268    }
269
270    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Compact].
271    pub fn compact(self) -> Self {
272        self.layout_variant(InputLayoutVariant::Compact)
273    }
274
275    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Expanded].
276    pub fn expanded(self) -> Self {
277        self.layout_variant(InputLayoutVariant::Expanded)
278    }
279}
280
281impl CornerRadiusExt for Input {
282    fn with_corner_radius(self, corner_radius: f32) -> Self {
283        self.corner_radius(corner_radius)
284    }
285}
286
287impl Component for Input {
288    fn render(&self) -> impl IntoElement {
289        let focus = use_focus();
290        let focus_status = use_focus_status(focus);
291        let holder = use_state(ParagraphHolder::default);
292        let mut area = use_state(Area::default);
293        let mut status = use_state(InputStatus::default);
294        let mut editable = use_editable(|| self.value.read().to_string(), EditableConfig::new);
295        let mut is_dragging = use_state(|| false);
296        let mut ime_preedit = use_state(|| None);
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 = self.value.read().is_empty() && self.placeholder.is_some();
320        let on_change = self.on_change.clone();
321        let on_validate = self.on_validate.clone();
322        let on_submit = self.on_submit.clone();
323
324        if &*self.value.read() != editable.editor().read().rope() {
325            editable.editor_mut().write().set(&self.value.read());
326            editable.editor_mut().write().editor_history().clear();
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 change
343                key => {
344                    if *key != Key::Named(NamedKey::Enter) && *key != Key::Named(NamedKey::Tab) {
345                        e.stop_propagation();
346                        movement_timeout.reset();
347                        editable.process_event(EditableEvent::KeyDown {
348                            key: &e.key,
349                            modifiers: e.modifiers,
350                        });
351                        let text = editable.editor().read().rope().to_string();
352
353                        let apply_change = match &on_validate {
354                            Some(on_validate) => {
355                                let mut editor = editable.editor_mut().write();
356                                let validator = InputValidator::new(text.clone());
357                                on_validate.call(validator.clone());
358                                if !validator.is_valid() {
359                                    if let Some(selection) = editor.undo() {
360                                        *editor.selection_mut() = selection;
361                                    }
362                                    editor.editor_history().clear_redos();
363                                }
364                                validator.is_valid()
365                            }
366                            None => true,
367                        };
368
369                        if apply_change && let Some(on_change) = &on_change {
370                            on_change.call(text);
371                        }
372                    }
373                }
374            }
375        };
376
377        let on_key_up = move |e: Event<KeyboardEventData>| {
378            e.stop_propagation();
379            editable.process_event(EditableEvent::KeyUp { key: &e.key });
380        };
381
382        let on_input_pointer_down = move |e: Event<PointerEventData>| {
383            e.stop_propagation();
384            is_dragging.set(true);
385            movement_timeout.reset();
386            if !display_placeholder {
387                let area = area.read().to_f64();
388                let global_location = e.global_location().clamp(area.min(), area.max());
389                let location = (global_location - area.min()).to_point();
390                editable.process_event(EditableEvent::Down {
391                    location,
392                    editor_line: EditorLine::SingleParagraph,
393                    holder: &holder.read(),
394                });
395            }
396            focus.request_focus();
397        };
398
399        let on_pointer_down = move |e: Event<PointerEventData>| {
400            e.stop_propagation();
401            is_dragging.set(true);
402            movement_timeout.reset();
403            if !display_placeholder {
404                editable.process_event(EditableEvent::Down {
405                    location: e.element_location(),
406                    editor_line: EditorLine::SingleParagraph,
407                    holder: &holder.read(),
408                });
409            }
410            focus.request_focus();
411        };
412
413        let on_global_mouse_move = move |e: Event<MouseEventData>| {
414            if focus.is_focused() && *is_dragging.read() {
415                let mut location = e.global_location;
416                location.x -= area.read().min_x() as f64;
417                location.y -= area.read().min_y() as f64;
418                editable.process_event(EditableEvent::Move {
419                    location,
420                    editor_line: EditorLine::SingleParagraph,
421                    holder: &holder.read(),
422                });
423            }
424        };
425
426        let on_pointer_enter = move |_| {
427            *status.write() = InputStatus::Hovering;
428            if enabled() {
429                Cursor::set(CursorIcon::Text);
430            } else {
431                Cursor::set(CursorIcon::NotAllowed);
432            }
433        };
434
435        let on_pointer_leave = move |_| {
436            if status() == InputStatus::Hovering {
437                Cursor::set(CursorIcon::default());
438                *status.write() = InputStatus::default();
439            }
440        };
441
442        let on_global_mouse_up = move |_| {
443            match *status.read() {
444                InputStatus::Idle if focus.is_focused() => {
445                    editable.process_event(EditableEvent::Release);
446                }
447                InputStatus::Hovering => {
448                    editable.process_event(EditableEvent::Release);
449                }
450                _ => {}
451            };
452
453            if focus.is_focused() {
454                if *is_dragging.read() {
455                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
456                    is_dragging.set(false);
457                } else {
458                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
459                    focus.request_unfocus();
460                }
461            }
462        };
463
464        let a11y_id = focus.a11y_id();
465
466        let (background, cursor_index, text_selection) =
467            if enabled() && focus_status() != FocusStatus::Not {
468                (
469                    theme_colors.hover_background,
470                    Some(editable.editor().read().cursor_pos()),
471                    editable
472                        .editor()
473                        .read()
474                        .get_visible_selection(EditorLine::SingleParagraph),
475                )
476            } else {
477                (theme_colors.background, None, None)
478            };
479
480        let border = if focus_status() == FocusStatus::Keyboard {
481            Border::new()
482                .fill(theme_colors.focus_border_fill)
483                .width(2.)
484                .alignment(BorderAlignment::Inner)
485        } else {
486            Border::new()
487                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
488                .width(1.)
489                .alignment(BorderAlignment::Inner)
490        };
491
492        let color = if display_placeholder {
493            theme_colors.placeholder_color
494        } else {
495            theme_colors.color
496        };
497
498        let value = self.value.read();
499        let text = match (self.mode.clone(), &self.placeholder) {
500            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
501            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
502            (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
503        };
504
505        let preedit_text = (!display_placeholder)
506            .then(|| ime_preedit.read().clone())
507            .flatten();
508
509        let a11_role = match self.mode {
510            InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
511            _ => AccessibilityRole::TextInput,
512        };
513
514        rect()
515            .a11y_id(a11y_id)
516            .a11y_focusable(self.enabled)
517            .a11y_auto_focus(self.auto_focus)
518            .a11y_alt(text.clone())
519            .a11y_role(a11_role)
520            .maybe(self.enabled, |rect| {
521                rect.on_key_up(on_key_up)
522                    .on_key_down(on_key_down)
523                    .on_pointer_down(on_input_pointer_down)
524                    .on_ime_preedit(on_ime_preedit)
525            })
526            .on_pointer_enter(on_pointer_enter)
527            .on_pointer_leave(on_pointer_leave)
528            .width(self.width.clone())
529            .background(background.mul_if(!self.enabled, 0.85))
530            .border(border)
531            .corner_radius(theme_layout.corner_radius)
532            .main_align(Alignment::center())
533            .cross_align(Alignment::center())
534            .child(
535                ScrollView::new()
536                    .height(Size::Inner)
537                    .direction(Direction::Horizontal)
538                    .show_scrollbar(false)
539                    .child(
540                        paragraph()
541                            .holder(holder.read().clone())
542                            .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
543                            .min_width(Size::func(move |context| {
544                                Some(context.parent + theme_layout.inner_margin.horizontal())
545                            }))
546                            .maybe(self.enabled, |rect| {
547                                rect.on_pointer_down(on_pointer_down)
548                                    .on_global_mouse_up(on_global_mouse_up)
549                                    .on_global_mouse_move(on_global_mouse_move)
550                            })
551                            .margin(theme_layout.inner_margin)
552                            .cursor_index(cursor_index)
553                            .cursor_color(cursor_color)
554                            .color(color)
555                            .max_lines(1)
556                            .highlights(text_selection.map(|h| vec![h]))
557                            .span(text.to_string())
558                            .map(preedit_text, |el, preedit_text| el.span(preedit_text)),
559                    ),
560            )
561    }
562
563    fn render_key(&self) -> DiffKey {
564        self.key.clone().or(self.default_key())
565    }
566}