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,
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.base.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.base.into());
122            appearance.text_color = Some(cosmic.accent.base.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 => {
152            corner_radii = &[0.0; 4];
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.base.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        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_color().into())
207            } else {
208                Some(component.on.into())
209            };
210
211            (component.base.into(), text_color, text_color)
212        })
213    }
214
215    fn disabled(&self, style: &Self::Class) -> Style {
216        if let Button::Custom { disabled, .. } = style {
217            return disabled(self);
218        }
219
220        appearance(self, false, false, true, style, |component| {
221            let mut background = Color::from(component.base);
222            background.a *= 0.5;
223            (
224                background,
225                Some(component.on_disabled.into()),
226                Some(component.on_disabled.into()),
227            )
228        })
229    }
230
231    fn drop_target(&self, style: &Self::Class) -> Style {
232        self.active(false, false, style)
233    }
234
235    fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
236        if let Button::Custom { hovered, .. } = style {
237            return hovered(focused, self);
238        }
239
240        appearance(
241            self,
242            focused || matches!(style, Button::Image),
243            selected,
244            false,
245            style,
246            |component| {
247                let text_color = if matches!(
248                    style,
249                    Button::Icon | Button::IconVertical | Button::HeaderBar
250                ) && selected
251                {
252                    Some(self.cosmic().accent_color().into())
253                } else {
254                    Some(component.on.into())
255                };
256
257                (component.hover.into(), text_color, text_color)
258            },
259        )
260    }
261
262    fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
263        if let Button::Custom { pressed, .. } = style {
264            return pressed(focused, self);
265        }
266
267        appearance(self, focused, selected, false, style, |component| {
268            let text_color = if matches!(
269                style,
270                Button::Icon | Button::IconVertical | Button::HeaderBar
271            ) && selected
272            {
273                Some(self.cosmic().accent_color().into())
274            } else {
275                Some(component.on.into())
276            };
277
278            (component.pressed.into(), text_color, text_color)
279        })
280    }
281
282    fn selection_background(&self) -> Background {
283        Background::Color(self.cosmic().primary.base.into())
284    }
285}