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