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