1use std::{
2 borrow::Cow,
3 cell::{
4 Ref,
5 RefCell,
6 },
7 rc::Rc,
8};
9
10use dioxus::prelude::*;
11use freya_core::platform::CursorIcon;
12use freya_elements::{
13 self as dioxus_elements,
14 events::{
15 keyboard::Key,
16 KeyboardData,
17 MouseEvent,
18 },
19};
20use freya_hooks::{
21 use_applied_theme,
22 use_editable,
23 use_focus,
24 use_platform,
25 EditableConfig,
26 EditableEvent,
27 EditableMode,
28 InputTheme,
29 InputThemeWith,
30 TextEditor,
31};
32
33use crate::ScrollView;
34
35#[derive(Default, Clone, PartialEq)]
37pub enum InputMode {
38 #[default]
40 Shown,
41 Hidden(char),
43}
44
45impl InputMode {
46 pub fn new_password() -> Self {
47 Self::Hidden('*')
48 }
49}
50
51#[derive(Debug, Default, PartialEq, Clone, Copy)]
53pub enum InputStatus {
54 #[default]
56 Idle,
57 Hovering,
59}
60
61#[derive(Clone)]
62pub struct InputValidator {
63 valid: Rc<RefCell<bool>>,
64 text: Rc<RefCell<String>>,
65}
66
67impl InputValidator {
68 pub fn new(text: String) -> Self {
69 Self {
70 valid: Rc::new(RefCell::new(true)),
71 text: Rc::new(RefCell::new(text)),
72 }
73 }
74
75 pub fn text(&self) -> Ref<String> {
77 self.text.borrow()
78 }
79
80 pub fn set_valid(&self, is_valid: bool) {
82 *self.valid.borrow_mut() = is_valid;
83 }
84
85 pub fn is_valid(&self) -> bool {
87 *self.valid.borrow()
88 }
89}
90
91#[derive(Props, Clone, PartialEq)]
93pub struct InputProps {
94 pub theme: Option<InputThemeWith>,
96 pub placeholder: ReadOnlySignal<Option<String>>,
98 pub value: ReadOnlySignal<String>,
100 pub onchange: EventHandler<String>,
102 #[props(default = InputMode::Shown, into)]
104 pub mode: InputMode,
105 #[props(default = false)]
107 pub auto_focus: bool,
108 pub onvalidate: Option<EventHandler<InputValidator>>,
110 #[props(default = "150".to_string())]
111 pub width: String,
112}
113
114#[cfg_attr(feature = "docs",
153 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png")
154)]
155#[allow(non_snake_case)]
156pub fn Input(
157 InputProps {
158 theme,
159 value,
160 onchange,
161 mode,
162 placeholder,
163 auto_focus,
164 onvalidate,
165 width,
166 }: InputProps,
167) -> Element {
168 let platform = use_platform();
169 let mut status = use_signal(InputStatus::default);
170 let mut editable = use_editable(
171 || EditableConfig::new(value.to_string()),
172 EditableMode::MultipleLinesSingleEditor,
173 );
174 let InputTheme {
175 border_fill,
176 focus_border_fill,
177 margin,
178 corner_radius,
179 font_theme,
180 placeholder_font_theme,
181 shadow,
182 background,
183 hover_background,
184 } = use_applied_theme!(&theme, input);
185 let mut focus = use_focus();
186 let mut drag_origin = use_signal(|| None);
187
188 let value = value.read();
189 let placeholder = placeholder.read();
190 let display_placeholder = value.is_empty() && placeholder.is_some();
191
192 if &*value != editable.editor().read().rope() {
193 editable.editor_mut().write().set(&value);
194 editable.editor_mut().write().editor_history().clear();
195 }
196
197 use_drop(move || {
198 if *status.peek() == InputStatus::Hovering {
199 platform.set_cursor(CursorIcon::default());
200 }
201 });
202
203 use_effect(move || {
204 if !focus.is_focused() {
205 editable.editor_mut().write().clear_selection();
206 }
207 });
208
209 let onkeydown = move |e: Event<KeyboardData>| {
210 if e.data.key != Key::Enter && e.data.key != Key::Tab {
211 e.stop_propagation();
212 editable.process_event(&EditableEvent::KeyDown(e.data));
213 let text = editable.editor().peek().to_string();
214
215 let apply_change = if let Some(onvalidate) = onvalidate {
216 let editor = editable.editor_mut();
217 let mut editor = editor.write();
218 let validator = InputValidator::new(text.clone());
219 onvalidate(validator.clone());
220 let is_valid = validator.is_valid();
221
222 if !is_valid {
223 let undo_result = editor.undo();
225 if let Some(idx) = undo_result {
226 editor.set_cursor_pos(idx);
227 }
228 editor.editor_history().clear_redos();
229 }
230
231 is_valid
232 } else {
233 true
234 };
235
236 if apply_change {
237 onchange.call(text);
238 }
239 }
240 };
241
242 let onkeyup = move |e: Event<KeyboardData>| {
243 e.stop_propagation();
244 editable.process_event(&EditableEvent::KeyUp(e.data));
245 };
246
247 let oninputmousedown = move |e: MouseEvent| {
248 if !display_placeholder {
249 editable.process_event(&EditableEvent::MouseDown(e.data, 0));
250 }
251 focus.request_focus();
252 };
253
254 let onmousedown = move |e: MouseEvent| {
255 e.stop_propagation();
256 drag_origin.set(Some(e.get_screen_coordinates() - e.element_coordinates));
257 if !display_placeholder {
258 editable.process_event(&EditableEvent::MouseDown(e.data, 0));
259 }
260 focus.request_focus();
261 };
262
263 let onglobalmousemove = move |mut e: MouseEvent| {
264 if focus.is_focused() {
265 if let Some(drag_origin) = drag_origin() {
266 let data = Rc::get_mut(&mut e.data).unwrap();
267 data.element_coordinates.x -= drag_origin.x;
268 data.element_coordinates.y -= drag_origin.y;
269 editable.process_event(&EditableEvent::MouseMove(e.data, 0));
270 }
271 }
272 };
273
274 let onmouseenter = move |_| {
275 platform.set_cursor(CursorIcon::Text);
276 *status.write() = InputStatus::Hovering;
277 };
278
279 let onmouseleave = move |_| {
280 platform.set_cursor(CursorIcon::default());
281 *status.write() = InputStatus::default();
282 };
283
284 let onglobalclick = move |_| {
285 match *status.read() {
286 InputStatus::Idle if focus.is_focused() => {
287 editable.process_event(&EditableEvent::Click);
288 }
289 InputStatus::Hovering => {
290 editable.process_event(&EditableEvent::Click);
291 }
292 _ => {}
293 };
294
295 if focus.is_focused() {
300 if drag_origin.read().is_some() {
301 drag_origin.set(None);
302 } else {
303 focus.request_unfocus();
304 }
305 }
306 };
307
308 let a11y_id = focus.attribute();
309 let cursor_reference = editable.cursor_attr();
310 let highlights = editable.highlights_attr(0);
311
312 let (background, cursor_char) = if focus.is_focused() {
313 (
314 hover_background,
315 editable.editor().read().cursor_pos().to_string(),
316 )
317 } else {
318 (background, "none".to_string())
319 };
320 let border = if focus.is_focused_with_keyboard() {
321 format!("2 inner {focus_border_fill}")
322 } else {
323 format!("1 inner {border_fill}")
324 };
325
326 let color = if display_placeholder {
327 placeholder_font_theme.color
328 } else {
329 font_theme.color
330 };
331
332 let text = match (mode, &*placeholder) {
333 (_, Some(placeholder)) if display_placeholder => Cow::Borrowed(placeholder.as_str()),
334 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
335 (InputMode::Shown, _) => Cow::Borrowed(value.as_str()),
336 };
337
338 rsx!(
339 rect {
340 width,
341 direction: "vertical",
342 color: "{color}",
343 background: "{background}",
344 border,
345 shadow: "{shadow}",
346 corner_radius: "{corner_radius}",
347 margin: "{margin}",
348 main_align: "center",
349 a11y_id,
350 a11y_role: "text-input",
351 a11y_auto_focus: "{auto_focus}",
352 a11y_value: "{text}",
353 onkeydown,
354 onkeyup,
355 overflow: "clip",
356 onmousedown: oninputmousedown,
357 onmouseenter,
358 onmouseleave,
359 ScrollView {
360 height: "auto",
361 direction: "horizontal",
362 show_scrollbar: false,
363 paragraph {
364 min_width: "calc(100% - 20)",
365 margin: "6 10",
366 onglobalclick,
367 onmousedown,
368 onglobalmousemove,
369 cursor_reference,
370 cursor_id: "0",
371 cursor_index: "{cursor_char}",
372 cursor_mode: "editable",
373 cursor_color: "{color}",
374 max_lines: "1",
375 highlights,
376 text {
377 "{text}"
378 }
379 }
380 }
381 }
382 )
383}
384
385#[cfg(test)]
386mod test {
387 use freya::prelude::*;
388 use freya_testing::prelude::*;
389
390 #[tokio::test]
391 pub async fn input() {
392 fn input_app() -> Element {
393 let mut value = use_signal(|| "Hello, Worl".to_string());
394
395 rsx!(Input {
396 value,
397 onchange: move |new_value| {
398 value.set(new_value);
399 }
400 })
401 }
402
403 let mut utils = launch_test(input_app);
404 let root = utils.root();
405 let text = root.get(0).get(0).get(0).get(0).get(0).get(0);
406 utils.wait_for_update().await;
407
408 assert_eq!(text.get(0).text(), Some("Hello, Worl"));
410
411 assert_eq!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
412
413 utils.push_event(TestEvent::Mouse {
415 name: EventName::MouseDown,
416 cursor: (115., 25.).into(),
417 button: Some(MouseButton::Left),
418 });
419 utils.wait_for_update().await;
420 utils.wait_for_update().await;
421 utils.wait_for_update().await;
422
423 assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
424
425 utils.push_event(TestEvent::Keyboard {
427 name: EventName::KeyDown,
428 key: Key::Character("d".to_string()),
429 code: Code::KeyD,
430 modifiers: Modifiers::default(),
431 });
432 utils.wait_for_update().await;
433
434 assert_eq!(text.get(0).text(), Some("Hello, World"));
436 }
437
438 #[tokio::test]
439 pub async fn validate() {
440 fn input_app() -> Element {
441 let mut value = use_signal(|| "A".to_string());
442
443 rsx!(Input {
444 value: value.read().clone(),
445 onvalidate: |validator: InputValidator| {
446 if validator.text().len() > 3 {
447 validator.set_valid(false)
448 }
449 },
450 onchange: move |new_value| {
451 value.set(new_value);
452 }
453 },)
454 }
455
456 let mut utils = launch_test(input_app);
457 let root = utils.root();
458 let text = root.get(0).get(0).get(0).get(0).get(0).get(0);
459 utils.wait_for_update().await;
460
461 assert_eq!(text.get(0).text(), Some("A"));
463
464 utils.push_event(TestEvent::Mouse {
466 name: EventName::MouseDown,
467 cursor: (115., 25.).into(),
468 button: Some(MouseButton::Left),
469 });
470 utils.wait_for_update().await;
471 utils.wait_for_update().await;
472 utils.wait_for_update().await;
473
474 assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
475
476 for c in ['B', 'C', 'D', 'E', 'F', 'G'] {
478 utils.push_event(TestEvent::Keyboard {
479 name: EventName::KeyDown,
480 key: Key::Character(c.to_string()),
481 code: Code::Unidentified,
482 modifiers: Modifiers::default(),
483 });
484 utils.wait_for_update().await;
485 }
486
487 assert_eq!(text.get(0).text(), Some("ABC"));
489 }
490}