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 keyboard_type: Default::default(),
408 capitalization: Default::default(),
409 ime_action: Default::default(),
410 auto_correct_enabled: None,
411 enabled: config.enabled,
412 read_only: config.read_only,
413 max_lines: None,
414 min_lines: 1,
415 cursor_color: config
416 .colors
417 .as_ref()
418 .map(|c| c.cursor_color(config.is_error)),
419 on_text_layout: None,
420 text_style: None,
421 keyboard_actions: None,
422 interaction_source: None,
423 line_limits: None,
424 }),
425 )
426 .semantics(Semantics {
427 role: Role::TextField,
428 label: config
429 .label
430 .clone()
431 .or_else(|| config.supporting_text.clone()),
432 focused: false,
433 enabled: config.enabled,
434 selectable_group: false,
435 });
436
437 outlined_field_decoration(
438 modifier,
439 anim_key,
440 label_str,
441 &config,
442 is_focused,
443 !value.is_empty(),
444 text_input,
445 )
446}
447
448pub fn OutlinedTextFieldState(
450 modifier: Modifier,
451 state: Rc<RefCell<TextFieldState>>,
452 on_value_change: impl Fn(String) + 'static,
453 config: OutlinedTextFieldConfig,
454) -> View {
455 let label_str: Option<Rc<str>> = config.label.clone().map(Rc::from);
456 let has_label = label_str.is_some();
457
458 let id = *remember(|| OTFS_COUNTER.fetch_add(1, Ordering::Relaxed));
460 let anim_key = format!("otfs_{id}");
461
462 let focus_tracker: Rc<Cell<bool>> = match config.focus_tracker.clone() {
463 Some(ft) => ft,
464 None => remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false)),
465 };
466 let is_focused = focus_tracker.get();
467 let has_content = !state.borrow().text.is_empty();
468 let should_float = has_content || is_focused;
469
470 let tf_placeholder = if has_label {
472 if should_float {
473 config.placeholder.clone().unwrap_or_default()
474 } else {
475 String::new()
476 }
477 } else {
478 config.placeholder.clone().unwrap_or_default()
479 };
480
481 let text_input = BasicTextField(
482 state,
483 Modifier::new().flex_grow(1.0),
484 tf_placeholder,
485 BasicTextFieldConfig {
486 line_limits: if config.single_line {
487 TextFieldLineLimits::SingleLine
488 } else {
489 TextFieldLineLimits::MultiLine {
490 min_height_in_lines: 1,
491 max_height_in_lines: usize::MAX,
492 }
493 },
494 on_change: Some(Rc::new(on_value_change)),
495 on_submit: config.on_submit.clone(),
496 focus_tracker: Some(focus_tracker),
497 enabled: config.enabled,
498 read_only: config.read_only,
499 ..Default::default()
500 },
501 );
502
503 outlined_field_decoration(
504 modifier,
505 anim_key,
506 label_str,
507 &config,
508 is_focused,
509 has_content,
510 text_input,
511 )
512}
513
514fn outlined_field_decoration(
515 modifier: Modifier,
516 anim_key: String,
517 label_str: Option<Rc<str>>,
518 config: &OutlinedTextFieldConfig,
519 is_focused: bool,
520 has_content: bool,
521 text_input: View,
522) -> View {
523 let th = theme();
524 let has_label = label_str.is_some();
525
526 let should_float = has_content || is_focused;
527 let float_t = animate_f32(
528 anim_key.clone(),
529 if should_float { 1.0 } else { 0.0 },
530 th.motion.color,
531 );
532
533 let target_border_w = if config.is_error || should_float {
534 OutlinedTextFieldDefaults::FOCUSED_BORDER_THICKNESS
535 } else {
536 OutlinedTextFieldDefaults::UNFOCUSED_BORDER_THICKNESS
537 };
538 let border_w = animate_f32(
539 format!("otf_bw_{}", anim_key),
540 target_border_w,
541 th.motion.color,
542 );
543
544 let (border_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
545 (
546 tc.indicator_color(config.enabled, config.is_error, is_focused),
547 tc.label_color(config.enabled, config.is_error, is_focused),
548 tc.container_color(config.enabled, config.is_error, is_focused),
549 )
550 } else {
551 (
552 if config.is_error {
553 th.error
554 } else if is_focused {
555 th.primary
556 } else {
557 th.outline
558 },
559 if config.is_error {
560 th.error
561 } else if is_focused {
562 th.primary
563 } else {
564 th.on_surface_variant
565 },
566 Color::TRANSPARENT,
567 )
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, label_color, 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 label_size = 16.0 - 4.0 * float_t;
867
868 let label_start_y = (56.0 - 16.0) / 2.0;
869 let label_end_y = if has_label { 8.0 } else { 0.0 };
870 let label_y = label_start_y - (label_start_y - label_end_y) * float_t;
871
872 let label_start_x = if has_label { 24.0 } else { 0.0 };
873 let label_end_x = if has_label { 20.0 } else { 0.0 };
874 let label_x = label_start_x - (label_start_x - label_end_x) * float_t;
875
876 let tf_placeholder = if has_label {
877 if should_float {
878 config.placeholder.unwrap_or_default()
879 } else {
880 String::new()
881 }
882 } else {
883 config.placeholder.unwrap_or_default()
884 };
885
886 let indicator_active = config.is_error || (config.enabled && is_focused);
887 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
888 let indicator_w = animate_f32(
889 format!("tf_ind_w_{}", anim_key),
890 indicator_target_w,
891 th.motion.color,
892 );
893
894 let (top_pad, bottom_pad) = if has_label { (8.0, 8.0) } else { (16.0, 16.0) };
895
896 let (prefix_color, suffix_color) = if let Some(ref tc) = config.colors {
897 (
898 tc.prefix_color(config.enabled, config.is_error, is_focused),
899 tc.suffix_color(config.enabled, config.is_error, is_focused),
900 )
901 } else {
902 (
903 if config.is_error {
904 th.error
905 } else {
906 th.on_surface
907 },
908 if config.is_error {
909 th.error
910 } else {
911 th.on_surface
912 },
913 )
914 };
915
916 let (lead_c, trail_c) = if let Some(ref tc) = config.colors {
917 (
918 tc.leading_icon_color(config.enabled, config.is_error, is_focused),
919 tc.trailing_icon_color(config.enabled, config.is_error, is_focused),
920 )
921 } else {
922 let c = if !config.enabled {
923 th.on_surface.with_alpha_f32(0.38)
924 } else if config.is_error {
925 th.error
926 } else {
927 th.on_surface_variant
928 };
929 (c, c)
930 };
931
932 let text_c = config
933 .colors
934 .as_ref()
935 .map(|c| c.text_color(config.enabled, config.is_error, is_focused))
936 .unwrap_or(if config.enabled {
937 th.on_surface
938 } else {
939 th.on_surface.with_alpha_f32(0.38)
940 });
941
942 let supporting = config.supporting_text.as_ref().map(|st| {
943 let c = if let Some(ref tc) = config.colors {
944 tc.supporting_text_color(config.enabled, config.is_error, is_focused)
945 } else if config.is_error {
946 th.error
947 } else {
948 th.on_surface_variant
949 };
950 Text(st.clone())
951 .color(c)
952 .size(th.typography.body_small)
953 .modifier(Modifier::new().padding_values(PaddingValues {
954 left: 16.0,
955 right: 16.0,
956 top: 4.0,
957 bottom: 0.0,
958 }))
959 });
960
961 let text_input = View::new(0, ViewKind::Box)
962 .modifier(
963 Modifier::new().flex_grow(1.0).text_input(TextInputConfig {
964 hint: tf_placeholder,
965 multiline: !config.single_line,
966 on_change: Some(Rc::new(on_value_change) as _),
967 on_submit: config.on_submit.clone().map(|f| {
968 let f = f.clone();
969 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
970 }),
971 focus_tracker: Some(focus_tracker),
972 value: value.clone(),
973 visual_transformation: config.visual_transformation.clone(),
974 keyboard_type: Default::default(),
975 capitalization: Default::default(),
976 ime_action: Default::default(),
977 auto_correct_enabled: None,
978 enabled: config.enabled,
979 read_only: config.read_only,
980 max_lines: None,
981 min_lines: 1,
982 cursor_color: config
983 .colors
984 .as_ref()
985 .map(|c| c.cursor_color(config.is_error)),
986 on_text_layout: None,
987 text_style: None,
988 keyboard_actions: None,
989 interaction_source: None,
990 line_limits: None,
991 }),
992 )
993 .semantics(Semantics {
994 role: Role::TextField,
995 label: config
996 .label
997 .clone()
998 .or_else(|| config.supporting_text.clone()),
999 focused: false,
1000 enabled: config.enabled,
1001 selectable_group: false,
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}