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