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