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