Skip to main content

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