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(
206            || self.value.to_string(),
207            EditableConfig::new,
208            EditableMode::MultipleLinesSingleEditor,
209        );
210        let mut is_dragging = use_state(|| false);
211        let mut ime_preedit = use_state(|| None);
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 theme = get_theme!(&self.theme, input);
221
222        let display_placeholder = self.value.is_empty() && self.placeholder.is_some();
223        let on_change = self.on_change.clone();
224        let on_validate = self.on_validate.clone();
225
226        if &*self.value != editable.editor().read().rope() {
227            editable.editor_mut().write().set(&self.value);
228            editable.editor_mut().write().editor_history().clear();
229        }
230
231        let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
232            ime_preedit.set(Some(e.data().text.clone()));
233        };
234
235        let on_key_down = move |e: Event<KeyboardEventData>| {
236            if e.key != Key::Enter && e.key != Key::Tab {
237                e.stop_propagation();
238                editable.process_event(EditableEvent::KeyDown {
239                    key: &e.key,
240                    modifiers: e.modifiers,
241                });
242                let text = editable.editor().peek().to_string();
243
244                let apply_change = if let Some(on_validate) = &on_validate {
245                    let editor = editable.editor_mut();
246                    let mut editor = editor.write();
247                    let validator = InputValidator::new(text.clone());
248                    on_validate.call(validator.clone());
249                    let is_valid = validator.is_valid();
250
251                    if !is_valid {
252                        // If it is not valid then undo the latest change and discard all the redos
253                        let undo_result = editor.undo();
254                        if let Some(idx) = undo_result {
255                            editor.set_cursor_pos(idx);
256                        }
257                        editor.editor_history().clear_redos();
258                    }
259
260                    is_valid
261                } else {
262                    true
263                };
264
265                if apply_change && let Some(onchange) = &on_change {
266                    onchange.call(text);
267                }
268            }
269        };
270
271        let on_key_up = move |e: Event<KeyboardEventData>| {
272            e.stop_propagation();
273            editable.process_event(EditableEvent::KeyUp { key: &e.key });
274        };
275
276        let on_input_pointer_down = move |e: Event<PointerEventData>| {
277            e.stop_propagation();
278            is_dragging.set(true);
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_id: 0,
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            if !display_placeholder {
296                editable.process_event(EditableEvent::Down {
297                    location: e.element_location(),
298                    editor_id: 0,
299                    holder: &holder.read(),
300                });
301            }
302            focus.request_focus();
303        };
304
305        let on_global_mouse_move = move |e: Event<MouseEventData>| {
306            if focus.is_focused() && *is_dragging.read() {
307                let mut location = e.global_location;
308                location.x -= area.read().min_x() as f64;
309                location.y -= area.read().min_y() as f64;
310                editable.process_event(EditableEvent::Move {
311                    location,
312                    editor_id: 0,
313                    holder: &holder.read(),
314                });
315            }
316        };
317
318        let on_pointer_enter = move |_| {
319            *status.write() = InputStatus::Hovering;
320            if enabled() {
321                Cursor::set(CursorIcon::Text);
322            } else {
323                Cursor::set(CursorIcon::NotAllowed);
324            }
325        };
326
327        let on_pointer_leave = move |_| {
328            if status() == InputStatus::Hovering {
329                Cursor::set(CursorIcon::default());
330                *status.write() = InputStatus::default();
331            }
332        };
333
334        let on_global_mouse_up = move |_| {
335            match *status.read() {
336                InputStatus::Idle if focus.is_focused() => {
337                    editable.process_event(EditableEvent::Release);
338                }
339                InputStatus::Hovering => {
340                    editable.process_event(EditableEvent::Release);
341                }
342                _ => {}
343            };
344
345            if focus.is_focused() {
346                if *is_dragging.read() {
347                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
348                    is_dragging.set(false);
349                } else {
350                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
351                    focus.request_unfocus();
352                }
353            }
354        };
355
356        let a11y_id = focus.a11y_id();
357
358        let (background, cursor_index, text_selection) =
359            if enabled() && focus_status() != FocusStatus::Not {
360                (
361                    theme.hover_background,
362                    Some(editable.editor().read().cursor_pos()),
363                    editable.editor().read().get_visible_selection(0),
364                )
365            } else {
366                (theme.background, None, None)
367            };
368
369        let border = if focus_status() == FocusStatus::Keyboard {
370            Border::new()
371                .fill(theme.focus_border_fill)
372                .width(2.)
373                .alignment(BorderAlignment::Inner)
374        } else {
375            Border::new()
376                .fill(theme.border_fill.mul_if(!self.enabled, 0.85))
377                .width(1.)
378                .alignment(BorderAlignment::Inner)
379        };
380
381        let color = if display_placeholder {
382            theme.placeholder_color
383        } else {
384            theme.color
385        };
386
387        let text = match (self.mode.clone(), &self.placeholder) {
388            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
389            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(self.value.len())),
390            (InputMode::Shown, _) => Cow::Borrowed(self.value.as_ref()),
391        };
392
393        let preedit_text = (!display_placeholder)
394            .then(|| ime_preedit.read().clone())
395            .flatten();
396
397        rect()
398            .a11y_id(a11y_id)
399            .a11y_focusable(self.enabled)
400            .a11y_auto_focus(self.auto_focus)
401            .a11y_alt(text.clone())
402            .maybe(self.enabled, |rect| {
403                rect.on_key_up(on_key_up)
404                    .on_key_down(on_key_down)
405                    .on_pointer_down(on_input_pointer_down)
406                    .on_ime_preedit(on_ime_preedit)
407            })
408            .on_pointer_enter(on_pointer_enter)
409            .on_pointer_leave(on_pointer_leave)
410            .width(self.width.clone())
411            .background(background.mul_if(!self.enabled, 0.85))
412            .border(border)
413            .corner_radius(theme.corner_radius)
414            .main_align(Alignment::center())
415            .cross_align(Alignment::center())
416            .child(
417                ScrollView::new()
418                    .height(Size::Inner)
419                    .direction(Direction::Horizontal)
420                    .show_scrollbar(false)
421                    .child(
422                        paragraph()
423                            .holder(holder.read().clone())
424                            .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
425                            .min_width(Size::func(move |context| {
426                                Some(context.parent + theme.inner_margin.horizontal())
427                            }))
428                            .maybe(self.enabled, |rect| {
429                                rect.on_pointer_down(on_pointer_down)
430                                    .on_global_mouse_up(on_global_mouse_up)
431                                    .on_global_mouse_move(on_global_mouse_move)
432                            })
433                            .margin(theme.inner_margin)
434                            .cursor_index(cursor_index)
435                            .cursor_color(color)
436                            .color(color)
437                            .max_lines(1)
438                            .highlights(text_selection.map(|h| vec![h]))
439                            .span(text.to_string())
440                            .map(preedit_text, |el, preedit_text| el.span(preedit_text)),
441                    ),
442            )
443    }
444
445    fn render_key(&self) -> DiffKey {
446        self.key.clone().or(self.default_key())
447    }
448}