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