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_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 || should_float {
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, label_color, 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 label_size = 16.0 - 4.0 * float_t;
561
562 let min_label_half_h: f32 = if has_label { 8.0 } else { 0.0 };
564
565 let label_start_y = (56.0 - 16.0) / 2.0;
567 let label_end_y = -min_label_half_h;
568 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
569
570 let label_start_x = if has_label { 24.0 } else { 0.0 };
572 let label_end_x = if has_label { 20.0 } else { 0.0 };
573 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
574
575 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
577
578 let (prefix_color, suffix_color) = if let Some(ref tc) = config.colors {
579 (
580 tc.prefix_color(config.enabled, config.is_error, is_focused),
581 tc.suffix_color(config.enabled, config.is_error, is_focused),
582 )
583 } else {
584 (
585 if config.is_error {
586 th.error
587 } else {
588 th.on_surface
589 },
590 if config.is_error {
591 th.error
592 } else {
593 th.on_surface
594 },
595 )
596 };
597
598 let (lead_c, trail_c) = if let Some(ref tc) = config.colors {
599 (
600 tc.leading_icon_color(config.enabled, config.is_error, is_focused),
601 tc.trailing_icon_color(config.enabled, config.is_error, is_focused),
602 )
603 } else {
604 let c = if !config.enabled {
605 th.on_surface.with_alpha_f32(0.38)
606 } else if config.is_error {
607 th.error
608 } else {
609 th.on_surface_variant
610 };
611 (c, c)
612 };
613
614 let text_c = config
615 .colors
616 .as_ref()
617 .map(|c| c.text_color(config.enabled, config.is_error, is_focused))
618 .unwrap_or(if config.enabled {
619 th.on_surface
620 } else {
621 th.on_surface.with_alpha_f32(0.38)
622 });
623
624 let supporting = config.supporting_text.as_ref().map(|st| {
625 let c = if let Some(ref tc) = config.colors {
626 tc.supporting_text_color(config.enabled, config.is_error, is_focused)
627 } else if config.is_error {
628 th.error
629 } else {
630 th.on_surface_variant
631 };
632 Text(st.clone())
633 .color(c)
634 .size(th.typography.body_small)
635 .modifier(Modifier::new().padding_values(PaddingValues {
636 left: 16.0,
637 right: 16.0,
638 top: 4.0,
639 bottom: 0.0,
640 }))
641 });
642
643 let label_cutout = label_str.as_ref().map(|lbl| {
646 let font_px = dp_to_px(label_size) * repose_core::locals::text_scale().0;
647 let m = measure_text(lbl, font_px, TextMeasureConfig::default());
648 let text_width_px = m.positions.last().copied().unwrap_or(0.0);
649 let text_width_dp = px_to_dp(text_width_px);
650 let pad = 1.0;
651 let line_h = 16.0;
652 (
653 label_x - pad,
654 label_y - pad,
655 label_x + text_width_dp + pad,
656 label_y + line_h + pad,
657 )
658 });
659
660 Column(modifier.min_width(OutlinedTextFieldDefaults::MIN_WIDTH)).child((
661 ZStack(
662 Modifier::new()
663 .fill_max_width()
664 .min_height(OutlinedTextFieldDefaults::MIN_HEIGHT),
665 )
666 .child((
667 Box(Modifier::new()
668 .fill_max_size()
669 .clip_rounded(th.shapes.small)
670 .background(container_bg)),
671 if has_label {
672 let mut bm = Modifier::new()
673 .fill_max_size()
674 .clip_rounded(th.shapes.small)
675 .border(border_w, border_color, th.shapes.small);
676 if let Some((l, t, r, b)) = label_cutout {
677 bm = bm.clip_rect(l, t, r, b, ClipOp::Difference);
678 }
679 Box(bm)
680 } else {
681 Box(Modifier::new()
682 .fill_max_size()
683 .clip_rounded(th.shapes.small)
684 .border(border_w, border_color, th.shapes.small))
685 },
686 Row(Modifier::new()
687 .fill_max_size()
688 .padding_values(PaddingValues {
689 left: 16.0,
690 right: 16.0,
691 top: top_pad,
692 bottom: bottom_pad,
693 })
694 .align_items(AlignItems::CENTER))
695 .child((
696 tint_icon(lead_c, config.leading_icon.clone()),
697 config
698 .prefix
699 .as_ref()
700 .map(|p| {
701 Text(p.clone())
702 .color(prefix_color)
703 .size(th.typography.body_large)
704 .single_line()
705 })
706 .unwrap_or(Box(Modifier::new())),
707 with_content_color(text_c, move || text_input),
708 config
709 .suffix
710 .as_ref()
711 .map(|s| {
712 Text(s.clone())
713 .color(suffix_color)
714 .size(th.typography.body_large)
715 .single_line()
716 })
717 .unwrap_or(Box(Modifier::new())),
718 tint_trailing_icon(trail_c, config.trailing_icon.clone()),
719 )),
720 if let Some(lbl) = label_str {
721 Box(Modifier::new()
722 .min_width(200.0)
723 .padding_values(PaddingValues {
724 left: label_x,
725 right: 20.0,
726 top: 0.0,
727 bottom: 0.0,
728 })
729 .absolute()
730 .offset(Some(0.0), Some(label_y), None, None))
731 .child(
732 Text(lbl.as_ref().to_string())
733 .color(label_color)
734 .size(label_size),
735 )
736 } else {
737 Box(Modifier::new())
738 },
739 )),
740 supporting.unwrap_or(Box(Modifier::new())),
741 ))
742}
743
744#[derive(Clone)]
746pub struct TextFieldConfig {
747 pub label: Option<String>,
748 pub placeholder: Option<String>,
749 pub leading_icon: Option<View>,
750 pub trailing_icon: Option<View>,
751 pub single_line: bool,
752 pub is_error: bool,
753 pub enabled: bool,
754 pub read_only: bool,
756 pub visual_transformation: Option<Rc<dyn VisualTransformation>>,
759 pub supporting_text: Option<String>,
761 pub prefix: Option<String>,
763 pub suffix: Option<String>,
765 pub on_submit: Option<Rc<dyn Fn(String)>>,
766 pub colors: Option<TextFieldColors>,
767}
768
769impl Default for TextFieldConfig {
770 fn default() -> Self {
771 Self {
772 label: None,
773 placeholder: None,
774 leading_icon: None,
775 trailing_icon: None,
776 single_line: true,
777 is_error: false,
778 enabled: true,
779 read_only: false,
780 visual_transformation: None,
781 supporting_text: None,
782 prefix: None,
783 suffix: None,
784 on_submit: None,
785 colors: None,
786 }
787 }
788}
789
790pub fn TextField(
797 modifier: Modifier,
798 value: String,
799 on_value_change: impl Fn(String) + 'static,
800 config: TextFieldConfig,
801) -> View {
802 let th = theme();
803 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
804 let has_label = label_str.is_some();
805
806 let id = *remember(|| TF_COUNTER.fetch_add(1, Ordering::Relaxed));
807 let anim_key = format!("tf_{id}");
808
809 let focus_tracker: Rc<Cell<bool>> =
810 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
811 let is_focused = focus_tracker.get();
812 let should_float = !value.is_empty() || is_focused;
813
814 let float_t = animate_f32(
815 anim_key.clone(),
816 if should_float { 1.0 } else { 0.0 },
817 th.motion.color,
818 );
819
820 let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
821 let enf = config.enabled && is_focused;
822 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
823 let lb = tc.label_color(config.enabled, config.is_error, enf);
824 let bg = tc.container_color(config.enabled, config.is_error, enf);
825 (ind, lb, bg)
826 } else {
827 let ind = if !config.enabled {
828 th.on_surface.with_alpha_f32(0.38)
829 } else if config.is_error {
830 th.error
831 } else if is_focused {
832 th.primary
833 } else {
834 th.on_surface_variant
835 };
836 let lb = if !config.enabled {
837 th.on_surface.with_alpha_f32(0.38)
838 } else if config.is_error {
839 th.error
840 } else if is_focused {
841 th.primary
842 } else {
843 th.on_surface_variant
844 };
845 let bg = if config.enabled {
846 th.surface_container_highest
847 } else {
848 th.on_surface
849 .with_alpha_f32(0.04)
850 .composite_over(th.surface)
851 };
852 (ind, lb, bg)
853 };
854
855 let label_size = 16.0 - 4.0 * float_t;
856
857 let label_start_y = (56.0 - 16.0) / 2.0;
858 let label_end_y = if has_label { 8.0 } else { 0.0 };
859 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
860
861 let label_start_x = if has_label { 24.0 } else { 0.0 };
862 let label_end_x = if has_label { 20.0 } else { 0.0 };
863 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
864
865 let tf_placeholder = if has_label {
866 if should_float {
867 config.placeholder.unwrap_or_default()
868 } else {
869 String::new()
870 }
871 } else {
872 config.placeholder.unwrap_or_default()
873 };
874
875 let indicator_active = config.is_error || (config.enabled && is_focused);
876 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
877 let indicator_w = animate_f32(
878 format!("tf_ind_w_{}", anim_key),
879 indicator_target_w,
880 th.motion.color,
881 );
882
883 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
884
885 let (prefix_color, suffix_color) = if let Some(ref tc) = config.colors {
886 (
887 tc.prefix_color(config.enabled, config.is_error, is_focused),
888 tc.suffix_color(config.enabled, config.is_error, is_focused),
889 )
890 } else {
891 (
892 if config.is_error {
893 th.error
894 } else {
895 th.on_surface
896 },
897 if config.is_error {
898 th.error
899 } else {
900 th.on_surface
901 },
902 )
903 };
904
905 let (lead_c, trail_c) = if let Some(ref tc) = config.colors {
906 (
907 tc.leading_icon_color(config.enabled, config.is_error, is_focused),
908 tc.trailing_icon_color(config.enabled, config.is_error, is_focused),
909 )
910 } else {
911 let c = if !config.enabled {
912 th.on_surface.with_alpha_f32(0.38)
913 } else if config.is_error {
914 th.error
915 } else {
916 th.on_surface_variant
917 };
918 (c, c)
919 };
920
921 let text_c = config
922 .colors
923 .as_ref()
924 .map(|c| c.text_color(config.enabled, config.is_error, is_focused))
925 .unwrap_or(if config.enabled {
926 th.on_surface
927 } else {
928 th.on_surface.with_alpha_f32(0.38)
929 });
930
931 let supporting = config.supporting_text.as_ref().map(|st| {
932 let c = if let Some(ref tc) = config.colors {
933 tc.supporting_text_color(config.enabled, config.is_error, is_focused)
934 } else if config.is_error {
935 th.error
936 } else {
937 th.on_surface_variant
938 };
939 Text(st.clone())
940 .color(c)
941 .size(th.typography.body_small)
942 .modifier(Modifier::new().padding_values(PaddingValues {
943 left: 16.0,
944 right: 16.0,
945 top: 4.0,
946 bottom: 0.0,
947 }))
948 });
949
950 let text_input = View::new(0, ViewKind::Box)
951 .modifier(
952 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
953 hint: tf_placeholder,
954 multiline: !config.single_line,
955 on_change: Some(Rc::new(on_value_change) as _),
956 on_submit: config.on_submit.clone().map(|f| {
957 let f = f.clone();
958 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
959 }),
960 focus_tracker: Some(focus_tracker),
961 value: value.clone(),
962 visual_transformation: config.visual_transformation.clone(),
963 enabled: config.enabled,
964 read_only: config.read_only,
965 cursor_color: config
966 .colors
967 .as_ref()
968 .map(|c| c.cursor_color(config.is_error)),
969 ..Default::default()
970 }),
971 )
972 .semantics(Semantics {
973 role: Role::TextField,
974 label: config
975 .label
976 .clone()
977 .or_else(|| config.supporting_text.clone()),
978 enabled: config.enabled,
979 ..Default::default()
980 });
981
982 Column(modifier.min_width(TextFieldDefaults::MIN_WIDTH)).child((
983 ZStack(
984 Modifier::new()
985 .fill_max_width()
986 .min_height(TextFieldDefaults::MIN_HEIGHT),
987 )
988 .child((
989 Box(Modifier::new()
991 .fill_max_size()
992 .clip_rounded_radii([
993 0.0, 0.0, th.shapes.extra_small, th.shapes.extra_small, ])
998 .background(container_bg)),
999 Row(Modifier::new()
1001 .fill_max_size()
1002 .padding_values(PaddingValues {
1003 left: 16.0,
1004 right: 16.0,
1005 top: top_pad,
1006 bottom: bottom_pad,
1007 })
1008 .align_items(AlignItems::CENTER))
1009 .child((
1010 tint_icon(lead_c, config.leading_icon.clone()),
1011 config
1012 .prefix
1013 .as_ref()
1014 .map(|p| {
1015 Text(p.clone())
1016 .color(prefix_color)
1017 .size(th.typography.body_large)
1018 .single_line()
1019 })
1020 .unwrap_or(Box(Modifier::new())),
1021 with_content_color(text_c, move || text_input),
1022 config
1023 .suffix
1024 .as_ref()
1025 .map(|s| {
1026 Text(s.clone())
1027 .color(suffix_color)
1028 .size(th.typography.body_large)
1029 .single_line()
1030 })
1031 .unwrap_or(Box(Modifier::new())),
1032 tint_trailing_icon(trail_c, config.trailing_icon.clone()),
1033 )),
1034 Box(Modifier::new()
1036 .fill_max_width()
1037 .height(indicator_w)
1038 .absolute()
1039 .offset(None, None, None, Some(0.0))
1040 .background(indicator_color)),
1041 if let Some(lbl) = label_str {
1043 Box(Modifier::new()
1044 .absolute()
1045 .offset(Some(label_x), Some(label_y), None, None))
1046 .child(
1047 Text(lbl.as_ref().to_string())
1048 .color(label_color)
1049 .size(label_size),
1050 )
1051 } else {
1052 Box(Modifier::new())
1053 },
1054 )),
1055 supporting.unwrap_or(Box(Modifier::new())),
1056 ))
1057}