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