1#![allow(non_snake_case)]
2
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5
6use repose_core::*;
7use repose_ui::{
8 BasicTextField, Box, Column, Row, Text, TextFieldState, TextStyle,
9 ViewExt, ZStack,
10 anim::animate_f32,
11 textfield::{TextMeasureConfig, measure_text},
12 TextFieldConfig as BasicTextFieldConfig,
13};
14
15use super::*;
16
17#[allow(dead_code)]
20#[derive(Clone, Debug)]
21pub struct TextFieldColors {
22 pub focused_text_color: Color,
23 pub unfocused_text_color: Color,
24 pub disabled_text_color: Color,
25 pub error_text_color: Color,
26 pub focused_container_color: Color,
27 pub unfocused_container_color: Color,
28 pub disabled_container_color: Color,
29 pub error_container_color: Color,
30 pub cursor_color: Color,
31 pub error_cursor_color: Color,
32 pub focused_indicator_color: Color,
33 pub unfocused_indicator_color: Color,
34 pub disabled_indicator_color: Color,
35 pub error_indicator_color: Color,
36 pub focused_leading_icon_color: Color,
37 pub unfocused_leading_icon_color: Color,
38 pub disabled_leading_icon_color: Color,
39 pub error_leading_icon_color: Color,
40 pub focused_trailing_icon_color: Color,
41 pub unfocused_trailing_icon_color: Color,
42 pub disabled_trailing_icon_color: Color,
43 pub error_trailing_icon_color: Color,
44 pub focused_label_color: Color,
45 pub unfocused_label_color: Color,
46 pub disabled_label_color: Color,
47 pub error_label_color: Color,
48 pub focused_placeholder_color: Color,
49 pub unfocused_placeholder_color: Color,
50 pub disabled_placeholder_color: Color,
51 pub error_placeholder_color: Color,
52 pub focused_supporting_text_color: Color,
53 pub unfocused_supporting_text_color: Color,
54 pub disabled_supporting_text_color: Color,
55 pub error_supporting_text_color: Color,
56 pub focused_prefix_color: Color,
57 pub unfocused_prefix_color: Color,
58 pub disabled_prefix_color: Color,
59 pub error_prefix_color: Color,
60 pub focused_suffix_color: Color,
61 pub unfocused_suffix_color: Color,
62 pub disabled_suffix_color: Color,
63 pub error_suffix_color: Color,
64}
65
66#[allow(dead_code)]
67impl TextFieldColors {
68 pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
69 if !enabled {
70 self.disabled_text_color
71 } else if is_error {
72 self.error_text_color
73 } else if focused {
74 self.focused_text_color
75 } else {
76 self.unfocused_text_color
77 }
78 }
79 pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
80 if !enabled {
81 self.disabled_container_color
82 } else if is_error {
83 self.error_container_color
84 } else if focused {
85 self.focused_container_color
86 } else {
87 self.unfocused_container_color
88 }
89 }
90 pub fn cursor_color(&self, is_error: bool) -> Color {
91 if is_error {
92 self.error_cursor_color
93 } else {
94 self.cursor_color
95 }
96 }
97 pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
98 if !enabled {
99 self.disabled_indicator_color
100 } else if is_error {
101 self.error_indicator_color
102 } else if focused {
103 self.focused_indicator_color
104 } else {
105 self.unfocused_indicator_color
106 }
107 }
108 pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
109 if !enabled {
110 self.disabled_leading_icon_color
111 } else if is_error {
112 self.error_leading_icon_color
113 } else if focused {
114 self.focused_leading_icon_color
115 } else {
116 self.unfocused_leading_icon_color
117 }
118 }
119 pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
120 if !enabled {
121 self.disabled_trailing_icon_color
122 } else if is_error {
123 self.error_trailing_icon_color
124 } else if focused {
125 self.focused_trailing_icon_color
126 } else {
127 self.unfocused_trailing_icon_color
128 }
129 }
130 pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
131 if !enabled {
132 self.disabled_label_color
133 } else if is_error {
134 self.error_label_color
135 } else if focused {
136 self.focused_label_color
137 } else {
138 self.unfocused_label_color
139 }
140 }
141 pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
142 if !enabled {
143 self.disabled_placeholder_color
144 } else if is_error {
145 self.error_placeholder_color
146 } else if focused {
147 self.focused_placeholder_color
148 } else {
149 self.unfocused_placeholder_color
150 }
151 }
152 pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
153 if !enabled {
154 self.disabled_supporting_text_color
155 } else if is_error {
156 self.error_supporting_text_color
157 } else if focused {
158 self.focused_supporting_text_color
159 } else {
160 self.unfocused_supporting_text_color
161 }
162 }
163}
164
165pub struct TextFieldDefaults;
167
168impl TextFieldDefaults {
169 pub const MIN_HEIGHT: f32 = 56.0;
171 pub const MIN_WIDTH: f32 = 280.0;
173
174 pub fn colors() -> TextFieldColors {
175 let th = theme();
176 TextFieldColors {
177 focused_text_color: th.on_surface,
178 unfocused_text_color: th.on_surface,
179 disabled_text_color: th.on_surface.with_alpha_f32(0.38),
180 error_text_color: th.on_surface,
181 focused_container_color: th.surface_container_highest,
182 unfocused_container_color: th.surface_container_highest,
183 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
184 error_container_color: th.surface_container_highest,
185 cursor_color: th.primary,
186 error_cursor_color: th.error,
187 focused_indicator_color: th.primary,
188 unfocused_indicator_color: th.on_surface_variant,
189 disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
190 error_indicator_color: th.error,
191 focused_leading_icon_color: th.on_surface_variant,
192 unfocused_leading_icon_color: th.on_surface_variant,
193 disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
194 error_leading_icon_color: th.error,
195 focused_trailing_icon_color: th.on_surface_variant,
196 unfocused_trailing_icon_color: th.on_surface_variant,
197 disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
198 error_trailing_icon_color: th.error,
199 focused_label_color: th.primary,
200 unfocused_label_color: th.on_surface_variant,
201 disabled_label_color: th.on_surface.with_alpha_f32(0.38),
202 error_label_color: th.error,
203 focused_placeholder_color: th.on_surface_variant,
204 unfocused_placeholder_color: th.on_surface_variant,
205 disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
206 error_placeholder_color: th.error,
207 focused_supporting_text_color: th.on_surface_variant,
208 unfocused_supporting_text_color: th.on_surface_variant,
209 disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
210 error_supporting_text_color: th.error,
211 focused_prefix_color: th.on_surface,
212 unfocused_prefix_color: th.on_surface,
213 disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
214 error_prefix_color: th.on_surface,
215 focused_suffix_color: th.on_surface,
216 unfocused_suffix_color: th.on_surface,
217 disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
218 error_suffix_color: th.on_surface,
219 }
220 }
221}
222
223#[derive(Clone)]
225pub struct OutlinedTextFieldConfig {
226 pub label: Option<String>,
230 pub placeholder: Option<String>,
234 pub leading_icon: Option<View>,
236 pub trailing_icon: Option<View>,
238 pub single_line: bool,
240 pub is_error: bool,
242 pub enabled: bool,
244 pub on_submit: Option<Rc<dyn Fn(String)>>,
246 pub colors: Option<TextFieldColors>,
248 pub focus_tracker: Option<Rc<Cell<bool>>>,
252}
253
254impl Default for OutlinedTextFieldConfig {
255 fn default() -> Self {
256 Self {
257 label: None,
258 placeholder: None,
259 leading_icon: None,
260 trailing_icon: None,
261 single_line: true,
262 is_error: false,
263 enabled: true,
264 on_submit: None,
265 colors: None,
266 focus_tracker: None,
267 }
268 }
269}
270
271pub fn OutlinedTextField(
293 modifier: Modifier,
294 value: String,
295 on_value_change: impl Fn(String) + 'static,
296 config: OutlinedTextFieldConfig,
297) -> View {
298 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
299 let has_label = label_str.is_some();
300
301 let anim_key = match &label_str {
303 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
304 None => "otf_nolabel".into(),
305 };
306
307 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
311 Some(ft) => ft,
312 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
313 };
314 let is_focused = focus_tracker.get();
315 let should_float = !value.is_empty() || is_focused;
316
317 let tf_placeholder = if has_label {
318 if should_float {
319 config.placeholder.clone().unwrap_or_default()
320 } else {
321 String::new()
322 }
323 } else {
324 config.placeholder.clone().unwrap_or_default()
325 };
326
327 let text_input = View::new(0, ViewKind::Box)
328 .modifier(
329 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
330 hint: tf_placeholder,
331 multiline: false,
332 on_change: Some(Rc::new(on_value_change) as _),
333 on_submit: config.on_submit.clone().map(|f| {
334 let f = f.clone();
335 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
336 }),
337 focus_tracker: Some(focus_tracker),
338 value: value.clone(),
339 visual_transformation: None,
340 keyboard_type: Default::default(),
341 capitalization: Default::default(),
342 ime_action: Default::default(),
343 enabled: config.enabled,
344 read_only: false,
345 max_lines: None,
346 min_lines: 1,
347 cursor_color: config
348 .colors
349 .as_ref()
350 .map(|c| c.cursor_color(config.is_error)),
351 on_text_layout: None,
352 text_style: None,
353 keyboard_actions: None,
354 interaction_source: None,
355 line_limits: None,
356 }),
357 )
358 .semantics(Semantics {
359 role: Role::TextField,
360 label: None,
361 focused: false,
362 enabled: true,
363 selectable_group: false,
364 });
365
366 outlined_field_decoration(
367 modifier,
368 anim_key,
369 label_str,
370 &config,
371 is_focused,
372 !value.is_empty(),
373 text_input,
374 )
375}
376
377pub fn OutlinedTextFieldState(
379 modifier: Modifier,
380 state: Rc<RefCell<TextFieldState>>,
381 on_value_change: impl Fn(String) + 'static,
382 config: OutlinedTextFieldConfig,
383) -> View {
384 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
385 let has_label = label_str.is_some();
386
387 let anim_key = match &label_str {
389 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
390 None => "otf_nolabel".into(),
391 };
392
393 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
394 Some(ft) => ft,
395 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
396 };
397 let is_focused = focus_tracker.get();
398 let has_content = !state.borrow().text.is_empty();
399 let should_float = has_content || is_focused;
400
401 let tf_placeholder = if has_label {
403 if should_float {
404 config.placeholder.clone().unwrap_or_default()
405 } else {
406 String::new()
407 }
408 } else {
409 config.placeholder.clone().unwrap_or_default()
410 };
411
412 let text_input = BasicTextField(
413 state,
414 Modifier::new().flex_grow(1.0),
415 tf_placeholder,
416 BasicTextFieldConfig {
417 line_limits: if config.single_line {
418 TextFieldLineLimits::SingleLine
419 } else {
420 TextFieldLineLimits::MultiLine {
421 min_height_in_lines: 1,
422 max_height_in_lines: usize::MAX,
423 }
424 },
425 on_change: Some(Rc::new(on_value_change)),
426 on_submit: config.on_submit.clone(),
427 focus_tracker: Some(focus_tracker),
428 enabled: config.enabled,
429 ..Default::default()
430 },
431 );
432
433 outlined_field_decoration(
434 modifier,
435 anim_key,
436 label_str,
437 &config,
438 is_focused,
439 has_content,
440 text_input,
441 )
442}
443
444fn outlined_field_decoration(
445 modifier: Modifier,
446 anim_key: String,
447 label_str: Option<Rc<str>>,
448 config: &OutlinedTextFieldConfig,
449 is_focused: bool,
450 has_content: bool,
451 text_input: View,
452) -> View {
453 let th = theme();
454 let has_label = label_str.is_some();
455
456 let should_float = has_content || is_focused;
457 let float_t = animate_f32(
458 anim_key.clone(),
459 if should_float { 1.0 } else { 0.0 },
460 th.motion.color,
461 );
462
463 let target_border_w = if config.is_error || should_float {
464 OutlinedTextFieldDefaults::FOCUSED_BORDER_THICKNESS
465 } else {
466 OutlinedTextFieldDefaults::UNFOCUSED_BORDER_THICKNESS
467 };
468 let border_w = animate_f32(
469 format!("otf_bw_{}", anim_key),
470 target_border_w,
471 th.motion.color,
472 );
473
474 let (border_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
475 (
476 tc.indicator_color(config.enabled, config.is_error, is_focused),
477 tc.label_color(config.enabled, config.is_error, is_focused),
478 tc.container_color(config.enabled, config.is_error, is_focused),
479 )
480 } else {
481 (
482 if config.is_error {
483 th.error
484 } else if is_focused {
485 th.primary
486 } else {
487 th.outline
488 },
489 if config.is_error {
490 th.error
491 } else if is_focused {
492 th.primary
493 } else {
494 th.on_surface_variant
495 },
496 th.surface,
497 )
498 };
499
500 let label_size = 16.0 - 4.0 * float_t;
502
503 let min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
505
506 let label_start_y = (56.0 - 16.0) / 2.0;
508 let label_end_y = -min_label_half_h;
509 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
510
511 let label_start_x = if has_label { 24.0 } else { 0.0 };
513 let label_end_x = if has_label { 20.0 } else { 0.0 };
514 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
515
516 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
518
519 let label_cutout = label_str.as_ref().map(|lbl| {
522 let font_px = dp_to_px(label_size) * repose_core::locals::text_scale().0;
523 let m = measure_text(lbl, font_px, TextMeasureConfig::default());
524 let text_width_px = m.positions.last().copied().unwrap_or(0.0);
525 let text_width_dp = px_to_dp(text_width_px);
526 let pad = 1.0;
527 let line_h = 16.0;
528 (
529 label_x - pad,
530 label_y - pad,
531 label_x + text_width_dp + pad,
532 label_y + line_h + pad,
533 )
534 });
535
536 ZStack(
537 modifier
538 .min_height(OutlinedTextFieldDefaults::MIN_HEIGHT)
539 .min_width(OutlinedTextFieldDefaults::MIN_WIDTH),
540 )
541 .child((
542 Box(Modifier::new()
544 .fill_max_size()
545 .clip_rounded(th.shapes.small)
546 .background(container_bg)),
547 if has_label {
549 let mut bm = Modifier::new()
550 .fill_max_size()
551 .clip_rounded(th.shapes.small)
552 .border(border_w, border_color, th.shapes.small);
553 if let Some((l, t, r, b)) = label_cutout {
554 bm = bm.clip_rect(l, t, r, b, ClipOp::Difference);
555 }
556 Box(bm)
557 } else {
558 Box(Modifier::new()
559 .fill_max_size()
560 .clip_rounded(th.shapes.small)
561 .border(border_w, border_color, th.shapes.small))
562 },
563 Row(Modifier::new()
565 .fill_max_size()
566 .padding_values(PaddingValues {
567 left: 16.0,
568 right: 16.0,
569 top: top_pad,
570 bottom: bottom_pad,
571 })
572 .align_items(AlignItems::CENTER))
573 .child((
574 config.leading_icon.clone().unwrap_or(Box(Modifier::new())),
575 text_input,
576 config.trailing_icon.clone().unwrap_or(Box(Modifier::new())),
577 )),
578 if let Some(lbl) = label_str {
580 Box(Modifier::new()
581 .min_width(200.0)
582 .padding_values(PaddingValues {
583 left: label_x,
584 right: 20.0,
585 top: 0.0,
586 bottom: 0.0,
587 })
588 .absolute()
589 .offset(Some(0.0), Some(label_y), None, None))
590 .child(
591 Text(lbl.as_ref().to_string())
592 .color(label_color)
593 .size(label_size),
594 )
595 } else {
596 Box(Modifier::new())
597 },
598 ))
599}
600
601#[derive(Clone)]
603pub struct TextFieldConfig {
604 pub label: Option<String>,
605 pub placeholder: Option<String>,
606 pub leading_icon: Option<View>,
607 pub trailing_icon: Option<View>,
608 pub single_line: bool,
609 pub is_error: bool,
610 pub enabled: bool,
611 pub on_submit: Option<Rc<dyn Fn(String)>>,
612 pub colors: Option<TextFieldColors>,
613}
614
615impl Default for TextFieldConfig {
616 fn default() -> Self {
617 Self {
618 label: None,
619 placeholder: None,
620 leading_icon: None,
621 trailing_icon: None,
622 single_line: true,
623 is_error: false,
624 enabled: true,
625 on_submit: None,
626 colors: None,
627 }
628 }
629}
630
631pub fn TextField(
638 modifier: Modifier,
639 value: String,
640 on_value_change: impl Fn(String) + 'static,
641 config: TextFieldConfig,
642) -> View {
643 let th = theme();
644 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
645 let has_label = label_str.is_some();
646
647 let anim_key = match &label_str {
648 Some(l) => format!("tf_{}", &l[..l.len().min(32)]),
649 None => "tf_nolabel".into(),
650 };
651
652 let focus_tracker: Rc<Cell<bool>> =
653 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
654 let is_focused = focus_tracker.get();
655 let should_float = !value.is_empty() || is_focused;
656
657 let float_t = animate_f32(
658 anim_key.clone(),
659 if should_float { 1.0 } else { 0.0 },
660 th.motion.color,
661 );
662
663 let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
664 let enf = config.enabled && is_focused;
665 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
666 let lb = tc.label_color(config.enabled, config.is_error, enf);
667 let bg = tc.container_color(config.enabled, config.is_error, enf);
668 (ind, lb, bg)
669 } else {
670 let ind = if config.is_error {
671 th.error
672 } else if float_t > 0.5 {
673 th.primary
674 } else {
675 th.on_surface_variant
676 };
677 let lb = if config.is_error {
678 th.error
679 } else if float_t > 0.5 {
680 th.primary
681 } else {
682 th.on_surface_variant
683 };
684 let bg = if config.enabled {
685 th.surface_container_highest
686 } else {
687 th.on_surface
688 .with_alpha_f32(0.04)
689 .composite_over(th.surface)
690 };
691 (ind, lb, bg)
692 };
693
694 let label_size = 16.0 - 4.0 * float_t;
695
696 let label_start_y = (56.0 - 16.0) / 2.0;
697 let label_end_y = if has_label { 8.0 } else { 0.0 };
698 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
699
700 let label_start_x = if has_label { 24.0 } else { 0.0 };
701 let label_end_x = if has_label { 20.0 } else { 0.0 };
702 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
703
704 let tf_placeholder = if has_label {
705 if should_float {
706 config.placeholder.unwrap_or_default()
707 } else {
708 String::new()
709 }
710 } else {
711 config.placeholder.unwrap_or_default()
712 };
713
714 let indicator_active = config.is_error || (config.enabled && is_focused);
715 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
716 let indicator_w = animate_f32(
717 format!("tf_ind_w_{}", anim_key),
718 indicator_target_w,
719 th.motion.color,
720 );
721
722 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
723
724 Column(
725 modifier
726 .min_height(TextFieldDefaults::MIN_HEIGHT)
727 .min_width(TextFieldDefaults::MIN_WIDTH),
728 )
729 .child((
730 Box(Modifier::new()
732 .fill_max_size()
733 .clip_rounded(th.shapes.extra_small)
734 .background(container_bg))
735 .child(
736 Column(Modifier::new().fill_max_size()).child((
737 Row(Modifier::new()
739 .fill_max_size()
740 .padding_values(PaddingValues {
741 left: 16.0,
742 right: 16.0,
743 top: top_pad,
744 bottom: bottom_pad,
745 })
746 .align_items(AlignItems::CENTER))
747 .child((
748 config.leading_icon.unwrap_or(Box(Modifier::new())),
749 View::new(0, ViewKind::Box)
750 .modifier(
751 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
752 hint: tf_placeholder,
753 multiline: !config.single_line,
754 on_change: Some(Rc::new(on_value_change) as _),
755 on_submit: config.on_submit.clone().map(|f| {
756 let f = f.clone();
757 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
758 }),
759 focus_tracker: Some(focus_tracker.clone()),
760 value: value.clone(),
761 visual_transformation: None,
762 keyboard_type: Default::default(),
763 capitalization: Default::default(),
764 ime_action: Default::default(),
765 enabled: config.enabled,
766 read_only: false,
767 max_lines: None,
768 min_lines: 1,
769 cursor_color: config
770 .colors
771 .as_ref()
772 .map(|c| c.cursor_color(config.is_error)),
773 on_text_layout: None,
774 text_style: None,
775 keyboard_actions: None,
776 interaction_source: None,
777 line_limits: None,
778 }),
779 )
780 .semantics(Semantics {
781 role: Role::TextField,
782 label: None,
783 focused: false,
784 enabled: true,
785 selectable_group: false,
786 }),
787 config.trailing_icon.unwrap_or(Box(Modifier::new())),
788 )),
789 Box(Modifier::new()
791 .fill_max_width()
792 .height(indicator_w)
793 .absolute()
794 .offset(None, None, None, Some(0.0))
795 .background(indicator_color)),
796 )),
797 ),
798 if let Some(lbl) = label_str {
800 Box(Modifier::new()
801 .min_width(200.0)
802 .padding_values(PaddingValues {
803 left: label_x,
804 right: 20.0,
805 top: 0.0,
806 bottom: 0.0,
807 })
808 .absolute()
809 .offset(Some(0.0), Some(label_y), None, None))
810 .child(
811 Text(lbl.as_ref().to_string())
812 .color(label_color)
813 .size(label_size),
814 )
815 } else {
816 Box(Modifier::new())
817 },
818 ))
819}