iced_aw/style/
number_input.rs1use super::{Status, StyleFn};
6use iced_core::{Background, Color, Theme};
7use iced_widget::{container, text, text_input};
8
9#[derive(Clone, Copy, Debug)]
11pub struct Style {
12 pub button_background: Option<Background>,
14 pub icon_color: Color,
16}
17
18impl Default for Style {
19 fn default() -> Self {
20 Self {
21 button_background: None,
22 icon_color: Color::BLACK,
23 }
24 }
25}
26
27pub trait Catalog {
29 type Class<'a>;
31
32 fn default<'a>() -> Self::Class<'a>;
34
35 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
37}
38
39impl Catalog for Theme {
40 type Class<'a> = StyleFn<'a, Self, Style>;
41
42 fn default<'a>() -> Self::Class<'a> {
43 Box::new(primary)
44 }
45
46 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
47 class(self, status)
48 }
49}
50
51pub trait ExtendedCatalog:
53 text_input::Catalog + container::Catalog + text::Catalog + self::Catalog
54{
55 #[must_use]
57 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
58 <Self as text_input::Catalog>::default()
59 }
60
61 fn style(&self, class: &<Self as self::Catalog>::Class<'_>, status: Status) -> Style;
63}
64
65impl ExtendedCatalog for Theme {
66 fn style(&self, class: &<Self as self::Catalog>::Class<'_>, status: Status) -> Style {
67 class(self, status)
68 }
69}
70
71#[must_use]
73pub fn primary(theme: &Theme, status: Status) -> Style {
74 let palette = theme.extended_palette();
75 let base = Style {
76 button_background: Some(palette.primary.strong.color.into()),
77 icon_color: palette.primary.strong.text,
78 };
79
80 match status {
81 Status::Disabled => Style {
82 button_background: base.button_background.map(|bg| match bg {
83 Background::Color(color) => Background::Color(Color {
84 a: color.a * 0.5,
85 ..color
86 }),
87 Background::Gradient(grad) => Background::Gradient(grad),
88 }),
89 icon_color: Color {
90 a: base.icon_color.a * 0.5,
91 ..base.icon_color
92 },
93 },
94 _ => base,
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use iced_core::Theme;
102
103 #[test]
104 fn style_default() {
105 let style = Style::default();
106 assert!(style.button_background.is_none());
107 assert_eq!(style.icon_color, Color::BLACK);
108 }
109
110 #[test]
111 fn primary_theme_active() {
112 let theme = Theme::TokyoNight;
113 let style = primary(&theme, Status::Active);
114
115 assert!(style.button_background.is_some());
116 #[allow(clippy::panic)]
117 if let Some(Background::Color(_)) = style.button_background {
118 } else {
120 panic!("Expected button_background to be Some(Background::Color)");
121 }
122 }
123
124 #[test]
125 fn primary_theme_hovered() {
126 let theme = Theme::TokyoNight;
127 let style = primary(&theme, Status::Hovered);
128
129 assert!(style.button_background.is_some());
130 }
131
132 #[test]
133 fn primary_theme_focused() {
134 let theme = Theme::TokyoNight;
135 let style = primary(&theme, Status::Focused);
136
137 assert!(style.button_background.is_some());
138 }
139
140 #[test]
141 fn primary_theme_selected() {
142 let theme = Theme::TokyoNight;
143 let style = primary(&theme, Status::Selected);
144
145 assert!(style.button_background.is_some());
146 }
147
148 #[test]
149 fn primary_theme_disabled() {
150 let theme = Theme::TokyoNight;
151 let style = primary(&theme, Status::Disabled);
152
153 assert!(style.button_background.is_some());
154 }
155
156 #[test]
157 fn disabled_reduces_alpha() {
158 let theme = Theme::TokyoNight;
159 let base_style = primary(&theme, Status::Active);
160 let disabled_style = primary(&theme, Status::Disabled);
161
162 assert!(
164 disabled_style.icon_color.a <= base_style.icon_color.a,
165 "Disabled icon color should have reduced alpha"
166 );
167
168 if let (Some(Background::Color(base_color)), Some(Background::Color(disabled_color))) = (
170 base_style.button_background,
171 disabled_style.button_background,
172 ) {
173 assert!(
174 disabled_color.a <= base_color.a,
175 "Disabled button background should have reduced alpha"
176 );
177 }
178 }
179
180 #[test]
181 fn disabled_has_half_alpha() {
182 let theme = Theme::TokyoNight;
183 let base_style = primary(&theme, Status::Active);
184 let disabled_style = primary(&theme, Status::Disabled);
185
186 let expected_icon_alpha = base_style.icon_color.a * 0.5;
188 assert!(
189 (disabled_style.icon_color.a - expected_icon_alpha).abs() < 0.01,
190 "Disabled icon alpha should be approximately half"
191 );
192 }
193
194 #[test]
195 fn non_disabled_statuses_use_base_style() {
196 let theme = Theme::TokyoNight;
197 let active_style = primary(&theme, Status::Active);
198 let hovered_style = primary(&theme, Status::Hovered);
199 let focused_style = primary(&theme, Status::Focused);
200
201 assert_eq!(
203 format!("{:?}", active_style.button_background),
204 format!("{:?}", hovered_style.button_background)
205 );
206 assert_eq!(
207 format!("{:?}", active_style.button_background),
208 format!("{:?}", focused_style.button_background)
209 );
210 assert_eq!(active_style.icon_color, hovered_style.icon_color);
211 assert_eq!(active_style.icon_color, focused_style.icon_color);
212 }
213
214 #[test]
215 fn catalog_default_class() {
216 let _class = <Theme as Catalog>::default();
217 }
218
219 #[test]
220 fn catalog_style() {
221 let theme = Theme::TokyoNight;
222 let class = <Theme as Catalog>::default();
223 let style = <Theme as Catalog>::style(&theme, &class, Status::Active);
224
225 assert!(style.button_background.is_some());
226 }
227
228 #[test]
229 fn catalog_style_with_different_statuses() {
230 let theme = Theme::TokyoNight;
231 let class = <Theme as Catalog>::default();
232
233 let active_style = <Theme as Catalog>::style(&theme, &class, Status::Active);
234 let hovered_style = <Theme as Catalog>::style(&theme, &class, Status::Hovered);
235 let disabled_style = <Theme as Catalog>::style(&theme, &class, Status::Disabled);
236
237 assert!(active_style.button_background.is_some());
238 assert!(hovered_style.button_background.is_some());
239 assert!(disabled_style.button_background.is_some());
240 }
241
242 #[test]
243 fn extended_catalog_style() {
244 let theme = Theme::TokyoNight;
245 let class = <Theme as Catalog>::default();
246 let style = <Theme as ExtendedCatalog>::style(&theme, &class, Status::Active);
247
248 assert!(style.button_background.is_some());
249 }
250
251 #[test]
252 fn extended_catalog_default_input() {
253 let _class = <Theme as ExtendedCatalog>::default_input();
254 }
255
256 #[test]
257 fn disabled_preserves_gradient_backgrounds() {
258 let gradient = iced_core::Gradient::Linear(iced_core::gradient::Linear {
260 angle: 0.0.into(),
261 stops: [None; 8],
262 });
263 let style_with_gradient = Style {
264 button_background: Some(Background::Gradient(gradient)),
265 icon_color: Color::WHITE,
266 };
267
268 #[allow(clippy::panic)]
271 if let Some(Background::Color(_)) = style_with_gradient.button_background {
272 panic!("Expected gradient to be preserved");
273 }
274 }
275}