Skip to main content

sequoia_style/
button_style.rs

1use sequoia_core::Color;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq)]
4pub struct ButtonStyle {
5    pub background: Option<Color>,
6
7    pub hover: Option<Color>,
8
9    pub pressed: Option<Color>,
10
11    pub disabled: Option<Color>,
12
13    pub text: Option<Color>,
14
15    pub text_disabled: Option<Color>,
16
17    pub font_size: Option<f32>,
18
19    pub font_weight: Option<u16>,
20
21    pub radius: Option<f32>,
22
23    pub padding: Option<(f32, f32)>,
24
25    pub border_width: Option<f32>,
26
27    pub border_color: Option<Color>,
28}
29
30impl ButtonStyle {
31    pub const fn new() -> Self {
32        Self {
33            background: None,
34            hover: None,
35            pressed: None,
36            disabled: None,
37            text: None,
38            text_disabled: None,
39            font_size: None,
40            font_weight: None,
41            radius: None,
42            padding: None,
43            border_width: None,
44            border_color: None,
45        }
46    }
47
48    pub const PRIMARY: Self = Self {
49        background: Some(Color::rgb8(0x35, 0x6F, 0xD1)),
50        hover: Some(Color::rgb8(0x45, 0x82, 0xE0)),
51        pressed: Some(Color::rgb8(0x2C, 0x5A, 0xA8)),
52        disabled: None,
53        text: Some(Color::WHITE),
54        text_disabled: None,
55        font_size: None,
56        font_weight: Some(600),
57        radius: None,
58        padding: None,
59        border_width: None,
60        border_color: None,
61    };
62
63    pub const SECONDARY: Self = Self {
64        background: Some(Color::rgb8(0x33, 0x36, 0x3D)),
65        hover: Some(Color::rgb8(0x40, 0x44, 0x4E)),
66        pressed: Some(Color::rgb8(0x2A, 0x2D, 0x33)),
67        disabled: None,
68        text: Some(Color::rgb8(0xE6, 0xE6, 0xE6)),
69        text_disabled: None,
70        font_size: None,
71        font_weight: None,
72        radius: None,
73        padding: None,
74        border_width: None,
75        border_color: None,
76    };
77
78    pub const DANGER: Self = Self {
79        background: Some(Color::rgb8(0xB3, 0x26, 0x1E)),
80        hover: Some(Color::rgb8(0xD3, 0x2F, 0x2F)),
81        pressed: Some(Color::rgb8(0x8A, 0x1A, 0x14)),
82        disabled: None,
83        text: Some(Color::WHITE),
84        text_disabled: None,
85        font_size: None,
86        font_weight: Some(600),
87        radius: None,
88        padding: None,
89        border_width: None,
90        border_color: None,
91    };
92
93    pub const GHOST: Self = Self {
94        background: Some(Color::TRANSPARENT),
95        hover: Some(Color::rgb8(0x33, 0x36, 0x3D)),
96        pressed: Some(Color::rgb8(0x2A, 0x2D, 0x33)),
97        disabled: None,
98        text: Some(Color::rgb8(0x35, 0x6F, 0xD1)),
99        text_disabled: None,
100        font_size: None,
101        font_weight: None,
102        radius: None,
103        padding: None,
104        border_width: None,
105        border_color: None,
106    };
107
108    /// Neutral outlined button for secondary actions on light or dark surfaces.
109    pub const OUTLINE: Self = Self {
110        background: Some(Color::TRANSPARENT),
111        hover: Some(Color::hexa(0x64748B1F)),
112        pressed: Some(Color::hexa(0x64748B33)),
113        disabled: None,
114        text: Some(Color::hex(0x64748B)),
115        text_disabled: None,
116        font_size: None,
117        font_weight: Some(500),
118        radius: None,
119        padding: None,
120        border_width: Some(1.0),
121        border_color: Some(Color::hexa(0x64748B66)),
122    };
123
124    pub const DARK_FILLED: Self = Self {
125        background: Some(Color::hex(0x1F1F1F)),
126        hover: Some(Color::hex(0x2B2B2B)),
127        pressed: Some(Color::hex(0x141414)),
128        disabled: None,
129        text: Some(Color::hex(0xEDEDED)),
130        text_disabled: None,
131        font_size: Some(12.0),
132        font_weight: Some(500),
133        radius: None,
134        padding: Some((12.0, 6.0)),
135        border_width: None,
136        border_color: None,
137    };
138
139    pub const OUTLINE_ROSE: Self = Self {
140        background: Some(Color::TRANSPARENT),
141        hover: Some(Color::hexa(0xB0868626)),
142        pressed: Some(Color::hexa(0xB086864D)),
143        disabled: None,
144        text: Some(Color::hex(0xB08686)),
145        text_disabled: None,
146        font_size: Some(12.0),
147        font_weight: Some(600),
148        radius: None,
149        padding: Some((12.0, 6.0)),
150        border_width: Some(1.0),
151        border_color: Some(Color::hex(0xB08686)),
152    };
153
154    pub const fn primary() -> Self {
155        Self::PRIMARY
156    }
157
158    pub const fn secondary() -> Self {
159        Self::SECONDARY
160    }
161
162    pub const fn danger() -> Self {
163        Self::DANGER
164    }
165
166    pub const fn ghost() -> Self {
167        Self::GHOST
168    }
169
170    pub const fn outline() -> Self {
171        Self::OUTLINE
172    }
173
174    pub const fn dark_filled() -> Self {
175        Self::DARK_FILLED
176    }
177
178    pub const fn outline_rose() -> Self {
179        Self::OUTLINE_ROSE
180    }
181
182    pub const fn background(mut self, color: Color) -> Self {
183        self.background = Some(color);
184        self
185    }
186
187    pub const fn hover(mut self, color: Color) -> Self {
188        self.hover = Some(color);
189        self
190    }
191
192    pub const fn pressed(mut self, color: Color) -> Self {
193        self.pressed = Some(color);
194        self
195    }
196
197    pub const fn disabled(mut self, color: Color) -> Self {
198        self.disabled = Some(color);
199        self
200    }
201
202    pub const fn text(mut self, color: Color) -> Self {
203        self.text = Some(color);
204        self
205    }
206
207    pub const fn text_disabled(mut self, color: Color) -> Self {
208        self.text_disabled = Some(color);
209        self
210    }
211
212    pub const fn font_size(mut self, size: f32) -> Self {
213        self.font_size = Some(size);
214        self
215    }
216
217    pub const fn font_weight(mut self, weight: u16) -> Self {
218        self.font_weight = Some(weight);
219        self
220    }
221
222    pub const fn radius(mut self, radius: f32) -> Self {
223        self.radius = Some(radius);
224        self
225    }
226
227    pub const fn padding_xy(mut self, pad_x: f32, pad_y: f32) -> Self {
228        self.padding = Some((pad_x, pad_y));
229        self
230    }
231
232    pub const fn border_width(mut self, width: f32) -> Self {
233        self.border_width = Some(width);
234        self
235    }
236
237    pub const fn border_color(mut self, color: Color) -> Self {
238        self.border_color = Some(color);
239        self
240    }
241
242    pub const fn border(mut self, width: f32, color: Color) -> Self {
243        self.border_width = Some(width);
244        self.border_color = Some(color);
245        self
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn presets_differ_per_state() {
255        assert_ne!(ButtonStyle::PRIMARY.background, ButtonStyle::PRIMARY.hover);
256        assert_ne!(ButtonStyle::PRIMARY.hover, ButtonStyle::PRIMARY.pressed);
257        assert_ne!(
258            ButtonStyle::DANGER.background,
259            ButtonStyle::PRIMARY.background
260        );
261    }
262
263    #[test]
264    fn const_composition_in_const_context() {
265        const CUSTOM: ButtonStyle = ButtonStyle::PRIMARY.radius(12.0);
266        assert_eq!(CUSTOM.radius, Some(12.0));
267        assert_eq!(CUSTOM.background, ButtonStyle::PRIMARY.background);
268    }
269
270    #[test]
271    fn struct_update_syntax_keeps_base() {
272        const CUSTOM: ButtonStyle = ButtonStyle {
273            background: Some(Color::hex(0xB3261E)),
274            ..ButtonStyle::PRIMARY
275        };
276        assert_eq!(CUSTOM.background, Some(Color::hex(0xB3261E)));
277        assert_eq!(CUSTOM.hover, ButtonStyle::PRIMARY.hover);
278    }
279
280    #[test]
281    fn border_builders_set_both_fields() {
282        const S: ButtonStyle = ButtonStyle::new().border(1.0, Color::WHITE);
283        assert_eq!(S.border_width, Some(1.0));
284        assert_eq!(S.border_color, Some(Color::WHITE));
285    }
286
287    #[test]
288    fn new_variants_share_metrics_but_differ_in_look() {
289        assert_eq!(
290            ButtonStyle::DARK_FILLED.padding,
291            ButtonStyle::OUTLINE_ROSE.padding
292        );
293        assert_eq!(ButtonStyle::DARK_FILLED.padding, Some((12.0, 6.0)));
294        assert_eq!(ButtonStyle::DARK_FILLED.font_size, Some(12.0));
295        assert_eq!(ButtonStyle::OUTLINE_ROSE.font_size, Some(12.0));
296
297        assert_eq!(ButtonStyle::DARK_FILLED.font_weight, Some(500));
298        assert_eq!(ButtonStyle::OUTLINE_ROSE.font_weight, Some(600));
299        assert_eq!(
300            ButtonStyle::DARK_FILLED.background,
301            Some(Color::hex(0x1F1F1F))
302        );
303        assert_eq!(ButtonStyle::DARK_FILLED.text, Some(Color::hex(0xEDEDED)));
304        assert_eq!(ButtonStyle::DARK_FILLED.border_width, None);
305        assert_eq!(
306            ButtonStyle::OUTLINE_ROSE.background,
307            Some(Color::TRANSPARENT)
308        );
309        assert_eq!(ButtonStyle::OUTLINE_ROSE.text, Some(Color::hex(0xB08686)));
310        assert_eq!(ButtonStyle::OUTLINE_ROSE.border_width, Some(1.0));
311        assert_eq!(
312            ButtonStyle::OUTLINE_ROSE.border_color,
313            Some(Color::hex(0xB08686))
314        );
315        assert_ne!(
316            ButtonStyle::DARK_FILLED.hover,
317            ButtonStyle::DARK_FILLED.background
318        );
319        assert_ne!(
320            ButtonStyle::OUTLINE_ROSE.hover,
321            ButtonStyle::OUTLINE_ROSE.background
322        );
323    }
324}