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 editable.editor_mut().write().clear_selection();
196 }
197
198 use_drop(move || {
199 if *status.peek() == InputStatus::Hovering {
200 platform.set_cursor(CursorIcon::default());
201 }
202 });
203
204 use_effect(move || {
205 if !focus.is_focused() {
206 editable.editor_mut().write().clear_selection();
207 }
208 });
209
210 let onkeydown = move |e: Event<KeyboardData>| {
211 if e.data.key != Key::Enter && e.data.key != Key::Tab {
212 e.stop_propagation();
213 editable.process_event(&EditableEvent::KeyDown(e.data));
214 let text = editable.editor().peek().to_string();
215
216 let apply_change = if let Some(onvalidate) = onvalidate {
217 let editor = editable.editor_mut();
218 let mut editor = editor.write();
219 let validator = InputValidator::new(text.clone());
220 onvalidate(validator.clone());
221 let is_valid = validator.is_valid();
222
223 if !is_valid {
224 let undo_result = editor.undo();
226 if let Some(idx) = undo_result {
227 editor.set_cursor_pos(idx);
228 }
229 editor.editor_history().clear_redos();
230 }
231
232 is_valid
233 } else {
234 true
235 };
236
237 if apply_change {
238 onchange.call(text);
239 }
240 }
241 };
242
243 let onkeyup = move |e: Event<KeyboardData>| {
244 e.stop_propagation();
245 editable.process_event(&EditableEvent::KeyUp(e.data));
246 };
247
248 let oninputmousedown = move |e: MouseEvent| {
249 if !display_placeholder {
250 editable.process_event(&EditableEvent::MouseDown(e.data, 0));
251 }
252 focus.request_focus();
253 };
254
255 let onmousedown = move |e: MouseEvent| {
256 e.stop_propagation();
257 drag_origin.set(Some(e.get_screen_coordinates() - e.element_coordinates));
258 if !display_placeholder {
259 editable.process_event(&EditableEvent::MouseDown(e.data, 0));
260 }
261 focus.request_focus();
262 };
263
264 let onglobalmousemove = move |mut e: MouseEvent| {
265 if focus.is_focused() {
266 if let Some(drag_origin) = drag_origin() {
267 let data = Rc::get_mut(&mut e.data).unwrap();
268 data.element_coordinates.x -= drag_origin.x;
269 data.element_coordinates.y -= drag_origin.y;
270 editable.process_event(&EditableEvent::MouseMove(e.data, 0));
271 }
272 }
273 };
274
275 let onmouseenter = move |_| {
276 platform.set_cursor(CursorIcon::Text);
277 *status.write() = InputStatus::Hovering;
278 };
279
280 let onmouseleave = move |_| {
281 platform.set_cursor(CursorIcon::default());
282 *status.write() = InputStatus::default();
283 };
284
285 let onglobalclick = move |_| {
286 match *status.read() {
287 InputStatus::Idle if focus.is_focused() => {
288 editable.process_event(&EditableEvent::Click);
289 }
290 InputStatus::Hovering => {
291 editable.process_event(&EditableEvent::Click);
292 }
293 _ => {}
294 };
295
296 if focus.is_focused() {
301 if drag_origin.read().is_some() {
302 drag_origin.set(None);
303 } else {
304 focus.request_unfocus();
305 }
306 }
307 };
308
309 let a11y_id = focus.attribute();
310 let cursor_reference = editable.cursor_attr();
311 let highlights = editable.highlights_attr(0);
312
313 let (background, cursor_char) = if focus.is_focused() {
314 (
315 hover_background,
316 editable.editor().read().cursor_pos().to_string(),
317 )
318 } else {
319 (background, "none".to_string())
320 };
321 let border = if focus.is_focused_with_keyboard() {
322 format!("2 inner {focus_border_fill}")
323 } else {
324 format!("1 inner {border_fill}")
325 };
326
327 let color = if display_placeholder {
328 placeholder_font_theme.color
329 } else {
330 font_theme.color
331 };
332
333 let text = match (mode, &*placeholder) {
334 (_, Some(placeholder)) if display_placeholder => Cow::Borrowed(placeholder.as_str()),
335 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
336 (InputMode::Shown, _) => Cow::Borrowed(value.as_str()),
337 };
338
339 rsx!(
340 rect {
341 width,
342 direction: "vertical",
343 color: "{color}",
344 background: "{background}",
345 border,
346 shadow: "{shadow}",
347 corner_radius: "{corner_radius}",
348 margin: "{margin}",
349 main_align: "center",
350 a11y_id,
351 a11y_role: "text-input",
352 a11y_auto_focus: "{auto_focus}",
353 a11y_value: "{text}",
354 onkeydown,
355 onkeyup,
356 overflow: "clip",
357 onmousedown: oninputmousedown,
358 onmouseenter,
359 onmouseleave,
360 ScrollView {
361 height: "auto",
362 direction: "horizontal",
363 show_scrollbar: false,
364 paragraph {
365 min_width: "calc(100% - 20)",
366 margin: "6 10",
367 onglobalclick,
368 onmousedown,
369 onglobalmousemove,
370 cursor_reference,
371 cursor_id: "0",
372 cursor_index: "{cursor_char}",
373 cursor_mode: "editable",
374 cursor_color: "{color}",
375 max_lines: "1",
376 highlights,
377 text {
378 "{text}"
379 }
380 }
381 }
382 }
383 )
384}
385
386#[cfg(test)]
387mod test {
388 use freya::prelude::*;
389 use freya_testing::prelude::*;
390
391 #[tokio::test]
392 pub async fn input() {
393 fn input_app() -> Element {
394 let mut value = use_signal(|| "Hello, Worl".to_string());
395
396 rsx!(Input {
397 value,
398 onchange: move |new_value| {
399 value.set(new_value);
400 }
401 })
402 }
403
404 let mut utils = launch_test(input_app);
405 let root = utils.root();
406 let text = root.get(0).get(0).get(0).get(0).get(0).get(0);
407 utils.wait_for_update().await;
408
409 assert_eq!(text.get(0).text(), Some("Hello, Worl"));
411
412 assert_eq!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
413
414 utils.push_event(TestEvent::Mouse {
416 name: EventName::MouseDown,
417 cursor: (115., 25.).into(),
418 button: Some(MouseButton::Left),
419 });
420 utils.wait_for_update().await;
421 utils.wait_for_update().await;
422 utils.wait_for_update().await;
423
424 assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
425
426 utils.push_event(TestEvent::Keyboard {
428 name: EventName::KeyDown,
429 key: Key::Character("d".to_string()),
430 code: Code::KeyD,
431 modifiers: Modifiers::default(),
432 });
433 utils.wait_for_update().await;
434
435 assert_eq!(text.get(0).text(), Some("Hello, World"));
437 }
438
439 #[tokio::test]
440 pub async fn validate() {
441 fn input_app() -> Element {
442 let mut value = use_signal(|| "A".to_string());
443
444 rsx!(Input {
445 value: value.read().clone(),
446 onvalidate: |validator: InputValidator| {
447 if validator.text().len() > 3 {
448 validator.set_valid(false)
449 }
450 },
451 onchange: move |new_value| {
452 value.set(new_value);
453 }
454 },)
455 }
456
457 let mut utils = launch_test(input_app);
458 let root = utils.root();
459 let text = root.get(0).get(0).get(0).get(0).get(0).get(0);
460 utils.wait_for_update().await;
461
462 assert_eq!(text.get(0).text(), Some("A"));
464
465 utils.push_event(TestEvent::Mouse {
467 name: EventName::MouseDown,
468 cursor: (115., 25.).into(),
469 button: Some(MouseButton::Left),
470 });
471 utils.wait_for_update().await;
472 utils.wait_for_update().await;
473 utils.wait_for_update().await;
474
475 assert_ne!(utils.focus_id(), ACCESSIBILITY_ROOT_ID);
476
477 for c in ['B', 'C', 'D', 'E', 'F', 'G'] {
479 utils.push_event(TestEvent::Keyboard {
480 name: EventName::KeyDown,
481 key: Key::Character(c.to_string()),
482 code: Code::Unidentified,
483 modifiers: Modifiers::default(),
484 });
485 utils.wait_for_update().await;
486 }
487
488 assert_eq!(text.get(0).text(), Some("ABC"));
490 }
491}