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) -> &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    },
399    List,
400    Primary,
401    Secondary,
402    Tooltip,
403    #[default]
404    Transparent,
405}
406
407impl<'a> Container<'a> {
408    pub fn custom<F: Fn(&Theme) -> iced_container::Style + 'a>(f: F) -> Self {
409        Self::Custom(Box::new(f))
410    }
411
412    #[must_use]
413    pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style {
414        iced_container::Style {
415            icon_color: Some(Color::from(theme.background.on)),
416            text_color: Some(Color::from(theme.background.on)),
417            background: Some(iced::Background::Color(theme.background.base.into())),
418            border: Border {
419                radius: theme.corner_radii.radius_s.into(),
420                ..Default::default()
421            },
422            shadow: Shadow::default(),
423        }
424    }
425
426    #[must_use]
427    pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style {
428        iced_container::Style {
429            icon_color: Some(Color::from(theme.primary.on)),
430            text_color: Some(Color::from(theme.primary.on)),
431            background: Some(iced::Background::Color(theme.primary.base.into())),
432            border: Border {
433                radius: theme.corner_radii.radius_s.into(),
434                ..Default::default()
435            },
436            shadow: Shadow::default(),
437        }
438    }
439
440    #[must_use]
441    pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style {
442        iced_container::Style {
443            icon_color: Some(Color::from(theme.secondary.on)),
444            text_color: Some(Color::from(theme.secondary.on)),
445            background: Some(iced::Background::Color(theme.secondary.base.into())),
446            border: Border {
447                radius: theme.corner_radii.radius_s.into(),
448                ..Default::default()
449            },
450            shadow: Shadow::default(),
451        }
452    }
453}
454
455impl<'a> From<iced_container::StyleFn<'a, Theme>> for Container<'a> {
456    fn from(value: iced_container::StyleFn<'a, Theme>) -> Self {
457        Self::custom(value)
458    }
459}
460
461impl iced_container::Catalog for Theme {
462    type Class<'a> = Container<'a>;
463
464    fn default<'a>() -> Self::Class<'a> {
465        Container::default()
466    }
467
468    fn style(&self, class: &Self::Class<'_>) -> iced_container::Style {
469        let cosmic = self.cosmic();
470
471        // Ensures visually aligned radii for content and window corners
472        let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
473
474        match class {
475            Container::Transparent => iced_container::Style::default(),
476
477            Container::Custom(f) => f(self),
478
479            Container::WindowBackground => iced_container::Style {
480                icon_color: Some(Color::from(cosmic.background.on)),
481                text_color: Some(Color::from(cosmic.background.on)),
482                background: Some(iced::Background::Color(cosmic.background.base.into())),
483                border: Border {
484                    radius: [
485                        cosmic.corner_radii.radius_0[0],
486                        cosmic.corner_radii.radius_0[1],
487                        window_corner_radius[2],
488                        window_corner_radius[3],
489                    ]
490                    .into(),
491                    ..Default::default()
492                },
493                shadow: Shadow::default(),
494            },
495
496            Container::List => {
497                let component = &self.current_container().component;
498                iced_container::Style {
499                    icon_color: Some(component.on.into()),
500                    text_color: Some(component.on.into()),
501                    background: Some(Background::Color(component.base.into())),
502                    border: iced::Border {
503                        radius: cosmic.corner_radii.radius_s.into(),
504                        ..Default::default()
505                    },
506                    shadow: Shadow::default(),
507                }
508            }
509
510            Container::HeaderBar { focused } => {
511                let (icon_color, text_color) = if *focused {
512                    (
513                        Color::from(cosmic.accent_text_color()),
514                        Color::from(cosmic.background.on),
515                    )
516                } else {
517                    use crate::ext::ColorExt;
518                    let unfocused_color = Color::from(cosmic.background.component.on)
519                        .blend_alpha(cosmic.background.base.into(), 0.5);
520                    (unfocused_color, unfocused_color)
521                };
522
523                iced_container::Style {
524                    icon_color: Some(icon_color),
525                    text_color: Some(text_color),
526                    background: Some(iced::Background::Color(cosmic.background.base.into())),
527                    border: Border {
528                        radius: [
529                            window_corner_radius[0],
530                            window_corner_radius[1],
531                            cosmic.corner_radii.radius_0[2],
532                            cosmic.corner_radii.radius_0[3],
533                        ]
534                        .into(),
535                        ..Default::default()
536                    },
537                    shadow: Shadow::default(),
538                }
539            }
540
541            Container::ContextDrawer => {
542                let mut a = Container::primary(cosmic);
543
544                if cosmic.is_high_contrast {
545                    a.border.width = 1.;
546                    a.border.color = cosmic.primary.divider.into();
547                }
548                a
549            }
550
551            Container::Background => Container::background(cosmic),
552
553            Container::Primary => Container::primary(cosmic),
554
555            Container::Secondary => Container::secondary(cosmic),
556
557            Container::Dropdown => iced_container::Style {
558                icon_color: None,
559                text_color: None,
560                background: Some(iced::Background::Color(cosmic.bg_component_color().into())),
561                border: Border {
562                    color: cosmic.bg_component_divider().into(),
563                    width: 1.0,
564                    radius: cosmic.corner_radii.radius_s.into(),
565                },
566                shadow: Shadow::default(),
567            },
568
569            Container::Tooltip => iced_container::Style {
570                icon_color: None,
571                text_color: None,
572                background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())),
573                border: Border {
574                    radius: cosmic.corner_radii.radius_l.into(),
575                    ..Default::default()
576                },
577                shadow: Shadow::default(),
578            },
579
580            Container::Card => {
581                let cosmic = self.cosmic();
582
583                match self.layer {
584                    cosmic_theme::Layer::Background => iced_container::Style {
585                        icon_color: Some(Color::from(cosmic.background.component.on)),
586                        text_color: Some(Color::from(cosmic.background.component.on)),
587                        background: Some(iced::Background::Color(
588                            cosmic.background.component.base.into(),
589                        )),
590                        border: Border {
591                            radius: cosmic.corner_radii.radius_s.into(),
592                            ..Default::default()
593                        },
594                        shadow: Shadow::default(),
595                    },
596                    cosmic_theme::Layer::Primary => iced_container::Style {
597                        icon_color: Some(Color::from(cosmic.primary.component.on)),
598                        text_color: Some(Color::from(cosmic.primary.component.on)),
599                        background: Some(iced::Background::Color(
600                            cosmic.primary.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::Secondary => iced_container::Style {
609                        icon_color: Some(Color::from(cosmic.secondary.component.on)),
610                        text_color: Some(Color::from(cosmic.secondary.component.on)),
611                        background: Some(iced::Background::Color(
612                            cosmic.secondary.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                }
621            }
622
623            Container::Dialog => iced_container::Style {
624                icon_color: Some(Color::from(cosmic.primary.on)),
625                text_color: Some(Color::from(cosmic.primary.on)),
626                background: Some(iced::Background::Color(cosmic.primary.base.into())),
627                border: Border {
628                    color: cosmic.primary.divider.into(),
629                    width: 1.0,
630                    radius: cosmic.corner_radii.radius_m.into(),
631                },
632                shadow: Shadow {
633                    color: cosmic.shade.into(),
634                    offset: Vector::new(0.0, 4.0),
635                    blur_radius: 16.0,
636                },
637            },
638        }
639    }
640}
641
642#[derive(Default)]
643pub enum Slider {
644    #[default]
645    Standard,
646    Custom {
647        active: Rc<dyn Fn(&Theme) -> slider::Style>,
648        hovered: Rc<dyn Fn(&Theme) -> slider::Style>,
649        dragging: Rc<dyn Fn(&Theme) -> slider::Style>,
650    },
651}
652
653/*
654 * Slider
655 */
656impl slider::Catalog for Theme {
657    type Class<'a> = Slider;
658
659    fn default<'a>() -> Self::Class<'a> {
660        Slider::default()
661    }
662
663    fn style(&self, class: &Self::Class<'_>, status: slider::Status) -> slider::Style {
664        let cosmic: &cosmic_theme::Theme = self.cosmic();
665        let hc = self.theme_type.is_high_contrast();
666        let is_dark = self.theme_type.is_dark();
667
668        let mut appearance = match class {
669            Slider::Standard =>
670            //TODO: no way to set rail thickness
671            {
672                let (active_track, inactive_track) = if hc {
673                    (
674                        cosmic.accent_text_color(),
675                        if is_dark {
676                            cosmic.palette.neutral_5
677                        } else {
678                            cosmic.palette.neutral_3
679                        },
680                    )
681                } else {
682                    (cosmic.accent.base, cosmic.palette.neutral_6)
683                };
684                slider::Style {
685                    rail: Rail {
686                        backgrounds: (
687                            Background::Color(active_track.into()),
688                            Background::Color(inactive_track.into()),
689                        ),
690                        border: Border {
691                            radius: cosmic.corner_radii.radius_xs.into(),
692                            color: if hc && !is_dark {
693                                self.current_container().component.border.into()
694                            } else {
695                                Color::TRANSPARENT
696                            },
697                            width: if hc && !is_dark { 1. } else { 0. },
698                        },
699                        width: 4.0,
700                    },
701
702                    handle: slider::Handle {
703                        shape: slider::HandleShape::Rectangle {
704                            height: 20,
705                            width: 20,
706                            border_radius: cosmic.corner_radii.radius_m.into(),
707                        },
708                        border_color: Color::TRANSPARENT,
709                        border_width: 0.0,
710                        background: Background::Color(cosmic.accent.base.into()),
711                    },
712
713                    breakpoint: slider::Breakpoint {
714                        color: cosmic.on_bg_color().into(),
715                    },
716                }
717            }
718            Slider::Custom { active, .. } => active(self),
719        };
720        match status {
721            slider::Status::Active => appearance,
722            slider::Status::Hovered => match class {
723                Slider::Standard => {
724                    appearance.handle.shape = slider::HandleShape::Rectangle {
725                        height: 26,
726                        width: 26,
727                        border_radius: cosmic.corner_radii.radius_m.into(),
728                    };
729                    appearance.handle.border_width = 3.0;
730                    appearance.handle.border_color =
731                        self.cosmic().palette.neutral_10.with_alpha(0.1).into();
732                    appearance
733                }
734                Slider::Custom { hovered, .. } => hovered(self),
735            },
736            slider::Status::Dragged => match class {
737                Slider::Standard => {
738                    let mut style = {
739                        appearance.handle.shape = slider::HandleShape::Rectangle {
740                            height: 26,
741                            width: 26,
742                            border_radius: cosmic.corner_radii.radius_m.into(),
743                        };
744                        appearance.handle.border_width = 3.0;
745                        appearance.handle.border_color =
746                            self.cosmic().palette.neutral_10.with_alpha(0.1).into();
747                        appearance
748                    };
749                    style.handle.border_color =
750                        self.cosmic().palette.neutral_10.with_alpha(0.2).into();
751                    style
752                }
753                Slider::Custom { dragging, .. } => dragging(self),
754            },
755        }
756    }
757}
758
759impl menu::Catalog for Theme {
760    type Class<'a> = ();
761
762    fn default<'a>() -> <Self as menu::Catalog>::Class<'a> {}
763
764    fn style(&self, class: &<Self as menu::Catalog>::Class<'_>) -> menu::Style {
765        let cosmic = self.cosmic();
766
767        menu::Style {
768            text_color: cosmic.on_bg_color().into(),
769            background: Background::Color(cosmic.background.base.into()),
770            border: Border {
771                radius: cosmic.corner_radii.radius_m.into(),
772                ..Default::default()
773            },
774            selected_text_color: cosmic.accent_text_color().into(),
775            selected_background: Background::Color(cosmic.background.component.hover.into()),
776        }
777    }
778}
779
780impl pick_list::Catalog for Theme {
781    type Class<'a> = ();
782
783    fn default<'a>() -> <Self as pick_list::Catalog>::Class<'a> {}
784
785    fn style(
786        &self,
787        class: &<Self as pick_list::Catalog>::Class<'_>,
788        status: pick_list::Status,
789    ) -> pick_list::Style {
790        let cosmic = &self.cosmic();
791        let hc = cosmic.is_high_contrast;
792        let appearance = pick_list::Style {
793            text_color: cosmic.on_bg_color().into(),
794            background: Color::TRANSPARENT.into(),
795            placeholder_color: cosmic.on_bg_color().into(),
796            border: Border {
797                radius: cosmic.corner_radii.radius_m.into(),
798                width: if hc { 1. } else { 0. },
799                color: if hc {
800                    self.current_container().component.border.into()
801                } else {
802                    Color::TRANSPARENT
803                },
804            },
805            // icon_size: 0.7, // TODO: how to replace
806            handle_color: cosmic.on_bg_color().into(),
807        };
808
809        match status {
810            pick_list::Status::Active => appearance,
811            pick_list::Status::Hovered => pick_list::Style {
812                background: Background::Color(cosmic.background.base.into()),
813                ..appearance
814            },
815            pick_list::Status::Opened => appearance,
816        }
817    }
818}
819
820/*
821 * TODO: Radio
822 */
823impl radio::Catalog for Theme {
824    type Class<'a> = ();
825
826    fn default<'a>() -> Self::Class<'a> {}
827
828    fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style {
829        let cur_container = self.current_container();
830        let theme = self.cosmic();
831
832        match status {
833            radio::Status::Active { is_selected } => radio::Style {
834                background: if is_selected {
835                    Color::from(theme.accent.base).into()
836                } else {
837                    // TODO: this seems to be defined weirdly in FIGMA
838                    Color::from(cur_container.small_widget).into()
839                },
840                dot_color: theme.accent.on.into(),
841                border_width: 1.0,
842                border_color: if is_selected {
843                    Color::from(theme.accent.base)
844                } else {
845                    Color::from(theme.palette.neutral_8)
846                },
847                text_color: None,
848            },
849            radio::Status::Hovered { is_selected } => {
850                let bg = if is_selected {
851                    theme.accent.base
852                } else {
853                    self.current_container().small_widget
854                };
855                // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables.
856                let hovered_bg = Color::from(over(theme.palette.neutral_0.with_alpha(0.1), bg));
857                radio::Style {
858                    background: hovered_bg.into(),
859                    dot_color: theme.accent.on.into(),
860                    border_width: 1.0,
861                    border_color: if is_selected {
862                        Color::from(theme.accent.base)
863                    } else {
864                        Color::from(theme.palette.neutral_8)
865                    },
866                    text_color: None,
867                }
868            }
869        }
870    }
871}
872
873/*
874 * Toggler
875 */
876impl toggler::Catalog for Theme {
877    type Class<'a> = ();
878
879    fn default<'a>() -> Self::Class<'a> {}
880
881    fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style {
882        let cosmic = self.cosmic();
883        const HANDLE_MARGIN: f32 = 2.0;
884        let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1);
885
886        let mut active = toggler::Style {
887            background: if matches!(status, toggler::Status::Active { is_toggled: true }) {
888                cosmic.accent.base.into()
889            } else {
890                if cosmic.is_dark {
891                    cosmic.palette.neutral_6.into()
892                } else {
893                    cosmic.palette.neutral_5.into()
894                }
895            },
896            foreground: cosmic.palette.neutral_2.into(),
897            border_radius: cosmic.radius_xl().into(),
898            handle_radius: cosmic
899                .radius_xl()
900                .map(|x| (x - HANDLE_MARGIN).max(0.0))
901                .into(),
902            handle_margin: HANDLE_MARGIN,
903            background_border_width: 0.0,
904            background_border_color: Color::TRANSPARENT,
905            foreground_border_width: 0.0,
906            foreground_border_color: Color::TRANSPARENT,
907        };
908        match status {
909            toggler::Status::Active { is_toggled } => active,
910            toggler::Status::Hovered { is_toggled } => {
911                let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true });
912                toggler::Style {
913                    background: if is_active {
914                        over(neutral_10, cosmic.accent_color())
915                    } else {
916                        over(
917                            neutral_10,
918                            if cosmic.is_dark {
919                                cosmic.palette.neutral_6
920                            } else {
921                                cosmic.palette.neutral_5
922                            },
923                        )
924                    }
925                    .into(),
926                    ..active
927                }
928            }
929            toggler::Status::Disabled => {
930                active.background.a /= 2.;
931                active.foreground.a /= 2.;
932                active
933            }
934        }
935    }
936}
937
938/*
939 * TODO: Pane Grid
940 */
941impl pane_grid::Catalog for Theme {
942    type Class<'a> = ();
943
944    fn default<'a>() -> <Self as pane_grid::Catalog>::Class<'a> {}
945
946    fn style(&self, class: &<Self as pane_grid::Catalog>::Class<'_>) -> pane_grid::Style {
947        let theme = self.cosmic();
948
949        pane_grid::Style {
950            hovered_region: Highlight {
951                background: Background::Color(theme.bg_color().into()),
952                border: Border {
953                    radius: theme.corner_radii.radius_0.into(),
954                    width: 2.0,
955                    color: theme.bg_divider().into(),
956                },
957            },
958            picked_split: pane_grid::Line {
959                color: theme.accent.base.into(),
960                width: 2.0,
961            },
962            hovered_split: pane_grid::Line {
963                color: theme.accent.hover.into(),
964                width: 2.0,
965            },
966        }
967    }
968}
969
970/*
971 * TODO: Progress Bar
972 */
973#[derive(Default)]
974pub enum ProgressBar {
975    #[default]
976    Primary,
977    Success,
978    Danger,
979    Custom(Box<dyn Fn(&Theme) -> progress_bar::Style>),
980}
981
982impl ProgressBar {
983    pub fn custom<F: Fn(&Theme) -> progress_bar::Style + 'static>(f: F) -> Self {
984        Self::Custom(Box::new(f))
985    }
986}
987
988impl progress_bar::Catalog for Theme {
989    type Class<'a> = ProgressBar;
990
991    fn default<'a>() -> Self::Class<'a> {
992        ProgressBar::default()
993    }
994
995    fn style(&self, class: &Self::Class<'_>) -> progress_bar::Style {
996        let theme = self.cosmic();
997
998        let (active_track, inactive_track) = if theme.is_high_contrast {
999            (
1000                theme.accent_text_color(),
1001                if theme.is_dark {
1002                    theme.palette.neutral_6
1003                } else {
1004                    theme.palette.neutral_4
1005                },
1006            )
1007        } else {
1008            (theme.accent.base, theme.background.divider)
1009        };
1010        let border = Border {
1011            radius: theme.corner_radii.radius_xl.into(),
1012            color: if theme.is_high_contrast && !theme.is_dark {
1013                self.current_container().component.border.into()
1014            } else {
1015                Color::TRANSPARENT
1016            },
1017            width: if theme.is_high_contrast && !theme.is_dark {
1018                1.
1019            } else {
1020                0.
1021            },
1022        };
1023        match class {
1024            ProgressBar::Primary => progress_bar::Style {
1025                background: Color::from(inactive_track).into(),
1026                bar: Color::from(active_track).into(),
1027                border,
1028            },
1029            ProgressBar::Success => progress_bar::Style {
1030                background: Color::from(inactive_track).into(),
1031                bar: Color::from(theme.success.base).into(),
1032                border,
1033            },
1034            ProgressBar::Danger => progress_bar::Style {
1035                background: Color::from(inactive_track).into(),
1036                bar: Color::from(theme.destructive.base).into(),
1037                border,
1038            },
1039            ProgressBar::Custom(f) => f(self),
1040        }
1041    }
1042}
1043
1044/*
1045 * TODO: Rule
1046 */
1047#[derive(Default)]
1048pub enum Rule {
1049    #[default]
1050    Default,
1051    LightDivider,
1052    HeavyDivider,
1053    Custom(Box<dyn Fn(&Theme) -> rule::Style>),
1054}
1055
1056impl Rule {
1057    pub fn custom<F: Fn(&Theme) -> rule::Style + 'static>(f: F) -> Self {
1058        Self::Custom(Box::new(f))
1059    }
1060}
1061
1062impl rule::Catalog for Theme {
1063    type Class<'a> = Rule;
1064
1065    fn default<'a>() -> Self::Class<'a> {
1066        Rule::default()
1067    }
1068
1069    fn style(&self, class: &Self::Class<'_>) -> rule::Style {
1070        match class {
1071            Rule::Default => rule::Style {
1072                color: self.current_container().divider.into(),
1073                width: 1,
1074                radius: 0.0.into(),
1075                fill_mode: rule::FillMode::Full,
1076            },
1077            Rule::LightDivider => rule::Style {
1078                color: self.current_container().divider.into(),
1079                width: 1,
1080                radius: 0.0.into(),
1081                fill_mode: rule::FillMode::Padded(8),
1082            },
1083            Rule::HeavyDivider => rule::Style {
1084                color: self.current_container().divider.into(),
1085                width: 4,
1086                radius: 2.0.into(),
1087                fill_mode: rule::FillMode::Full,
1088            },
1089            Rule::Custom(f) => f(self),
1090        }
1091    }
1092}
1093
1094#[derive(Default, Clone, Copy)]
1095pub enum Scrollable {
1096    #[default]
1097    Permanent,
1098    Minimal,
1099}
1100
1101/*
1102 * TODO: Scrollable
1103 */
1104impl scrollable::Catalog for Theme {
1105    type Class<'a> = Scrollable;
1106
1107    fn default<'a>() -> Self::Class<'a> {
1108        Scrollable::default()
1109    }
1110
1111    fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style {
1112        match status {
1113            scrollable::Status::Active => {
1114                let cosmic = self.cosmic();
1115                let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7);
1116                let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7);
1117                let mut a = scrollable::Style {
1118                    container: iced_container::transparent(self),
1119                    vertical_rail: scrollable::Rail {
1120                        border: Border {
1121                            radius: cosmic.corner_radii.radius_s.into(),
1122                            ..Default::default()
1123                        },
1124                        background: None,
1125                        scroller: scrollable::Scroller {
1126                            color: if cosmic.is_dark {
1127                                neutral_6.into()
1128                            } else {
1129                                neutral_5.into()
1130                            },
1131                            border: Border {
1132                                radius: cosmic.corner_radii.radius_s.into(),
1133                                ..Default::default()
1134                            },
1135                        },
1136                    },
1137                    horizontal_rail: scrollable::Rail {
1138                        border: Border {
1139                            radius: cosmic.corner_radii.radius_s.into(),
1140                            ..Default::default()
1141                        },
1142                        background: None,
1143                        scroller: scrollable::Scroller {
1144                            color: if cosmic.is_dark {
1145                                neutral_6.into()
1146                            } else {
1147                                neutral_5.into()
1148                            },
1149                            border: Border {
1150                                radius: cosmic.corner_radii.radius_s.into(),
1151                                ..Default::default()
1152                            },
1153                        },
1154                    },
1155                    gap: None,
1156                };
1157                let small_widget_container = self
1158                    .current_container()
1159                    .small_widget
1160                    .clone()
1161                    .with_alpha(0.7);
1162
1163                if matches!(class, Scrollable::Permanent) {
1164                    a.horizontal_rail.background =
1165                        Some(Background::Color(small_widget_container.into()));
1166                    a.vertical_rail.background =
1167                        Some(Background::Color(small_widget_container.into()));
1168                }
1169
1170                a
1171            }
1172            // TODO handle vertical / horizontal
1173            scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => {
1174                let cosmic = self.cosmic();
1175                let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7);
1176                let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7);
1177
1178                // if is_mouse_over_scrollbar {
1179                //     let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2);
1180                //     neutral_5 = over(hover_overlay, neutral_5);
1181                // }
1182                let mut a: scrollable::Style = scrollable::Style {
1183                    container: iced_container::Style::default(),
1184                    vertical_rail: scrollable::Rail {
1185                        border: Border {
1186                            radius: cosmic.corner_radii.radius_s.into(),
1187                            ..Default::default()
1188                        },
1189                        background: None,
1190                        scroller: scrollable::Scroller {
1191                            color: if cosmic.is_dark {
1192                                neutral_6.into()
1193                            } else {
1194                                neutral_5.into()
1195                            },
1196                            border: Border {
1197                                radius: cosmic.corner_radii.radius_s.into(),
1198                                ..Default::default()
1199                            },
1200                        },
1201                    },
1202                    horizontal_rail: scrollable::Rail {
1203                        border: Border {
1204                            radius: cosmic.corner_radii.radius_s.into(),
1205                            ..Default::default()
1206                        },
1207                        background: None,
1208                        scroller: scrollable::Scroller {
1209                            color: if cosmic.is_dark {
1210                                neutral_6.into()
1211                            } else {
1212                                neutral_5.into()
1213                            },
1214                            border: Border {
1215                                radius: cosmic.corner_radii.radius_s.into(),
1216                                ..Default::default()
1217                            },
1218                        },
1219                    },
1220                    gap: None,
1221                };
1222
1223                if matches!(class, Scrollable::Permanent) {
1224                    let small_widget_container = self
1225                        .current_container()
1226                        .small_widget
1227                        .clone()
1228                        .with_alpha(0.7);
1229
1230                    a.horizontal_rail.background =
1231                        Some(Background::Color(small_widget_container.into()));
1232                    a.vertical_rail.background =
1233                        Some(Background::Color(small_widget_container.into()));
1234                }
1235
1236                a
1237            }
1238        }
1239    }
1240}
1241
1242#[derive(Clone, Default)]
1243pub enum Svg {
1244    /// Apply a custom appearance filter
1245    Custom(Rc<dyn Fn(&Theme) -> svg::Style>),
1246    /// No filtering is applied
1247    #[default]
1248    Default,
1249}
1250
1251impl Svg {
1252    pub fn custom<F: Fn(&Theme) -> svg::Style + 'static>(f: F) -> Self {
1253        Self::Custom(Rc::new(f))
1254    }
1255}
1256
1257impl svg::Catalog for Theme {
1258    type Class<'a> = Svg;
1259
1260    fn default<'a>() -> Self::Class<'a> {
1261        Svg::default()
1262    }
1263
1264    fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style {
1265        #[allow(clippy::match_same_arms)]
1266        match class {
1267            Svg::Default => svg::Style::default(),
1268            Svg::Custom(appearance) => appearance(self),
1269        }
1270    }
1271}
1272
1273/*
1274 * TODO: Text
1275 */
1276#[derive(Clone, Copy, Default)]
1277pub enum Text {
1278    Accent,
1279    #[default]
1280    Default,
1281    Color(Color),
1282    // TODO: Can't use dyn Fn since this must be copy
1283    Custom(fn(&Theme) -> iced_widget::text::Style),
1284}
1285
1286impl From<Color> for Text {
1287    fn from(color: Color) -> Self {
1288        Self::Color(color)
1289    }
1290}
1291
1292impl iced_widget::text::Catalog for Theme {
1293    type Class<'a> = Text;
1294
1295    fn default<'a>() -> Self::Class<'a> {
1296        Text::default()
1297    }
1298
1299    fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style {
1300        match class {
1301            Text::Accent => iced_widget::text::Style {
1302                color: Some(self.cosmic().accent_text_color().into()),
1303            },
1304            Text::Default => iced_widget::text::Style { color: None },
1305            Text::Color(c) => iced_widget::text::Style { color: Some(*c) },
1306            Text::Custom(f) => f(self),
1307        }
1308    }
1309}
1310
1311#[derive(Copy, Clone, Default)]
1312pub enum TextInput {
1313    #[default]
1314    Default,
1315    Search,
1316}
1317
1318/*
1319 * TODO: Text Input
1320 */
1321impl text_input::Catalog for Theme {
1322    type Class<'a> = TextInput;
1323
1324    fn default<'a>() -> Self::Class<'a> {
1325        TextInput::default()
1326    }
1327
1328    fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style {
1329        let palette = self.cosmic();
1330        let bg = self.current_container().small_widget.with_alpha(0.25);
1331
1332        let neutral_9 = palette.palette.neutral_9;
1333        let value = neutral_9.into();
1334        let placeholder = neutral_9.with_alpha(0.7).into();
1335        let selection = palette.accent.base.into();
1336
1337        let mut appearance = match class {
1338            TextInput::Default => text_input::Style {
1339                background: Color::from(bg).into(),
1340                border: Border {
1341                    radius: palette.corner_radii.radius_s.into(),
1342                    width: 1.0,
1343                    color: self.current_container().component.divider.into(),
1344                },
1345                icon: self.current_container().on.into(),
1346                placeholder,
1347                value,
1348                selection,
1349            },
1350            TextInput::Search => text_input::Style {
1351                background: Color::from(bg).into(),
1352                border: Border {
1353                    radius: palette.corner_radii.radius_m.into(),
1354                    ..Default::default()
1355                },
1356                icon: self.current_container().on.into(),
1357                placeholder,
1358                value,
1359                selection,
1360            },
1361        };
1362
1363        match status {
1364            text_input::Status::Active => appearance,
1365            text_input::Status::Hovered => {
1366                let bg = self.current_container().small_widget.with_alpha(0.25);
1367
1368                match class {
1369                    TextInput::Default => text_input::Style {
1370                        background: Color::from(bg).into(),
1371                        border: Border {
1372                            radius: palette.corner_radii.radius_s.into(),
1373                            width: 1.0,
1374                            color: self.current_container().on.into(),
1375                        },
1376                        icon: self.current_container().on.into(),
1377                        placeholder,
1378                        value,
1379                        selection,
1380                    },
1381                    TextInput::Search => text_input::Style {
1382                        background: Color::from(bg).into(),
1383                        border: Border {
1384                            radius: palette.corner_radii.radius_m.into(),
1385                            ..Default::default()
1386                        },
1387                        icon: self.current_container().on.into(),
1388                        placeholder,
1389                        value,
1390                        selection,
1391                    },
1392                }
1393            }
1394            text_input::Status::Focused => {
1395                let bg = self.current_container().small_widget.with_alpha(0.25);
1396
1397                match class {
1398                    TextInput::Default => text_input::Style {
1399                        background: Color::from(bg).into(),
1400                        border: Border {
1401                            radius: palette.corner_radii.radius_s.into(),
1402                            width: 1.0,
1403                            color: palette.accent.base.into(),
1404                        },
1405                        icon: self.current_container().on.into(),
1406                        placeholder,
1407                        value,
1408                        selection,
1409                    },
1410                    TextInput::Search => text_input::Style {
1411                        background: Color::from(bg).into(),
1412                        border: Border {
1413                            radius: palette.corner_radii.radius_m.into(),
1414                            ..Default::default()
1415                        },
1416                        icon: self.current_container().on.into(),
1417                        placeholder,
1418                        value,
1419                        selection,
1420                    },
1421                }
1422            }
1423            text_input::Status::Disabled => {
1424                appearance.background = match appearance.background {
1425                    Background::Color(color) => Background::Color(Color {
1426                        a: color.a * 0.5,
1427                        ..color
1428                    }),
1429                    Background::Gradient(gradient) => {
1430                        Background::Gradient(gradient.scale_alpha(0.5))
1431                    }
1432                };
1433                appearance.border.color.a /= 2.;
1434                appearance.icon.a /= 2.;
1435                appearance.placeholder.a /= 2.;
1436                appearance.value.a /= 2.;
1437                appearance
1438            }
1439        }
1440    }
1441}
1442
1443#[derive(Default)]
1444pub enum TextEditor<'a> {
1445    #[default]
1446    Default,
1447    Custom(text_editor::StyleFn<'a, Theme>),
1448}
1449
1450impl iced_widget::text_editor::Catalog for Theme {
1451    type Class<'a> = TextEditor<'a>;
1452
1453    fn default<'a>() -> Self::Class<'a> {
1454        TextEditor::default()
1455    }
1456
1457    fn style(
1458        &self,
1459        class: &Self::Class<'_>,
1460        status: iced_widget::text_editor::Status,
1461    ) -> iced_widget::text_editor::Style {
1462        if let TextEditor::Custom(style) = class {
1463            return style(self, status);
1464        }
1465
1466        let cosmic = self.cosmic();
1467
1468        let selection = cosmic.accent.base.into();
1469        let value = cosmic.palette.neutral_9.into();
1470        let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into();
1471        let icon = cosmic.background.on.into();
1472
1473        match status {
1474            iced_widget::text_editor::Status::Active
1475            | iced_widget::text_editor::Status::Hovered
1476            | iced_widget::text_editor::Status::Disabled => iced_widget::text_editor::Style {
1477                background: iced::Color::from(cosmic.bg_color()).into(),
1478                border: Border {
1479                    radius: cosmic.corner_radii.radius_0.into(),
1480                    width: f32::from(cosmic.space_xxxs()),
1481                    color: iced::Color::from(cosmic.bg_divider()),
1482                },
1483                icon,
1484                placeholder,
1485                value,
1486                selection,
1487            },
1488            iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style {
1489                background: iced::Color::from(cosmic.bg_color()).into(),
1490                border: Border {
1491                    radius: cosmic.corner_radii.radius_0.into(),
1492                    width: f32::from(cosmic.space_xxxs()),
1493                    color: iced::Color::from(cosmic.accent.base),
1494                },
1495                icon,
1496                placeholder,
1497                value,
1498                selection,
1499            },
1500        }
1501    }
1502}
1503
1504#[cfg(feature = "markdown")]
1505impl iced_widget::markdown::Catalog for Theme {
1506    fn code_block<'a>() -> <Self as iced_container::Catalog>::Class<'a> {
1507        Container::custom(|_| iced_container::Style {
1508            background: Some(iced::color!(0x111111).into()),
1509            text_color: Some(Color::WHITE),
1510            border: iced::border::rounded(2),
1511            ..iced_container::Style::default()
1512        })
1513    }
1514}
1515
1516#[cfg(feature = "qr_code")]
1517impl iced_widget::qr_code::Catalog for Theme {
1518    type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>;
1519
1520    fn default<'a>() -> Self::Class<'a> {
1521        Box::new(|_theme| iced_widget::qr_code::Style {
1522            cell: Color::BLACK,
1523            background: Color::WHITE,
1524        })
1525    }
1526
1527    fn style(&self, class: &Self::Class<'_>) -> iced_widget::qr_code::Style {
1528        class(self)
1529    }
1530}
1531
1532impl combo_box::Catalog for Theme {}