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