1use 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#[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 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#[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 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#[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 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
653impl 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 {
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 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
820impl 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 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 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
873impl 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
938impl 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#[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#[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
1101impl 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 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 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 Custom(Rc<dyn Fn(&Theme) -> svg::Style>),
1246 #[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#[derive(Clone, Copy, Default)]
1277pub enum Text {
1278 Accent,
1279 #[default]
1280 Default,
1281 Color(Color),
1282 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
1318impl 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 {}