cosmic/theme/style/
iced.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Contains stylesheet implementations for widgets native to iced.
5
6use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme};
7use cosmic_theme::composite::over;
8use iced::{
9    overlay::menu,
10    widget::{
11        button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container,
12        pane_grid, pick_list, progress_bar, radio, rule, scrollable,
13        slider::{self, Rail},
14        svg, toggler,
15    },
16};
17use iced_core::{Background, Border, Color, Shadow, Vector};
18use iced_widget::{pane_grid::Highlight, text_editor, text_input};
19use palette::WithAlpha;
20use std::rc::Rc;
21
22pub mod application {
23    use crate::Theme;
24    use iced_runtime::Appearance;
25
26    #[derive(Default)]
27    pub enum Application {
28        #[default]
29        Default,
30        Custom(Box<dyn Fn(&Theme) -> Appearance>),
31    }
32
33    impl Application {
34        pub fn custom<F: Fn(&Theme) -> Appearance + 'static>(f: F) -> Self {
35            Self::Custom(Box::new(f))
36        }
37    }
38
39    pub fn appearance(theme: &Theme) -> Appearance {
40        let cosmic = theme.cosmic();
41
42        Appearance {
43            icon_color: cosmic.bg_color().into(),
44            background_color: cosmic.bg_color().into(),
45            text_color: cosmic.on_bg_color().into(),
46        }
47    }
48}
49
50/// Styles for the button widget from iced-rs.
51#[derive(Default)]
52pub enum Button {
53    Deactivated,
54    Destructive,
55    Positive,
56    #[default]
57    Primary,
58    Secondary,
59    Text,
60    Link,
61    LinkActive,
62    Transparent,
63    Card,
64    Custom(Box<dyn Fn(&Theme, iced_button::Status) -> iced_button::Style>),
65}
66
67impl iced_button::Catalog for Theme {
68    type Class<'a> = Button;
69
70    fn default<'a>() -> Self::Class<'a> {
71        Button::default()
72    }
73
74    fn style(&self, class: &Self::Class<'_>, status: iced_button::Status) -> iced_button::Style {
75        if let Button::Custom(f) = class {
76            return f(self, status);
77        }
78        let cosmic = self.cosmic();
79        let corner_radii = &cosmic.corner_radii;
80        let component = class.cosmic(self);
81
82        let mut appearance = iced_button::Style {
83            border_radius: match class {
84                Button::Link => corner_radii.radius_0.into(),
85                Button::Card => corner_radii.radius_xs.into(),
86                _ => corner_radii.radius_xl.into(),
87            },
88            border: Border {
89                radius: match class {
90                    Button::Link => corner_radii.radius_0.into(),
91                    Button::Card => corner_radii.radius_xs.into(),
92                    _ => corner_radii.radius_xl.into(),
93                },
94                ..Default::default()
95            },
96            background: match class {
97                Button::Link | Button::Text => None,
98                Button::LinkActive => Some(Background::Color(component.divider.into())),
99                _ => Some(Background::Color(component.base.into())),
100            },
101            text_color: match class {
102                Button::Link | Button::LinkActive => component.base.into(),
103                _ => component.on.into(),
104            },
105            ..iced_button::Style::default()
106        };
107
108        match status {
109            iced_button::Status::Active => {}
110            iced_button::Status::Hovered => {
111                appearance.background = match class {
112                    Button::Link => None,
113                    Button::LinkActive => Some(Background::Color(component.divider.into())),
114                    _ => Some(Background::Color(component.hover.into())),
115                };
116            }
117            iced_button::Status::Pressed => {
118                appearance.background = match class {
119                    Button::Link => None,
120                    Button::LinkActive => Some(Background::Color(component.divider.into())),
121                    _ => Some(Background::Color(component.pressed.into())),
122                };
123            }
124            iced_button::Status::Disabled => {
125                // Card color is not transparent when it isn't clickable
126                if matches!(class, Button::Card) {
127                    return appearance;
128                }
129                appearance.background = appearance.background.map(|background| match background {
130                    Background::Color(color) => Background::Color(Color {
131                        a: color.a * 0.5,
132                        ..color
133                    }),
134                    Background::Gradient(gradient) => {
135                        Background::Gradient(gradient.scale_alpha(0.5))
136                    }
137                });
138                appearance.text_color = Color {
139                    a: appearance.text_color.a * 0.5,
140                    ..appearance.text_color
141                };
142            }
143        };
144        appearance
145    }
146}
147
148impl Button {
149    #[allow(clippy::trivially_copy_pass_by_ref)]
150    #[allow(clippy::match_same_arms)]
151    fn cosmic<'a>(&'a self, theme: &'a Theme) -> &'a CosmicComponent {
152        let cosmic = theme.cosmic();
153        match self {
154            Self::Primary => &cosmic.accent_button,
155            Self::Secondary => &theme.current_container().component,
156            Self::Positive => &cosmic.success_button,
157            Self::Destructive => &cosmic.destructive_button,
158            Self::Text => &cosmic.text_button,
159            Self::Link => &cosmic.link_button,
160            Self::LinkActive => &cosmic.link_button,
161            Self::Transparent => &TRANSPARENT_COMPONENT,
162            Self::Deactivated => &theme.current_container().component,
163            Self::Card => &theme.current_container().component,
164            Self::Custom { .. } => &TRANSPARENT_COMPONENT,
165        }
166    }
167}
168
169/*
170 * TODO: Checkbox
171 */
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum Checkbox {
174    Primary,
175    Secondary,
176    Success,
177    Danger,
178}
179
180impl Default for Checkbox {
181    fn default() -> Self {
182        Self::Primary
183    }
184}
185
186impl iced_checkbox::Catalog for Theme {
187    type Class<'a> = Checkbox;
188
189    fn default<'a>() -> Self::Class<'a> {
190        Checkbox::default()
191    }
192
193    #[allow(clippy::too_many_lines)]
194    fn style(
195        &self,
196        class: &Self::Class<'_>,
197        status: iced_checkbox::Status,
198    ) -> iced_checkbox::Style {
199        let cosmic = self.cosmic();
200
201        let corners = &cosmic.corner_radii;
202
203        let disabled = matches!(status, iced_checkbox::Status::Disabled { .. });
204        match status {
205            iced_checkbox::Status::Active { is_checked }
206            | iced_checkbox::Status::Disabled { is_checked } => {
207                let mut active = match class {
208                    Checkbox::Primary => iced_checkbox::Style {
209                        background: Background::Color(if is_checked {
210                            cosmic.accent.base.into()
211                        } else {
212                            self.current_container().small_widget.into()
213                        }),
214                        icon_color: cosmic.accent.on.into(),
215                        border: Border {
216                            radius: corners.radius_xs.into(),
217                            width: if is_checked { 0.0 } else { 1.0 },
218                            color: if is_checked {
219                                cosmic.accent.base
220                            } else {
221                                cosmic.palette.neutral_8
222                            }
223                            .into(),
224                        },
225
226                        text_color: None,
227                    },
228                    Checkbox::Secondary => iced_checkbox::Style {
229                        background: Background::Color(if is_checked {
230                            cosmic.background.component.base.into()
231                        } else {
232                            self.current_container().small_widget.into()
233                        }),
234                        icon_color: cosmic.background.on.into(),
235                        border: Border {
236                            radius: corners.radius_xs.into(),
237                            width: if is_checked { 0.0 } else { 1.0 },
238                            color: cosmic.palette.neutral_8.into(),
239                        },
240                        text_color: None,
241                    },
242                    Checkbox::Success => iced_checkbox::Style {
243                        background: Background::Color(if is_checked {
244                            cosmic.success.base.into()
245                        } else {
246                            self.current_container().small_widget.into()
247                        }),
248                        icon_color: cosmic.success.on.into(),
249                        border: Border {
250                            radius: corners.radius_xs.into(),
251                            width: if is_checked { 0.0 } else { 1.0 },
252                            color: if is_checked {
253                                cosmic.success.base
254                            } else {
255                                cosmic.palette.neutral_8
256                            }
257                            .into(),
258                        },
259                        text_color: None,
260                    },
261                    Checkbox::Danger => iced_checkbox::Style {
262                        background: Background::Color(if is_checked {
263                            cosmic.destructive.base.into()
264                        } else {
265                            self.current_container().small_widget.into()
266                        }),
267                        icon_color: cosmic.destructive.on.into(),
268                        border: Border {
269                            radius: corners.radius_xs.into(),
270                            width: if is_checked { 0.0 } else { 1.0 },
271                            color: if is_checked {
272                                cosmic.destructive.base
273                            } else {
274                                cosmic.palette.neutral_8
275                            }
276                            .into(),
277                        },
278                        text_color: None,
279                    },
280                };
281                if disabled {
282                    match &mut active.background {
283                        Background::Color(color) => {
284                            color.a /= 2.;
285                        }
286                        Background::Gradient(gradient) => {
287                            *gradient = gradient.scale_alpha(0.5);
288                        }
289                    }
290                    if let Some(c) = active.text_color.as_mut() {
291                        c.a /= 2.
292                    };
293                    active.border.color.a /= 2.;
294                }
295                active
296            }
297            iced_checkbox::Status::Hovered { is_checked } => {
298                let cur_container = self.current_container().small_widget;
299                // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables.
300                let hovered_bg = over(cosmic.palette.neutral_0.with_alpha(0.1), cur_container);
301                match class {
302                    Checkbox::Primary => iced_checkbox::Style {
303                        background: Background::Color(if is_checked {
304                            cosmic.accent.hover_state_color().into()
305                        } else {
306                            hovered_bg.into()
307                        }),
308                        icon_color: cosmic.accent.on.into(),
309                        border: Border {
310                            radius: corners.radius_xs.into(),
311                            width: if is_checked { 0.0 } else { 1.0 },
312                            color: if is_checked {
313                                cosmic.accent.base
314                            } else {
315                                cosmic.palette.neutral_8
316                            }
317                            .into(),
318                        },
319                        text_color: None,
320                    },
321                    Checkbox::Secondary => iced_checkbox::Style {
322                        background: Background::Color(if is_checked {
323                            self.current_container().component.hover.into()
324                        } else {
325                            hovered_bg.into()
326                        }),
327                        icon_color: self.current_container().on.into(),
328                        border: Border {
329                            radius: corners.radius_xs.into(),
330                            width: if is_checked { 0.0 } else { 1.0 },
331                            color: if is_checked {
332                                self.current_container().base
333                            } else {
334                                cosmic.palette.neutral_8
335                            }
336                            .into(),
337                        },
338                        text_color: None,
339                    },
340                    Checkbox::Success => iced_checkbox::Style {
341                        background: Background::Color(if is_checked {
342                            cosmic.success.hover.into()
343                        } else {
344                            hovered_bg.into()
345                        }),
346                        icon_color: cosmic.success.on.into(),
347                        border: Border {
348                            radius: corners.radius_xs.into(),
349                            width: if is_checked { 0.0 } else { 1.0 },
350                            color: if is_checked {
351                                cosmic.success.base
352                            } else {
353                                cosmic.palette.neutral_8
354                            }
355                            .into(),
356                        },
357                        text_color: None,
358                    },
359                    Checkbox::Danger => iced_checkbox::Style {
360                        background: Background::Color(if is_checked {
361                            cosmic.destructive.hover.into()
362                        } else {
363                            hovered_bg.into()
364                        }),
365                        icon_color: cosmic.destructive.on.into(),
366                        border: Border {
367                            radius: corners.radius_xs.into(),
368                            width: if is_checked { 0.0 } else { 1.0 },
369                            color: if is_checked {
370                                cosmic.destructive.base
371                            } else {
372                                cosmic.palette.neutral_8
373                            }
374                            .into(),
375                        },
376                        text_color: None,
377                    },
378                }
379            }
380        }
381    }
382}
383
384/*
385 * TODO: Container
386 */
387#[derive(Default)]
388pub enum Container<'a> {
389    WindowBackground,
390    Background,
391    Card,
392    ContextDrawer,
393    Custom(Box<dyn Fn(&Theme) -> iced_container::Style + 'a>),
394    Dialog,
395    Dropdown,
396    HeaderBar {
397        focused: bool,
398        sharp_corners: bool,
399    },
400    List,
401    Primary,
402    Secondary,
403    Tooltip,
404    #[default]
405    Transparent,
406}
407
408impl<'a> Container<'a> {
409    pub fn custom<F: Fn(&Theme) -> iced_container::Style + 'a>(f: F) -> Self {
410        Self::Custom(Box::new(f))
411    }
412
413    #[must_use]
414    pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style {
415        iced_container::Style {
416            icon_color: Some(Color::from(theme.background.on)),
417            text_color: Some(Color::from(theme.background.on)),
418            background: Some(iced::Background::Color(theme.background.base.into())),
419            border: Border {
420                radius: theme.corner_radii.radius_s.into(),
421                ..Default::default()
422            },
423            shadow: Shadow::default(),
424        }
425    }
426
427    #[must_use]
428    pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style {
429        iced_container::Style {
430            icon_color: Some(Color::from(theme.primary.on)),
431            text_color: Some(Color::from(theme.primary.on)),
432            background: Some(iced::Background::Color(theme.primary.base.into())),
433            border: Border {
434                radius: theme.corner_radii.radius_s.into(),
435                ..Default::default()
436            },
437            shadow: Shadow::default(),
438        }
439    }
440
441    #[must_use]
442    pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style {
443        iced_container::Style {
444            icon_color: Some(Color::from(theme.secondary.on)),
445            text_color: Some(Color::from(theme.secondary.on)),
446            background: Some(iced::Background::Color(theme.secondary.base.into())),
447            border: Border {
448                radius: theme.corner_radii.radius_s.into(),
449                ..Default::default()
450            },
451            shadow: Shadow::default(),
452        }
453    }
454}
455
456impl<'a> From<iced_container::StyleFn<'a, Theme>> for Container<'a> {
457    fn from(value: iced_container::StyleFn<'a, Theme>) -> Self {
458        Self::custom(value)
459    }
460}
461
462impl iced_container::Catalog for Theme {
463    type Class<'a> = Container<'a>;
464
465    fn default<'a>() -> Self::Class<'a> {
466        Container::default()
467    }
468
469    fn style(&self, class: &Self::Class<'_>) -> iced_container::Style {
470        let cosmic = self.cosmic();
471
472        // Ensures visually aligned radii for content and window corners
473        let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
474
475        match class {
476            Container::Transparent => iced_container::Style::default(),
477
478            Container::Custom(f) => f(self),
479
480            Container::WindowBackground => iced_container::Style {
481                icon_color: Some(Color::from(cosmic.background.on)),
482                text_color: Some(Color::from(cosmic.background.on)),
483                background: Some(iced::Background::Color(cosmic.background.base.into())),
484                border: Border {
485                    radius: [
486                        cosmic.corner_radii.radius_0[0],
487                        cosmic.corner_radii.radius_0[1],
488                        window_corner_radius[2],
489                        window_corner_radius[3],
490                    ]
491                    .into(),
492                    ..Default::default()
493                },
494                shadow: Shadow::default(),
495            },
496
497            Container::List => {
498                let component = &self.current_container().component;
499                iced_container::Style {
500                    icon_color: Some(component.on.into()),
501                    text_color: Some(component.on.into()),
502                    background: Some(Background::Color(component.base.into())),
503                    border: iced::Border {
504                        radius: cosmic.corner_radii.radius_s.into(),
505                        ..Default::default()
506                    },
507                    shadow: Shadow::default(),
508                }
509            }
510
511            Container::HeaderBar {
512                focused,
513                sharp_corners,
514            } => {
515                let (icon_color, text_color) = if *focused {
516                    (
517                        Color::from(cosmic.accent_text_color()),
518                        Color::from(cosmic.background.on),
519                    )
520                } else {
521                    use crate::ext::ColorExt;
522                    let unfocused_color = Color::from(cosmic.background.component.on)
523                        .blend_alpha(cosmic.background.base.into(), 0.5);
524                    (unfocused_color, unfocused_color)
525                };
526
527                iced_container::Style {
528                    icon_color: Some(icon_color),
529                    text_color: Some(text_color),
530                    background: Some(iced::Background::Color(cosmic.background.base.into())),
531                    border: Border {
532                        radius: [
533                            if *sharp_corners {
534                                cosmic.corner_radii.radius_0[0]
535                            } else {
536                                window_corner_radius[0]
537                            },
538                            if *sharp_corners {
539                                cosmic.corner_radii.radius_0[1]
540                            } else {
541                                window_corner_radius[1]
542                            },
543                            cosmic.corner_radii.radius_0[2],
544                            cosmic.corner_radii.radius_0[3],
545                        ]
546                        .into(),
547                        ..Default::default()
548                    },
549                    shadow: Shadow::default(),
550                }
551            }
552
553            Container::ContextDrawer => {
554                let mut a = Container::primary(cosmic);
555
556                if cosmic.is_high_contrast {
557                    a.border.width = 1.;
558                    a.border.color = cosmic.primary.divider.into();
559                }
560                a
561            }
562
563            Container::Background => Container::background(cosmic),
564
565            Container::Primary => Container::primary(cosmic),
566
567            Container::Secondary => Container::secondary(cosmic),
568
569            Container::Dropdown => iced_container::Style {
570                icon_color: None,
571                text_color: None,
572                background: Some(iced::Background::Color(cosmic.bg_component_color().into())),
573                border: Border {
574                    color: cosmic.bg_component_divider().into(),
575                    width: 1.0,
576                    radius: cosmic.corner_radii.radius_s.into(),
577                },
578                shadow: Shadow::default(),
579            },
580
581            Container::Tooltip => iced_container::Style {
582                icon_color: None,
583                text_color: None,
584                background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())),
585                border: Border {
586                    radius: cosmic.corner_radii.radius_l.into(),
587                    ..Default::default()
588                },
589                shadow: Shadow::default(),
590            },
591
592            Container::Card => {
593                let cosmic = self.cosmic();
594
595                match self.layer {
596                    cosmic_theme::Layer::Background => iced_container::Style {
597                        icon_color: Some(Color::from(cosmic.background.component.on)),
598                        text_color: Some(Color::from(cosmic.background.component.on)),
599                        background: Some(iced::Background::Color(
600                            cosmic.background.component.base.into(),
601                        )),
602                        border: Border {
603                            radius: cosmic.corner_radii.radius_s.into(),
604                            ..Default::default()
605                        },
606                        shadow: Shadow::default(),
607                    },
608                    cosmic_theme::Layer::Primary => iced_container::Style {
609                        icon_color: Some(Color::from(cosmic.primary.component.on)),
610                        text_color: Some(Color::from(cosmic.primary.component.on)),
611                        background: Some(iced::Background::Color(
612                            cosmic.primary.component.base.into(),
613                        )),
614                        border: Border {
615                            radius: cosmic.corner_radii.radius_s.into(),
616                            ..Default::default()
617                        },
618                        shadow: Shadow::default(),
619                    },
620                    cosmic_theme::Layer::Secondary => iced_container::Style {
621                        icon_color: Some(Color::from(cosmic.secondary.component.on)),
622                        text_color: Some(Color::from(cosmic.secondary.component.on)),
623                        background: Some(iced::Background::Color(
624                            cosmic.secondary.component.base.into(),
625                        )),
626                        border: Border {
627                            radius: cosmic.corner_radii.radius_s.into(),
628                            ..Default::default()
629                        },
630                        shadow: Shadow::default(),
631                    },
632                }
633            }
634
635            Container::Dialog => iced_container::Style {
636                icon_color: Some(Color::from(cosmic.primary.on)),
637                text_color: Some(Color::from(cosmic.primary.on)),
638                background: Some(iced::Background::Color(cosmic.primary.base.into())),
639                border: Border {
640                    color: cosmic.primary.divider.into(),
641                    width: 1.0,
642                    radius: cosmic.corner_radii.radius_m.into(),
643                },
644                shadow: Shadow {
645                    color: cosmic.shade.into(),
646                    offset: Vector::new(0.0, 4.0),
647                    blur_radius: 16.0,
648                },
649            },
650        }
651    }
652}
653
654#[derive(Default)]
655pub enum Slider {
656    #[default]
657    Standard,
658    Custom {
659        active: Rc<dyn Fn(&Theme) -> slider::Style>,
660        hovered: Rc<dyn Fn(&Theme) -> slider::Style>,
661        dragging: Rc<dyn Fn(&Theme) -> slider::Style>,
662    },
663}
664
665/*
666 * Slider
667 */
668impl slider::Catalog for Theme {
669    type Class<'a> = Slider;
670
671    fn default<'a>() -> Self::Class<'a> {
672        Slider::default()
673    }
674
675    fn style(&self, class: &Self::Class<'_>, status: slider::Status) -> slider::Style {
676        let cosmic: &cosmic_theme::Theme = self.cosmic();
677        let hc = self.theme_type.is_high_contrast();
678        let is_dark = self.theme_type.is_dark();
679
680        let mut appearance = match class {
681            Slider::Standard =>
682            //TODO: no way to set rail thickness
683            {
684                let (active_track, inactive_track) = if hc {
685                    (
686                        cosmic.accent_text_color(),
687                        if is_dark {
688                            cosmic.palette.neutral_5
689                        } else {
690                            cosmic.palette.neutral_3
691                        },
692                    )
693                } else {
694                    (cosmic.accent.base, cosmic.palette.neutral_6)
695                };
696                slider::Style {
697                    rail: Rail {
698                        backgrounds: (
699                            Background::Color(active_track.into()),
700                            Background::Color(inactive_track.into()),
701                        ),
702                        border: Border {
703                            radius: cosmic.corner_radii.radius_xs.into(),
704                            color: if hc && !is_dark {
705                                self.current_container().component.border.into()
706                            } else {
707                                Color::TRANSPARENT
708                            },
709                            width: if hc && !is_dark { 1. } else { 0. },
710                        },
711                        width: 4.0,
712                    },
713
714                    handle: slider::Handle {
715                        shape: slider::HandleShape::Rectangle {
716                            height: 20,
717                            width: 20,
718                            border_radius: cosmic.corner_radii.radius_m.into(),
719                        },
720                        border_color: Color::TRANSPARENT,
721                        border_width: 0.0,
722                        background: Background::Color(cosmic.accent.base.into()),
723                    },
724
725                    breakpoint: slider::Breakpoint {
726                        color: cosmic.on_bg_color().into(),
727                    },
728                }
729            }
730            Slider::Custom { active, .. } => active(self),
731        };
732        match status {
733            slider::Status::Active => appearance,
734            slider::Status::Hovered => match class {
735                Slider::Standard => {
736                    appearance.handle.shape = slider::HandleShape::Rectangle {
737                        height: 26,
738                        width: 26,
739                        border_radius: cosmic.corner_radii.radius_m.into(),
740                    };
741                    appearance.handle.border_width = 3.0;
742                    appearance.handle.border_color =
743                        self.cosmic().palette.neutral_10.with_alpha(0.1).into();
744                    appearance
745                }
746                Slider::Custom { hovered, .. } => hovered(self),
747            },
748            slider::Status::Dragged => match class {
749                Slider::Standard => {
750                    let mut style = {
751                        appearance.handle.shape = slider::HandleShape::Rectangle {
752                            height: 26,
753                            width: 26,
754                            border_radius: cosmic.corner_radii.radius_m.into(),
755                        };
756                        appearance.handle.border_width = 3.0;
757                        appearance.handle.border_color =
758                            self.cosmic().palette.neutral_10.with_alpha(0.1).into();
759                        appearance
760                    };
761                    style.handle.border_color =
762                        self.cosmic().palette.neutral_10.with_alpha(0.2).into();
763                    style
764                }
765                Slider::Custom { dragging, .. } => dragging(self),
766            },
767        }
768    }
769}
770
771impl menu::Catalog for Theme {
772    type Class<'a> = ();
773
774    fn default<'a>() -> <Self as menu::Catalog>::Class<'a> {}
775
776    fn style(&self, class: &<Self as menu::Catalog>::Class<'_>) -> menu::Style {
777        let cosmic = self.cosmic();
778
779        menu::Style {
780            text_color: cosmic.on_bg_color().into(),
781            background: Background::Color(cosmic.background.base.into()),
782            border: Border {
783                radius: cosmic.corner_radii.radius_m.into(),
784                ..Default::default()
785            },
786            selected_text_color: cosmic.accent_text_color().into(),
787            selected_background: Background::Color(cosmic.background.component.hover.into()),
788        }
789    }
790}
791
792impl pick_list::Catalog for Theme {
793    type Class<'a> = ();
794
795    fn default<'a>() -> <Self as pick_list::Catalog>::Class<'a> {}
796
797    fn style(
798        &self,
799        class: &<Self as pick_list::Catalog>::Class<'_>,
800        status: pick_list::Status,
801    ) -> pick_list::Style {
802        let cosmic = &self.cosmic();
803        let hc = cosmic.is_high_contrast;
804        let appearance = pick_list::Style {
805            text_color: cosmic.on_bg_color().into(),
806            background: Color::TRANSPARENT.into(),
807            placeholder_color: cosmic.on_bg_color().into(),
808            border: Border {
809                radius: cosmic.corner_radii.radius_m.into(),
810                width: if hc { 1. } else { 0. },
811                color: if hc {
812                    self.current_container().component.border.into()
813                } else {
814                    Color::TRANSPARENT
815                },
816            },
817            // icon_size: 0.7, // TODO: how to replace
818            handle_color: cosmic.on_bg_color().into(),
819        };
820
821        match status {
822            pick_list::Status::Active => appearance,
823            pick_list::Status::Hovered => pick_list::Style {
824                background: Background::Color(cosmic.background.base.into()),
825                ..appearance
826            },
827            pick_list::Status::Opened => appearance,
828        }
829    }
830}
831
832/*
833 * TODO: Radio
834 */
835impl radio::Catalog for Theme {
836    type Class<'a> = ();
837
838    fn default<'a>() -> Self::Class<'a> {}
839
840    fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style {
841        let cur_container = self.current_container();
842        let theme = self.cosmic();
843
844        match status {
845            radio::Status::Active { is_selected } => radio::Style {
846                background: if is_selected {
847                    Color::from(theme.accent.base).into()
848                } else {
849                    // TODO: this seems to be defined weirdly in FIGMA
850                    Color::from(cur_container.small_widget).into()
851                },
852                dot_color: theme.accent.on.into(),
853                border_width: 1.0,
854                border_color: if is_selected {
855                    Color::from(theme.accent.base)
856                } else {
857                    Color::from(theme.palette.neutral_8)
858                },
859                text_color: None,
860            },
861            radio::Status::Hovered { is_selected } => {
862                let bg = if is_selected {
863                    theme.accent.base
864                } else {
865                    self.current_container().small_widget
866                };
867                // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables.
868                let hovered_bg = Color::from(over(theme.palette.neutral_0.with_alpha(0.1), bg));
869                radio::Style {
870                    background: hovered_bg.into(),
871                    dot_color: theme.accent.on.into(),
872                    border_width: 1.0,
873                    border_color: if is_selected {
874                        Color::from(theme.accent.base)
875                    } else {
876                        Color::from(theme.palette.neutral_8)
877                    },
878                    text_color: None,
879                }
880            }
881        }
882    }
883}
884
885/*
886 * Toggler
887 */
888impl toggler::Catalog for Theme {
889    type Class<'a> = ();
890
891    fn default<'a>() -> Self::Class<'a> {}
892
893    fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style {
894        let cosmic = self.cosmic();
895        const HANDLE_MARGIN: f32 = 2.0;
896        let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1);
897
898        let mut active = toggler::Style {
899            background: if matches!(status, toggler::Status::Active { is_toggled: true }) {
900                cosmic.accent.base.into()
901            } else if cosmic.is_dark {
902                cosmic.palette.neutral_6.into()
903            } else {
904                cosmic.palette.neutral_5.into()
905            },
906            foreground: cosmic.palette.neutral_2.into(),
907            border_radius: cosmic.radius_xl().into(),
908            handle_radius: cosmic
909                .radius_xl()
910                .map(|x| (x - HANDLE_MARGIN).max(0.0))
911                .into(),
912            handle_margin: HANDLE_MARGIN,
913            background_border_width: 0.0,
914            background_border_color: Color::TRANSPARENT,
915            foreground_border_width: 0.0,
916            foreground_border_color: Color::TRANSPARENT,
917        };
918        match status {
919            toggler::Status::Active { is_toggled } => active,
920            toggler::Status::Hovered { is_toggled } => {
921                let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true });
922                toggler::Style {
923                    background: if is_active {
924                        over(neutral_10, cosmic.accent_color())
925                    } else {
926                        over(
927                            neutral_10,
928                            if cosmic.is_dark {
929                                cosmic.palette.neutral_6
930                            } else {
931                                cosmic.palette.neutral_5
932                            },
933                        )
934                    }
935                    .into(),
936                    ..active
937                }
938            }
939            toggler::Status::Disabled => {
940                active.background.a /= 2.;
941                active.foreground.a /= 2.;
942                active
943            }
944        }
945    }
946}
947
948/*
949 * TODO: Pane Grid
950 */
951impl pane_grid::Catalog for Theme {
952    type Class<'a> = ();
953
954    fn default<'a>() -> <Self as pane_grid::Catalog>::Class<'a> {}
955
956    fn style(&self, class: &<Self as pane_grid::Catalog>::Class<'_>) -> pane_grid::Style {
957        let theme = self.cosmic();
958
959        pane_grid::Style {
960            hovered_region: Highlight {
961                background: Background::Color(theme.bg_color().into()),
962                border: Border {
963                    radius: theme.corner_radii.radius_0.into(),
964                    width: 2.0,
965                    color: theme.bg_divider().into(),
966                },
967            },
968            picked_split: pane_grid::Line {
969                color: theme.accent.base.into(),
970                width: 2.0,
971            },
972            hovered_split: pane_grid::Line {
973                color: theme.accent.hover.into(),
974                width: 2.0,
975            },
976        }
977    }
978}
979
980/*
981 * TODO: Progress Bar
982 */
983#[derive(Default)]
984pub enum ProgressBar {
985    #[default]
986    Primary,
987    Success,
988    Danger,
989    Custom(Box<dyn Fn(&Theme) -> progress_bar::Style>),
990}
991
992impl ProgressBar {
993    pub fn custom<F: Fn(&Theme) -> progress_bar::Style + 'static>(f: F) -> Self {
994        Self::Custom(Box::new(f))
995    }
996}
997
998impl progress_bar::Catalog for Theme {
999    type Class<'a> = ProgressBar;
1000
1001    fn default<'a>() -> Self::Class<'a> {
1002        ProgressBar::default()
1003    }
1004
1005    fn style(&self, class: &Self::Class<'_>) -> progress_bar::Style {
1006        let theme = self.cosmic();
1007
1008        let (active_track, inactive_track) = if theme.is_high_contrast {
1009            (
1010                theme.accent_text_color(),
1011                if theme.is_dark {
1012                    theme.palette.neutral_6
1013                } else {
1014                    theme.palette.neutral_4
1015                },
1016            )
1017        } else {
1018            (theme.accent.base, theme.background.divider)
1019        };
1020        let border = Border {
1021            radius: theme.corner_radii.radius_xl.into(),
1022            color: if theme.is_high_contrast && !theme.is_dark {
1023                self.current_container().component.border.into()
1024            } else {
1025                Color::TRANSPARENT
1026            },
1027            width: if theme.is_high_contrast && !theme.is_dark {
1028                1.
1029            } else {
1030                0.
1031            },
1032        };
1033        match class {
1034            ProgressBar::Primary => progress_bar::Style {
1035                background: Color::from(inactive_track).into(),
1036                bar: Color::from(active_track).into(),
1037                border,
1038            },
1039            ProgressBar::Success => progress_bar::Style {
1040                background: Color::from(inactive_track).into(),
1041                bar: Color::from(theme.success.base).into(),
1042                border,
1043            },
1044            ProgressBar::Danger => progress_bar::Style {
1045                background: Color::from(inactive_track).into(),
1046                bar: Color::from(theme.destructive.base).into(),
1047                border,
1048            },
1049            ProgressBar::Custom(f) => f(self),
1050        }
1051    }
1052}
1053
1054/*
1055 * TODO: Rule
1056 */
1057#[derive(Default)]
1058pub enum Rule {
1059    #[default]
1060    Default,
1061    LightDivider,
1062    HeavyDivider,
1063    Custom(Box<dyn Fn(&Theme) -> rule::Style>),
1064}
1065
1066impl Rule {
1067    pub fn custom<F: Fn(&Theme) -> rule::Style + 'static>(f: F) -> Self {
1068        Self::Custom(Box::new(f))
1069    }
1070}
1071
1072impl rule::Catalog for Theme {
1073    type Class<'a> = Rule;
1074
1075    fn default<'a>() -> Self::Class<'a> {
1076        Rule::default()
1077    }
1078
1079    fn style(&self, class: &Self::Class<'_>) -> rule::Style {
1080        match class {
1081            Rule::Default => rule::Style {
1082                color: self.current_container().divider.into(),
1083                width: 1,
1084                radius: 0.0.into(),
1085                fill_mode: rule::FillMode::Full,
1086            },
1087            Rule::LightDivider => rule::Style {
1088                color: self.current_container().divider.into(),
1089                width: 1,
1090                radius: 0.0.into(),
1091                fill_mode: rule::FillMode::Padded(8),
1092            },
1093            Rule::HeavyDivider => rule::Style {
1094                color: self.current_container().divider.into(),
1095                width: 4,
1096                radius: 2.0.into(),
1097                fill_mode: rule::FillMode::Full,
1098            },
1099            Rule::Custom(f) => f(self),
1100        }
1101    }
1102}
1103
1104#[derive(Default, Clone, Copy)]
1105pub enum Scrollable {
1106    #[default]
1107    Permanent,
1108    Minimal,
1109}
1110
1111/*
1112 * TODO: Scrollable
1113 */
1114impl scrollable::Catalog for Theme {
1115    type Class<'a> = Scrollable;
1116
1117    fn default<'a>() -> Self::Class<'a> {
1118        Scrollable::default()
1119    }
1120
1121    fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style {
1122        match status {
1123            scrollable::Status::Active => {
1124                let cosmic = self.cosmic();
1125                let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7);
1126                let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7);
1127                let mut a = scrollable::Style {
1128                    container: iced_container::transparent(self),
1129                    vertical_rail: scrollable::Rail {
1130                        border: Border {
1131                            radius: cosmic.corner_radii.radius_s.into(),
1132                            ..Default::default()
1133                        },
1134                        background: None,
1135                        scroller: scrollable::Scroller {
1136                            color: if cosmic.is_dark {
1137                                neutral_6.into()
1138                            } else {
1139                                neutral_5.into()
1140                            },
1141                            border: Border {
1142                                radius: cosmic.corner_radii.radius_s.into(),
1143                                ..Default::default()
1144                            },
1145                        },
1146                    },
1147                    horizontal_rail: scrollable::Rail {
1148                        border: Border {
1149                            radius: cosmic.corner_radii.radius_s.into(),
1150                            ..Default::default()
1151                        },
1152                        background: None,
1153                        scroller: scrollable::Scroller {
1154                            color: if cosmic.is_dark {
1155                                neutral_6.into()
1156                            } else {
1157                                neutral_5.into()
1158                            },
1159                            border: Border {
1160                                radius: cosmic.corner_radii.radius_s.into(),
1161                                ..Default::default()
1162                            },
1163                        },
1164                    },
1165                    gap: None,
1166                };
1167                let small_widget_container = self.current_container().small_widget.with_alpha(0.7);
1168
1169                if matches!(class, Scrollable::Permanent) {
1170                    a.horizontal_rail.background =
1171                        Some(Background::Color(small_widget_container.into()));
1172                    a.vertical_rail.background =
1173                        Some(Background::Color(small_widget_container.into()));
1174                }
1175
1176                a
1177            }
1178            // TODO handle vertical / horizontal
1179            scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => {
1180                let cosmic = self.cosmic();
1181                let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7);
1182                let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7);
1183
1184                // if is_mouse_over_scrollbar {
1185                //     let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2);
1186                //     neutral_5 = over(hover_overlay, neutral_5);
1187                // }
1188                let mut a: scrollable::Style = scrollable::Style {
1189                    container: iced_container::Style::default(),
1190                    vertical_rail: scrollable::Rail {
1191                        border: Border {
1192                            radius: cosmic.corner_radii.radius_s.into(),
1193                            ..Default::default()
1194                        },
1195                        background: None,
1196                        scroller: scrollable::Scroller {
1197                            color: if cosmic.is_dark {
1198                                neutral_6.into()
1199                            } else {
1200                                neutral_5.into()
1201                            },
1202                            border: Border {
1203                                radius: cosmic.corner_radii.radius_s.into(),
1204                                ..Default::default()
1205                            },
1206                        },
1207                    },
1208                    horizontal_rail: scrollable::Rail {
1209                        border: Border {
1210                            radius: cosmic.corner_radii.radius_s.into(),
1211                            ..Default::default()
1212                        },
1213                        background: None,
1214                        scroller: scrollable::Scroller {
1215                            color: if cosmic.is_dark {
1216                                neutral_6.into()
1217                            } else {
1218                                neutral_5.into()
1219                            },
1220                            border: Border {
1221                                radius: cosmic.corner_radii.radius_s.into(),
1222                                ..Default::default()
1223                            },
1224                        },
1225                    },
1226                    gap: None,
1227                };
1228
1229                if matches!(class, Scrollable::Permanent) {
1230                    let small_widget_container =
1231                        self.current_container().small_widget.with_alpha(0.7);
1232
1233                    a.horizontal_rail.background =
1234                        Some(Background::Color(small_widget_container.into()));
1235                    a.vertical_rail.background =
1236                        Some(Background::Color(small_widget_container.into()));
1237                }
1238
1239                a
1240            }
1241        }
1242    }
1243}
1244
1245#[derive(Clone, Default)]
1246pub enum Svg {
1247    /// Apply a custom appearance filter
1248    Custom(Rc<dyn Fn(&Theme) -> svg::Style>),
1249    /// No filtering is applied
1250    #[default]
1251    Default,
1252}
1253
1254impl Svg {
1255    pub fn custom<F: Fn(&Theme) -> svg::Style + 'static>(f: F) -> Self {
1256        Self::Custom(Rc::new(f))
1257    }
1258}
1259
1260impl svg::Catalog for Theme {
1261    type Class<'a> = Svg;
1262
1263    fn default<'a>() -> Self::Class<'a> {
1264        Svg::default()
1265    }
1266
1267    fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style {
1268        #[allow(clippy::match_same_arms)]
1269        match class {
1270            Svg::Default => svg::Style::default(),
1271            Svg::Custom(appearance) => appearance(self),
1272        }
1273    }
1274}
1275
1276/*
1277 * TODO: Text
1278 */
1279#[derive(Clone, Copy, Default)]
1280pub enum Text {
1281    Accent,
1282    #[default]
1283    Default,
1284    Color(Color),
1285    // TODO: Can't use dyn Fn since this must be copy
1286    Custom(fn(&Theme) -> iced_widget::text::Style),
1287}
1288
1289impl From<Color> for Text {
1290    fn from(color: Color) -> Self {
1291        Self::Color(color)
1292    }
1293}
1294
1295impl iced_widget::text::Catalog for Theme {
1296    type Class<'a> = Text;
1297
1298    fn default<'a>() -> Self::Class<'a> {
1299        Text::default()
1300    }
1301
1302    fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style {
1303        match class {
1304            Text::Accent => iced_widget::text::Style {
1305                color: Some(self.cosmic().accent_text_color().into()),
1306            },
1307            Text::Default => iced_widget::text::Style { color: None },
1308            Text::Color(c) => iced_widget::text::Style { color: Some(*c) },
1309            Text::Custom(f) => f(self),
1310        }
1311    }
1312}
1313
1314#[derive(Copy, Clone, Default)]
1315pub enum TextInput {
1316    #[default]
1317    Default,
1318    Search,
1319}
1320
1321/*
1322 * TODO: Text Input
1323 */
1324impl text_input::Catalog for Theme {
1325    type Class<'a> = TextInput;
1326
1327    fn default<'a>() -> Self::Class<'a> {
1328        TextInput::default()
1329    }
1330
1331    fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style {
1332        let palette = self.cosmic();
1333        let bg = self.current_container().small_widget.with_alpha(0.25);
1334
1335        let neutral_9 = palette.palette.neutral_9;
1336        let value = neutral_9.into();
1337        let placeholder = neutral_9.with_alpha(0.7).into();
1338        let selection = palette.accent.base.into();
1339
1340        let mut appearance = match class {
1341            TextInput::Default => text_input::Style {
1342                background: Color::from(bg).into(),
1343                border: Border {
1344                    radius: palette.corner_radii.radius_s.into(),
1345                    width: 1.0,
1346                    color: self.current_container().component.divider.into(),
1347                },
1348                icon: self.current_container().on.into(),
1349                placeholder,
1350                value,
1351                selection,
1352            },
1353            TextInput::Search => text_input::Style {
1354                background: Color::from(bg).into(),
1355                border: Border {
1356                    radius: palette.corner_radii.radius_m.into(),
1357                    ..Default::default()
1358                },
1359                icon: self.current_container().on.into(),
1360                placeholder,
1361                value,
1362                selection,
1363            },
1364        };
1365
1366        match status {
1367            text_input::Status::Active => appearance,
1368            text_input::Status::Hovered => {
1369                let bg = self.current_container().small_widget.with_alpha(0.25);
1370
1371                match class {
1372                    TextInput::Default => text_input::Style {
1373                        background: Color::from(bg).into(),
1374                        border: Border {
1375                            radius: palette.corner_radii.radius_s.into(),
1376                            width: 1.0,
1377                            color: self.current_container().on.into(),
1378                        },
1379                        icon: self.current_container().on.into(),
1380                        placeholder,
1381                        value,
1382                        selection,
1383                    },
1384                    TextInput::Search => text_input::Style {
1385                        background: Color::from(bg).into(),
1386                        border: Border {
1387                            radius: palette.corner_radii.radius_m.into(),
1388                            ..Default::default()
1389                        },
1390                        icon: self.current_container().on.into(),
1391                        placeholder,
1392                        value,
1393                        selection,
1394                    },
1395                }
1396            }
1397            text_input::Status::Focused => {
1398                let bg = self.current_container().small_widget.with_alpha(0.25);
1399
1400                match class {
1401                    TextInput::Default => text_input::Style {
1402                        background: Color::from(bg).into(),
1403                        border: Border {
1404                            radius: palette.corner_radii.radius_s.into(),
1405                            width: 1.0,
1406                            color: palette.accent.base.into(),
1407                        },
1408                        icon: self.current_container().on.into(),
1409                        placeholder,
1410                        value,
1411                        selection,
1412                    },
1413                    TextInput::Search => text_input::Style {
1414                        background: Color::from(bg).into(),
1415                        border: Border {
1416                            radius: palette.corner_radii.radius_m.into(),
1417                            ..Default::default()
1418                        },
1419                        icon: self.current_container().on.into(),
1420                        placeholder,
1421                        value,
1422                        selection,
1423                    },
1424                }
1425            }
1426            text_input::Status::Disabled => {
1427                appearance.background = match appearance.background {
1428                    Background::Color(color) => Background::Color(Color {
1429                        a: color.a * 0.5,
1430                        ..color
1431                    }),
1432                    Background::Gradient(gradient) => {
1433                        Background::Gradient(gradient.scale_alpha(0.5))
1434                    }
1435                };
1436                appearance.border.color.a /= 2.;
1437                appearance.icon.a /= 2.;
1438                appearance.placeholder.a /= 2.;
1439                appearance.value.a /= 2.;
1440                appearance
1441            }
1442        }
1443    }
1444}
1445
1446#[derive(Default)]
1447pub enum TextEditor<'a> {
1448    #[default]
1449    Default,
1450    Custom(text_editor::StyleFn<'a, Theme>),
1451}
1452
1453impl iced_widget::text_editor::Catalog for Theme {
1454    type Class<'a> = TextEditor<'a>;
1455
1456    fn default<'a>() -> Self::Class<'a> {
1457        TextEditor::default()
1458    }
1459
1460    fn style(
1461        &self,
1462        class: &Self::Class<'_>,
1463        status: iced_widget::text_editor::Status,
1464    ) -> iced_widget::text_editor::Style {
1465        if let TextEditor::Custom(style) = class {
1466            return style(self, status);
1467        }
1468
1469        let cosmic = self.cosmic();
1470
1471        let selection = cosmic.accent.base.into();
1472        let value = cosmic.palette.neutral_9.into();
1473        let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into();
1474        let icon = cosmic.background.on.into();
1475
1476        match status {
1477            iced_widget::text_editor::Status::Active
1478            | iced_widget::text_editor::Status::Hovered
1479            | iced_widget::text_editor::Status::Disabled => iced_widget::text_editor::Style {
1480                background: iced::Color::from(cosmic.bg_color()).into(),
1481                border: Border {
1482                    radius: cosmic.corner_radii.radius_0.into(),
1483                    width: f32::from(cosmic.space_xxxs()),
1484                    color: iced::Color::from(cosmic.bg_divider()),
1485                },
1486                icon,
1487                placeholder,
1488                value,
1489                selection,
1490            },
1491            iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style {
1492                background: iced::Color::from(cosmic.bg_color()).into(),
1493                border: Border {
1494                    radius: cosmic.corner_radii.radius_0.into(),
1495                    width: f32::from(cosmic.space_xxxs()),
1496                    color: iced::Color::from(cosmic.accent.base),
1497                },
1498                icon,
1499                placeholder,
1500                value,
1501                selection,
1502            },
1503        }
1504    }
1505}
1506
1507#[cfg(feature = "markdown")]
1508impl iced_widget::markdown::Catalog for Theme {
1509    fn code_block<'a>() -> <Self as iced_container::Catalog>::Class<'a> {
1510        Container::custom(|_| iced_container::Style {
1511            background: Some(iced::color!(0x111111).into()),
1512            text_color: Some(Color::WHITE),
1513            border: iced::border::rounded(2),
1514            ..iced_container::Style::default()
1515        })
1516    }
1517}
1518
1519#[cfg(feature = "qr_code")]
1520impl iced_widget::qr_code::Catalog for Theme {
1521    type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>;
1522
1523    fn default<'a>() -> Self::Class<'a> {
1524        Box::new(|_theme| iced_widget::qr_code::Style {
1525            cell: Color::BLACK,
1526            background: Color::WHITE,
1527        })
1528    }
1529
1530    fn style(&self, class: &Self::Class<'_>) -> iced_widget::qr_code::Style {
1531        class(self)
1532    }
1533}
1534
1535impl combo_box::Catalog for Theme {}