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