1#![allow(non_snake_case)]
2
3use std::cell::{Cell, RefCell};
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use repose_core::*;
8use repose_ui::{
9 BasicTextField, Box, Column, Row, Text, TextFieldConfig as BasicTextFieldConfig,
10 TextFieldState, TextStyle, ViewExt, ZStack,
11 anim::{animate_color, animate_f32},
12 textfield::{TextMeasureConfig, measure_text},
13};
14
15use super::*;
16
17static OTF_COUNTER: AtomicU64 = AtomicU64::new(0);
18static OTFS_COUNTER: AtomicU64 = AtomicU64::new(0);
19static TF_COUNTER: AtomicU64 = AtomicU64::new(0);
20
21fn tint_icon(color: Color, icon: Option<View>) -> View {
23 match icon {
24 Some(v) => Box(Modifier::new().padding_values(PaddingValues {
25 left: 0.0,
26 right: 12.0,
27 top: 0.0,
28 bottom: 0.0,
29 }))
30 .child(with_content_color(color, move || v)),
31 None => Box(Modifier::new()),
32 }
33}
34
35fn tint_trailing_icon(color: Color, icon: Option<View>) -> View {
37 match icon {
38 Some(v) => Box(Modifier::new().padding_values(PaddingValues {
39 left: 12.0,
40 right: 0.0,
41 top: 0.0,
42 bottom: 0.0,
43 }))
44 .child(with_content_color(color, move || v)),
45 None => Box(Modifier::new()),
46 }
47}
48
49#[allow(dead_code)]
52#[derive(Clone, Debug)]
53pub struct TextFieldColors {
54 pub focused_text_color: Color,
55 pub unfocused_text_color: Color,
56 pub disabled_text_color: Color,
57 pub error_text_color: Color,
58 pub focused_container_color: Color,
59 pub unfocused_container_color: Color,
60 pub disabled_container_color: Color,
61 pub error_container_color: Color,
62 pub cursor_color: Color,
63 pub error_cursor_color: Color,
64 pub focused_indicator_color: Color,
65 pub unfocused_indicator_color: Color,
66 pub disabled_indicator_color: Color,
67 pub error_indicator_color: Color,
68 pub focused_leading_icon_color: Color,
69 pub unfocused_leading_icon_color: Color,
70 pub disabled_leading_icon_color: Color,
71 pub error_leading_icon_color: Color,
72 pub focused_trailing_icon_color: Color,
73 pub unfocused_trailing_icon_color: Color,
74 pub disabled_trailing_icon_color: Color,
75 pub error_trailing_icon_color: Color,
76 pub focused_label_color: Color,
77 pub unfocused_label_color: Color,
78 pub disabled_label_color: Color,
79 pub error_label_color: Color,
80 pub focused_placeholder_color: Color,
81 pub unfocused_placeholder_color: Color,
82 pub disabled_placeholder_color: Color,
83 pub error_placeholder_color: Color,
84 pub focused_supporting_text_color: Color,
85 pub unfocused_supporting_text_color: Color,
86 pub disabled_supporting_text_color: Color,
87 pub error_supporting_text_color: Color,
88 pub focused_prefix_color: Color,
89 pub unfocused_prefix_color: Color,
90 pub disabled_prefix_color: Color,
91 pub error_prefix_color: Color,
92 pub focused_suffix_color: Color,
93 pub unfocused_suffix_color: Color,
94 pub disabled_suffix_color: Color,
95 pub error_suffix_color: Color,
96}
97
98#[allow(dead_code)]
99impl TextFieldColors {
100 pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
101 if !enabled {
102 self.disabled_text_color
103 } else if is_error {
104 self.error_text_color
105 } else if focused {
106 self.focused_text_color
107 } else {
108 self.unfocused_text_color
109 }
110 }
111 pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
112 if !enabled {
113 self.disabled_container_color
114 } else if is_error {
115 self.error_container_color
116 } else if focused {
117 self.focused_container_color
118 } else {
119 self.unfocused_container_color
120 }
121 }
122 pub fn cursor_color(&self, is_error: bool) -> Color {
123 if is_error {
124 self.error_cursor_color
125 } else {
126 self.cursor_color
127 }
128 }
129 pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
130 if !enabled {
131 self.disabled_indicator_color
132 } else if is_error {
133 self.error_indicator_color
134 } else if focused {
135 self.focused_indicator_color
136 } else {
137 self.unfocused_indicator_color
138 }
139 }
140 pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
141 if !enabled {
142 self.disabled_leading_icon_color
143 } else if is_error {
144 self.error_leading_icon_color
145 } else if focused {
146 self.focused_leading_icon_color
147 } else {
148 self.unfocused_leading_icon_color
149 }
150 }
151 pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
152 if !enabled {
153 self.disabled_trailing_icon_color
154 } else if is_error {
155 self.error_trailing_icon_color
156 } else if focused {
157 self.focused_trailing_icon_color
158 } else {
159 self.unfocused_trailing_icon_color
160 }
161 }
162 pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
163 if !enabled {
164 self.disabled_label_color
165 } else if is_error {
166 self.error_label_color
167 } else if focused {
168 self.focused_label_color
169 } else {
170 self.unfocused_label_color
171 }
172 }
173 pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
174 if !enabled {
175 self.disabled_placeholder_color
176 } else if is_error {
177 self.error_placeholder_color
178 } else if focused {
179 self.focused_placeholder_color
180 } else {
181 self.unfocused_placeholder_color
182 }
183 }
184 pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
185 if !enabled {
186 self.disabled_supporting_text_color
187 } else if is_error {
188 self.error_supporting_text_color
189 } else if focused {
190 self.focused_supporting_text_color
191 } else {
192 self.unfocused_supporting_text_color
193 }
194 }
195 pub fn prefix_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
196 if !enabled {
197 self.disabled_prefix_color
198 } else if is_error {
199 self.error_prefix_color
200 } else if focused {
201 self.focused_prefix_color
202 } else {
203 self.unfocused_prefix_color
204 }
205 }
206 pub fn suffix_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
207 if !enabled {
208 self.disabled_suffix_color
209 } else if is_error {
210 self.error_suffix_color
211 } else if focused {
212 self.focused_suffix_color
213 } else {
214 self.unfocused_suffix_color
215 }
216 }
217}
218
219pub struct TextFieldDefaults;
221
222impl TextFieldDefaults {
223 pub const MIN_HEIGHT: f32 = 56.0;
225 pub const MIN_WIDTH: f32 = 280.0;
227
228 pub fn colors() -> TextFieldColors {
229 let th = theme();
230 TextFieldColors {
231 focused_text_color: th.on_surface,
232 unfocused_text_color: th.on_surface,
233 disabled_text_color: th.on_surface.with_alpha_f32(0.38),
234 error_text_color: th.on_surface,
235 focused_container_color: th.surface_container_highest,
236 unfocused_container_color: th.surface_container_highest,
237 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
238 error_container_color: th.surface_container_highest,
239 cursor_color: th.primary,
240 error_cursor_color: th.error,
241 focused_indicator_color: th.primary,
242 unfocused_indicator_color: th.on_surface_variant,
243 disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
244 error_indicator_color: th.error,
245 focused_leading_icon_color: th.on_surface_variant,
246 unfocused_leading_icon_color: th.on_surface_variant,
247 disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
248 error_leading_icon_color: th.error,
249 focused_trailing_icon_color: th.on_surface_variant,
250 unfocused_trailing_icon_color: th.on_surface_variant,
251 disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
252 error_trailing_icon_color: th.error,
253 focused_label_color: th.primary,
254 unfocused_label_color: th.on_surface_variant,
255 disabled_label_color: th.on_surface.with_alpha_f32(0.38),
256 error_label_color: th.error,
257 focused_placeholder_color: th.on_surface_variant,
258 unfocused_placeholder_color: th.on_surface_variant,
259 disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
260 error_placeholder_color: th.error,
261 focused_supporting_text_color: th.on_surface_variant,
262 unfocused_supporting_text_color: th.on_surface_variant,
263 disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
264 error_supporting_text_color: th.error,
265 focused_prefix_color: th.on_surface,
266 unfocused_prefix_color: th.on_surface,
267 disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
268 error_prefix_color: th.on_surface,
269 focused_suffix_color: th.on_surface,
270 unfocused_suffix_color: th.on_surface,
271 disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
272 error_suffix_color: th.on_surface,
273 }
274 }
275}
276
277#[derive(Clone)]
279pub struct OutlinedTextFieldConfig {
280 pub label: Option<String>,
284 pub placeholder: Option<String>,
288 pub leading_icon: Option<View>,
290 pub trailing_icon: Option<View>,
292 pub single_line: bool,
294 pub is_error: bool,
296 pub enabled: bool,
298 pub read_only: bool,
300 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
303 pub supporting_text: Option<String>,
305 pub prefix: Option<String>,
307 pub suffix: Option<String>,
309 pub on_submit: Option<Rc<dyn Fn(String)>>,
311 pub colors: Option<TextFieldColors>,
313 pub focus_tracker: Option<Rc<Cell<bool>>>,
317}
318
319impl Default for OutlinedTextFieldConfig {
320 fn default() -> Self {
321 Self {
322 label: None,
323 placeholder: None,
324 leading_icon: None,
325 trailing_icon: None,
326 single_line: true,
327 is_error: false,
328 enabled: true,
329 read_only: false,
330 visual_transformation: None,
331 supporting_text: None,
332 prefix: None,
333 suffix: None,
334 on_submit: None,
335 colors: None,
336 focus_tracker: None,
337 }
338 }
339}
340
341pub fn OutlinedTextField(
362 modifier: Modifier,
363 value: String,
364 on_value_change: impl Fn(String) + 'static,
365 config: OutlinedTextFieldConfig,
366) -> View {
367 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
368 let has_label = label_str.is_some();
369
370 let id = *remember(|| OTF_COUNTER.fetch_add(1, Ordering::Relaxed));
372 let anim_key = format!("otf_{id}");
373
374 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
378 Some(ft) => ft,
379 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
380 };
381 let is_focused = focus_tracker.get();
382 let should_float = !value.is_empty() || is_focused;
383
384 let tf_placeholder = if has_label {
385 if should_float {
386 config.placeholder.clone().unwrap_or_default()
387 } else {
388 String::new()
389 }
390 } else {
391 config.placeholder.clone().unwrap_or_default()
392 };
393
394 let text_input = View::new(0, ViewKind::Box)
395 .modifier(
396 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
397 hint: tf_placeholder,
398 multiline: !config.single_line,
399 on_change: Some(Rc::new(on_value_change) as _),
400 on_submit: config.on_submit.clone().map(|f| {
401 let f = f.clone();
402 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
403 }),
404 focus_tracker: Some(focus_tracker),
405 value: value.clone(),
406 visual_transformation: config.visual_transformation.clone(),
407 enabled: config.enabled,
408 read_only: config.read_only,
409 cursor_color: config
410 .colors
411 .as_ref()
412 .map(|c| c.cursor_color(config.is_error)),
413 ..Default::default()
414 }),
415 )
416 .semantics(Semantics {
417 role: Role::TextField,
418 label: config
419 .label
420 .clone()
421 .or_else(|| config.supporting_text.clone()),
422 enabled: config.enabled,
423 ..Default::default()
424 });
425
426 outlined_field_decoration(
427 modifier,
428 anim_key,
429 label_str,
430 &config,
431 is_focused,
432 !value.is_empty(),
433 text_input,
434 )
435}
436
437pub fn OutlinedTextFieldState(
439 modifier: Modifier,
440 state: Rc<RefCell<TextFieldState>>,
441 on_value_change: impl Fn(String) + 'static,
442 config: OutlinedTextFieldConfig,
443) -> View {
444 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
445 let has_label = label_str.is_some();
446
447 let id = *remember(|| OTFS_COUNTER.fetch_add(1, Ordering::Relaxed));
449 let anim_key = format!("otfs_{id}");
450
451 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
452 Some(ft) => ft,
453 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
454 };
455 let is_focused = focus_tracker.get();
456 let has_content = !state.borrow().text.is_empty();
457 let should_float = has_content || is_focused;
458
459 let tf_placeholder = if has_label {
461 if should_float {
462 config.placeholder.clone().unwrap_or_default()
463 } else {
464 String::new()
465 }
466 } else {
467 config.placeholder.clone().unwrap_or_default()
468 };
469
470 let text_input = BasicTextField(
471 state,
472 Modifier::new().flex_grow(1.0),
473 tf_placeholder,
474 BasicTextFieldConfig {
475 line_limits: if config.single_line {
476 TextFieldLineLimits::SingleLine
477 } else {
478 TextFieldLineLimits::MultiLine {
479 min_height_in_lines: 1,
480 max_height_in_lines: usize::MAX,
481 }
482 },
483 on_change: Some(Rc::new(on_value_change)),
484 on_submit: config.on_submit.clone(),
485 focus_tracker: Some(focus_tracker),
486 enabled: config.enabled,
487 read_only: config.read_only,
488 ..Default::default()
489 },
490 );
491
492 outlined_field_decoration(
493 modifier,
494 anim_key,
495 label_str,
496 &config,
497 is_focused,
498 has_content,
499 text_input,
500 )
501}
502
503fn outlined_field_decoration(
504 modifier: Modifier,
505 anim_key: String,
506 label_str: Option<Rc<str>>,
507 config: &OutlinedTextFieldConfig,
508 is_focused: bool,
509 has_content: bool,
510 text_input: View,
511) -> View {
512 let th = theme();
513 let has_label = label_str.is_some();
514
515 let should_float = has_content || is_focused;
516 let float_t = animate_f32(
517 anim_key.clone(),
518 if should_float { 1.0 } else { 0.0 },
519 th.motion.color,
520 );
521
522 let target_border_w = if config.is_error || is_focused {
523 OutlinedTextFieldDefaults::FOCUSED_BORDER_THICKNESS
524 } else {
525 OutlinedTextFieldDefaults::UNFOCUSED_BORDER_THICKNESS
526 };
527 let border_w = animate_f32(
528 format!("otf_bw_{}", anim_key),
529 target_border_w,
530 th.motion.color,
531 );
532
533 let (border_color_target, label_color_target, container_bg) = if let Some(ref tc) = config.colors {
534 (
535 tc.indicator_color(config.enabled, config.is_error, is_focused),
536 tc.label_color(config.enabled, config.is_error, is_focused),
537 tc.container_color(config.enabled, config.is_error, is_focused),
538 )
539 } else {
540 (
541 if config.is_error {
542 th.error
543 } else if is_focused {
544 th.primary
545 } else {
546 th.outline
547 },
548 if config.is_error {
549 th.error
550 } else if is_focused {
551 th.primary
552 } else {
553 th.on_surface_variant
554 },
555 Color::TRANSPARENT,
556 )
557 };
558
559 let border_color = animate_color(
560 format!("otf_bc_{}", anim_key),
561 border_color_target,
562 th.motion.color,
563 );
564 let label_color = animate_color(
565 format!("otf_lc_{}", anim_key),
566 label_color_target,
567 th.motion.color,
568 );
569
570 let label_size = 16.0 - 4.0 * float_t;
572
573 let min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
575
576 let label_start_y = (56.0 - 16.0) / 2.0;
578 let label_end_y = -min_label_half_h;
579 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
580
581 let label_start_x = if has_label { 24.0 } else { 0.0 };
583 let label_end_x = if has_label { 20.0 } else { 0.0 };
584 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
585
586 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
588
589 let (prefix_color, suffix_color) = if let Some(ref tc) = config.colors {
590 (
591 tc.prefix_color(config.enabled, config.is_error, is_focused),
592 tc.suffix_color(config.enabled, config.is_error, is_focused),
593 )
594 } else {
595 (
596 if config.is_error {
597 th.error
598 } else {
599 th.on_surface
600 },
601 if config.is_error {
602 th.error
603 } else {
604 th.on_surface
605 },
606 )
607 };
608
609 let (lead_c, trail_c) = if let Some(ref tc) = config.colors {
610 (
611 tc.leading_icon_color(config.enabled, config.is_error, is_focused),
612 tc.trailing_icon_color(config.enabled, config.is_error, is_focused),
613 )
614 } else {
615 let c = if !config.enabled {
616 th.on_surface.with_alpha_f32(0.38)
617 } else if config.is_error {
618 th.error
619 } else {
620 th.on_surface_variant
621 };
622 (c, c)
623 };
624
625 let text_c = config
626 .colors
627 .as_ref()
628 .map(|c| c.text_color(config.enabled, config.is_error, is_focused))
629 .unwrap_or(if config.enabled {
630 th.on_surface
631 } else {
632 th.on_surface.with_alpha_f32(0.38)
633 });
634
635 let supporting = config.supporting_text.as_ref().map(|st| {
636 let c = if let Some(ref tc) = config.colors {
637 tc.supporting_text_color(config.enabled, config.is_error, is_focused)
638 } else if config.is_error {
639 th.error
640 } else {
641 th.on_surface_variant
642 };
643 Text(st.clone())
644 .color(c)
645 .size(th.typography.body_small)
646 .modifier(Modifier::new().padding_values(PaddingValues {
647 left: 16.0,
648 right: 16.0,
649 top: 4.0,
650 bottom: 0.0,
651 }))
652 });
653
654 let label_cutout = label_str.as_ref().map(|lbl| {
657 let font_px = dp_to_px(label_size) * repose_core::locals::text_scale().0;
658 let m = measure_text(lbl, font_px, TextMeasureConfig::default());
659 let text_width_px = m.positions.last().copied().unwrap_or(0.0);
660 let text_width_dp = px_to_dp(text_width_px);
661 let pad = 1.0;
662 let line_h = 16.0;
663 (
664 label_x - pad,
665 label_y - pad,
666 label_x + text_width_dp + pad,
667 label_y + line_h + pad,
668 )
669 });
670
671 Column(modifier.min_width(OutlinedTextFieldDefaults::MIN_WIDTH)).child((
672 ZStack(
673 Modifier::new()
674 .fill_max_width()
675 .min_height(OutlinedTextFieldDefaults::MIN_HEIGHT),
676 )
677 .child((
678 Box(Modifier::new()
679 .fill_max_size()
680 .clip_rounded(th.shapes.small)
681 .background(container_bg)),
682 if has_label {
683 let mut bm = Modifier::new()
684 .fill_max_size()
685 .clip_rounded(th.shapes.small)
686 .border(border_w, border_color, th.shapes.small);
687 if let Some((l, t, r, b)) = label_cutout {
688 bm = bm.clip_rect(l, t, r, b, ClipOp::Difference);
689 }
690 Box(bm)
691 } else {
692 Box(Modifier::new()
693 .fill_max_size()
694 .clip_rounded(th.shapes.small)
695 .border(border_w, border_color, th.shapes.small))
696 },
697 Row(Modifier::new()
698 .fill_max_size()
699 .padding_values(PaddingValues {
700 left: 16.0,
701 right: 16.0,
702 top: top_pad,
703 bottom: bottom_pad,
704 })
705 .align_items(AlignItems::CENTER))
706 .child((
707 tint_icon(lead_c, config.leading_icon.clone()),
708 config
709 .prefix
710 .as_ref()
711 .map(|p| {
712 Text(p.clone())
713 .color(prefix_color)
714 .size(th.typography.body_large)
715 .single_line()
716 })
717 .unwrap_or(Box(Modifier::new())),
718 with_content_color(text_c, move || text_input),
719 config
720 .suffix
721 .as_ref()
722 .map(|s| {
723 Text(s.clone())
724 .color(suffix_color)
725 .size(th.typography.body_large)
726 .single_line()
727 })
728 .unwrap_or(Box(Modifier::new())),
729 tint_trailing_icon(trail_c, config.trailing_icon.clone()),
730 )),
731 if let Some(lbl) = label_str {
732 Box(Modifier::new()
733 .min_width(200.0)
734 .padding_values(PaddingValues {
735 left: label_x,
736 right: 20.0,
737 top: 0.0,
738 bottom: 0.0,
739 })
740 .absolute()
741 .offset(Some(0.0), Some(label_y), None, None))
742 .child(
743 Text(lbl.as_ref().to_string())
744 .color(label_color)
745 .size(label_size),
746 )
747 } else {
748 Box(Modifier::new())
749 },
750 )),
751 supporting.unwrap_or(Box(Modifier::new())),
752 ))
753}
754
755#[derive(Clone)]
757pub struct TextFieldConfig {
758 pub label: Option<String>,
759 pub placeholder: Option<String>,
760 pub leading_icon: Option<View>,
761 pub trailing_icon: Option<View>,
762 pub single_line: bool,
763 pub is_error: bool,
764 pub enabled: bool,
765 pub read_only: bool,
767 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
770 pub supporting_text: Option<String>,
772 pub prefix: Option<String>,
774 pub suffix: Option<String>,
776 pub on_submit: Option<Rc<dyn Fn(String)>>,
777 pub colors: Option<TextFieldColors>,
778}
779
780impl Default for TextFieldConfig {
781 fn default() -> Self {
782 Self {
783 label: None,
784 placeholder: None,
785 leading_icon: None,
786 trailing_icon: None,
787 single_line: true,
788 is_error: false,
789 enabled: true,
790 read_only: false,
791 visual_transformation: None,
792 supporting_text: None,
793 prefix: None,
794 suffix: None,
795 on_submit: None,
796 colors: None,
797 }
798 }
799}
800
801pub fn TextField(
808 modifier: Modifier,
809 value: String,
810 on_value_change: impl Fn(String) + 'static,
811 config: TextFieldConfig,
812) -> View {
813 let th = theme();
814 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
815 let has_label = label_str.is_some();
816
817 let id = *remember(|| TF_COUNTER.fetch_add(1, Ordering::Relaxed));
818 let anim_key = format!("tf_{id}");
819
820 let focus_tracker: Rc<Cell<bool>> =
821 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
822 let is_focused = focus_tracker.get();
823 let should_float = !value.is_empty() || is_focused;
824
825 let float_t = animate_f32(
826 anim_key.clone(),
827 if should_float { 1.0 } else { 0.0 },
828 th.motion.color,
829 );
830
831 let (indicator_color_target, label_color_target, container_bg) = if let Some(ref tc) = config.colors {
832 let enf = config.enabled && is_focused;
833 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
834 let lb = tc.label_color(config.enabled, config.is_error, enf);
835 let bg = tc.container_color(config.enabled, config.is_error, enf);
836 (ind, lb, bg)
837 } else {
838 let ind = if !config.enabled {
839 th.on_surface.with_alpha_f32(0.38)
840 } else if config.is_error {
841 th.error
842 } else if is_focused {
843 th.primary
844 } else {
845 th.on_surface_variant
846 };
847 let lb = if !config.enabled {
848 th.on_surface.with_alpha_f32(0.38)
849 } else if config.is_error {
850 th.error
851 } else if is_focused {
852 th.primary
853 } else {
854 th.on_surface_variant
855 };
856 let bg = if config.enabled {
857 th.surface_container_highest
858 } else {
859 th.on_surface
860 .with_alpha_f32(0.04)
861 .composite_over(th.surface)
862 };
863 (ind, lb, bg)
864 };
865
866 let indicator_color = animate_color(
867 format!("tf_ind_c_{}", anim_key),
868 indicator_color_target,
869 th.motion.color,
870 );
871 let label_color = animate_color(
872 format!("tf_lc_{}", anim_key),
873 label_color_target,
874 th.motion.color,
875 );
876
877 let label_size = 16.0 - 4.0 * float_t;
878
879 let label_start_y = (56.0 - 16.0) / 2.0;
880 let label_end_y = if has_label { 8.0 } else { 0.0 };
881 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
882
883 let label_start_x = if has_label { 24.0 } else { 0.0 };
884 let label_end_x = if has_label { 20.0 } else { 0.0 };
885 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
886
887 let tf_placeholder = if has_label {
888 if should_float {
889 config.placeholder.unwrap_or_default()
890 } else {
891 String::new()
892 }
893 } else {
894 config.placeholder.unwrap_or_default()
895 };
896
897 let indicator_active = config.is_error || (config.enabled && is_focused);
898 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
899 let indicator_w = animate_f32(
900 format!("tf_ind_w_{}", anim_key),
901 indicator_target_w,
902 th.motion.color,
903 );
904
905 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
906
907 let (prefix_color, suffix_color) = if let Some(ref tc) = config.colors {
908 (
909 tc.prefix_color(config.enabled, config.is_error, is_focused),
910 tc.suffix_color(config.enabled, config.is_error, is_focused),
911 )
912 } else {
913 (
914 if config.is_error {
915 th.error
916 } else {
917 th.on_surface
918 },
919 if config.is_error {
920 th.error
921 } else {
922 th.on_surface
923 },
924 )
925 };
926
927 let (lead_c, trail_c) = if let Some(ref tc) = config.colors {
928 (
929 tc.leading_icon_color(config.enabled, config.is_error, is_focused),
930 tc.trailing_icon_color(config.enabled, config.is_error, is_focused),
931 )
932 } else {
933 let c = if !config.enabled {
934 th.on_surface.with_alpha_f32(0.38)
935 } else if config.is_error {
936 th.error
937 } else {
938 th.on_surface_variant
939 };
940 (c, c)
941 };
942
943 let text_c = config
944 .colors
945 .as_ref()
946 .map(|c| c.text_color(config.enabled, config.is_error, is_focused))
947 .unwrap_or(if config.enabled {
948 th.on_surface
949 } else {
950 th.on_surface.with_alpha_f32(0.38)
951 });
952
953 let supporting = config.supporting_text.as_ref().map(|st| {
954 let c = if let Some(ref tc) = config.colors {
955 tc.supporting_text_color(config.enabled, config.is_error, is_focused)
956 } else if config.is_error {
957 th.error
958 } else {
959 th.on_surface_variant
960 };
961 Text(st.clone())
962 .color(c)
963 .size(th.typography.body_small)
964 .modifier(Modifier::new().padding_values(PaddingValues {
965 left: 16.0,
966 right: 16.0,
967 top: 4.0,
968 bottom: 0.0,
969 }))
970 });
971
972 let text_input = View::new(0, ViewKind::Box)
973 .modifier(
974 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
975 hint: tf_placeholder,
976 multiline: !config.single_line,
977 on_change: Some(Rc::new(on_value_change) as _),
978 on_submit: config.on_submit.clone().map(|f| {
979 let f = f.clone();
980 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
981 }),
982 focus_tracker: Some(focus_tracker),
983 value: value.clone(),
984 visual_transformation: config.visual_transformation.clone(),
985 enabled: config.enabled,
986 read_only: config.read_only,
987 cursor_color: config
988 .colors
989 .as_ref()
990 .map(|c| c.cursor_color(config.is_error)),
991 ..Default::default()
992 }),
993 )
994 .semantics(Semantics {
995 role: Role::TextField,
996 label: config
997 .label
998 .clone()
999 .or_else(|| config.supporting_text.clone()),
1000 enabled: config.enabled,
1001 ..Default::default()
1002 });
1003
1004 Column(modifier.min_width(TextFieldDefaults::MIN_WIDTH)).child((
1005 ZStack(
1006 Modifier::new()
1007 .fill_max_width()
1008 .min_height(TextFieldDefaults::MIN_HEIGHT),
1009 )
1010 .child((
1011 Box(Modifier::new()
1013 .fill_max_size()
1014 .clip_rounded_radii([
1015 0.0, 0.0, th.shapes.extra_small, th.shapes.extra_small, ])
1020 .background(container_bg)),
1021 Row(Modifier::new()
1023 .fill_max_size()
1024 .padding_values(PaddingValues {
1025 left: 16.0,
1026 right: 16.0,
1027 top: top_pad,
1028 bottom: bottom_pad,
1029 })
1030 .align_items(AlignItems::CENTER))
1031 .child((
1032 tint_icon(lead_c, config.leading_icon.clone()),
1033 config
1034 .prefix
1035 .as_ref()
1036 .map(|p| {
1037 Text(p.clone())
1038 .color(prefix_color)
1039 .size(th.typography.body_large)
1040 .single_line()
1041 })
1042 .unwrap_or(Box(Modifier::new())),
1043 with_content_color(text_c, move || text_input),
1044 config
1045 .suffix
1046 .as_ref()
1047 .map(|s| {
1048 Text(s.clone())
1049 .color(suffix_color)
1050 .size(th.typography.body_large)
1051 .single_line()
1052 })
1053 .unwrap_or(Box(Modifier::new())),
1054 tint_trailing_icon(trail_c, config.trailing_icon.clone()),
1055 )),
1056 Box(Modifier::new()
1058 .fill_max_width()
1059 .height(indicator_w)
1060 .absolute()
1061 .offset(None, None, None, Some(0.0))
1062 .background(indicator_color)),
1063 if let Some(lbl) = label_str {
1065 Box(Modifier::new()
1066 .absolute()
1067 .offset(Some(label_x), Some(label_y), None, None))
1068 .child(
1069 Text(lbl.as_ref().to_string())
1070 .color(label_color)
1071 .size(label_size),
1072 )
1073 } else {
1074 Box(Modifier::new())
1075 },
1076 )),
1077 supporting.unwrap_or(Box(Modifier::new())),
1078 ))
1079}