1use iced::Background;
2use iced::border::Border;
3use iced::widget::text_input;
4
5use crate::button::ButtonRadius;
6use crate::theme::Theme;
7use crate::tokens::{AccentColor, accent_color, accent_soft, accent_text};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum InputSize {
11 Size1,
12 Size2,
13 Size3,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum InputVariant {
18 Classic,
19 Surface,
20 Soft,
21 Ghost,
22}
23
24#[derive(Clone, Copy, Debug)]
25pub struct InputProps {
26 pub size: InputSize,
27 pub variant: InputVariant,
28 pub color: AccentColor,
29 pub radius: Option<ButtonRadius>,
30 pub disabled: bool,
31 pub read_only: bool,
32}
33
34impl Default for InputProps {
35 fn default() -> Self {
36 Self {
37 size: InputSize::Size2,
38 variant: InputVariant::Surface,
39 color: AccentColor::Gray,
40 radius: None,
41 disabled: false,
42 read_only: false,
43 }
44 }
45}
46
47impl InputProps {
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn size(mut self, size: InputSize) -> Self {
53 self.size = size;
54 self
55 }
56
57 pub fn variant(mut self, variant: InputVariant) -> Self {
58 self.variant = variant;
59 self
60 }
61
62 pub fn color(mut self, color: AccentColor) -> Self {
63 self.color = color;
64 self
65 }
66
67 pub fn radius(mut self, radius: ButtonRadius) -> Self {
68 self.radius = Some(radius);
69 self
70 }
71
72 pub fn disabled(mut self, disabled: bool) -> Self {
73 self.disabled = disabled;
74 self
75 }
76
77 pub fn read_only(mut self, read_only: bool) -> Self {
78 self.read_only = read_only;
79 self
80 }
81}
82
83impl InputSize {
84 fn padding(self, theme: &Theme) -> [f32; 2] {
85 match self {
86 InputSize::Size1 => [
87 theme.styles.input.size1_padding_y,
88 theme.styles.input.size1_padding_x,
89 ],
90 InputSize::Size2 => [
91 theme.styles.input.size2_padding_y,
92 theme.styles.input.size2_padding_x,
93 ],
94 InputSize::Size3 => [
95 theme.styles.input.size3_padding_y,
96 theme.styles.input.size3_padding_x,
97 ],
98 }
99 }
100
101 fn text_size(self) -> u32 {
102 match self {
103 InputSize::Size1 => 14,
104 InputSize::Size2 => 14,
105 InputSize::Size3 => 16,
106 }
107 }
108}
109
110pub fn input<'a, Message: Clone + 'a, F>(
111 value: &'a str,
112 placeholder: &'a str,
113 on_input: Option<F>,
114 props: InputProps,
115 theme: &Theme,
116) -> text_input::TextInput<'a, Message>
117where
118 F: Fn(String) -> Message + 'a,
119{
120 let theme = theme.clone();
121 let mut widget = text_input::TextInput::new(placeholder, value)
122 .padding(props.size.padding(&theme))
123 .size(props.size.text_size())
124 .style(move |_iced_theme, status| input_style(&theme, props, status));
125
126 if let Some(on_input) = on_input {
127 if props.disabled {
128 widget = widget.on_input_maybe(None::<fn(String) -> Message>);
129 } else {
130 widget = widget.on_input(on_input);
131 }
132 } else {
133 widget = widget.on_input_maybe(None::<fn(String) -> Message>);
134 }
135
136 widget
137}
138
139fn input_radius(theme: &Theme, props: InputProps) -> f32 {
140 match props.radius {
141 Some(ButtonRadius::None) => 0.0,
142 Some(ButtonRadius::Small) => theme.radius.sm,
143 Some(ButtonRadius::Medium) => theme.radius.md,
144 Some(ButtonRadius::Large) => theme.radius.lg,
145 Some(ButtonRadius::Full) => 9999.0,
146 None => theme.radius.sm,
147 }
148}
149
150fn input_style(theme: &Theme, props: InputProps, status: text_input::Status) -> text_input::Style {
151 let palette = theme.palette;
152 let radius = input_radius(theme, props);
153 let accent = accent_color(&palette, props.color);
154 let text_color = accent_text(&palette, props.color);
155 let soft_bg = accent_soft(&palette, props.color);
156
157 let mut border = Border {
158 radius: radius.into(),
159 width: theme.styles.input.border_width,
160 color: palette.border,
161 };
162 let mut background = match props.variant {
163 InputVariant::Classic | InputVariant::Surface => Background::Color(palette.background),
164 InputVariant::Soft => Background::Color(soft_bg),
165 InputVariant::Ghost => Background::Color(iced::Color::TRANSPARENT),
166 };
167 let mut value = match props.variant {
168 InputVariant::Soft => text_color,
169 InputVariant::Ghost => palette.foreground,
170 _ => palette.foreground,
171 };
172 let mut placeholder = match props.variant {
173 InputVariant::Soft => text_color,
174 InputVariant::Ghost => palette.muted_foreground,
175 _ => palette.muted_foreground,
176 };
177
178 if matches!(props.variant, InputVariant::Ghost) {
179 border.width = 0.0;
180 border.color = iced::Color::TRANSPARENT;
181 }
182
183 match status {
184 text_input::Status::Hovered => {
185 if !matches!(props.variant, InputVariant::Ghost) {
186 border.color = palette.ring;
187 }
188 }
189 text_input::Status::Focused { .. } => {
190 if !matches!(props.variant, InputVariant::Ghost) {
191 border.color = palette.ring;
192 border.width = theme.styles.input.focused_border_width;
193 }
194 }
195 text_input::Status::Disabled => {
196 background = Background::Color(palette.muted);
197 value = palette.muted_foreground;
198 placeholder = palette.muted_foreground;
199 }
200 text_input::Status::Active => {}
201 }
202
203 text_input::Style {
204 background,
205 border,
206 icon: palette.muted_foreground,
207 placeholder,
208 value,
209 selection: accent,
210 }
211}