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.on.into());
130            appearance.text_color = Some(cosmic.background.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.on.into());
138            appearance.text_color = Some(cosmic.background.on.into());
139        }
140        Button::MenuFolder => {
141            // Menu folders cannot be disabled, ignore customized icon and text color
142            let component = &cosmic.background.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 =
155                    Some(Background::Color(cosmic.primary.component.hover.into()));
156                appearance.icon_color = Some(cosmic.accent.base.into());
157                appearance.text_color = Some(cosmic.accent_text_color().into());
158            } else {
159                appearance.background = Some(Background::Color(background));
160                appearance.icon_color = icon;
161                appearance.text_color = text;
162            }
163        }
164        Button::MenuItem => {
165            let (background, text, icon) = color(&cosmic.background.component);
166            appearance.background = Some(Background::Color(background));
167            appearance.icon_color = icon;
168            appearance.text_color = text;
169            corner_radii = &cosmic.corner_radii.radius_s;
170        }
171        Button::MenuRoot => {
172            appearance.background = None;
173            appearance.icon_color = None;
174            appearance.text_color = None;
175        }
176    }
177
178    appearance.border_radius = (*corner_radii).into();
179
180    if focused {
181        appearance.outline_width = 1.0;
182        appearance.outline_color = cosmic.accent.base.into();
183        appearance.border_width = 2.0;
184        appearance.border_color = Color::TRANSPARENT;
185    }
186
187    appearance
188}
189
190impl Catalog for crate::Theme {
191    type Class = Button;
192
193    fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
194        if let Button::Custom { active, .. } = style {
195            return active(focused, self);
196        }
197
198        appearance(self, focused, selected, false, style, move |component| {
199            let text_color = if matches!(
200                style,
201                Button::Icon | Button::IconVertical | Button::HeaderBar
202            ) && selected
203            {
204                Some(self.cosmic().accent_text_color().into())
205            } else {
206                Some(component.on.into())
207            };
208
209            (component.base.into(), text_color, text_color)
210        })
211    }
212
213    fn disabled(&self, style: &Self::Class) -> Style {
214        if let Button::Custom { disabled, .. } = style {
215            return disabled(self);
216        }
217
218        appearance(self, false, false, true, style, |component| {
219            let mut background = Color::from(component.base);
220            background.a *= 0.5;
221            (
222                background,
223                Some(component.on_disabled.into()),
224                Some(component.on_disabled.into()),
225            )
226        })
227    }
228
229    fn drop_target(&self, style: &Self::Class) -> Style {
230        self.active(false, false, style)
231    }
232
233    fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
234        if let Button::Custom { hovered, .. } = style {
235            return hovered(focused, self);
236        }
237
238        appearance(
239            self,
240            focused || matches!(style, Button::Image),
241            selected,
242            false,
243            style,
244            |component| {
245                let text_color = if matches!(
246                    style,
247                    Button::Icon | Button::IconVertical | Button::HeaderBar
248                ) && selected
249                {
250                    Some(self.cosmic().accent_text_color().into())
251                } else {
252                    Some(component.on.into())
253                };
254
255                (component.hover.into(), text_color, text_color)
256            },
257        )
258    }
259
260    fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style {
261        if let Button::Custom { pressed, .. } = style {
262            return pressed(focused, self);
263        }
264
265        appearance(self, focused, selected, false, style, |component| {
266            let text_color = if matches!(
267                style,
268                Button::Icon | Button::IconVertical | Button::HeaderBar
269            ) && selected
270            {
271                Some(self.cosmic().accent_text_color().into())
272            } else {
273                Some(component.on.into())
274            };
275
276            (component.pressed.into(), text_color, text_color)
277        })
278    }
279
280    fn selection_background(&self) -> Background {
281        Background::Color(self.cosmic().primary.base.into())
282    }
283}