cosmic/theme/style/
button.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Contains stylesheet implementation for [`crate::widget::button`].
5
6use cosmic_theme::Component;
7use iced_core::{Background, Color};
8
9use crate::{
10    theme::TRANSPARENT_COMPONENT,
11    widget::button::{Catalog, Style},
12};
13
14#[derive(Default)]
15pub enum Button {
16    AppletIcon,
17    AppletMenu,
18    Custom {
19        active: Box<dyn Fn(bool, &crate::Theme) -> Style>,
20        disabled: Box<dyn Fn(&crate::Theme) -> Style>,
21        hovered: Box<dyn Fn(bool, &crate::Theme) -> Style>,
22        pressed: Box<dyn Fn(bool, &crate::Theme) -> Style>,
23    },
24    Destructive,
25    HeaderBar,
26    Icon,
27    IconVertical,
28    Image,
29    Link,
30    ListItem([f32; 4]),
31    MenuFolder,
32    MenuItem,
33    MenuRoot,
34    NavToggle,
35    #[default]
36    Standard,
37    Suggested,
38    Text,
39    Transparent,
40}
41
42pub fn appearance(
43    theme: &crate::Theme,
44    focused: bool,
45    selected: bool,
46    disabled: bool,
47    style: &Button,
48    color: impl Fn(&Component) -> (Color, Option<Color>, Option<Color>),
49) -> Style {
50    let cosmic = theme.cosmic();
51    let mut corner_radii = &cosmic.corner_radii.radius_xl;
52    let mut appearance = Style::new();
53    let hc = theme.theme_type.is_high_contrast();
54    match style {
55        Button::Standard
56        | Button::Text
57        | Button::Suggested
58        | Button::Destructive
59        | Button::Transparent => {
60            let style_component = match style {
61                Button::Standard => &cosmic.button,
62                Button::Text => &cosmic.text_button,
63                Button::Suggested => &cosmic.accent_button,
64                Button::Destructive => &cosmic.destructive_button,
65                Button::Transparent => &TRANSPARENT_COMPONENT,
66                _ => return appearance,
67            };
68
69            let (background, text, icon) = color(style_component);
70            appearance.background = Some(Background::Color(background));
71            if !matches!(style, Button::Standard) {
72                appearance.text_color = text;
73                appearance.icon_color = icon;
74            } else if hc {
75                appearance.border_color = style_component.border.into();
76                appearance.border_width = 1.;
77            }
78        }
79
80        Button::Icon | Button::IconVertical | Button::HeaderBar | Button::NavToggle => {
81            if matches!(style, Button::IconVertical) {
82                corner_radii = &cosmic.corner_radii.radius_m;
83                if selected {
84                    appearance.overlay = Some(Background::Color(Color::from(
85                        cosmic.icon_button.selected_state_color(),
86                    )));
87                }
88            }
89            if matches!(style, Button::NavToggle) {
90                corner_radii = &cosmic.corner_radii.radius_s;
91            }
92
93            let (background, text, icon) = color(&cosmic.icon_button);
94            appearance.background = Some(Background::Color(background));
95            // Only override icon button colors when it is disabled
96            appearance.icon_color = if disabled { icon } else { None };
97            appearance.text_color = if disabled { text } else { None };
98        }
99
100        Button::Image => {
101            appearance.background = None;
102            appearance.text_color = Some(cosmic.accent_text_color().into());
103            appearance.icon_color = Some(cosmic.accent.base.into());
104
105            corner_radii = &cosmic.corner_radii.radius_s;
106            appearance.border_radius = (*corner_radii).into();
107
108            if focused || selected {
109                appearance.border_width = 2.0;
110                appearance.border_color = cosmic.accent.base.into();
111            } else if hc {
112                appearance.border_color = theme.current_container().component.divider.into();
113                appearance.border_width = 1.;
114            }
115
116            return appearance;
117        }
118
119        Button::Link => {
120            appearance.background = None;
121            appearance.icon_color = Some(cosmic.accent_text_color().into());
122            appearance.text_color = Some(cosmic.accent_text_color().into());
123            corner_radii = &cosmic.corner_radii.radius_0;
124        }
125
126        Button::Custom { .. } => (),
127        Button::AppletMenu => {
128            let (background, _, _) = color(&cosmic.text_button);
129            appearance.background = Some(Background::Color(background));
130
131            appearance.icon_color = Some(cosmic.background.on.into());
132            appearance.text_color = Some(cosmic.background.on.into());
133            corner_radii = &cosmic.corner_radii.radius_0;
134        }
135        Button::AppletIcon => {
136            let (background, _, _) = color(&cosmic.text_button);
137            appearance.background = Some(Background::Color(background));
138
139            appearance.icon_color = Some(cosmic.background.on.into());
140            appearance.text_color = Some(cosmic.background.on.into());
141        }
142        Button::MenuFolder => {
143            // Menu folders cannot be disabled, ignore customized icon and text color
144            let component = &cosmic.background.component;
145            let (background, _, _) = color(component);
146            appearance.background = Some(Background::Color(background));
147            appearance.icon_color = Some(component.on.into());
148            appearance.text_color = Some(component.on.into());
149            corner_radii = &cosmic.corner_radii.radius_s;
150        }
151        Button::ListItem(radii) => {
152            corner_radii = radii;
153            let (background, text, icon) = color(&cosmic.background.component);
154
155            if selected {
156                appearance.background =
157                    Some(Background::Color(cosmic.primary.component.hover.into()));
158                appearance.icon_color = Some(cosmic.accent.base.into());
159                appearance.text_color = Some(cosmic.accent_text_color().into());
160            } else {
161                appearance.background = Some(Background::Color(background));
162                appearance.icon_color = icon;
163                appearance.text_color = text;
164            }
165        }
166        Button::MenuItem => {
167            let (background, text, icon) = color(&cosmic.background.component);
168            appearance.background = Some(Background::Color(background));
169            appearance.icon_color = icon;
170            appearance.text_color = text;
171            corner_radii = &cosmic.corner_radii.radius_s;
172        }
173        Button::MenuRoot => {
174            appearance.background = None;
175            appearance.icon_color = None;
176            appearance.text_color = None;
177        }
178    }
179
180    appearance.border_radius = (*corner_radii).into();
181
182    if focused {
183        appearance.outline_width = 1.0;
184        appearance.outline_color = cosmic.accent.base.into();
185        appearance.border_width = 2.0;
186        appearance.border_color = Color::TRANSPARENT;
187    }
188
189    appearance
190}
191
192impl Catalog for crate::Theme {
193    type Class = Button;
194
195    fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
196        if let Button::Custom { active, .. } = style {
197            return active(focused, self);
198        }
199
200        let mut s = appearance(self, focused, selected, false, style, move |component| {
201            let text_color = if matches!(
202                style,
203                Button::Icon | Button::IconVertical | Button::HeaderBar
204            ) && selected
205            {
206                Some(self.cosmic().accent_text_color().into())
207            } else {
208                Some(component.on.into())
209            };
210
211            (component.base.into(), text_color, text_color)
212        });
213
214        if let Button::ListItem(_) = style {
215            if !selected {
216                s.background = None;
217            }
218        }
219
220        s
221    }
222
223    fn disabled(&self, style: &Self::Class) -> Style {
224        if let Button::Custom { disabled, .. } = style {
225            return disabled(self);
226        }
227
228        appearance(self, false, false, true, style, |component| {
229            let mut background = Color::from(component.base);
230            background.a *= 0.5;
231            (
232                background,
233                Some(component.on_disabled.into()),
234                Some(component.on_disabled.into()),
235            )
236        })
237    }
238
239    fn drop_target(&self, style: &Self::Class) -> Style {
240        self.active(false, false, style)
241    }
242
243    fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
244        if let Button::Custom { hovered, .. } = style {
245            return hovered(focused, self);
246        }
247
248        let mut s = appearance(
249            self,
250            focused || matches!(style, Button::Image),
251            selected,
252            false,
253            style,
254            |component| {
255                let text_color = if matches!(
256                    style,
257                    Button::Icon | Button::IconVertical | Button::HeaderBar
258                ) && selected
259                {
260                    Some(self.cosmic().accent_text_color().into())
261                } else {
262                    Some(component.on.into())
263                };
264
265                (component.hover.into(), text_color, text_color)
266            },
267        );
268
269        if let Button::ListItem(_) = style {
270            if !selected {
271                s.background = None;
272            }
273        }
274
275        s
276    }
277
278    fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
279        if let Button::Custom { pressed, .. } = style {
280            return pressed(focused, self);
281        }
282
283        appearance(self, focused, selected, false, style, |component| {
284            let text_color = if matches!(
285                style,
286                Button::Icon | Button::IconVertical | Button::HeaderBar
287            ) && selected
288            {
289                Some(self.cosmic().accent_text_color().into())
290            } else {
291                Some(component.on.into())
292            };
293
294            (component.pressed.into(), text_color, text_color)
295        })
296    }
297
298    fn selection_background(&self) -> Background {
299        Background::Color(self.cosmic().primary.base.into())
300    }
301}