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}