Skip to main content

iced_aw/style/
number_input.rs

1//! Display fields that can only be filled with numeric type.
2//!
3//! *This API requires the following crate features to be activated: `number_input`*
4
5use super::{Status, StyleFn};
6use iced_core::{Background, Color, Theme};
7use iced_widget::{container, text, text_input};
8
9/// The appearance of a [`NumberInput`](crate::widget::number_input::NumberInput).
10#[derive(Clone, Copy, Debug)]
11pub struct Style {
12    /// The background of the [`NumberInput`](crate::widget::number_input::NumberInput).
13    pub button_background: Option<Background>,
14    /// The Color of the arrows of [`NumberInput`](crate::widget::number_input::NumberInput).
15    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
27/// The Catalog of a [`NumberInput`](crate::widget::number_input::NumberInput).
28pub trait Catalog {
29    ///Style for the trait to use.
30    type Class<'a>;
31
32    /// The default class produced by the [`Catalog`].
33    fn default<'a>() -> Self::Class<'a>;
34
35    /// The [`Style`] of a class with the given status.
36    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
51/// The Extended Catalog of a [`NumberInput`](crate::widget::number_input::NumberInput).
52pub trait ExtendedCatalog:
53    text_input::Catalog + container::Catalog + text::Catalog + self::Catalog
54{
55    /// The default class produced by the [`Catalog`].
56    #[must_use]
57    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
58        <Self as text_input::Catalog>::default()
59    }
60
61    /// The [`Style`] of a class with the given status.
62    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/// The primary theme of a [`Badge`](crate::widget::badge::Badge).
72#[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            // Background is a color
119        } 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        // Icon color alpha should be reduced
163        assert!(
164            disabled_style.icon_color.a <= base_style.icon_color.a,
165            "Disabled icon color should have reduced alpha"
166        );
167
168        // Button background alpha should be reduced for Color backgrounds
169        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        // Icon color should have approximately half the alpha
187        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        // All non-disabled statuses should have the same style
202        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        // Create a style with a gradient background
259        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        // If we had a disabled version, it should preserve the gradient
269        // This tests that gradients are handled in the match statement
270        #[allow(clippy::panic)]
271        if let Some(Background::Color(_)) = style_with_gradient.button_background {
272            panic!("Expected gradient to be preserved");
273        }
274    }
275}