Skip to main content

egui_shadcn/
input.rs

1use crate::theme::Theme;
2use crate::tokens::{
3    ColorPalette, ControlSize, InputVariant as TokenInputVariant, input_tokens, mix,
4};
5use egui::{
6    Color32, CornerRadius, FontId, Painter, Rect, Response, Sense, Stroke, StrokeKind, TextEdit,
7    TextStyle, Ui, UiBuilder, Vec2, WidgetText, pos2, vec2,
8};
9use log::trace;
10use std::fmt::Debug;
11use std::hash::Hash;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
14pub enum InputVariant {
15    Classic,
16
17    #[default]
18    Surface,
19
20    Soft,
21}
22
23impl From<InputVariant> for TokenInputVariant {
24    fn from(variant: InputVariant) -> Self {
25        match variant {
26            InputVariant::Surface => TokenInputVariant::Surface,
27            InputVariant::Classic => TokenInputVariant::Classic,
28            InputVariant::Soft => TokenInputVariant::Soft,
29        }
30    }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
34pub enum InputSize {
35    Size1,
36
37    #[default]
38    Size2,
39
40    Size3,
41}
42
43impl InputSize {
44    pub fn height(self) -> f32 {
45        match self {
46            InputSize::Size1 => 24.0,
47            InputSize::Size2 => 32.0,
48            InputSize::Size3 => 40.0,
49        }
50    }
51
52    pub fn font_size(self) -> f32 {
53        match self {
54            InputSize::Size1 => 12.0,
55            InputSize::Size2 => 14.0,
56            InputSize::Size3 => 16.0,
57        }
58    }
59
60    pub fn font(self) -> FontId {
61        FontId::proportional(self.font_size())
62    }
63
64    pub fn padding(self) -> Vec2 {
65        match self {
66            InputSize::Size1 => vec2(6.0, 4.0),
67            InputSize::Size2 => vec2(8.0, 6.0),
68            InputSize::Size3 => vec2(12.0, 8.0),
69        }
70    }
71
72    pub fn rounding(self) -> CornerRadius {
73        match self {
74            InputSize::Size1 => CornerRadius::same(4),
75            InputSize::Size2 => CornerRadius::same(6),
76            InputSize::Size3 => CornerRadius::same(8),
77        }
78    }
79
80    pub fn slot_gap(self) -> f32 {
81        match self {
82            InputSize::Size1 => 4.0,
83            InputSize::Size2 => 6.0,
84            InputSize::Size3 => 8.0,
85        }
86    }
87
88    pub fn slot_icon_size(self) -> f32 {
89        match self {
90            InputSize::Size1 => 12.0,
91            InputSize::Size2 => 14.0,
92            InputSize::Size3 => 16.0,
93        }
94    }
95}
96
97impl From<ControlSize> for InputSize {
98    fn from(size: ControlSize) -> Self {
99        match size {
100            ControlSize::Sm | ControlSize::IconSm => InputSize::Size1,
101            ControlSize::Md | ControlSize::Icon => InputSize::Size2,
102            ControlSize::Lg | ControlSize::IconLg => InputSize::Size3,
103        }
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
108pub enum InputRadius {
109    None,
110
111    Small,
112
113    #[default]
114    Medium,
115
116    Large,
117
118    Full,
119}
120
121impl InputRadius {
122    pub fn corner_radius(self) -> CornerRadius {
123        match self {
124            InputRadius::None => CornerRadius::same(0),
125            InputRadius::Small => CornerRadius::same(4),
126            InputRadius::Medium => CornerRadius::same(6),
127            InputRadius::Large => CornerRadius::same(8),
128            InputRadius::Full => CornerRadius::same(255),
129        }
130    }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
134pub enum InputType {
135    #[default]
136    Text,
137
138    Password,
139
140    Email,
141
142    Number,
143
144    Search,
145
146    Tel,
147
148    Url,
149}
150
151impl InputType {
152    pub fn is_password(self) -> bool {
153        matches!(self, InputType::Password)
154    }
155}
156
157#[derive(Clone, Debug)]
158pub struct InputStyle {
159    pub bg: Color32,
160
161    pub bg_hover: Color32,
162
163    pub bg_focus: Color32,
164
165    pub border: Color32,
166
167    pub border_hover: Color32,
168
169    pub border_focus: Color32,
170
171    pub text_color: Color32,
172
173    pub placeholder_color: Color32,
174
175    pub selection_bg: Color32,
176
177    pub selection_fg: Color32,
178
179    pub focus_ring: Color32,
180
181    pub focus_ring_width: f32,
182
183    pub invalid_border: Color32,
184
185    pub invalid_ring: Color32,
186
187    pub disabled_opacity: f32,
188
189    pub rounding: CornerRadius,
190
191    pub slot_color: Color32,
192}
193
194impl InputStyle {
195    pub fn from_palette(palette: &ColorPalette, variant: InputVariant) -> Self {
196        match variant {
197            InputVariant::Surface => Self {
198                bg: Color32::TRANSPARENT,
199                bg_hover: Color32::TRANSPARENT,
200                bg_focus: Color32::TRANSPARENT,
201                border: palette.input,
202                border_hover: palette.input,
203                border_focus: palette.ring,
204                text_color: palette.foreground,
205                placeholder_color: palette.muted_foreground,
206                selection_bg: palette.primary,
207                selection_fg: palette.primary_foreground,
208
209                focus_ring: Color32::from_rgba_unmultiplied(
210                    palette.ring.r(),
211                    palette.ring.g(),
212                    palette.ring.b(),
213                    128,
214                ),
215                focus_ring_width: 3.0,
216                invalid_border: palette.destructive,
217                invalid_ring: Color32::from_rgba_unmultiplied(
218                    palette.destructive.r(),
219                    palette.destructive.g(),
220                    palette.destructive.b(),
221                    51,
222                ),
223                disabled_opacity: 0.5,
224                rounding: CornerRadius::same(6),
225                slot_color: palette.muted_foreground,
226            },
227            InputVariant::Classic => Self {
228                bg: palette.background,
229                bg_hover: palette.background,
230                bg_focus: palette.background,
231                border: palette.input,
232                border_hover: palette.input,
233                border_focus: palette.ring,
234                text_color: palette.foreground,
235                placeholder_color: palette.muted_foreground,
236                selection_bg: palette.primary,
237                selection_fg: palette.primary_foreground,
238                focus_ring: Color32::from_rgba_unmultiplied(
239                    palette.ring.r(),
240                    palette.ring.g(),
241                    palette.ring.b(),
242                    128,
243                ),
244                focus_ring_width: 3.0,
245                invalid_border: palette.destructive,
246                invalid_ring: Color32::from_rgba_unmultiplied(
247                    palette.destructive.r(),
248                    palette.destructive.g(),
249                    palette.destructive.b(),
250                    51,
251                ),
252                disabled_opacity: 0.5,
253                rounding: CornerRadius::same(6),
254                slot_color: palette.muted_foreground,
255            },
256            InputVariant::Soft => Self {
257                bg: Color32::from_rgba_unmultiplied(
258                    palette.primary.r(),
259                    palette.primary.g(),
260                    palette.primary.b(),
261                    30,
262                ),
263                bg_hover: Color32::from_rgba_unmultiplied(
264                    palette.primary.r(),
265                    palette.primary.g(),
266                    palette.primary.b(),
267                    40,
268                ),
269                bg_focus: Color32::from_rgba_unmultiplied(
270                    palette.primary.r(),
271                    palette.primary.g(),
272                    palette.primary.b(),
273                    50,
274                ),
275                border: Color32::TRANSPARENT,
276                border_hover: Color32::TRANSPARENT,
277                border_focus: Color32::TRANSPARENT,
278                text_color: palette.foreground,
279                placeholder_color: palette.muted_foreground,
280                selection_bg: palette.primary,
281                selection_fg: palette.primary_foreground,
282                focus_ring: Color32::from_rgba_unmultiplied(
283                    palette.ring.r(),
284                    palette.ring.g(),
285                    palette.ring.b(),
286                    128,
287                ),
288                focus_ring_width: 3.0,
289                invalid_border: palette.destructive,
290                invalid_ring: Color32::from_rgba_unmultiplied(
291                    palette.destructive.r(),
292                    palette.destructive.g(),
293                    palette.destructive.b(),
294                    51,
295                ),
296                disabled_opacity: 0.5,
297                rounding: CornerRadius::same(6),
298                slot_color: palette.foreground,
299            },
300        }
301    }
302
303    pub fn from_palette_with_accent(
304        palette: &ColorPalette,
305        variant: InputVariant,
306        accent: Color32,
307    ) -> Self {
308        let mut style = Self::from_palette(palette, variant);
309        match variant {
310            InputVariant::Soft => {
311                style.bg = Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 30);
312                style.bg_hover =
313                    Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 40);
314                style.bg_focus =
315                    Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 50);
316                style.selection_bg = accent;
317                style.selection_fg = palette.primary_foreground;
318                style.focus_ring =
319                    Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 128);
320            }
321            InputVariant::Surface | InputVariant::Classic => {
322                style.border_focus = accent;
323                style.focus_ring =
324                    Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 128);
325                style.selection_bg = accent;
326                style.selection_fg = palette.primary_foreground;
327            }
328        }
329        style
330    }
331
332    pub fn high_contrast(mut self) -> Self {
333        self.text_color = Color32::WHITE;
334        self.bg = mix(self.bg, Color32::WHITE, 0.1);
335        self.bg_hover = mix(self.bg_hover, Color32::WHITE, 0.1);
336        self
337    }
338}
339
340impl Default for InputStyle {
341    fn default() -> Self {
342        Self::from_palette(&ColorPalette::default(), InputVariant::Surface)
343    }
344}
345
346pub type SlotFn<'a> = &'a dyn Fn(&Painter, Rect, Color32);
347
348type BoxedSlotFn<'a> = Option<Box<dyn Fn(&Painter, Rect, Color32) + 'a>>;
349
350pub struct InputProps<'a, Id>
351where
352    Id: Hash + Debug,
353{
354    pub id_source: Id,
355
356    pub value: &'a mut String,
357
358    pub placeholder: &'a str,
359
360    pub variant: InputVariant,
361
362    pub size: InputSize,
363
364    pub radius: InputRadius,
365
366    pub input_type: InputType,
367
368    pub enabled: bool,
369
370    pub read_only: bool,
371
372    pub is_invalid: bool,
373
374    pub max_len: Option<usize>,
375
376    pub width: Option<f32>,
377
378    pub style: Option<InputStyle>,
379
380    pub accent_color: Option<Color32>,
381
382    pub high_contrast: bool,
383
384    #[allow(clippy::type_complexity)]
385    pub left_slot: BoxedSlotFn<'a>,
386
387    #[allow(clippy::type_complexity)]
388    pub right_slot: BoxedSlotFn<'a>,
389}
390
391impl<'a, Id: Hash + Debug> InputProps<'a, Id> {
392    pub fn new(id_source: Id, value: &'a mut String) -> Self {
393        Self {
394            id_source,
395            value,
396            placeholder: "",
397            variant: InputVariant::Surface,
398            size: InputSize::Size2,
399            radius: InputRadius::Medium,
400            input_type: InputType::Text,
401            enabled: true,
402            read_only: false,
403            is_invalid: false,
404            max_len: None,
405            width: None,
406            style: None,
407            accent_color: None,
408            high_contrast: false,
409            left_slot: None,
410            right_slot: None,
411        }
412    }
413
414    pub fn placeholder(mut self, placeholder: &'a str) -> Self {
415        self.placeholder = placeholder;
416        self
417    }
418
419    pub fn variant(mut self, variant: InputVariant) -> Self {
420        self.variant = variant;
421        self
422    }
423
424    pub fn size(mut self, size: InputSize) -> Self {
425        self.size = size;
426        self
427    }
428
429    pub fn radius(mut self, radius: InputRadius) -> Self {
430        self.radius = radius;
431        self
432    }
433
434    pub fn input_type(mut self, input_type: InputType) -> Self {
435        self.input_type = input_type;
436        self
437    }
438
439    pub fn enabled(mut self, enabled: bool) -> Self {
440        self.enabled = enabled;
441        self
442    }
443
444    pub fn read_only(mut self, read_only: bool) -> Self {
445        self.read_only = read_only;
446        self
447    }
448
449    pub fn invalid(mut self, is_invalid: bool) -> Self {
450        self.is_invalid = is_invalid;
451        self
452    }
453
454    pub fn max_len(mut self, max_len: usize) -> Self {
455        self.max_len = Some(max_len);
456        self
457    }
458
459    pub fn width(mut self, width: f32) -> Self {
460        self.width = Some(width);
461        self
462    }
463
464    pub fn style(mut self, style: InputStyle) -> Self {
465        self.style = Some(style);
466        self
467    }
468
469    pub fn accent_color(mut self, color: Color32) -> Self {
470        self.accent_color = Some(color);
471        self
472    }
473
474    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
475        self.high_contrast = high_contrast;
476        self
477    }
478
479    pub fn left_slot<F>(mut self, slot_fn: F) -> Self
480    where
481        F: Fn(&Painter, Rect, Color32) + 'a,
482    {
483        self.left_slot = Some(Box::new(slot_fn));
484        self
485    }
486
487    pub fn right_slot<F>(mut self, slot_fn: F) -> Self
488    where
489        F: Fn(&Painter, Rect, Color32) + 'a,
490    {
491        self.right_slot = Some(Box::new(slot_fn));
492        self
493    }
494}
495
496pub struct Input<'a, Id>
497where
498    Id: Hash + Debug,
499{
500    pub id_source: Id,
501    pub placeholder: &'a str,
502    pub variant: InputVariant,
503    pub size: InputSize,
504    pub radius: InputRadius,
505    pub input_type: InputType,
506    pub enabled: bool,
507    pub read_only: bool,
508    pub is_invalid: bool,
509    pub max_len: Option<usize>,
510    pub width: Option<f32>,
511    pub style: Option<InputStyle>,
512    pub accent_color: Option<Color32>,
513    pub high_contrast: bool,
514    #[allow(clippy::type_complexity)]
515    pub left_slot: BoxedSlotFn<'a>,
516    #[allow(clippy::type_complexity)]
517    pub right_slot: BoxedSlotFn<'a>,
518}
519
520impl<'a, Id: Hash + Debug> Input<'a, Id> {
521    pub fn new(id_source: Id) -> Input<'static, Id> {
522        Input {
523            id_source,
524            placeholder: "",
525            variant: InputVariant::Surface,
526            size: InputSize::Size2,
527            radius: InputRadius::Medium,
528            input_type: InputType::Text,
529            enabled: true,
530            read_only: false,
531            is_invalid: false,
532            max_len: None,
533            width: None,
534            style: None,
535            accent_color: None,
536            high_contrast: false,
537            left_slot: None,
538            right_slot: None,
539        }
540    }
541
542    pub fn placeholder(mut self, placeholder: &'a str) -> Self {
543        self.placeholder = placeholder;
544        self
545    }
546
547    pub fn variant(mut self, variant: InputVariant) -> Self {
548        self.variant = variant;
549        self
550    }
551
552    pub fn size(mut self, size: InputSize) -> Self {
553        self.size = size;
554        self
555    }
556
557    pub fn radius(mut self, radius: InputRadius) -> Self {
558        self.radius = radius;
559        self
560    }
561
562    pub fn input_type(mut self, input_type: InputType) -> Self {
563        self.input_type = input_type;
564        self
565    }
566
567    pub fn enabled(mut self, enabled: bool) -> Self {
568        self.enabled = enabled;
569        self
570    }
571
572    pub fn read_only(mut self, read_only: bool) -> Self {
573        self.read_only = read_only;
574        self
575    }
576
577    pub fn invalid(mut self, is_invalid: bool) -> Self {
578        self.is_invalid = is_invalid;
579        self
580    }
581
582    pub fn max_len(mut self, max_len: usize) -> Self {
583        self.max_len = Some(max_len);
584        self
585    }
586
587    pub fn width(mut self, width: f32) -> Self {
588        self.width = Some(width);
589        self
590    }
591
592    pub fn style(mut self, style: InputStyle) -> Self {
593        self.style = Some(style);
594        self
595    }
596
597    pub fn accent_color(mut self, color: Color32) -> Self {
598        self.accent_color = Some(color);
599        self
600    }
601
602    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
603        self.high_contrast = high_contrast;
604        self
605    }
606
607    pub fn left_slot<F>(mut self, slot_fn: F) -> Self
608    where
609        F: Fn(&Painter, Rect, Color32) + 'a,
610    {
611        self.left_slot = Some(Box::new(slot_fn));
612        self
613    }
614
615    pub fn right_slot<F>(mut self, slot_fn: F) -> Self
616    where
617        F: Fn(&Painter, Rect, Color32) + 'a,
618    {
619        self.right_slot = Some(Box::new(slot_fn));
620        self
621    }
622
623    pub fn show(self, ui: &mut Ui, theme: &Theme, value: &mut String) -> Response {
624        let props = InputProps {
625            id_source: self.id_source,
626            value,
627            placeholder: self.placeholder,
628            variant: self.variant,
629            size: self.size,
630            radius: self.radius,
631            input_type: self.input_type,
632            enabled: self.enabled,
633            read_only: self.read_only,
634            is_invalid: self.is_invalid,
635            max_len: self.max_len,
636            width: self.width,
637            style: self.style,
638            accent_color: self.accent_color,
639            high_contrast: self.high_contrast,
640            left_slot: self.left_slot,
641            right_slot: self.right_slot,
642        };
643        input_with_props(ui, theme, props)
644    }
645}
646
647#[derive(Clone, Debug)]
648pub struct InputConfig {
649    pub variant: TokenInputVariant,
650    pub size: InputSize,
651    pub is_invalid: bool,
652}
653
654impl Default for InputConfig {
655    fn default() -> Self {
656        Self {
657            variant: TokenInputVariant::Surface,
658            size: InputSize::Size2,
659            is_invalid: false,
660        }
661    }
662}
663
664pub fn resolve_input_style(palette: &ColorPalette, config: &InputConfig) -> InputStyle {
665    let variant = match config.variant {
666        TokenInputVariant::Surface => InputVariant::Surface,
667        TokenInputVariant::Classic => InputVariant::Classic,
668        TokenInputVariant::Soft => InputVariant::Soft,
669    };
670    InputStyle::from_palette(palette, variant)
671}
672
673pub fn input_with_props<Id>(ui: &mut Ui, theme: &Theme, props: InputProps<'_, Id>) -> Response
674where
675    Id: Hash + Debug,
676{
677    trace!(
678        "Rendering input variant={:?} size={:?} type={:?} invalid={} enabled={} read_only={}",
679        props.variant,
680        props.size,
681        props.input_type,
682        props.is_invalid,
683        props.enabled,
684        props.read_only
685    );
686
687    let apply_opacity = |color: Color32, opacity: f32| -> Color32 {
688        Color32::from_rgba_unmultiplied(
689            color.r(),
690            color.g(),
691            color.b(),
692            (color.a() as f32 * opacity) as u8,
693        )
694    };
695
696    let mut style = props.style.clone().unwrap_or_else(|| {
697        if let Some(accent) = props.accent_color {
698            InputStyle::from_palette_with_accent(&theme.palette, props.variant, accent)
699        } else {
700            InputStyle::from_palette(&theme.palette, props.variant)
701        }
702    });
703
704    if props.high_contrast {
705        style = style.high_contrast();
706    }
707
708    style.rounding = props.radius.corner_radius();
709
710    let effectively_disabled = !props.enabled || props.read_only;
711
712    let height = props.size.height();
713    let width = props.width.unwrap_or(200.0);
714    let padding = props.size.padding();
715    let slot_gap = props.size.slot_gap();
716    let slot_icon_size = props.size.slot_icon_size();
717
718    let slot_width = |slot: &BoxedSlotFn<'_>| -> f32 {
719        if slot.is_some() {
720            slot_icon_size + slot_gap * 2.0
721        } else {
722            0.0
723        }
724    };
725
726    let left_slot_width = slot_width(&props.left_slot);
727    let right_slot_width = slot_width(&props.right_slot);
728
729    let id = ui.make_persistent_id(&props.id_source);
730    let desired_size = vec2(width, height);
731
732    let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
733
734    let edit_id = id.with("edit");
735    let has_focus = response.has_focus() || ui.memory(|m| m.has_focus(edit_id));
736
737    let bg_color = if effectively_disabled {
738        apply_opacity(style.bg, style.disabled_opacity)
739    } else {
740        match (has_focus, response.hovered()) {
741            (true, _) => style.bg_focus,
742            (false, true) => style.bg_hover,
743            (false, false) => style.bg,
744        }
745    };
746
747    let border_color = match (
748        props.is_invalid,
749        has_focus,
750        response.hovered() && !effectively_disabled,
751    ) {
752        (true, _, _) => style.invalid_border,
753        (false, true, _) => style.border_focus,
754        (false, false, true) => style.border_hover,
755        _ => style.border,
756    };
757
758    {
759        let painter = ui.painter();
760        painter.rect_filled(rect, style.rounding, bg_color);
761
762        if border_color != Color32::TRANSPARENT {
763            painter.rect_stroke(
764                rect,
765                style.rounding,
766                Stroke::new(1.0, border_color),
767                StrokeKind::Inside,
768            );
769        }
770
771        if has_focus && !effectively_disabled {
772            let ring_color = if props.is_invalid {
773                style.invalid_ring
774            } else {
775                style.focus_ring
776            };
777            painter.rect_stroke(
778                rect,
779                style.rounding,
780                Stroke::new(style.focus_ring_width, ring_color),
781                StrokeKind::Outside,
782            );
783        }
784    }
785
786    let slot_color = |color: Color32| -> Color32 {
787        if effectively_disabled {
788            apply_opacity(color, style.disabled_opacity)
789        } else {
790            color
791        }
792    };
793
794    let paint_slot = |slot_fn: &BoxedSlotFn<'_>, align_left: bool| {
795        if let Some(slot_fn) = slot_fn.as_ref() {
796            let x = if align_left {
797                rect.left() + slot_gap
798            } else {
799                rect.right() - slot_gap - slot_icon_size
800            };
801            let slot_rect = Rect::from_min_size(
802                pos2(x, rect.top() + (height - slot_icon_size) / 2.0),
803                vec2(slot_icon_size, slot_icon_size),
804            );
805            slot_fn(ui.painter(), slot_rect, slot_color(style.slot_color));
806        }
807    };
808
809    paint_slot(&props.left_slot, true);
810    paint_slot(&props.right_slot, false);
811
812    let inner_rect = Rect::from_min_max(
813        pos2(
814            rect.left() + padding.x + left_slot_width,
815            rect.top() + padding.y,
816        ),
817        pos2(
818            rect.right() - padding.x - right_slot_width,
819            rect.bottom() - padding.y,
820        ),
821    );
822
823    let text_color = if effectively_disabled {
824        apply_opacity(style.text_color, 0.6)
825    } else {
826        style.text_color
827    };
828
829    let placeholder_colored: WidgetText = props.placeholder.into();
830    let placeholder_colored = placeholder_colored.color(style.placeholder_color);
831
832    let token_variant = TokenInputVariant::from(props.variant);
833    let tokens = input_tokens(&theme.palette, token_variant);
834
835    let vertical_margin = (inner_rect.height() / 2.0) - (props.size.font_size() * 0.54);
836
837    let response = ui.scope_builder(UiBuilder::new().max_rect(inner_rect), |inner_ui| {
838        inner_ui.set_clip_rect(inner_rect);
839
840        let mut inner_style = inner_ui.style().as_ref().clone();
841        inner_style
842            .text_styles
843            .insert(TextStyle::Body, props.size.font());
844        inner_style.visuals.selection.bg_fill = style.selection_bg;
845        inner_style.visuals.selection.stroke = Stroke::new(1.0, style.selection_fg);
846        inner_style.visuals.override_text_color = Some(text_color);
847        inner_style.visuals.extreme_bg_color = tokens.idle.bg_fill;
848
849        for visuals in [
850            &mut inner_style.visuals.widgets.inactive,
851            &mut inner_style.visuals.widgets.hovered,
852            &mut inner_style.visuals.widgets.active,
853        ] {
854            visuals.bg_fill = Color32::TRANSPARENT;
855            visuals.weak_bg_fill = Color32::TRANSPARENT;
856            visuals.bg_stroke = Stroke::NONE;
857        }
858
859        inner_ui.set_style(inner_style);
860
861        let mut edit = TextEdit::singleline(props.value)
862            .id(edit_id)
863            .hint_text(placeholder_colored)
864            .text_color(text_color)
865            .frame(false)
866            .margin(vec2(0.0, vertical_margin))
867            .desired_width(inner_rect.width());
868
869        if props.input_type.is_password() {
870            edit = edit.password(true);
871        }
872
873        if let Some(limit) = props.max_len {
874            edit = edit.char_limit(limit);
875        }
876
877        if props.read_only {
878            edit = edit.interactive(false);
879        }
880
881        inner_ui.add_enabled(props.enabled, edit)
882    });
883
884    if response.inner.clicked_elsewhere()
885        && rect.contains(ui.ctx().pointer_hover_pos().unwrap_or_default())
886    {
887        ui.memory_mut(|m| m.request_focus(edit_id));
888    }
889
890    response.inner
891}
892
893pub fn input(ui: &mut Ui, theme: &Theme, value: &mut String) -> Response {
894    input_with_config(ui, theme, value, "input", InputConfig::default())
895}
896
897pub fn input_with_config<Id: Hash + Debug>(
898    ui: &mut Ui,
899    theme: &Theme,
900    value: &mut String,
901    id_source: Id,
902    config: InputConfig,
903) -> Response {
904    let variant = match config.variant {
905        TokenInputVariant::Surface => InputVariant::Surface,
906        TokenInputVariant::Classic => InputVariant::Classic,
907        TokenInputVariant::Soft => InputVariant::Soft,
908    };
909
910    let props = InputProps::new(id_source, value)
911        .variant(variant)
912        .size(config.size)
913        .invalid(config.is_invalid);
914
915    input_with_props(ui, theme, props)
916}