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