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 gaps::Gaps,
14 prelude::{
15 Alignment,
16 Area,
17 Content,
18 Direction,
19 },
20 size::Size,
21};
22
23use crate::{
24 cursor_blink::use_cursor_blink,
25 get_theme,
26 scrollviews::ScrollView,
27 theming::component_themes::{
28 InputColorsThemePartial,
29 InputLayoutThemePartial,
30 InputLayoutThemePartialExt,
31 },
32};
33
34#[derive(Clone, PartialEq)]
35pub enum InputStyleVariant {
36 Normal,
37 Filled,
38 Flat,
39}
40
41#[derive(Clone, PartialEq)]
42pub enum InputLayoutVariant {
43 Normal,
44 Compact,
45 Expanded,
46}
47
48#[derive(Default, Clone, PartialEq)]
49pub enum InputMode {
50 #[default]
51 Shown,
52 Hidden(char),
53}
54
55impl InputMode {
56 pub fn new_password() -> Self {
57 Self::Hidden('*')
58 }
59}
60
61#[derive(Debug, Default, PartialEq, Clone, Copy)]
62pub enum InputStatus {
63 #[default]
65 Idle,
66 Hovering,
68}
69
70#[derive(Clone)]
71pub struct InputValidator {
72 valid: Rc<RefCell<bool>>,
73 text: Rc<RefCell<String>>,
74}
75
76impl InputValidator {
77 pub fn new(text: String) -> Self {
78 Self {
79 valid: Rc::new(RefCell::new(true)),
80 text: Rc::new(RefCell::new(text)),
81 }
82 }
83 pub fn text(&'_ self) -> Ref<'_, String> {
84 self.text.borrow()
85 }
86 pub fn set_valid(&self, is_valid: bool) {
87 *self.valid.borrow_mut() = is_valid;
88 }
89 pub fn is_valid(&self) -> bool {
90 *self.valid.borrow()
91 }
92}
93
94#[cfg_attr(feature = "docs",
141 doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
142 doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
143 doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
144)]
145#[derive(Clone, PartialEq)]
146pub struct Input {
147 pub(crate) theme_colors: Option<InputColorsThemePartial>,
148 pub(crate) theme_layout: Option<InputLayoutThemePartial>,
149 value: Writable<String>,
150 placeholder: Option<Cow<'static, str>>,
151 on_validate: Option<EventHandler<InputValidator>>,
152 on_submit: Option<EventHandler<String>>,
153 mode: InputMode,
154 auto_focus: bool,
155 width: Size,
156 enabled: bool,
157 key: DiffKey,
158 style_variant: InputStyleVariant,
159 layout_variant: InputLayoutVariant,
160 text_align: TextAlign,
161 a11y_id: Option<AccessibilityId>,
162 leading: Option<Element>,
163 trailing: Option<Element>,
164}
165
166impl KeyExt for Input {
167 fn write_key(&mut self) -> &mut DiffKey {
168 &mut self.key
169 }
170}
171
172impl Input {
173 pub fn new(value: impl Into<Writable<String>>) -> Self {
174 Input {
175 theme_colors: None,
176 theme_layout: None,
177 value: value.into(),
178 placeholder: None,
179 on_validate: None,
180 on_submit: None,
181 mode: InputMode::default(),
182 auto_focus: false,
183 width: Size::px(150.),
184 enabled: true,
185 key: DiffKey::default(),
186 style_variant: InputStyleVariant::Normal,
187 layout_variant: InputLayoutVariant::Normal,
188 text_align: TextAlign::default(),
189 a11y_id: None,
190 leading: None,
191 trailing: None,
192 }
193 }
194
195 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
196 self.enabled = enabled.into();
197 self
198 }
199
200 pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
201 self.placeholder = Some(placeholder.into());
202 self
203 }
204
205 pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
206 self.on_validate = Some(on_validate.into());
207 self
208 }
209
210 pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
211 self.on_submit = Some(on_submit.into());
212 self
213 }
214
215 pub fn mode(mut self, mode: InputMode) -> Self {
216 self.mode = mode;
217 self
218 }
219
220 pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
221 self.auto_focus = auto_focus.into();
222 self
223 }
224
225 pub fn width(mut self, width: impl Into<Size>) -> Self {
226 self.width = width.into();
227 self
228 }
229
230 pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
231 self.theme_colors = Some(theme);
232 self
233 }
234
235 pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
236 self.theme_layout = Some(theme);
237 self
238 }
239
240 pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
241 self.text_align = text_align.into();
242 self
243 }
244
245 pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
246 self.style_variant = style_variant.into();
247 self
248 }
249
250 pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
251 self.layout_variant = layout_variant.into();
252 self
253 }
254
255 pub fn filled(self) -> Self {
257 self.style_variant(InputStyleVariant::Filled)
258 }
259
260 pub fn flat(self) -> Self {
262 self.style_variant(InputStyleVariant::Flat)
263 }
264
265 pub fn compact(self) -> Self {
267 self.layout_variant(InputLayoutVariant::Compact)
268 }
269
270 pub fn expanded(self) -> Self {
272 self.layout_variant(InputLayoutVariant::Expanded)
273 }
274
275 pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
276 self.a11y_id = Some(a11y_id.into());
277 self
278 }
279
280 pub fn leading(mut self, leading: impl Into<Element>) -> Self {
282 self.leading = Some(leading.into());
283 self
284 }
285
286 pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
288 self.trailing = Some(trailing.into());
289 self
290 }
291}
292
293impl CornerRadiusExt for Input {
294 fn with_corner_radius(self, corner_radius: f32) -> Self {
295 self.corner_radius(corner_radius)
296 }
297}
298
299impl Component for Input {
300 fn render(&self) -> impl IntoElement {
301 let focus = use_hook(|| Focus::new_for_id(self.a11y_id.unwrap_or_else(Focus::new_id)));
302 let focus_status = use_focus_status(focus);
303 let holder = use_state(ParagraphHolder::default);
304 let mut area = use_state(Area::default);
305 let mut status = use_state(InputStatus::default);
306 let mut editable = use_editable(|| self.value.read().to_string(), EditableConfig::new);
307 let mut is_dragging = use_state(|| false);
308 let mut value = self.value.clone();
309
310 let theme_colors = match self.style_variant {
311 InputStyleVariant::Normal => get_theme!(&self.theme_colors, input),
312 InputStyleVariant::Filled => get_theme!(&self.theme_colors, filled_input),
313 InputStyleVariant::Flat => get_theme!(&self.theme_colors, flat_input),
314 };
315 let theme_layout = match self.layout_variant {
316 InputLayoutVariant::Normal => get_theme!(&self.theme_layout, input_layout),
317 InputLayoutVariant::Compact => get_theme!(&self.theme_layout, compact_input_layout),
318 InputLayoutVariant::Expanded => get_theme!(&self.theme_layout, expanded_input_layout),
319 };
320
321 let (mut movement_timeout, cursor_color) =
322 use_cursor_blink(focus_status() != FocusStatus::Not, theme_colors.color);
323
324 let enabled = use_reactive(&self.enabled);
325 use_drop(move || {
326 if status() == InputStatus::Hovering && enabled() {
327 Cursor::set(CursorIcon::default());
328 }
329 });
330
331 let display_placeholder = value.read().is_empty()
332 && self.placeholder.is_some()
333 && !editable.editor().read().has_preedit();
334 let on_validate = self.on_validate.clone();
335 let on_submit = self.on_submit.clone();
336
337 if *value.read() != editable.editor().read().committed_text() {
338 let mut editor = editable.editor_mut().write();
339 editor.clear_preedit();
340 editor.set(&value.read());
341 editor.editor_history().clear();
342 editor.clear_selection();
343 }
344
345 let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
346 let mut editor = editable.editor_mut().write();
347 if e.data().text.is_empty() {
348 editor.clear_preedit();
349 } else {
350 editor.set_preedit(&e.data().text);
351 }
352 };
353
354 let on_key_down = move |e: Event<KeyboardEventData>| {
355 match &e.key {
356 Key::Named(NamedKey::Enter) => {
358 if let Some(on_submit) = &on_submit {
359 let text = editable.editor().peek().committed_text();
360 on_submit.call(text);
361 }
362 }
363 Key::Named(NamedKey::Escape) => {
365 focus.request_unfocus();
366 Cursor::set(CursorIcon::default());
367 }
368 key => {
370 if *key != Key::Named(NamedKey::Tab) {
371 e.stop_propagation();
372 movement_timeout.reset();
373 editable.process_event(EditableEvent::KeyDown {
374 key: &e.key,
375 modifiers: e.modifiers,
376 });
377 let text = editable.editor().read().committed_text();
378
379 let apply_change = match &on_validate {
380 Some(on_validate) => {
381 let mut editor = editable.editor_mut().write();
382 let validator = InputValidator::new(text.clone());
383 on_validate.call(validator.clone());
384 if !validator.is_valid() {
385 if let Some(selection) = editor.undo() {
386 *editor.selection_mut() = selection;
387 }
388 editor.editor_history().clear_redos();
389 }
390 validator.is_valid()
391 }
392 None => true,
393 };
394
395 if apply_change {
396 *value.write() = text;
397 }
398 }
399 }
400 }
401 };
402
403 let on_key_up = move |e: Event<KeyboardEventData>| {
404 e.stop_propagation();
405 editable.process_event(EditableEvent::KeyUp { key: &e.key });
406 };
407
408 let on_input_pointer_down = move |e: Event<PointerEventData>| {
409 e.stop_propagation();
410 is_dragging.set(true);
411 movement_timeout.reset();
412 if !display_placeholder {
413 let area = area.read().to_f64();
414 let global_location = e.global_location().clamp(area.min(), area.max());
415 let location = (global_location - area.min()).to_point();
416 editable.process_event(EditableEvent::Down {
417 location,
418 editor_line: EditorLine::SingleParagraph,
419 holder: &holder.read(),
420 });
421 }
422 focus.request_focus();
423 };
424
425 let on_pointer_down = move |e: Event<PointerEventData>| {
426 e.stop_propagation();
427 is_dragging.set(true);
428 movement_timeout.reset();
429 if !display_placeholder {
430 editable.process_event(EditableEvent::Down {
431 location: e.element_location(),
432 editor_line: EditorLine::SingleParagraph,
433 holder: &holder.read(),
434 });
435 }
436 focus.request_focus();
437 };
438
439 let on_global_pointer_move = move |e: Event<PointerEventData>| {
440 if focus.is_focused() && *is_dragging.read() {
441 let mut location = e.global_location();
442 location.x -= area.read().min_x() as f64;
443 location.y -= area.read().min_y() as f64;
444 editable.process_event(EditableEvent::Move {
445 location,
446 editor_line: EditorLine::SingleParagraph,
447 holder: &holder.read(),
448 });
449 }
450 };
451
452 let on_pointer_enter = move |_| {
453 *status.write() = InputStatus::Hovering;
454 if enabled() {
455 Cursor::set(CursorIcon::Text);
456 } else {
457 Cursor::set(CursorIcon::NotAllowed);
458 }
459 };
460
461 let on_pointer_leave = move |_| {
462 if status() == InputStatus::Hovering {
463 Cursor::set(CursorIcon::default());
464 *status.write() = InputStatus::default();
465 }
466 };
467
468 let on_global_pointer_press = move |_: Event<PointerEventData>| {
469 match *status.read() {
470 InputStatus::Idle if focus.is_focused() => {
471 editable.process_event(EditableEvent::Release);
472 }
473 InputStatus::Hovering => {
474 editable.process_event(EditableEvent::Release);
475 }
476 _ => {}
477 };
478
479 if focus.is_focused() {
480 if *is_dragging.read() {
481 is_dragging.set(false);
483 } else {
484 focus.request_unfocus();
486 }
487 }
488 };
489
490 let on_pointer_press = move |e: Event<PointerEventData>| {
491 e.stop_propagation();
492 e.prevent_default();
493 match *status.read() {
494 InputStatus::Idle if focus.is_focused() => {
495 editable.process_event(EditableEvent::Release);
496 }
497 InputStatus::Hovering => {
498 editable.process_event(EditableEvent::Release);
499 }
500 _ => {}
501 };
502
503 if focus.is_focused() {
504 is_dragging.set_if_modified(false);
505 }
506 };
507
508 let a11y_id = focus.a11y_id();
509
510 let (background, cursor_index, text_selection) =
511 if enabled() && focus_status() != FocusStatus::Not {
512 (
513 theme_colors.hover_background,
514 Some(editable.editor().read().cursor_pos()),
515 editable
516 .editor()
517 .read()
518 .get_visible_selection(EditorLine::SingleParagraph),
519 )
520 } else {
521 (theme_colors.background, None, None)
522 };
523
524 let border = if focus_status().is_focused() {
525 Border::new()
526 .fill(theme_colors.focus_border_fill)
527 .width(2.)
528 .alignment(BorderAlignment::Inner)
529 } else {
530 Border::new()
531 .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
532 .width(1.)
533 .alignment(BorderAlignment::Inner)
534 };
535
536 let color = if display_placeholder {
537 theme_colors.placeholder_color
538 } else {
539 theme_colors.color
540 };
541
542 let value = self.value.read();
543 let a11y_text: Cow<str> = match (self.mode.clone(), &self.placeholder) {
544 (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
545 (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
546 (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
547 };
548
549 let a11_role = match self.mode {
550 InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
551 _ => AccessibilityRole::TextInput,
552 };
553
554 rect()
555 .a11y_id(a11y_id)
556 .a11y_focusable(self.enabled)
557 .a11y_auto_focus(self.auto_focus)
558 .a11y_alt(a11y_text)
559 .a11y_role(a11_role)
560 .maybe(self.enabled, |el| {
561 el.on_key_up(on_key_up)
562 .on_key_down(on_key_down)
563 .on_pointer_down(on_input_pointer_down)
564 .on_ime_preedit(on_ime_preedit)
565 .on_pointer_press(on_pointer_press)
566 .on_global_pointer_press(on_global_pointer_press)
567 .on_global_pointer_move(on_global_pointer_move)
568 })
569 .on_pointer_enter(on_pointer_enter)
570 .on_pointer_leave(on_pointer_leave)
571 .width(self.width.clone())
572 .background(background.mul_if(!self.enabled, 0.85))
573 .border(border)
574 .corner_radius(theme_layout.corner_radius)
575 .content(Content::Flex)
576 .direction(Direction::Horizontal)
577 .cross_align(Alignment::center())
578 .maybe_child(
579 self.leading
580 .clone()
581 .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
582 )
583 .child(
584 ScrollView::new()
585 .width(Size::flex(1.))
586 .height(Size::Inner)
587 .direction(Direction::Horizontal)
588 .show_scrollbar(false)
589 .child(
590 paragraph()
591 .holder(holder.read().clone())
592 .on_sized(move |e: Event<SizedEventData>| area.set(e.visible_area))
593 .min_width(Size::func(move |context| {
594 Some(context.parent + theme_layout.inner_margin.horizontal())
595 }))
596 .maybe(self.enabled, |el| el.on_pointer_down(on_pointer_down))
597 .margin(theme_layout.inner_margin)
598 .cursor_index(cursor_index)
599 .cursor_color(cursor_color)
600 .color(color)
601 .text_align(self.text_align)
602 .max_lines(1)
603 .highlights(text_selection.map(|h| vec![h]))
604 .maybe(display_placeholder, |el| {
605 el.span(self.placeholder.as_ref().unwrap().to_string())
606 })
607 .maybe(!display_placeholder, |el| {
608 let editor = editable.editor().read();
609 if editor.has_preedit() {
610 let (b, p, a) = editor.preedit_text_segments();
611 let (b, p, a) = match self.mode.clone() {
612 InputMode::Hidden(ch) => {
613 let ch = ch.to_string();
614 (
615 ch.repeat(b.chars().count()),
616 ch.repeat(p.chars().count()),
617 ch.repeat(a.chars().count()),
618 )
619 }
620 InputMode::Shown => (b, p, a),
621 };
622 el.span(b)
623 .span(
624 Span::new(p).text_decoration(TextDecoration::Underline),
625 )
626 .span(a)
627 } else {
628 let text = match self.mode.clone() {
629 InputMode::Hidden(ch) => {
630 ch.to_string().repeat(editor.rope().len_chars())
631 }
632 InputMode::Shown => editor.rope().to_string(),
633 };
634 el.span(text)
635 }
636 }),
637 ),
638 )
639 .maybe_child(
640 self.trailing
641 .clone()
642 .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
643 )
644 }
645
646 fn render_key(&self) -> DiffKey {
647 self.key.clone().or(self.default_key())
648 }
649}