Skip to main content

iced_shadcn/
input.rs

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}