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::InputThemePartial,
26};
27
28#[derive(Default, Clone, PartialEq)]
29pub enum InputMode {
30    #[default]
31    Shown,
32    Hidden(char),
33}
34
35impl InputMode {
36    pub fn new_password() -> Self {
37        Self::Hidden('*')
38    }
39}
40
41#[derive(Debug, Default, PartialEq, Clone, Copy)]
42pub enum InputStatus {
43    /// Default state.
44    #[default]
45    Idle,
46    /// Pointer is hovering the input.
47    Hovering,
48}
49
50#[derive(Clone)]
51pub struct InputValidator {
52    valid: Rc<RefCell<bool>>,
53    text: Rc<RefCell<String>>,
54}
55
56impl InputValidator {
57    pub fn new(text: String) -> Self {
58        Self {
59            valid: Rc::new(RefCell::new(true)),
60            text: Rc::new(RefCell::new(text)),
61        }
62    }
63    pub fn text(&'_ self) -> Ref<'_, String> {
64        self.text.borrow()
65    }
66    pub fn set_valid(&self, is_valid: bool) {
67        *self.valid.borrow_mut() = is_valid;
68    }
69    pub fn is_valid(&self) -> bool {
70        *self.valid.borrow()
71    }
72}
73
74/// Small box to write some text.
75///
76/// # Example
77///
78/// ```rust
79/// # use freya::prelude::*;
80/// fn app() -> impl IntoElement {
81///     let mut value = use_state(String::new);
82///
83///     rect()
84///         .expanded()
85///         .center()
86///         .spacing(6.)
87///         .child(
88///             Input::new()
89///                 .placeholder("Type your name")
90///                 .value(value.read().clone())
91///                 .on_change(move |v| value.set(v)),
92///         )
93///         .child(format!("Your name is {}", value.read()))
94/// }
95///
96/// # use freya_testing::prelude::*;
97/// # launch_doc(|| {
98/// #   rect().center().expanded().child(Input::new() .value("Ferris"))
99/// # }, "./images/gallery_input.png").render();
100/// ```
101/// # Preview
102/// ![Input Preview][input]
103#[cfg_attr(feature = "docs",
104    doc = embed_doc_image::embed_image!("input", "images/gallery_input.png")
105)]
106#[derive(Clone, PartialEq)]
107pub struct Input {
108    pub(crate) theme: Option<InputThemePartial>,
109    value: Cow<'static, str>,
110    placeholder: Option<Cow<'static, str>>,
111    on_change: Option<EventHandler<String>>,
112    on_validate: Option<EventHandler<InputValidator>>,
113    mode: InputMode,
114    auto_focus: bool,
115    width: Size,
116    enabled: bool,
117    key: DiffKey,
118}
119
120impl KeyExt for Input {
121    fn write_key(&mut self) -> &mut DiffKey {
122        &mut self.key
123    }
124}
125
126impl Default for Input {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl Input {
133    pub fn new() -> Self {
134        Input {
135            theme: None,
136            value: Cow::default(),
137            placeholder: None,
138            on_change: None,
139            on_validate: None,
140            mode: InputMode::default(),
141            auto_focus: false,
142            width: Size::px(150.),
143            enabled: true,
144            key: DiffKey::default(),
145        }
146    }
147
148    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
149        self.enabled = enabled.into();
150        self
151    }
152
153    pub fn value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
154        self.value = value.into();
155        self
156    }
157
158    pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
159        self.placeholder = Some(placeholder.into());
160        self
161    }
162
163    pub fn on_change(mut self, on_change: impl Into<EventHandler<String>>) -> Self {
164        self.on_change = Some(on_change.into());
165        self
166    }
167
168    pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
169        self.on_validate = Some(on_validate.into());
170        self
171    }
172
173    pub fn mode(mut self, mode: InputMode) -> Self {
174        self.mode = mode;
175        self
176    }
177
178    pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
179        self.auto_focus = auto_focus.into();
180        self
181    }
182
183    pub fn width(mut self, width: impl Into<Size>) -> Self {
184        self.width = width.into();
185        self
186    }
187
188    pub fn theme(mut self, theme: InputThemePartial) -> Self {
189        self.theme = Some(theme);
190        self
191    }
192
193    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
194        self.key = key.into();
195        self
196    }
197}
198
199impl Render for Input {
200    fn render(&self) -> impl IntoElement {
201        let focus = use_focus();
202        let focus_status = use_focus_status(focus);
203        let holder = use_state(ParagraphHolder::default);
204        let mut area = use_state(Area::default);
205        let mut status = use_state(InputStatus::default);
206        let mut editable = use_editable(|| self.value.to_string(), EditableConfig::new);
207        let mut is_dragging = use_state(|| false);
208        let mut ime_preedit = use_state(|| None);
209        let theme = get_theme!(&self.theme, input);
210        let (mut movement_timeout, cursor_color) =
211            use_cursor_blink(focus_status() != FocusStatus::Not, theme.color);
212
213        let enabled = use_reactive(&self.enabled);
214        use_drop(move || {
215            if status() == InputStatus::Hovering && enabled() {
216                Cursor::set(CursorIcon::default());
217            }
218        });
219
220        let display_placeholder = self.value.is_empty() && self.placeholder.is_some();
221        let on_change = self.on_change.clone();
222        let on_validate = self.on_validate.clone();
223
224        if &*self.value != editable.editor().read().rope() {
225            editable.editor_mut().write().set(&self.value);
226            editable.editor_mut().write().editor_history().clear();
227        }
228
229        let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
230            ime_preedit.set(Some(e.data().text.clone()));
231        };
232
233        let on_key_down = move |e: Event<KeyboardEventData>| {
234            if e.key != Key::Named(NamedKey::Enter) && e.key != Key::Named(NamedKey::Tab) {
235                e.stop_propagation();
236                movement_timeout.reset();
237                editable.process_event(EditableEvent::KeyDown {
238                    key: &e.key,
239                    modifiers: e.modifiers,
240                });
241                let text = editable.editor().peek().to_string();
242
243                let apply_change = if let Some(on_validate) = &on_validate {
244                    let editor = editable.editor_mut();
245                    let mut editor = editor.write();
246                    let validator = InputValidator::new(text.clone());
247                    on_validate.call(validator.clone());
248                    let is_valid = validator.is_valid();
249
250                    if !is_valid {
251                        // If it is not valid then undo the latest change and discard all the redos
252                        let undo_result = editor.undo();
253                        if let Some(idx) = undo_result {
254                            editor.move_cursor_to(idx);
255                        }
256                        editor.editor_history().clear_redos();
257                    }
258
259                    is_valid
260                } else {
261                    true
262                };
263
264                if apply_change && let Some(on_change) = &on_change {
265                    on_change.call(text);
266                }
267            }
268        };
269
270        let on_key_up = move |e: Event<KeyboardEventData>| {
271            e.stop_propagation();
272            editable.process_event(EditableEvent::KeyUp { key: &e.key });
273        };
274
275        let on_input_pointer_down = move |e: Event<PointerEventData>| {
276            e.stop_propagation();
277            is_dragging.set(true);
278            movement_timeout.reset();
279            if !display_placeholder {
280                let area = area.read().to_f64();
281                let global_location = e.global_location().clamp(area.min(), area.max());
282                let location = (global_location - area.min()).to_point();
283                editable.process_event(EditableEvent::Down {
284                    location,
285                    editor_line: EditorLine::SingleParagraph,
286                    holder: &holder.read(),
287                });
288            }
289            focus.request_focus();
290        };
291
292        let on_pointer_down = move |e: Event<PointerEventData>| {
293            e.stop_propagation();
294            is_dragging.set(true);
295            movement_timeout.reset();
296            if !display_placeholder {
297                editable.process_event(EditableEvent::Down {
298                    location: e.element_location(),
299                    editor_line: EditorLine::SingleParagraph,
300                    holder: &holder.read(),
301                });
302            }
303            focus.request_focus();
304        };
305
306        let on_global_mouse_move = move |e: Event<MouseEventData>| {
307            if focus.is_focused() && *is_dragging.read() {
308                let mut location = e.global_location;
309                location.x -= area.read().min_x() as f64;
310                location.y -= area.read().min_y() as f64;
311                editable.process_event(EditableEvent::Move {
312                    location,
313                    editor_line: EditorLine::SingleParagraph,
314                    holder: &holder.read(),
315                });
316            }
317        };
318
319        let on_pointer_enter = move |_| {
320            *status.write() = InputStatus::Hovering;
321            if enabled() {
322                Cursor::set(CursorIcon::Text);
323            } else {
324                Cursor::set(CursorIcon::NotAllowed);
325            }
326        };
327
328        let on_pointer_leave = move |_| {
329            if status() == InputStatus::Hovering {
330                Cursor::set(CursorIcon::default());
331                *status.write() = InputStatus::default();
332            }
333        };
334
335        let on_global_mouse_up = move |_| {
336            match *status.read() {
337                InputStatus::Idle if focus.is_focused() => {
338                    editable.process_event(EditableEvent::Release);
339                }
340                InputStatus::Hovering => {
341                    editable.process_event(EditableEvent::Release);
342                }
343                _ => {}
344            };
345
346            if focus.is_focused() {
347                if *is_dragging.read() {
348                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
349                    is_dragging.set(false);
350                } else {
351                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
352                    focus.request_unfocus();
353                }
354            }
355        };
356
357        let a11y_id = focus.a11y_id();
358
359        let (background, cursor_index, text_selection) =
360            if enabled() && focus_status() != FocusStatus::Not {
361                (
362                    theme.hover_background,
363                    Some(editable.editor().read().cursor_pos()),
364                    editable
365                        .editor()
366                        .read()
367                        .get_visible_selection(EditorLine::SingleParagraph),
368                )
369            } else {
370                (theme.background, None, None)
371            };
372
373        let border = if focus_status() == FocusStatus::Keyboard {
374            Border::new()
375                .fill(theme.focus_border_fill)
376                .width(2.)
377                .alignment(BorderAlignment::Inner)
378        } else {
379            Border::new()
380                .fill(theme.border_fill.mul_if(!self.enabled, 0.85))
381                .width(1.)
382                .alignment(BorderAlignment::Inner)
383        };
384
385        let color = if display_placeholder {
386            theme.placeholder_color
387        } else {
388            theme.color
389        };
390
391        let text = match (self.mode.clone(), &self.placeholder) {
392            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
393            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(self.value.len())),
394            (InputMode::Shown, _) => Cow::Borrowed(self.value.as_ref()),
395        };
396
397        let preedit_text = (!display_placeholder)
398            .then(|| ime_preedit.read().clone())
399            .flatten();
400
401        let a11_role = match self.mode {
402            InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
403            _ => AccessibilityRole::TextInput,
404        };
405
406        rect()
407            .a11y_id(a11y_id)
408            .a11y_focusable(self.enabled)
409            .a11y_auto_focus(self.auto_focus)
410            .a11y_alt(text.clone())
411            .a11y_role(a11_role)
412            .maybe(self.enabled, |rect| {
413                rect.on_key_up(on_key_up)
414                    .on_key_down(on_key_down)
415                    .on_pointer_down(on_input_pointer_down)
416                    .on_ime_preedit(on_ime_preedit)
417            })
418            .on_pointer_enter(on_pointer_enter)
419            .on_pointer_leave(on_pointer_leave)
420            .width(self.width.clone())
421            .background(background.mul_if(!self.enabled, 0.85))
422            .border(border)
423            .corner_radius(theme.corner_radius)
424            .main_align(Alignment::center())
425            .cross_align(Alignment::center())
426            .child(
427                ScrollView::new()
428                    .height(Size::Inner)
429                    .direction(Direction::Horizontal)
430                    .show_scrollbar(false)
431                    .child(
432                        paragraph()
433                            .holder(holder.read().clone())
434                            .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
435                            .min_width(Size::func(move |context| {
436                                Some(context.parent + theme.inner_margin.horizontal())
437                            }))
438                            .maybe(self.enabled, |rect| {
439                                rect.on_pointer_down(on_pointer_down)
440                                    .on_global_mouse_up(on_global_mouse_up)
441                                    .on_global_mouse_move(on_global_mouse_move)
442                            })
443                            .margin(theme.inner_margin)
444                            .cursor_index(cursor_index)
445                            .cursor_color(cursor_color)
446                            .color(color)
447                            .max_lines(1)
448                            .highlights(text_selection.map(|h| vec![h]))
449                            .span(text.to_string())
450                            .map(preedit_text, |el, preedit_text| el.span(preedit_text)),
451                    ),
452            )
453    }
454
455    fn render_key(&self) -> DiffKey {
456        self.key.clone().or(self.default_key())
457    }
458}