1use super::InteractionPreview;
17use crate::{
18 color::ColorRole,
19 motion::{self, MotionToken},
20 theme::{blend, color},
21 tokens::{
22 shape::RADIUS_EXTRA_SMALL,
23 state,
24 typography::{TypeRole, rem},
25 },
26};
27use gpui::{prelude::*, *};
28use gpui_base::input::{InputBaseState, InputModeKind, InputState, TextareaState};
29use gpui_kit::component::{
30 Sizable as _, Size,
31 input::{Input, Textarea},
32};
33
34pub const TEXT_FIELD_HEIGHT: f32 = 56.;
36pub const TEXT_FIELD_WIDTH: f32 = 210.;
38const PADDING: f32 = 16.;
40const ICON_PADDING: f32 = 12.;
42const ICON_SIZE: f32 = 24.;
43const ICON_GAP: f32 = 16.;
45const FILLED_FLOAT_TOP: f32 = 8.;
47const FILLED_INPUT_TOP: f32 = 24.;
49const NOTCH_PADDING: f32 = 4.;
51const FOCUS_INDICATOR: f32 = 3.;
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59pub enum TextFieldDensity {
60 #[default]
61 Default,
62 Compact,
63}
64
65impl TextFieldDensity {
66 pub const ALL: [Self; 2] = [Self::Default, Self::Compact];
67 pub const fn height(self) -> f32 {
69 match self {
70 Self::Default => TEXT_FIELD_HEIGHT,
71 Self::Compact => TEXT_FIELD_HEIGHT - 8.,
72 }
73 }
74 const fn lift(self) -> f32 {
76 (TEXT_FIELD_HEIGHT - self.height()) / 2.
77 }
78}
79
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
82pub enum TextFieldVariant {
83 #[default]
84 Filled,
85 Outlined,
86}
87
88impl TextFieldVariant {
89 pub const ALL: [Self; 2] = [Self::Filled, Self::Outlined];
90 pub const fn name(self) -> &'static str {
91 match self {
92 Self::Filled => "Filled",
93 Self::Outlined => "Outlined",
94 }
95 }
96}
97
98enum FieldInput {
99 Single(Entity<InputState>),
100 Multi(Entity<TextareaState>),
101 Static(SharedString),
103}
104
105#[derive(gpui::IntoElement)]
107pub struct TextField {
108 id: ElementId,
109 input: FieldInput,
110 variant: TextFieldVariant,
111 density: TextFieldDensity,
112 label: Option<SharedString>,
113 placeholder: Option<SharedString>,
114 leading: Option<AnyElement>,
115 trailing: Option<AnyElement>,
116 prefix: Option<SharedString>,
117 suffix: Option<SharedString>,
118 supporting: Option<SharedString>,
119 max_length: Option<usize>,
120 error: bool,
121 disabled: bool,
122 float_label: bool,
123 preview: Option<InteractionPreview>,
124 style: StyleRefinement,
125}
126
127impl TextField {
128 pub fn new(id: impl Into<ElementId>, state: &Entity<InputState>) -> Self {
130 Self::with_input(id, FieldInput::Single(state.clone()))
131 }
132
133 pub fn multi_line(id: impl Into<ElementId>, state: &Entity<TextareaState>) -> Self {
135 Self::with_input(id, FieldInput::Multi(state.clone()))
136 }
137
138 pub fn specimen(id: impl Into<ElementId>, value: impl Into<SharedString>) -> Self {
141 Self::with_input(id, FieldInput::Static(value.into()))
142 }
143
144 fn with_input(id: impl Into<ElementId>, input: FieldInput) -> Self {
145 Self {
146 id: id.into(),
147 input,
148 variant: TextFieldVariant::Filled,
149 density: TextFieldDensity::Default,
150 label: None,
151 placeholder: None,
152 leading: None,
153 trailing: None,
154 prefix: None,
155 suffix: None,
156 supporting: None,
157 max_length: None,
158 error: false,
159 disabled: false,
160 float_label: false,
161 preview: None,
162 style: StyleRefinement::default(),
163 }
164 }
165
166 pub fn variant(mut self, variant: TextFieldVariant) -> Self {
167 self.variant = variant;
168 self
169 }
170 pub fn density(mut self, density: TextFieldDensity) -> Self {
172 self.density = density;
173 self
174 }
175 pub fn outlined(self) -> Self {
176 self.variant(TextFieldVariant::Outlined)
177 }
178 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
181 self.label = Some(label.into());
182 self
183 }
184 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
187 self.placeholder = Some(placeholder.into());
188 self
189 }
190 pub fn float_label(mut self, float: bool) -> Self {
193 self.float_label = float;
194 self
195 }
196 pub fn leading_icon(mut self, icon: impl IntoElement) -> Self {
198 self.leading = Some(icon.into_any_element());
199 self
200 }
201 pub fn trailing_icon(mut self, icon: impl IntoElement) -> Self {
204 self.trailing = Some(icon.into_any_element());
205 self
206 }
207 pub fn prefix(mut self, prefix: impl Into<SharedString>) -> Self {
209 self.prefix = Some(prefix.into());
210 self
211 }
212 pub fn suffix(mut self, suffix: impl Into<SharedString>) -> Self {
214 self.suffix = Some(suffix.into());
215 self
216 }
217 pub fn supporting_text(mut self, text: impl Into<SharedString>) -> Self {
220 self.supporting = Some(text.into());
221 self
222 }
223 pub fn max_length(mut self, max: usize) -> Self {
225 self.max_length = Some(max);
226 self
227 }
228 pub fn error(mut self, error: bool) -> Self {
229 self.error = error;
230 self
231 }
232 pub fn disabled(mut self, disabled: bool) -> Self {
233 self.disabled = disabled;
234 self
235 }
236 pub fn preview_state(mut self, state: InteractionPreview) -> Self {
240 self.preview = Some(state);
241 self
242 }
243}
244
245impl Styled for TextField {
246 fn style(&mut self) -> &mut StyleRefinement {
247 &mut self.style
248 }
249}
250
251#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
253pub(crate) struct FieldState {
254 pub focused: bool,
255 pub hovered: bool,
256 pub error: bool,
257 pub disabled: bool,
258}
259
260#[derive(Clone, Copy, Debug, PartialEq)]
262pub(crate) struct FieldPaint {
263 pub container: Rgba,
264 pub indicator: Rgba,
266 pub indicator_width: f32,
267 pub label: Rgba,
268 pub input: Rgba,
269 pub placeholder: Rgba,
270 pub leading: Rgba,
271 pub trailing: Rgba,
272 pub supporting: Rgba,
273 pub state_layer: Rgba,
274 pub caret: Rgba,
276}
277
278fn alpha(mut c: Rgba, a: f32) -> Rgba {
279 c.a *= a;
280 c
281}
282
283pub(crate) fn field_paint(variant: TextFieldVariant, s: FieldState) -> FieldPaint {
285 use ColorRole as C;
286 let filled = variant == TextFieldVariant::Filled;
287 let on_surface = color(C::ON_SURFACE);
288 let variant_ink = color(C::ON_SURFACE_VARIANT);
289 if s.disabled {
290 let content = state::disabled(on_surface);
291 return FieldPaint {
292 container: if filled {
293 alpha(on_surface, 0.04)
294 } else {
295 alpha(on_surface, 0.)
296 },
297 indicator: if filled {
298 content
299 } else {
300 alpha(on_surface, state::DISABLED_CONTAINER)
301 },
302 indicator_width: 1.,
303 label: content,
304 input: content,
305 placeholder: content,
306 leading: content,
307 trailing: content,
308 supporting: content,
309 state_layer: alpha(on_surface, 0.),
310 caret: color(C::PRIMARY),
311 };
312 }
313 let (rest, hover) = if filled {
314 (variant_ink, on_surface)
315 } else {
316 (color(C::OUTLINE), on_surface)
317 };
318 let indicator = match (s.error, s.focused, s.hovered) {
319 (true, false, true) => color(C::ON_ERROR_CONTAINER),
320 (true, _, _) => color(C::ERROR),
321 (false, true, _) => color(C::PRIMARY),
322 (false, false, true) => hover,
323 _ => rest,
324 };
325 let label = match (s.error, s.focused, s.hovered) {
326 (true, false, true) => color(C::ON_ERROR_CONTAINER),
327 (true, _, _) => color(C::ERROR),
328 (false, true, _) => color(C::PRIMARY),
329 (false, false, true) if !filled => on_surface,
330 _ => variant_ink,
331 };
332 FieldPaint {
333 container: if filled {
334 color(C::SURFACE_CONTAINER_HIGHEST)
335 } else {
336 alpha(on_surface, 0.)
337 },
338 indicator,
339 indicator_width: if s.focused { FOCUS_INDICATOR } else { 1. },
340 label,
341 input: on_surface,
342 placeholder: variant_ink,
343 leading: variant_ink,
344 trailing: match (s.error, s.hovered && !s.focused) {
345 (true, true) => color(C::ON_ERROR_CONTAINER),
346 (true, false) => color(C::ERROR),
347 _ => variant_ink,
348 },
349 supporting: if s.error {
350 color(C::ERROR)
351 } else {
352 variant_ink
353 },
354 state_layer: if filled && s.hovered {
355 alpha(on_surface, state::HOVER)
356 } else {
357 alpha(on_surface, 0.)
358 },
359 caret: if s.error {
360 color(C::ERROR)
361 } else {
362 color(C::PRIMARY)
363 },
364 }
365}
366
367#[cfg(test)]
370pub(crate) fn label_geometry(
371 variant: TextFieldVariant,
372 leading: bool,
373 t: f32,
374) -> (f32, f32, f32, f32) {
375 density_label_geometry(variant, TextFieldDensity::Default, leading, t)
376}
377
378pub(crate) fn density_label_geometry(
380 variant: TextFieldVariant,
381 density: TextFieldDensity,
382 leading: bool,
383 t: f32,
384) -> (f32, f32, f32, f32) {
385 let rest_left = if leading {
386 ICON_PADDING + ICON_SIZE + ICON_GAP
387 } else {
388 PADDING
389 };
390 let (float_left, float_top) = match variant {
391 TextFieldVariant::Filled => (rest_left, FILLED_FLOAT_TOP - density.lift()),
392 TextFieldVariant::Outlined => (PADDING, -8.),
394 };
395 let rest_top = (density.height() - 24.) / 2.;
396 let large = TypeRole::BodyLarge.scale();
397 let small = TypeRole::BodySmall.scale();
398 let lerp = |a: f32, b: f32| a + (b - a) * t;
399 (
400 lerp(rest_left, float_left),
401 lerp(rest_top, float_top),
402 lerp(large.font_size.0, small.font_size.0) * 14.,
403 lerp(large.line_height.0, small.line_height.0) * 14.,
404 )
405}
406
407pub(crate) fn notch(label_px: f32, t: f32) -> (f32, f32) {
410 let t = t.clamp(0., 1.);
411 let full = label_px + 2. * NOTCH_PADDING;
412 let center = PADDING - NOTCH_PADDING + full / 2.;
413 let width = full * t;
414 (center - width / 2., width)
415}
416
417pub(crate) fn type_style<E: Styled>(element: E, role: TypeRole) -> E {
419 let scale = role.scale();
420 element
421 .font_family(scale.font_family)
422 .text_size(scale.font_size)
423 .line_height(scale.line_height)
424 .font_weight(FontWeight(scale.font_weight as f32))
425}
426
427fn read_value<M: InputModeKind>(state: &Entity<InputBaseState<M>>, cx: &App) -> SharedString {
428 state.read(cx).value()
429}
430
431fn focus_handle<M: InputModeKind>(state: &Entity<InputBaseState<M>>, cx: &App) -> FocusHandle {
432 state.read(cx).focus_handle(cx)
433}
434
435fn label_width(text: &SharedString, window: &Window) -> f32 {
437 let scale = TypeRole::BodySmall.scale();
438 let mut style = window.text_style();
439 style.font_family = scale.font_family.into();
440 style.font_weight = FontWeight(scale.font_weight as f32);
441 let rem = window.rem_size();
442 let size = rem * scale.font_size.0;
443 let run = style.to_run(text.len());
444 let line = window
445 .text_system()
446 .shape_line(text.clone(), size, &[run], None);
447 line.width.as_f32() / rem.as_f32() * 14.
449}
450
451impl RenderOnce for TextField {
452 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
453 let id = self.id.clone();
454 let (value, focus) = match &self.input {
455 FieldInput::Single(state) => (read_value(state, cx), Some(focus_handle(state, cx))),
456 FieldInput::Multi(state) => (read_value(state, cx), Some(focus_handle(state, cx))),
457 FieldInput::Static(value) => (value.clone(), None),
458 };
459 let multi_line = matches!(self.input, FieldInput::Multi(_));
460 let preview = self.preview;
461 let disabled = self.disabled || preview.is_some_and(InteractionPreview::is_disabled);
462 let focused = match preview {
463 Some(p) => p.is_focused() || p.is_pressed(),
464 None => !disabled && focus.as_ref().is_some_and(|f| f.is_focused(window)),
465 };
466 let hovered_preview = preview.is_some_and(InteractionPreview::is_hovered);
467 let populated = !value.is_empty();
468 let has_label = self.label.is_some();
469 let floating = focused || populated || self.float_label || !has_label;
470 let variant = self.variant;
471 let filled = variant == TextFieldVariant::Filled;
472 let leading = self.leading.is_some();
473 let density = self.density;
474 let height = density.height();
475
476 let float_t = motion::token_value(
478 (id.clone(), "label-float"),
479 if floating { 1. } else { 0. },
480 MotionToken::FastSpatial,
481 window,
482 cx,
483 );
484 let focus_t = motion::token_value(
485 (id.clone(), "focus"),
486 if focused { 1. } else { 0. },
487 MotionToken::FastEffects,
488 window,
489 cx,
490 )
491 .clamp(0., 1.);
492
493 let base = FieldState {
494 focused: false,
495 hovered: hovered_preview,
496 error: self.error,
497 disabled,
498 };
499 let rest = field_paint(variant, base);
500 let active = field_paint(
501 variant,
502 FieldState {
503 focused: true,
504 ..base
505 },
506 );
507 let mix = |a: Rgba, b: Rgba| blend(a, b, focus_t);
508 let indicator = mix(rest.indicator, active.indicator);
509 let outline_width = if focused {
511 active.indicator_width
512 } else {
513 rest.indicator_width
514 };
515 let label_color = mix(rest.label, active.label);
516 let hover_paint = field_paint(
517 variant,
518 FieldState {
519 hovered: true,
520 ..base
521 },
522 );
523 let live_hover = preview.is_none() && !disabled && !focused;
524 let group = SharedString::new_static("material-text-field");
528
529 let mut container = div()
530 .id(id.clone())
531 .group(group.clone())
532 .relative()
533 .flex()
534 .w_full()
535 .min_h(rem(height))
536 .when(!multi_line, |this| this.h(rem(height)))
537 .when(multi_line, |this| this.items_start())
538 .when(!multi_line, |this| this.items_center())
539 .map(|this| match variant {
540 TextFieldVariant::Filled => this
541 .rounded_t(rem(RADIUS_EXTRA_SMALL.as_f32()))
542 .bg(rest.container),
543 TextFieldVariant::Outlined => this.rounded(rem(RADIUS_EXTRA_SMALL.as_f32())),
544 })
545 .when(!disabled, |this| this.cursor_text());
546 if let Some(focus) = focus.clone().filter(|_| !disabled && preview.is_none()) {
547 container = container.on_mouse_down(MouseButton::Left, move |_, window, cx| {
548 focus.focus(window, cx);
549 });
550 }
551
552 if filled {
554 container = container.child(
555 div()
556 .absolute()
557 .inset_0()
558 .rounded_t(rem(RADIUS_EXTRA_SMALL.as_f32()))
559 .bg(rest.state_layer)
560 .when(live_hover, |this| {
561 let layer = hover_paint.state_layer;
562 this.group_hover(group.clone(), move |s| s.bg(layer))
563 }),
564 );
565 container = container
568 .child(
569 div()
570 .debug_selector(|| "text-field-indicator".into())
571 .absolute()
572 .left_0()
573 .right_0()
574 .bottom_0()
575 .h(px(rest.indicator_width))
576 .bg(rest.indicator)
577 .when(live_hover, |this| {
578 let ink = hover_paint.indicator;
579 this.group_hover(group.clone(), move |s| s.bg(ink))
580 }),
581 )
582 .when(focus_t > 0., |this| {
583 this.child(
584 div()
585 .debug_selector(|| "text-field-focus-indicator".into())
586 .absolute()
587 .left_0()
588 .right_0()
589 .bottom_0()
590 .h(px(active.indicator_width))
591 .bg(active.indicator)
592 .opacity(focus_t),
593 )
594 });
595 } else {
596 let label_px = self
597 .label
598 .as_ref()
599 .map(|label| label_width(label, window))
600 .unwrap_or(0.);
601 let (start, width) = if has_label {
602 notch(label_px, float_t)
603 } else {
604 (0., 0.)
605 };
606 container = container.children(outline(
607 start,
608 width,
609 indicator,
610 outline_width,
611 live_hover.then_some((group.clone(), hover_paint.indicator)),
612 ));
613 }
614
615 let pad_left = if leading { ICON_PADDING } else { PADDING };
616 let pad_right = if self.trailing.is_some() {
617 ICON_PADDING
618 } else {
619 PADDING
620 };
621 let input_top = if filled && has_label {
622 FILLED_INPUT_TOP - density.lift()
623 } else {
624 (height - 24.) / 2.
625 };
626 let input_bottom = height - 24. - input_top;
627 let show_content = floating || !has_label;
628 let show_placeholder = !populated && show_content;
629
630 let icon_slot = |icon: AnyElement, ink: Rgba| {
631 div()
632 .flex()
633 .flex_shrink_0()
634 .items_center()
635 .justify_center()
636 .size(rem(ICON_SIZE))
637 .when(multi_line, |this| this.mt(rem((height - 24.) / 2.)))
638 .text_color(ink)
639 .child(icon)
640 };
641 let affix = |text: SharedString| {
642 type_style(div().flex_shrink_0(), TypeRole::BodyLarge)
643 .text_color(rest.placeholder)
644 .child(text)
645 };
646 let caret = Some(Hsla::from(active.caret));
649 match &self.input {
650 FieldInput::Single(state) => state.update(cx, |s, _| s.set_caret_color(caret)),
651 FieldInput::Multi(state) => state.update(cx, |s, _| s.set_caret_color(caret)),
652 FieldInput::Static(_) => {}
653 }
654 let editor: AnyElement = match &self.input {
655 FieldInput::Single(state) => Input::new(state)
656 .appearance(false)
657 .with_size(Size::XSmall)
658 .disabled(disabled)
659 .when_some(self.label.clone(), |this, label| this.aria_label(label))
660 .p_0()
661 .h(rem(24.))
662 .text_size(TypeRole::BodyLarge.scale().font_size)
663 .line_height(TypeRole::BodyLarge.scale().line_height)
664 .into_any_element(),
665 FieldInput::Multi(state) => Textarea::new(state)
666 .appearance(false)
667 .with_size(Size::XSmall)
668 .disabled(disabled)
669 .when_some(self.label.clone(), |this, label| this.aria_label(label))
670 .p_0()
671 .mx(px(-4.))
673 .text_size(TypeRole::BodyLarge.scale().font_size)
674 .line_height(TypeRole::BodyLarge.scale().line_height)
675 .into_any_element(),
676 FieldInput::Static(value) => div()
677 .flex_1()
678 .min_w_0()
679 .overflow_hidden()
680 .whitespace_nowrap()
681 .child(value.clone())
682 .into_any_element(),
683 };
684 let placeholder = self.placeholder.clone().filter(|_| show_placeholder);
685 let input_row = div()
686 .relative()
687 .flex()
688 .flex_1()
689 .min_w_0()
690 .when(!multi_line, |this| this.items_center().h(rem(24.)))
691 .when(multi_line, |this| this.items_start())
692 .gap(rem(2.))
693 .map(|this| type_style(this, TypeRole::BodyLarge))
694 .text_color(rest.input)
695 .children(self.prefix.clone().filter(|_| show_content).map(affix))
696 .child(
697 div()
698 .relative()
699 .flex_1()
700 .min_w_0()
701 .child(
704 div()
705 .when(disabled, |this| {
706 this.text_color(color(ColorRole::ON_SURFACE))
707 .opacity(state::DISABLED_CONTENT)
708 })
709 .child(editor),
710 )
711 .when_some(placeholder, |this, placeholder| {
712 this.child(
713 div()
714 .absolute()
715 .top_0()
716 .left_0()
717 .right_0()
718 .overflow_hidden()
719 .whitespace_nowrap()
720 .text_color(rest.placeholder)
721 .child(placeholder),
722 )
723 }),
724 )
725 .children(self.suffix.clone().filter(|_| show_content).map(affix));
726
727 container = container.child(
728 div()
729 .relative()
730 .flex()
731 .w_full()
732 .when(!multi_line, |this| this.items_center().h_full())
733 .when(multi_line, |this| this.items_start())
734 .pl(rem(pad_left))
735 .pr(rem(pad_right))
736 .gap(rem(ICON_GAP))
737 .children(self.leading.map(|icon| icon_slot(icon, rest.leading)))
738 .child(
739 div()
740 .flex()
741 .flex_1()
742 .min_w_0()
743 .pt(rem(input_top))
744 .pb(rem(input_bottom))
745 .child(input_row),
746 )
747 .children(self.trailing.map(|icon| {
748 icon_slot(icon, mix(rest.trailing, active.trailing)).when(live_hover, |this| {
749 let ink = hover_paint.trailing;
750 this.group_hover(group.clone(), move |s| s.text_color(ink))
751 })
752 })),
753 );
754
755 if let Some(label) = self.label.clone() {
756 let (left, top, size, line) =
757 density_label_geometry(variant, density, leading, float_t);
758 container = container.child(
759 div()
760 .debug_selector(|| "text-field-label".into())
761 .absolute()
762 .left(rem(left))
763 .top(rem(top))
764 .max_w(relative(1.))
765 .whitespace_nowrap()
766 .overflow_hidden()
767 .font_family(TypeRole::BodyLarge.scale().font_family)
768 .text_size(rem(size))
769 .line_height(rem(line))
770 .text_color(label_color)
771 .when(live_hover, |this| {
774 let ink = hover_paint.label;
775 this.group_hover(group.clone(), move |s| s.text_color(ink))
776 })
777 .child(label),
778 );
779 }
780
781 let counter = self
782 .max_length
783 .map(|max| SharedString::from(format!("{}/{max}", value.chars().count())));
784 let supporting_row = (self.supporting.is_some() || counter.is_some()).then(|| {
785 type_style(div(), TypeRole::BodySmall)
786 .flex()
787 .justify_between()
788 .gap(rem(16.))
789 .px(rem(PADDING))
790 .pt(rem(4.))
791 .text_color(rest.supporting)
792 .child(
793 div()
794 .debug_selector(|| "text-field-supporting".into())
795 .min_w_0()
796 .flex_1()
797 .children(self.supporting.clone()),
798 )
799 .children(counter.map(|counter| div().flex_shrink_0().child(counter)))
800 });
801
802 let mut outer = div()
803 .flex()
804 .flex_col()
805 .flex_shrink_0()
806 .w(rem(TEXT_FIELD_WIDTH))
807 .child(container)
808 .children(supporting_row);
809 outer.style().refine(&self.style);
810 outer
811 }
812}
813
814fn outline(
817 start: f32,
818 width: f32,
819 ink: Rgba,
820 weight: f32,
821 hover: Option<(SharedString, Rgba)>,
822) -> Vec<AnyElement> {
823 let radius = rem(RADIUS_EXTRA_SMALL.as_f32());
824 let weight = px(weight);
825 let outline_hover = hover.clone();
826 let hovering = move |el: Div| match outline_hover.clone() {
827 Some((group, hover)) => el.group_hover(group, move |s| s.border_color(hover)),
828 None => el,
829 };
830 let fill_hover = {
831 let hover = hover.clone();
832 move |el: Div| match hover.clone() {
833 Some((group, hover)) => el.group_hover(group, move |s| s.bg(hover)),
834 None => el,
835 }
836 };
837 let start_piece = div()
838 .debug_selector(|| "text-field-outline-start".into())
839 .absolute()
840 .top_0()
841 .bottom_0()
842 .left_0()
843 .border_l(weight)
844 .border_t(weight)
845 .border_b(weight)
846 .border_color(ink);
847 if width <= 0. {
848 return vec![
850 hovering(start_piece.right_0().border_r(weight).rounded(radius)).into_any_element(),
851 ];
852 }
853 vec![
854 hovering(start_piece.w(rem(start)).rounded_l(radius)).into_any_element(),
855 hovering(
856 div()
857 .debug_selector(|| "text-field-outline-end".into())
858 .absolute()
859 .top_0()
860 .bottom_0()
861 .right_0()
862 .left(rem(start + width))
863 .border_r(weight)
864 .border_t(weight)
865 .border_b(weight)
866 .rounded_r(radius)
867 .border_color(ink),
868 )
869 .into_any_element(),
870 fill_hover(
871 div()
872 .absolute()
873 .bottom_0()
874 .left(rem(start))
875 .w(rem(width))
876 .h(weight)
877 .bg(ink),
878 )
879 .into_any_element(),
880 ]
881}
882
883#[cfg(test)]
884#[path = "text_field_tests.rs"]
885mod tests;