Skip to main content

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