1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6
7use crate::app::ContrastPolicy;
8
9use super::{Color, HostTerminalColors, Paint};
10
11#[cfg_attr(
13 feature = "terminal-serde",
14 derive(serde::Serialize, serde::Deserialize)
15)]
16#[derive(Clone, Copy, Debug)]
17pub enum ColorTransform {
18 Dim(f32),
20 Lighten(f32),
22 Elevate(f32),
31 Opacity(f32),
37 OpacityToward {
39 factor: f32,
41 target: Color,
43 },
44 Tint(Color, f32),
46}
47
48impl PartialEq for ColorTransform {
49 fn eq(&self, other: &Self) -> bool {
50 match (*self, *other) {
51 (Self::Dim(a), Self::Dim(b))
52 | (Self::Lighten(a), Self::Lighten(b))
53 | (Self::Elevate(a), Self::Elevate(b))
54 | (Self::Opacity(a), Self::Opacity(b)) => a.to_bits() == b.to_bits(),
55 (
56 Self::OpacityToward {
57 factor: fa,
58 target: ta,
59 },
60 Self::OpacityToward {
61 factor: fb,
62 target: tb,
63 },
64 ) => fa.to_bits() == fb.to_bits() && ta == tb,
65 (Self::Tint(color_a, alpha_a), Self::Tint(color_b, alpha_b)) => {
66 color_a == color_b && alpha_a.to_bits() == alpha_b.to_bits()
67 }
68 _ => false,
69 }
70 }
71}
72
73impl Eq for ColorTransform {}
74
75impl Hash for ColorTransform {
76 fn hash<H: Hasher>(&self, state: &mut H) {
77 match *self {
78 Self::Dim(amount) => {
79 0u8.hash(state);
80 amount.to_bits().hash(state);
81 }
82 Self::Lighten(amount) => {
83 1u8.hash(state);
84 amount.to_bits().hash(state);
85 }
86 Self::Opacity(amount) => {
87 2u8.hash(state);
88 amount.to_bits().hash(state);
89 }
90 Self::OpacityToward { factor, target } => {
91 4u8.hash(state);
92 factor.to_bits().hash(state);
93 target.hash(state);
94 }
95 Self::Tint(color, alpha) => {
96 3u8.hash(state);
97 color.hash(state);
98 alpha.to_bits().hash(state);
99 }
100 Self::Elevate(amount) => {
101 5u8.hash(state);
102 amount.to_bits().hash(state);
103 }
104 }
105 }
106}
107
108impl ColorTransform {
109 pub fn apply(self, color: Color) -> Color {
111 self.apply_with_backdrop(color, None)
112 }
113
114 pub fn apply_with_backdrop(self, color: Color, backdrop: Option<Color>) -> Color {
116 if matches!(color, Color::Transparent | Color::Backdrop) {
117 return color;
118 }
119 match self {
120 Self::Dim(amount) => color.dim_by(amount),
121 Self::Lighten(amount) => color.lighten_by(amount),
122 Self::Elevate(amount) => color.elevate_by(amount),
123 Self::Opacity(opacity) => backdrop.map_or(color, |bg| {
124 color.blend_toward(bg, (1.0 - opacity).clamp(0.0, 1.0))
125 }),
126 Self::OpacityToward { factor, target } => {
127 color.blend_toward(target, (1.0 - factor).clamp(0.0, 1.0))
128 }
129 Self::Tint(target, alpha) => color.blend_toward(target, alpha),
130 }
131 }
132
133 pub fn apply_paint(self, paint: Paint) -> Paint {
138 self.apply_paint_with_backdrop(paint, None)
139 }
140
141 pub fn apply_paint_with_backdrop(self, paint: Paint, backdrop: Option<Paint>) -> Paint {
143 if matches!(paint, Paint::Solid(Color::Transparent | Color::Backdrop)) {
144 return paint;
145 }
146 if let Self::Opacity(opacity) = self {
147 let alpha = (paint.alpha_u8() as f32 * opacity.clamp(0.0, 1.0))
148 .round()
149 .clamp(0.0, 255.0) as u8;
150 return Paint::from_color_alpha_u8(paint.color(), alpha);
151 }
152 let backdrop = backdrop.map(Paint::color);
153 match paint {
154 Paint::Solid(color) => Paint::Solid(self.apply_with_backdrop(color, backdrop)),
155 Paint::Alpha { color, alpha } => {
156 Paint::from_color_alpha_u8(self.apply_with_backdrop(color, backdrop), alpha)
157 }
158 Paint::Animated { .. } => {
160 Paint::Solid(self.apply_with_backdrop(paint.resolved().color(), backdrop))
161 }
162 }
163 }
164
165 pub(crate) fn needs_backdrop(self) -> bool {
166 matches!(self, Self::Opacity(_))
167 }
168
169 fn normalized(self) -> Self {
170 match self {
171 Self::Dim(amount) => Self::Dim(amount.clamp(0.0, 1.0)),
172 Self::Lighten(amount) => Self::Lighten(amount.clamp(0.0, 1.0)),
173 Self::Elevate(amount) => Self::Elevate(amount.clamp(0.0, 1.0)),
174 Self::Opacity(opacity) => Self::Opacity(opacity.clamp(0.0, 1.0)),
175 Self::OpacityToward { factor, target } => Self::OpacityToward {
176 factor: factor.clamp(0.0, 1.0),
177 target,
178 },
179 Self::Tint(color, alpha) => Self::Tint(color, alpha.clamp(0.0, 1.0)),
180 }
181 }
182}
183
184pub trait ThemeExtension: Clone + fmt::Debug + PartialEq + 'static {}
190
191impl<T> ThemeExtension for T where T: Clone + fmt::Debug + PartialEq + 'static {}
192
193trait ThemeExtensionValue: Any {
194 fn as_any(&self) -> &dyn Any;
195 fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool;
196}
197
198impl<T> ThemeExtensionValue for T
199where
200 T: ThemeExtension,
201{
202 fn as_any(&self) -> &dyn Any {
203 self
204 }
205
206 fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool {
207 other.as_any().downcast_ref::<T>() == Some(self)
208 }
209}
210
211#[derive(Clone, Default)]
212#[doc(hidden)]
213pub struct ThemeExtensions(HashMap<TypeId, Arc<dyn ThemeExtensionValue>>);
214
215impl ThemeExtensions {
216 fn insert<T>(&mut self, extension: T)
217 where
218 T: ThemeExtension,
219 {
220 self.0.insert(TypeId::of::<T>(), Arc::new(extension));
221 }
222
223 fn get<T>(&self) -> Option<&T>
224 where
225 T: ThemeExtension,
226 {
227 self.0
228 .get(&TypeId::of::<T>())
229 .and_then(|value| value.as_any().downcast_ref::<T>())
230 }
231
232 fn remove<T>(&mut self)
233 where
234 T: ThemeExtension,
235 {
236 self.0.remove(&TypeId::of::<T>());
237 }
238}
239
240impl PartialEq for ThemeExtensions {
241 fn eq(&self, other: &Self) -> bool {
242 self.0.len() == other.0.len()
243 && self.0.iter().all(|(type_id, value)| {
244 other
245 .0
246 .get(type_id)
247 .is_some_and(|other_value| value.eq_value(other_value.as_ref()))
248 })
249 }
250}
251
252impl Eq for ThemeExtensions {}
253
254impl fmt::Debug for ThemeExtensions {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.debug_struct("ThemeExtensions")
257 .field("count", &self.0.len())
258 .finish()
259 }
260}
261
262#[cfg_attr(
264 feature = "terminal-serde",
265 derive(serde::Serialize, serde::Deserialize)
266)]
267#[derive(Clone, Copy, Debug, Default)]
268pub struct Style {
269 pub fg: Option<Paint>,
271 pub bg: Option<Paint>,
273 pub fg_transform: Option<ColorTransform>,
275 pub bg_transform: Option<ColorTransform>,
277 pub contrast_policy: Option<ContrastPolicy>,
279 pub bold: Option<bool>,
281 pub dim: Option<bool>,
283 pub italic: Option<bool>,
285 pub underline: Option<bool>,
287 pub reverse: Option<bool>,
289 pub strikethrough: Option<bool>,
291 pub underline_color: Option<Paint>,
293 pub dim_amount: Option<f32>,
300 pub tint: Option<(Color, f32)>,
308}
309
310impl PartialEq for Style {
311 fn eq(&self, other: &Self) -> bool {
312 self.fg == other.fg
313 && self.bg == other.bg
314 && self.fg_transform == other.fg_transform
315 && self.bg_transform == other.bg_transform
316 && self.contrast_policy == other.contrast_policy
317 && self.bold == other.bold
318 && self.dim == other.dim
319 && self.italic == other.italic
320 && self.underline == other.underline
321 && self.reverse == other.reverse
322 && self.strikethrough == other.strikethrough
323 && self.underline_color == other.underline_color
324 && self.dim_amount.map(f32::to_bits) == other.dim_amount.map(f32::to_bits)
325 && self.tint.map(|(c, a)| (c, a.to_bits())) == other.tint.map(|(c, a)| (c, a.to_bits()))
326 }
327}
328
329impl Eq for Style {}
330
331#[cfg(all(test, feature = "terminal-serde"))]
332mod terminal_serde_tests {
333 use super::*;
334
335 #[test]
336 fn color_transform_round_trips() {
337 let transform = ColorTransform::OpacityToward {
338 factor: 0.42,
339 target: Color::rgb(1, 2, 3),
340 };
341 let json = serde_json::to_string(&transform).unwrap();
342 assert_eq!(
343 serde_json::from_str::<ColorTransform>(&json).unwrap(),
344 transform
345 );
346 }
347
348 #[test]
349 fn style_round_trips() {
350 let style = Style::default()
351 .fg(Paint::rgb(20, 30, 40))
352 .bg(Paint::rgba(1, 2, 3, 180))
353 .bold()
354 .underline()
355 .contrast_policy(ContrastPolicy::BlackOrWhite)
356 .tint_by(Color::Cyan, 0.25);
357 let json = serde_json::to_string(&style).unwrap();
358 assert_eq!(serde_json::from_str::<Style>(&json).unwrap(), style);
359 }
360}
361
362#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
368pub enum StyleSlot {
369 #[default]
371 Inherit,
372 Extend(Style),
374 Replace(Style),
376}
377
378impl StyleSlot {
379 pub fn replace(style: Style) -> Self {
381 Self::Replace(style)
382 }
383
384 pub fn extend(style: Style) -> Self {
386 Self::Extend(style)
387 }
388
389 pub fn explicit_style(self) -> Option<Style> {
391 match self {
392 Self::Inherit => None,
393 Self::Extend(style) | Self::Replace(style) => Some(style),
394 }
395 }
396
397 pub fn has_explicit_style(self) -> bool {
399 self.explicit_style().is_some_and(|style| !style.is_empty())
400 }
401
402 pub fn is_empty(self) -> bool {
404 matches!(self, Self::Replace(style) if style.is_empty())
405 }
406}
407
408impl From<Style> for StyleSlot {
409 fn from(style: Style) -> Self {
410 Self::Replace(style)
411 }
412}
413
414#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
416pub enum ThemeRole {
417 Base,
419 Accent,
421 Selection,
423 TextSelection,
425 UnfocusedSelection,
427 Hover,
429 DragSource,
435 DropTarget,
441 DropTargetActive,
447 Focus,
449 Active,
451 ItemHover,
453 Border,
455 Disabled,
457 Muted,
459 Error,
461 InputFocusContent,
463 TextAreaFocusContent,
465 DocumentViewFocusContent,
467 HexAreaFocusContent,
469 HexAreaCursor,
471 TerminalFocusContent,
473 ScrollbarThumb,
475 ScrollbarThumbFocus,
477 ScrollbarTrack,
479 SplitterHover,
481 SplitterActive,
483}
484
485impl Hash for Style {
486 fn hash<H: Hasher>(&self, state: &mut H) {
487 self.fg.hash(state);
488 self.bg.hash(state);
489 self.fg_transform.hash(state);
490 self.bg_transform.hash(state);
491 self.contrast_policy.hash(state);
492 self.bold.hash(state);
493 self.dim.hash(state);
494 self.italic.hash(state);
495 self.underline.hash(state);
496 self.reverse.hash(state);
497 self.strikethrough.hash(state);
498 self.underline_color.hash(state);
499 self.dim_amount.map(f32::to_bits).hash(state);
500 if let Some((c, a)) = self.tint {
501 c.hash(state);
502 a.to_bits().hash(state);
503 }
504 }
505}
506
507impl Style {
508 pub fn new() -> Self {
510 Self::default()
511 }
512
513 pub fn resolved_fg(&self) -> Option<Color> {
519 self.fg.map(Paint::color)
520 }
521
522 pub fn resolved_bg(&self) -> Option<Color> {
528 self.bg.map(Paint::color)
529 }
530
531 pub fn fg(mut self, color: impl Into<Paint>) -> Self {
533 self.fg = Some(color.into());
534 self
535 }
536
537 pub fn bg(mut self, color: impl Into<Paint>) -> Self {
539 self.bg = Some(color.into());
540 self
541 }
542
543 pub fn fg_alpha(mut self, color: Color, alpha: f32) -> Self {
545 self.fg = Some(Paint::from_color_alpha(color, alpha));
546 self
547 }
548
549 pub fn bg_alpha(mut self, color: Color, alpha: f32) -> Self {
551 self.bg = Some(Paint::from_color_alpha(color, alpha));
552 self
553 }
554
555 pub fn transform_fg(mut self, transform: ColorTransform) -> Self {
557 self.fg_transform = Some(transform.normalized());
558 self
559 }
560
561 pub fn transform_bg(mut self, transform: ColorTransform) -> Self {
563 self.bg_transform = Some(transform.normalized());
564 self
565 }
566
567 pub fn contrast_policy(mut self, policy: ContrastPolicy) -> Self {
582 self.contrast_policy = Some(policy);
583 self
584 }
585
586 pub fn bold(mut self) -> Self {
588 self.bold = Some(true);
589 self
590 }
591
592 pub fn not_bold(mut self) -> Self {
598 self.bold = Some(false);
599 self
600 }
601
602 pub fn dim(mut self) -> Self {
604 self.dim = Some(true);
605 self
606 }
607
608 pub fn dim_by(mut self, amount: f32) -> Self {
615 let amount = amount.clamp(0.0, 1.0);
616 self.fg_transform = Some(ColorTransform::Dim(amount));
617 self.bg_transform = Some(ColorTransform::Dim(amount));
618 self.dim_amount = Some(amount);
619 self
620 }
621
622 pub fn tint_by(mut self, color: Color, alpha: f32) -> Self {
645 let alpha = alpha.clamp(0.0, 1.0);
646 self.fg_transform = Some(ColorTransform::Tint(color, alpha));
647 self.bg_transform = Some(ColorTransform::Tint(color, alpha));
648 self.tint = Some((color, alpha));
649 self
650 }
651
652 pub fn lighten_by(mut self, amount: f32) -> Self {
659 let amount = amount.clamp(0.0, 1.0);
660 self.fg_transform = Some(ColorTransform::Lighten(amount));
661 self.bg_transform = Some(ColorTransform::Lighten(amount));
662 self
663 }
664
665 pub fn elevate_by(mut self, amount: f32) -> Self {
684 let amount = amount.clamp(0.0, 1.0);
685 self.fg_transform = Some(ColorTransform::Elevate(amount));
686 self.bg_transform = Some(ColorTransform::Elevate(amount));
687 self
688 }
689
690 pub fn italic(mut self) -> Self {
692 self.italic = Some(true);
693 self
694 }
695
696 pub fn underline(mut self) -> Self {
698 self.underline = Some(true);
699 self
700 }
701
702 pub fn reverse(mut self) -> Self {
704 self.reverse = Some(true);
705 self
706 }
707
708 pub fn strikethrough(mut self) -> Self {
710 self.strikethrough = Some(true);
711 self
712 }
713
714 pub fn underline_color(mut self, color: impl Into<Paint>) -> Self {
716 self.underline_color = Some(color.into());
717 self.underline = Some(true);
718 self
719 }
720
721 pub fn is_empty(&self) -> bool {
725 self.fg.is_none()
726 && self.bg.is_none()
727 && self.fg_transform.is_none()
728 && self.bg_transform.is_none()
729 && self.contrast_policy.is_none()
730 && self.bold.is_none()
731 && self.dim.is_none()
732 && self.italic.is_none()
733 && self.underline.is_none()
734 && self.reverse.is_none()
735 && self.strikethrough.is_none()
736 && self.underline_color.is_none()
737 && self.dim_amount.is_none()
738 && self.tint.is_none()
739 }
740
741 pub fn patch(self, other: Style) -> Self {
755 let (bg, bg_transform) = merge_channel(
756 self.bg,
757 self.bg_transform,
758 other.bg,
759 other.bg_transform,
760 None,
761 );
762 let backdrop = bg.or(other.bg).or(self.bg);
763 let (fg, fg_transform) = merge_channel(
764 self.fg,
765 self.fg_transform,
766 other.fg,
767 other.fg_transform,
768 backdrop,
769 );
770
771 Self {
772 fg,
773 bg,
774 fg_transform,
775 bg_transform,
776 contrast_policy: other.contrast_policy.or(self.contrast_policy),
777 bold: other.bold.or(self.bold),
778 dim: other.dim.or(self.dim),
779 italic: other.italic.or(self.italic),
780 underline: other.underline.or(self.underline),
781 reverse: other.reverse.or(self.reverse),
782 strikethrough: other.strikethrough.or(self.strikethrough),
783 underline_color: merge_underline_color(other.underline_color, self.underline_color),
784 dim_amount: other.dim_amount.or(self.dim_amount),
785 tint: other.tint.or(self.tint),
786 }
787 }
788
789 pub(crate) fn resolve_color_transforms(self) -> Self {
790 let bg = resolve_channel(self.bg, self.bg_transform, None);
791 let mut fg = resolve_channel(self.fg, self.fg_transform, bg);
792 let mut fg_transform_remaining = None;
793
794 if matches!(fg, Some(Paint::Solid(Color::Transparent))) {
795 if let Some(c) = bg
796 && !matches!(c, Paint::Solid(Color::Transparent | Color::Backdrop))
797 {
798 fg = if let Some(t) = self.fg_transform {
799 Some(t.apply_paint_with_backdrop(c, bg))
800 } else {
801 Some(c)
802 };
803 } else {
804 fg_transform_remaining = self.fg_transform;
805 }
806 }
807 Self {
808 fg,
809 bg,
810 fg_transform: fg_transform_remaining,
811 bg_transform: None,
812 ..self
813 }
814 }
815}
816
817fn merge_underline_color(overlay: Option<Paint>, base: Option<Paint>) -> Option<Paint> {
818 match overlay {
819 None => base,
820 Some(Paint::Solid(Color::Transparent)) => base,
821 Some(c) => Some(c),
822 }
823}
824
825pub(crate) fn merge_channel(
826 base_color: Option<Paint>,
827 base_transform: Option<ColorTransform>,
828 overlay_color: Option<Paint>,
829 overlay_transform: Option<ColorTransform>,
830 backdrop: Option<Paint>,
831) -> (Option<Paint>, Option<ColorTransform>) {
832 let mut color = resolve_channel(base_color, base_transform, backdrop);
833 let mut transform = None;
834
835 if let Some(overlay_color) = overlay_color
836 && !matches!(overlay_color, Paint::Solid(Color::Transparent))
837 {
838 color = Some(overlay_color);
839 }
840
841 if let Some(overlay_transform) = overlay_transform {
842 if let Some(current) = color
843 && (!overlay_transform.needs_backdrop() || backdrop.is_some())
844 {
845 color = Some(overlay_transform.apply_paint_with_backdrop(current, backdrop));
846 } else {
847 transform = Some(overlay_transform.normalized());
848 }
849 }
850
851 (color, transform)
852}
853
854pub(crate) fn resolve_channel(
855 color: Option<Paint>,
856 transform: Option<ColorTransform>,
857 backdrop: Option<Paint>,
858) -> Option<Paint> {
859 match (color, transform) {
860 (Some(color), Some(transform)) => {
861 Some(transform.apply_paint_with_backdrop(color, backdrop))
862 }
863 (color, None) => color,
864 (None, Some(_)) => None,
865 }
866}
867
868#[cfg(test)]
869mod tests {
870 use super::{CaretShape, ColorTransform, Style, Theme, ThemePalette, ThemeRole};
871 use crate::app::ContrastPolicy;
872 use crate::style::{Color, HostTerminalColors, Paint};
873
874 fn p(color: Color) -> Option<Paint> {
875 Some(Paint::Solid(color))
876 }
877
878 #[test]
879 fn tint_by_transforms_this_styles_own_colors_as_well_as_the_backdrop_hook() {
880 let tinted = Style::new()
884 .fg(Color::Rgb(240, 240, 240))
885 .bg(Color::Rgb(200, 40, 40))
886 .tint_by(Color::Rgb(0, 0, 0), 0.5);
887
888 assert_eq!(
889 tinted.fg_transform,
890 Some(ColorTransform::Tint(Color::Rgb(0, 0, 0), 0.5)),
891 );
892 assert_eq!(
893 tinted.bg_transform,
894 Some(ColorTransform::Tint(Color::Rgb(0, 0, 0), 0.5)),
895 );
896 assert_eq!(tinted.tint, Some((Color::Rgb(0, 0, 0), 0.5)));
897
898 let resolved = tinted.resolve_color_transforms();
899 assert_eq!(resolved.fg, p(Color::Rgb(120, 120, 120)));
900 assert_eq!(resolved.bg, p(Color::Rgb(100, 20, 20)));
901 }
902
903 #[test]
904 fn elevate_transform_matches_the_absolute_elevate_step() {
905 let surface = Color::Rgb(6, 14, 19);
909
910 assert_eq!(
911 ColorTransform::Elevate(0.08).apply(surface),
912 surface.elevate_by(0.08),
913 );
914 assert_ne!(
915 ColorTransform::Elevate(0.08).apply(surface),
916 ColorTransform::Lighten(0.08).apply(surface),
917 );
918 }
919
920 #[test]
921 fn elevate_transform_reverses_direction_on_a_light_surface() {
922 let light = Color::Rgb(245, 245, 245);
923 let lifted = ColorTransform::Elevate(0.08).apply(light);
924
925 assert!(
926 lifted.luminance() < light.luminance(),
927 "elevating a light surface dims it, got {lifted:?}",
928 );
929 }
930
931 #[test]
932 fn tint_by_mirrors_dim_by_so_neither_hook_is_silent_on_a_widget() {
933 let dimmed = Style::new().fg(Color::Rgb(200, 200, 200)).dim_by(0.5);
936 let tinted = Style::new()
937 .fg(Color::Rgb(200, 200, 200))
938 .tint_by(Color::Black, 0.5);
939
940 assert!(dimmed.fg_transform.is_some() && dimmed.dim_amount.is_some());
941 assert!(tinted.fg_transform.is_some() && tinted.tint.is_some());
942 }
943
944 #[test]
945 fn resolved_style_channels_extract_opaque_and_alpha_colors() {
946 let style = Style::new()
947 .fg(Paint::rgba(10, 20, 30, 128))
948 .bg(Color::Backdrop);
949
950 assert_eq!(style.resolved_fg(), Some(Color::Rgb(10, 20, 30)));
951 assert_eq!(style.resolved_bg(), Some(Color::Backdrop));
952 assert_eq!(Style::default().resolved_fg(), None);
953 assert_eq!(Style::default().resolved_bg(), None);
954 }
955
956 #[test]
957 fn concretize_backdrop_resolves_all_sentinels_and_preserves_colors() {
958 let fallback = Color::Rgb(10, 20, 30);
959
960 for backdrop in [Color::Reset, Color::Backdrop, Color::Transparent] {
961 let mut theme = Theme::default();
962 theme.surface.backdrop = backdrop;
963 assert_eq!(theme.concretize_backdrop(Some(fallback)), fallback);
964 }
965
966 let mut theme = Theme::default();
967 theme.surface.backdrop = Color::Blue;
968 assert_eq!(theme.concretize_backdrop(Some(fallback)), Color::Blue);
969 }
970
971 #[test]
972 fn concretize_backdrop_uses_reset_without_primary_background() {
973 let mut theme = Theme::default();
974 theme.surface.backdrop = Color::Backdrop;
975 theme.surface.panel = Color::Reset;
976
977 assert_eq!(theme.concretize_backdrop(None), Color::Reset);
978 }
979
980 #[derive(Clone, Debug, PartialEq)]
981 struct BrandTheme {
982 accent_badge: Color,
983 }
984
985 #[test]
986 fn drag_drop_roles_initially_resolve_to_hover() {
987 let theme = Theme::default().hover(Style::new().fg(Color::White).bg(Color::Blue));
988
989 assert_eq!(theme.role(ThemeRole::DragSource), theme.hover);
990 assert_eq!(theme.role(ThemeRole::DropTarget), theme.hover);
991 assert_eq!(theme.role(ThemeRole::DropTargetActive), theme.hover);
992 }
993
994 #[test]
995 fn text_selection_role_is_distinct_from_item_selection() {
996 let theme = Theme::default()
997 .selection(Style::new().fg(Color::Red))
998 .text_selection(Style::new().fg(Color::Blue));
999
1000 assert_eq!(theme.role(ThemeRole::Selection), theme.selection);
1001 assert_eq!(theme.role(ThemeRole::TextSelection), theme.text_selection);
1002 assert_ne!(
1003 theme.role(ThemeRole::Selection),
1004 theme.role(ThemeRole::TextSelection)
1005 );
1006 }
1007
1008 #[test]
1009 fn focus_decoration_does_not_change_unfocused_selection() {
1010 let selection = Style::new().fg(Color::Yellow).bg(Color::Blue);
1011 let theme = Theme::default()
1012 .selection(selection)
1013 .focus(Style::new().fg(Color::Green))
1014 .focus_decoration(false);
1015
1016 assert!(theme.role(ThemeRole::Focus).is_empty());
1017 assert_eq!(theme.role(ThemeRole::UnfocusedSelection), selection);
1018 }
1019
1020 #[test]
1021 fn theme_palette_derives_distinct_selection_colors() {
1022 let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue)
1023 .selection(Color::Green)
1024 .text_selection(Color::Magenta)
1025 .into_theme();
1026
1027 assert_eq!(theme.selection.fg, p(Color::Green));
1028 assert_eq!(theme.text_selection.fg, p(Color::Magenta));
1029 }
1030
1031 #[test]
1032 fn theme_palette_derives_and_overrides_caret_defaults() {
1033 let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue).into_theme();
1034
1035 assert_eq!(theme.caret.shape, CaretShape::Block);
1036 assert_eq!(theme.caret.color, Some(Color::Blue));
1037
1038 let themed = ThemePalette::new(Color::White, Color::Black, Color::Blue)
1039 .caret_shape(CaretShape::Underline)
1040 .caret_color(Color::Magenta)
1041 .into_theme();
1042
1043 assert_eq!(themed.caret.shape, CaretShape::Underline);
1044 assert_eq!(themed.caret.color, Some(Color::Magenta));
1045 }
1046
1047 #[test]
1048 fn from_host_colors_uses_host_palette() {
1049 let mut ansi = std::array::from_fn(|i| Color::rgb(i as u8, i as u8, i as u8));
1050 ansi[1] = Color::rgb(210, 30, 40);
1051 ansi[2] = Color::rgb(30, 210, 40);
1052 ansi[3] = Color::rgb(210, 180, 40);
1053 ansi[4] = Color::rgb(30, 80, 210);
1054 let colors = HostTerminalColors {
1055 fg: Color::rgb(230, 231, 232),
1056 bg: Color::rgb(10, 11, 12),
1057 ansi,
1058 };
1059
1060 let theme = Theme::from_host_colors(colors);
1061
1062 assert_eq!(theme.primary.fg, p(colors.fg));
1063 assert_eq!(theme.primary.bg, p(colors.bg));
1064 assert_eq!(theme.accent.fg, p(colors.ansi[4]));
1065 assert_eq!(theme.status.success, colors.ansi[2]);
1066 assert_eq!(theme.status.warning, colors.ansi[3]);
1067 assert_eq!(theme.status.error, colors.ansi[1]);
1068 assert_eq!(theme.status.info, colors.ansi[4]);
1069 }
1070
1071 #[test]
1072 fn transform_fg_dims_inherited_color() {
1073 let base = Style::new().fg(Color::rgb(100, 120, 140));
1074 let overlay = Style::new().transform_fg(ColorTransform::Dim(0.5));
1075
1076 assert_eq!(
1077 base.patch(overlay).resolve_color_transforms().fg,
1078 p(Color::rgb(50, 60, 70))
1079 );
1080 }
1081
1082 #[test]
1083 fn lower_fg_transform_does_not_affect_overlay_color() {
1084 let base = Style::new()
1085 .fg(Color::rgb(100, 120, 140))
1086 .transform_fg(ColorTransform::Dim(0.5));
1087 let overlay = Style::new().fg(Color::rgb(10, 20, 30));
1088
1089 assert_eq!(
1090 base.patch(overlay).resolve_color_transforms().fg,
1091 p(Color::rgb(10, 20, 30))
1092 );
1093 }
1094
1095 #[test]
1096 fn patch_transparent_fg_preserves_base() {
1097 let base = Style::new().fg(Color::rgb(10, 20, 30));
1098 let overlay = Style::new().fg(Color::Transparent);
1099 assert_eq!(
1100 base.patch(overlay).resolve_color_transforms().fg,
1101 p(Color::rgb(10, 20, 30))
1102 );
1103 }
1104
1105 #[test]
1106 fn patch_transparent_bg_preserves_base() {
1107 let base = Style::new().bg(Color::rgb(40, 50, 60));
1108 let overlay = Style::new().bg(Color::Transparent);
1109 assert_eq!(
1110 base.patch(overlay).resolve_color_transforms().bg,
1111 p(Color::rgb(40, 50, 60))
1112 );
1113 }
1114
1115 #[test]
1116 fn patch_alpha_zero_bg_is_not_transparent_sentinel() {
1117 let base = Style::new().bg(Color::rgb(40, 50, 60));
1118 let overlay = Style::new().bg_alpha(Color::Red, 0.0);
1119 assert_eq!(
1120 base.patch(overlay).resolve_color_transforms().bg,
1121 Some(Paint::Alpha {
1122 color: Color::Red,
1123 alpha: 0,
1124 })
1125 );
1126 }
1127
1128 #[test]
1129 fn color_transform_apply_paint_preserves_alpha() {
1130 let paint = Paint::Alpha {
1131 color: Color::rgb(100, 120, 140),
1132 alpha: 128,
1133 };
1134
1135 assert_eq!(
1136 ColorTransform::Dim(0.5).apply_paint(paint),
1137 Paint::Alpha {
1138 color: Color::rgb(50, 60, 70),
1139 alpha: 128,
1140 }
1141 );
1142 }
1143
1144 #[test]
1145 fn patch_transparent_underline_color_preserves_base() {
1146 let base = Style::new().underline_color(Color::Red);
1147 let overlay = Style::new().underline_color(Color::Transparent);
1148 let patched = base.patch(overlay);
1149 assert_eq!(patched.underline_color, p(Color::Red));
1150 }
1151
1152 #[test]
1156 fn style_elevate_by_moves_away_from_the_surface_on_both_polarities() {
1157 let dark = Color::rgb(20, 22, 30);
1158 let light = Color::rgb(235, 235, 240);
1159
1160 let dark_lifted = Style::new()
1161 .bg(dark)
1162 .elevate_by(0.08)
1163 .resolve_color_transforms()
1164 .bg;
1165 let light_lifted = Style::new()
1166 .bg(light)
1167 .elevate_by(0.08)
1168 .resolve_color_transforms()
1169 .bg;
1170
1171 assert_eq!(dark_lifted, p(ColorTransform::Elevate(0.08).apply(dark)));
1172 assert_eq!(light_lifted, p(ColorTransform::Elevate(0.08).apply(light)));
1173
1174 let lighter = |paint: Option<Paint>, base: Color| match paint {
1175 Some(Paint::Solid(color)) => color.luminance() > base.luminance(),
1176 other => panic!("expected a solid color, got {other:?}"),
1177 };
1178 assert!(lighter(dark_lifted, dark), "a dark surface must lift");
1179 assert!(!lighter(light_lifted, light), "a light surface must darken");
1180 }
1181
1182 #[test]
1185 fn style_elevate_by_lifts_the_base_color_rather_than_replacing_it() {
1186 let marked = Color::rgb(120, 40, 45);
1187 let plain = Color::rgb(30, 32, 40);
1188 let hover = Style::new().elevate_by(0.08);
1189
1190 let marked_hovered = Style::new()
1191 .bg(marked)
1192 .patch(hover)
1193 .resolve_color_transforms()
1194 .bg;
1195 let plain_hovered = Style::new()
1196 .bg(plain)
1197 .patch(hover)
1198 .resolve_color_transforms()
1199 .bg;
1200
1201 assert_ne!(
1202 marked_hovered,
1203 p(marked),
1204 "hover did not lift the base color"
1205 );
1206 assert_ne!(
1207 marked_hovered, plain_hovered,
1208 "hovering a marked surface collapsed it onto the plain hover color"
1209 );
1210 }
1211
1212 #[test]
1213 fn builder_order_does_not_change_transform_result() {
1214 let a = Style::new()
1215 .transform_fg(ColorTransform::Dim(0.5))
1216 .fg(Color::rgb(100, 120, 140));
1217 let b = Style::new()
1218 .fg(Color::rgb(100, 120, 140))
1219 .transform_fg(ColorTransform::Dim(0.5));
1220
1221 assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
1222 assert_eq!(a.resolve_color_transforms().fg, p(Color::rgb(50, 60, 70)));
1223 }
1224
1225 #[test]
1226 fn transform_chain_applies_in_patch_order() {
1227 let style = Style::new()
1228 .fg(Color::rgb(100, 120, 140))
1229 .patch(Style::new().transform_fg(ColorTransform::Dim(0.5)))
1230 .patch(Style::new().transform_fg(ColorTransform::Lighten(0.5)))
1231 .resolve_color_transforms();
1232
1233 assert_eq!(style.fg, p(Color::rgb(153, 158, 163)));
1234 }
1235
1236 #[test]
1237 fn state_cascade_stacks_bg_transforms_on_resolved_color() {
1238 let style = Style::new()
1242 .bg(Color::rgb(100, 100, 100))
1243 .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
1244 .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
1245 .resolve_color_transforms();
1246
1247 assert_eq!(style.bg, p(Color::rgb(25, 25, 25)));
1248 }
1249
1250 #[test]
1251 fn opacity_turns_foreground_into_alpha_paint() {
1252 let style = Style::new()
1253 .fg(Color::rgb(245, 167, 66))
1254 .bg(Color::rgb(255, 255, 255))
1255 .transform_fg(ColorTransform::Opacity(0.6))
1256 .resolve_color_transforms();
1257
1258 assert_eq!(
1259 style.fg,
1260 Some(Paint::Alpha {
1261 color: Color::rgb(245, 167, 66),
1262 alpha: 153,
1263 })
1264 );
1265 }
1266
1267 #[test]
1268 fn opacity_builder_order_is_independent_when_background_arrives_later() {
1269 let a = Style::new()
1270 .transform_fg(ColorTransform::Opacity(0.6))
1271 .fg(Color::rgb(245, 167, 66))
1272 .bg(Color::rgb(255, 255, 255));
1273 let b = Style::new()
1274 .fg(Color::rgb(245, 167, 66))
1275 .bg(Color::rgb(255, 255, 255))
1276 .transform_fg(ColorTransform::Opacity(0.6));
1277
1278 assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
1279 }
1280
1281 #[test]
1282 fn opacity_toward_uses_fixed_target_not_backdrop() {
1283 let c = Color::rgb(0, 100, 200);
1284 let target = Color::rgb(200, 10, 30);
1285 assert_eq!(
1286 ColorTransform::OpacityToward {
1287 factor: 1.0,
1288 target,
1289 }
1290 .apply_with_backdrop(c, Some(Color::White)),
1291 c
1292 );
1293 assert_eq!(
1294 ColorTransform::OpacityToward {
1295 factor: 0.0,
1296 target,
1297 }
1298 .apply_with_backdrop(c, Some(Color::White)),
1299 target
1300 );
1301 }
1302
1303 #[test]
1304 fn patch_prefers_overlay_contrast_policy() {
1305 let base = Style::new().contrast_policy(ContrastPolicy::Wcag);
1306 let overlay = Style::new().contrast_policy(ContrastPolicy::Off);
1307
1308 assert_eq!(
1309 base.patch(overlay).contrast_policy,
1310 Some(ContrastPolicy::Off)
1311 );
1312 }
1313
1314 #[test]
1315 fn theme_extensions_roundtrip_and_affect_equality() {
1316 let a = Theme::default().with_extension(BrandTheme {
1317 accent_badge: Color::rgb(1, 2, 3),
1318 });
1319 let b = Theme::default().with_extension(BrandTheme {
1320 accent_badge: Color::rgb(1, 2, 3),
1321 });
1322 let c = Theme::default().with_extension(BrandTheme {
1323 accent_badge: Color::rgb(9, 8, 7),
1324 });
1325
1326 assert_eq!(a.extension::<BrandTheme>(), b.extension::<BrandTheme>());
1327 assert_eq!(a, b);
1328 assert_ne!(a, c);
1329 }
1330}
1331
1332#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1334pub enum CaretShape {
1335 #[default]
1337 Block,
1338 Bar,
1340 Underline,
1342}
1343
1344#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1349pub struct CaretPalette {
1350 pub shape: CaretShape,
1352 pub color: Option<Color>,
1354}
1355
1356impl CaretPalette {
1357 pub fn new(shape: CaretShape, color: Option<Color>) -> Self {
1359 Self { shape, color }
1360 }
1361
1362 pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
1364 self.color = color.into();
1365 self
1366 }
1367}
1368
1369#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1373pub struct BorderGlyphs {
1374 pub top_left: &'static str,
1376 pub top: &'static str,
1378 pub top_right: &'static str,
1380 pub left: &'static str,
1382 pub right: &'static str,
1384 pub bottom_left: &'static str,
1386 pub bottom: &'static str,
1388 pub bottom_right: &'static str,
1390}
1391
1392impl Default for BorderGlyphs {
1393 fn default() -> Self {
1394 Self::PLAIN
1395 }
1396}
1397
1398impl BorderGlyphs {
1399 pub const PLAIN: Self = Self {
1401 top_left: "┌",
1402 top: "─",
1403 top_right: "┐",
1404 left: "│",
1405 right: "│",
1406 bottom_left: "└",
1407 bottom: "─",
1408 bottom_right: "┘",
1409 };
1410
1411 pub fn new(parts: BorderGlyphsParts) -> Self {
1413 Self {
1414 top_left: parts.top_left,
1415 top: parts.top,
1416 top_right: parts.top_right,
1417 left: parts.left,
1418 right: parts.right,
1419 bottom_left: parts.bottom_left,
1420 bottom: parts.bottom,
1421 bottom_right: parts.bottom_right,
1422 }
1423 }
1424}
1425
1426pub struct BorderGlyphsParts {
1428 pub top_left: &'static str,
1430 pub top: &'static str,
1432 pub top_right: &'static str,
1434 pub left: &'static str,
1436 pub right: &'static str,
1438 pub bottom_left: &'static str,
1440 pub bottom: &'static str,
1442 pub bottom_right: &'static str,
1444}
1445
1446#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1448pub enum BorderStyle {
1449 #[default]
1451 Plain,
1452 Rounded,
1454 Double,
1456 Thick,
1458 LightDoubleDashed,
1460 HeavyDoubleDashed,
1462 LightTripleDashed,
1464 HeavyTripleDashed,
1466 LightQuadrupleDashed,
1468 HeavyQuadrupleDashed,
1470 Custom {
1472 glyphs: BorderGlyphs,
1474 },
1475}
1476
1477#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1479pub enum ScrollbarVariant {
1480 Integrated,
1483 #[default]
1485 Standalone,
1486}
1487
1488#[derive(Clone, Debug, Default, PartialEq)]
1496pub struct ScrollbarConfig {
1497 pub variant: ScrollbarVariant,
1499 pub gap: u16,
1501 pub thumb: Option<char>,
1503 pub thumb_style: Option<Style>,
1505 pub thumb_focus_style: Option<Style>,
1507 pub track_style: Option<Style>,
1509}
1510
1511impl ScrollbarConfig {
1512 pub fn new() -> Self {
1514 Self::default()
1515 }
1516
1517 pub fn variant(mut self, variant: ScrollbarVariant) -> Self {
1519 self.variant = variant;
1520 self
1521 }
1522
1523 pub fn gap(mut self, gap: u16) -> Self {
1525 self.gap = gap;
1526 self
1527 }
1528
1529 pub fn thumb(mut self, ch: char) -> Self {
1531 self.thumb = Some(ch);
1532 self
1533 }
1534
1535 pub fn thumb_style(mut self, style: Style) -> Self {
1537 self.thumb_style = Some(style);
1538 self
1539 }
1540
1541 pub fn thumb_focus_style(mut self, style: Style) -> Self {
1543 self.thumb_focus_style = Some(style);
1544 self
1545 }
1546
1547 pub fn track_style(mut self, style: Style) -> Self {
1549 self.track_style = Some(style);
1550 self
1551 }
1552}
1553
1554#[derive(Clone, Debug, PartialEq)]
1558pub struct FileIconPalette {
1559 pub azure: Color,
1561 pub blue: Color,
1563 pub cyan: Color,
1565 pub green: Color,
1567 pub grey: Color,
1569 pub orange: Color,
1571 pub purple: Color,
1573 pub red: Color,
1575 pub yellow: Color,
1577}
1578
1579impl Default for FileIconPalette {
1580 fn default() -> Self {
1581 Self {
1582 azure: Color::hex_u24(0x61AFEF), blue: Color::hex_u24(0x4175E6), cyan: Color::hex_u24(0x56B6C2), green: Color::hex_u24(0x98C379), grey: Color::hex_u24(0xABB2BF), orange: Color::hex_u24(0xD19A66), purple: Color::hex_u24(0xC678DD), red: Color::hex_u24(0xE06C75), yellow: Color::hex_u24(0xE5C07B), }
1593 }
1594}
1595
1596#[derive(Clone, Copy, Debug, PartialEq)]
1598pub struct GitStatusPalette {
1599 pub modified: Color,
1601 pub added: Color,
1603 pub deleted: Color,
1605 pub renamed: Color,
1607 pub untracked: Color,
1609 pub conflicted: Color,
1611}
1612
1613impl Default for GitStatusPalette {
1614 fn default() -> Self {
1615 Self {
1616 modified: Color::hex_u24(0xE5B767),
1617 added: Color::hex_u24(0x7EC699),
1618 deleted: Color::hex_u24(0xE57E7E),
1619 renamed: Color::hex_u24(0x76C5E5),
1620 untracked: Color::hex_u24(0xC59AE5),
1621 conflicted: Color::hex_u24(0xE57E7E),
1622 }
1623 }
1624}
1625
1626#[derive(Clone, Copy, Debug, PartialEq)]
1628pub struct ScrollbarPalette {
1629 pub track: Option<Color>,
1631 pub thumb: Color,
1633 pub thumb_focus: Option<Color>,
1635}
1636
1637impl Default for ScrollbarPalette {
1638 fn default() -> Self {
1639 Self {
1640 track: None,
1641 thumb: Color::DarkGray,
1642 thumb_focus: Some(Color::Gray),
1643 }
1644 }
1645}
1646
1647#[derive(Clone, Copy, Debug, PartialEq)]
1649pub struct SplitterPalette {
1650 pub hover: Color,
1652 pub active: Color,
1654}
1655
1656impl Default for SplitterPalette {
1657 fn default() -> Self {
1658 Self {
1659 hover: Color::hex_u24(0x2563EB),
1660 active: Color::hex_u24(0x22D3EE),
1661 }
1662 }
1663}
1664
1665#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1667pub struct SurfacePalette {
1668 pub panel: Color,
1670 pub element: Color,
1672 pub menu: Color,
1674 pub backdrop: Color,
1676}
1677
1678#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1680pub struct StatusPalette {
1681 pub success: Color,
1683 pub warning: Color,
1685 pub error: Color,
1687 pub info: Color,
1689}
1690
1691#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1693pub struct DiffPalette {
1694 pub context: Style,
1696 pub added: Style,
1698 pub removed: Style,
1700 pub empty: Style,
1702 pub added_word: Style,
1704 pub removed_word: Style,
1706 pub added_marker: Style,
1708 pub removed_marker: Style,
1710 pub context_line_number: Style,
1712 pub added_line_number: Style,
1714 pub removed_line_number: Style,
1716 pub context_separator_style: Style,
1718 pub patch_header: Style,
1720}
1721
1722#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1724pub struct DocumentPalette {
1725 pub heading_styles: [Style; 6],
1727 pub code_inline: Style,
1729 pub code_block: Style,
1731 pub emphasis: Style,
1733 pub strong: Style,
1735 pub strikethrough: Style,
1737 pub link: Style,
1739 pub blockquote_bar: Style,
1741 pub table_border: Style,
1743 pub table_header: Style,
1745 pub hr: Style,
1747 pub list_item: Style,
1749 pub list_enumeration: Style,
1751 pub diagram_node_fill_style: Style,
1753 pub diagram_node_border_style: Style,
1755 pub diagram_node_label_style: Style,
1757 pub diagram_edge_style: Style,
1759 pub diagram_muted_style: Style,
1761}
1762
1763impl Default for DocumentPalette {
1764 fn default() -> Self {
1765 Self {
1766 heading_styles: [
1767 Style::new().bold().fg(Color::LightBlue),
1768 Style::new().bold().fg(Color::LightBlue),
1769 Style::new().bold().fg(Color::LightBlue),
1770 Style::new().bold(),
1771 Style::new().bold(),
1772 Style::new().bold().dim(),
1773 ],
1774 code_inline: Style::new().fg(Color::Green),
1775 code_block: Style::default(),
1776 emphasis: Style::new().italic(),
1777 strong: Style::new().bold(),
1778 strikethrough: Style::new().strikethrough(),
1779 link: Style::new().fg(Color::LightBlue).underline(),
1780 blockquote_bar: Style::new().fg(Color::DarkGray).dim(),
1781 table_border: Style::new().fg(Color::DarkGray).dim(),
1782 table_header: Style::new().bold(),
1783 hr: Style::new().fg(Color::DarkGray).dim(),
1784 list_item: Style::new().fg(Color::LightBlue).bold(),
1785 list_enumeration: Style::new().fg(Color::LightBlue).bold(),
1786 diagram_node_fill_style: Style::default(),
1787 diagram_node_border_style: Style::default(),
1788 diagram_node_label_style: Style::default(),
1789 diagram_edge_style: Style::default(),
1790 diagram_muted_style: Style::default(),
1791 }
1792 }
1793}
1794
1795#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1797pub struct SyntaxPalette {
1798 pub comment: Style,
1800 pub keyword: Style,
1802 pub string: Style,
1804 pub number: Style,
1806 pub constant: Style,
1808 pub function: Style,
1810 pub builtin: Style,
1812 pub type_name: Style,
1814 pub variable: Style,
1816 pub parameter: Style,
1818 pub operator: Style,
1820}
1821
1822impl Default for SyntaxPalette {
1823 fn default() -> Self {
1824 let number = Style::new().fg(Color::Yellow);
1825 let function = Style::new().fg(Color::Cyan);
1826 let variable = Style::new().fg(Color::White);
1827
1828 Self {
1829 comment: Style::new().fg(Color::DarkGray).italic().dim(),
1830 keyword: Style::new().fg(Color::LightMagenta),
1831 string: Style::new().fg(Color::Green),
1832 number,
1833 constant: number.lighten_by(0.12),
1834 function,
1835 builtin: function.italic(),
1836 type_name: Style::new().fg(Color::LightBlue),
1837 variable,
1838 parameter: variable.italic(),
1839 operator: Style::new().fg(Color::LightRed),
1840 }
1841 }
1842}
1843
1844#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1846pub struct InputPalette {
1847 pub focus: Style,
1852}
1853
1854#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1856pub struct TextAreaPalette {
1857 pub focus: Style,
1862}
1863
1864#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1866pub struct DocumentViewPalette {
1867 pub focus: Style,
1872}
1873
1874#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1876pub struct HexAreaPalette {
1877 pub focus: Style,
1879 pub cursor: Style,
1881}
1882
1883#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1885pub struct TerminalPalette {
1886 pub focus: Style,
1891}
1892
1893#[derive(Clone, Debug, PartialEq)]
1895pub struct Theme {
1896 pub primary: Style,
1898 pub accent: Style,
1903 pub caret: CaretPalette,
1905 pub selection: Style,
1907 pub text_selection: Style,
1909 pub focus: Style,
1915 pub focus_decoration: bool,
1921 pub hover: Style,
1923 pub border: Style,
1928 pub muted: Style,
1933 pub surface: SurfacePalette,
1935 pub status: StatusPalette,
1937 pub border_active: Color,
1939 pub file_icons: FileIconPalette,
1941 pub git_status: GitStatusPalette,
1943 pub diff: DiffPalette,
1945 pub document: DocumentPalette,
1947 pub syntax: SyntaxPalette,
1949 pub input: InputPalette,
1951 pub text_area: TextAreaPalette,
1953 pub document_view: DocumentViewPalette,
1955 pub hex_area: HexAreaPalette,
1957 pub terminal: TerminalPalette,
1959 pub scrollbar: ScrollbarPalette,
1961 pub splitter: SplitterPalette,
1963 #[doc(hidden)]
1965 pub extensions: ThemeExtensions,
1966}
1967
1968impl Theme {
1969 pub fn from_host_colors(colors: HostTerminalColors) -> Self {
1971 ThemePalette::new(colors.fg, colors.bg, colors.ansi[4])
1972 .success(colors.ansi[2])
1973 .warning(colors.ansi[3])
1974 .error(colors.ansi[1])
1975 .info(colors.ansi[4])
1976 .into_theme()
1977 .with_extension(colors)
1978 }
1979
1980 pub fn role(&self, role: ThemeRole) -> Style {
1982 match role {
1983 ThemeRole::Base => self.primary,
1984 ThemeRole::Accent => {
1985 let mut style = self.accent;
1986 if style.fg.is_none() {
1987 style.fg = self.primary.fg;
1988 }
1989 if style.fg_transform.is_none() {
1990 style.fg_transform = self.primary.fg_transform;
1991 }
1992 style
1993 }
1994 ThemeRole::Selection | ThemeRole::UnfocusedSelection => self.selection,
1995 ThemeRole::TextSelection => self.text_selection,
1996 ThemeRole::Hover
1997 | ThemeRole::DragSource
1998 | ThemeRole::DropTarget
1999 | ThemeRole::DropTargetActive
2000 | ThemeRole::ItemHover => self.hover,
2001 ThemeRole::Focus if self.focus_decoration => self.focus,
2002 ThemeRole::Focus => Style::default(),
2003 ThemeRole::Active => self.selection,
2004 ThemeRole::Border => self.primary.patch(self.border),
2005 ThemeRole::Disabled | ThemeRole::Muted => self.primary.patch(self.muted),
2006 ThemeRole::Error => Style::new().fg(self.status.error),
2007 ThemeRole::InputFocusContent if self.focus_decoration => self.input.focus,
2008 ThemeRole::TextAreaFocusContent if self.focus_decoration => self.text_area.focus,
2009 ThemeRole::DocumentViewFocusContent if self.focus_decoration => {
2010 self.document_view.focus
2011 }
2012 ThemeRole::HexAreaFocusContent if self.focus_decoration => self.hex_area.focus,
2013 ThemeRole::HexAreaCursor if self.focus_decoration => self.hex_area.cursor,
2014 ThemeRole::TerminalFocusContent if self.focus_decoration => self.terminal.focus,
2015 ThemeRole::InputFocusContent
2016 | ThemeRole::TextAreaFocusContent
2017 | ThemeRole::DocumentViewFocusContent
2018 | ThemeRole::HexAreaFocusContent
2019 | ThemeRole::HexAreaCursor
2020 | ThemeRole::TerminalFocusContent => Style::default(),
2021 ThemeRole::ScrollbarThumb => Style::new().bg(self.scrollbar.thumb),
2022 ThemeRole::ScrollbarThumbFocus if self.focus_decoration => self
2023 .scrollbar
2024 .thumb_focus
2025 .map(|color| Style::new().bg(color))
2026 .unwrap_or_default(),
2027 ThemeRole::ScrollbarThumbFocus => Style::default(),
2028 ThemeRole::ScrollbarTrack => self
2029 .scrollbar
2030 .track
2031 .map(|color| Style::new().bg(color))
2032 .unwrap_or_default(),
2033 ThemeRole::SplitterHover => Style::new().fg(self.splitter.hover),
2034 ThemeRole::SplitterActive => Style::new().fg(self.splitter.active),
2035 }
2036 }
2037
2038 pub fn concretize_backdrop(&self, host_bg: Option<Color>) -> Color {
2043 if !self.surface.backdrop.is_sentinel() {
2044 return self.surface.backdrop;
2045 }
2046 host_bg
2047 .filter(|color| !color.is_sentinel())
2048 .unwrap_or_else(|| self.surface.panel.resolve(Color::Reset))
2049 }
2050
2051 pub fn custom(primary_fg: Color, primary_bg: Color, accent: Color) -> Self {
2058 let success = Color::Green;
2059 let warning = Color::Yellow;
2060 let error = Color::Red;
2061 let info = accent;
2062 let muted = primary_fg.blend_toward(primary_bg, 0.42);
2063 let border_active = accent.lighten_by(0.08);
2064 Self {
2065 primary: Style::new().fg(primary_fg).bg(primary_bg),
2066 accent: Style::new().fg(accent),
2067 caret: CaretPalette::new(CaretShape::default(), Some(accent)),
2068 selection: Style::new()
2069 .fg(accent)
2070 .bg(primary_bg.blend_toward(accent, 0.22)),
2071 text_selection: Style::new()
2072 .fg(accent)
2073 .bg(primary_bg.blend_toward(accent, 0.22)),
2074 focus: Style::new().fg(border_active),
2075 focus_decoration: true,
2076 hover: Style::default(),
2077 border: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.40)),
2078 muted: Style::new().fg(muted),
2079 surface: SurfacePalette {
2080 panel: primary_bg.elevate_by(0.07),
2081 element: primary_bg.elevate_by(0.04),
2082 menu: primary_bg.elevate_by(0.12),
2083 backdrop: primary_bg,
2084 },
2085 status: StatusPalette {
2086 success,
2087 warning,
2088 error,
2089 info,
2090 },
2091 border_active,
2092 file_icons: FileIconPalette::default(),
2093 git_status: GitStatusPalette::default(),
2094 diff: DiffPalette {
2095 context: Style::default(),
2096 added: Style::new().bg(primary_bg.blend_toward(success, 0.14)),
2097 removed: Style::new().bg(primary_bg.blend_toward(error, 0.16)),
2098 empty: Style::new().dim(),
2099 added_word: Style::new().bg(primary_bg.blend_toward(success, 0.24)),
2100 removed_word: Style::new().bg(primary_bg.blend_toward(error, 0.28)),
2101 added_marker: Style::new().fg(success),
2102 removed_marker: Style::new().fg(error),
2103 context_line_number: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.50)),
2104 added_line_number: Style::default(),
2105 removed_line_number: Style::default(),
2106 context_separator_style: Style::new()
2107 .fg(primary_fg.blend_toward(primary_bg, 0.40))
2108 .dim(),
2109 patch_header: Style::new()
2110 .fg(accent.blend_toward(primary_fg, 0.35))
2111 .bold(),
2112 },
2113 document: DocumentPalette {
2114 heading_styles: [
2115 Style::new().bold().fg(accent.lighten_by(0.20)),
2116 Style::new().bold().fg(accent.lighten_by(0.12)),
2117 Style::new().bold().fg(accent),
2118 Style::new().bold().fg(primary_fg),
2119 Style::new().bold().fg(primary_fg),
2120 Style::new().bold().fg(primary_fg).dim(),
2121 ],
2122 code_inline: Style::new().fg(success),
2123 code_block: Style::default(),
2124 emphasis: Style::new().italic(),
2125 strong: Style::new().bold(),
2126 strikethrough: Style::new().strikethrough(),
2127 link: Style::new().fg(accent).underline(),
2128 blockquote_bar: Style::new().fg(muted).dim(),
2129 table_border: Style::new()
2130 .fg(primary_fg.blend_toward(primary_bg, 0.40))
2131 .dim(),
2132 table_header: Style::new().bold(),
2133 hr: Style::new()
2134 .fg(primary_fg.blend_toward(primary_bg, 0.40))
2135 .dim(),
2136 list_item: Style::new().fg(accent).bold(),
2137 list_enumeration: Style::new().fg(accent).bold(),
2138 diagram_node_fill_style: Style::new().bg(primary_bg.blend_toward(accent, 0.10)),
2139 diagram_node_border_style: Style::new().fg(accent.lighten_by(0.08)),
2140 diagram_node_label_style: Style::new().fg(primary_fg),
2141 diagram_edge_style: Style::new().fg(accent.blend_toward(primary_fg, 0.20)),
2142 diagram_muted_style: Style::new().fg(muted).dim(),
2143 },
2144 syntax: SyntaxPalette {
2145 comment: Style::new().fg(muted).italic().dim(),
2146 keyword: Style::new().fg(accent),
2147 string: Style::new().fg(accent.blend_toward(success, 0.55)),
2148 number: Style::new().fg(accent.blend_toward(Color::Yellow, 0.60)),
2149 constant: Style::new()
2150 .fg(accent.blend_toward(Color::Yellow, 0.52).lighten_by(0.10)),
2151 function: Style::new().fg(info.blend_toward(accent, 0.12)),
2152 builtin: Style::new().fg(info.blend_toward(accent, 0.28)).italic(),
2153 type_name: Style::new().fg(accent.blend_toward(info, 0.32)),
2154 variable: Style::new().fg(primary_fg),
2155 parameter: Style::new()
2156 .fg(primary_fg.blend_toward(accent, 0.12))
2157 .italic(),
2158 operator: Style::new().fg(accent.blend_toward(error, 0.45)),
2159 },
2160 input: InputPalette::default(),
2161 text_area: TextAreaPalette::default(),
2162 document_view: DocumentViewPalette::default(),
2163 hex_area: HexAreaPalette {
2164 focus: Style::default(),
2165 cursor: Style::new().fg(accent),
2166 },
2167 terminal: TerminalPalette::default(),
2168 scrollbar: ScrollbarPalette {
2169 track: Some(primary_bg.elevate_by(0.05)),
2170 thumb: primary_bg.elevate_by(0.20),
2171 thumb_focus: Some(accent.lighten_by(0.08)),
2172 },
2173 splitter: SplitterPalette {
2174 hover: accent.lighten_by(0.08),
2175 active: accent.lighten_by(0.18),
2176 },
2177 extensions: ThemeExtensions::default(),
2178 }
2179 }
2180
2181 pub fn primary(mut self, style: Style) -> Self {
2194 self.primary = style;
2195 self
2196 }
2197
2198 pub fn accent(mut self, style: Style) -> Self {
2204 self.accent = style;
2205 self
2206 }
2207
2208 pub fn caret(mut self, palette: CaretPalette) -> Self {
2212 self.caret = palette;
2213 self
2214 }
2215
2216 pub fn caret_shape(mut self, shape: CaretShape) -> Self {
2218 self.caret.shape = shape;
2219 self
2220 }
2221
2222 pub fn caret_color(mut self, color: impl Into<Option<Color>>) -> Self {
2226 self.caret.color = color.into();
2227 self
2228 }
2229
2230 pub fn focus(mut self, style: Style) -> Self {
2236 self.focus = style;
2237 self
2238 }
2239
2240 pub fn focus_decoration(mut self, focus_decoration: bool) -> Self {
2244 self.focus_decoration = focus_decoration;
2245 self
2246 }
2247
2248 pub fn with_extension<T>(mut self, extension: T) -> Self
2254 where
2255 T: ThemeExtension,
2256 {
2257 self.extensions.insert(extension);
2258 self
2259 }
2260
2261 pub fn without_extension<T>(mut self) -> Self
2263 where
2264 T: ThemeExtension,
2265 {
2266 self.extensions.remove::<T>();
2267 self
2268 }
2269
2270 pub fn extension<T>(&self) -> Option<&T>
2272 where
2273 T: ThemeExtension,
2274 {
2275 self.extensions.get::<T>()
2276 }
2277
2278 pub fn extension_cloned<T>(&self) -> Option<T>
2280 where
2281 T: ThemeExtension,
2282 {
2283 self.extension::<T>().cloned()
2284 }
2285
2286 pub fn selection(mut self, style: Style) -> Self {
2291 self.selection = style;
2292 self
2293 }
2294
2295 pub fn text_selection(mut self, style: Style) -> Self {
2300 self.text_selection = style;
2301 self
2302 }
2303
2304 pub fn hover(mut self, style: Style) -> Self {
2310 self.hover = style;
2311 self
2312 }
2313
2314 pub fn border(mut self, style: Style) -> Self {
2319 self.border = style;
2320 self
2321 }
2322
2323 pub fn muted(mut self, style: Style) -> Self {
2328 self.muted = style;
2329 self
2330 }
2331
2332 pub fn scrollbar(mut self, palette: ScrollbarPalette) -> Self {
2334 self.scrollbar = palette;
2335 self
2336 }
2337
2338 pub fn splitter(mut self, palette: SplitterPalette) -> Self {
2340 self.splitter = palette;
2341 self
2342 }
2343
2344 pub fn file_icons(mut self, palette: FileIconPalette) -> Self {
2346 self.file_icons = palette;
2347 self
2348 }
2349
2350 pub fn git_status(mut self, palette: GitStatusPalette) -> Self {
2352 self.git_status = palette;
2353 self
2354 }
2355
2356 pub fn diff(mut self, palette: DiffPalette) -> Self {
2358 self.diff = palette;
2359 self
2360 }
2361
2362 pub fn document(mut self, palette: DocumentPalette) -> Self {
2364 self.document = palette;
2365 self
2366 }
2367
2368 pub fn syntax(mut self, palette: SyntaxPalette) -> Self {
2370 self.syntax = palette;
2371 self
2372 }
2373
2374 pub fn input(mut self, palette: InputPalette) -> Self {
2376 self.input = palette;
2377 self
2378 }
2379
2380 pub fn text_area(mut self, palette: TextAreaPalette) -> Self {
2382 self.text_area = palette;
2383 self
2384 }
2385
2386 pub fn document_view(mut self, palette: DocumentViewPalette) -> Self {
2388 self.document_view = palette;
2389 self
2390 }
2391
2392 pub fn hex_area(mut self, palette: HexAreaPalette) -> Self {
2394 self.hex_area = palette;
2395 self
2396 }
2397
2398 pub fn terminal(mut self, palette: TerminalPalette) -> Self {
2400 self.terminal = palette;
2401 self
2402 }
2403}
2404
2405#[derive(Clone, Debug)]
2442pub struct ThemePalette {
2443 pub text: Color,
2445 pub background: Color,
2447 pub accent: Color,
2449 pub caret: CaretPalette,
2452 pub selection: Option<Color>,
2454 pub text_selection: Option<Color>,
2456 pub border: Option<Color>,
2458 pub muted: Option<Color>,
2461 pub scrollbar: Option<Color>,
2463 pub success: Option<Color>,
2465 pub warning: Option<Color>,
2467 pub error: Option<Color>,
2469 pub info: Option<Color>,
2471}
2472
2473impl ThemePalette {
2474 pub fn new(text: Color, background: Color, accent: Color) -> Self {
2476 Self {
2477 text,
2478 background,
2479 accent,
2480 caret: CaretPalette::new(CaretShape::default(), Some(accent)),
2481 selection: None,
2482 text_selection: None,
2483 border: None,
2484 muted: None,
2485 scrollbar: None,
2486 success: None,
2487 warning: None,
2488 error: None,
2489 info: None,
2490 }
2491 }
2492
2493 pub fn selection(mut self, color: Color) -> Self {
2495 self.selection = Some(color);
2496 self
2497 }
2498
2499 pub fn text_selection(mut self, color: Color) -> Self {
2501 self.text_selection = Some(color);
2502 self
2503 }
2504
2505 pub fn border(mut self, color: Color) -> Self {
2507 self.border = Some(color);
2508 self
2509 }
2510
2511 pub fn muted(mut self, color: Color) -> Self {
2513 self.muted = Some(color);
2514 self
2515 }
2516
2517 pub fn scrollbar(mut self, color: Color) -> Self {
2519 self.scrollbar = Some(color);
2520 self
2521 }
2522
2523 pub fn success(mut self, color: Color) -> Self {
2525 self.success = Some(color);
2526 self
2527 }
2528
2529 pub fn warning(mut self, color: Color) -> Self {
2531 self.warning = Some(color);
2532 self
2533 }
2534
2535 pub fn error(mut self, color: Color) -> Self {
2537 self.error = Some(color);
2538 self
2539 }
2540
2541 pub fn info(mut self, color: Color) -> Self {
2543 self.info = Some(color);
2544 self
2545 }
2546
2547 pub fn caret(mut self, palette: CaretPalette) -> Self {
2549 self.caret = palette;
2550 self
2551 }
2552
2553 pub fn caret_shape(mut self, shape: CaretShape) -> Self {
2555 self.caret.shape = shape;
2556 self
2557 }
2558
2559 pub fn caret_color(mut self, color: impl Into<Option<Color>>) -> Self {
2563 self.caret.color = color.into();
2564 self
2565 }
2566
2567 pub fn into_theme(self) -> Theme {
2569 Theme::from(self)
2570 }
2571}
2572
2573impl From<ThemePalette> for Theme {
2574 fn from(p: ThemePalette) -> Self {
2575 let border_color = p
2576 .border
2577 .unwrap_or_else(|| p.text.blend_toward(p.background, 0.40));
2578 let muted_color = p
2579 .muted
2580 .unwrap_or_else(|| p.text.blend_toward(p.background, 0.42));
2581 let scrollbar_thumb = p.scrollbar.unwrap_or_else(|| p.background.elevate_by(0.20));
2582
2583 let success = p.success.unwrap_or(Color::hex_u24(0x34D399));
2584 let warning = p.warning.unwrap_or(Color::hex_u24(0xFBBF24));
2585 let error = p.error.unwrap_or(Color::hex_u24(0xF43F5E));
2586 let info = p.info.unwrap_or(p.accent);
2587 let border_active = p.border.unwrap_or(p.accent).lighten_by(0.08);
2588 let selection = p.selection.unwrap_or(p.accent);
2589 let text_selection = p.text_selection.unwrap_or(p.accent);
2590
2591 Theme {
2592 primary: Style::new().fg(p.text).bg(p.background),
2593 accent: Style::new().fg(p.accent),
2594 caret: p.caret,
2595 selection: Style::new()
2596 .fg(selection)
2597 .bg(p.background.blend_toward(selection, 0.22)),
2598 text_selection: Style::new()
2599 .fg(text_selection)
2600 .bg(p.background.blend_toward(text_selection, 0.22)),
2601 focus: Style::new().fg(border_active),
2602 focus_decoration: true,
2603 hover: Style::default(),
2604 border: Style::new().fg(border_color),
2605 muted: Style::new().fg(muted_color),
2606 surface: SurfacePalette {
2607 panel: p.background.elevate_by(0.07),
2608 element: p.background.elevate_by(0.04),
2609 menu: p.background.elevate_by(0.12),
2610 backdrop: p.background,
2611 },
2612 status: StatusPalette {
2613 success,
2614 warning,
2615 error,
2616 info,
2617 },
2618 border_active,
2619 file_icons: FileIconPalette {
2620 green: success,
2621 red: error,
2622 yellow: warning,
2623 azure: info,
2624 blue: p.accent,
2625 cyan: info.lighten_by(0.10),
2626 grey: muted_color,
2627 orange: warning.blend_toward(error, 0.40),
2628 purple: p.accent.blend_toward(error, 0.30),
2629 },
2630 git_status: GitStatusPalette {
2631 modified: warning,
2632 added: success,
2633 deleted: error,
2634 renamed: info,
2635 untracked: p.accent.blend_toward(error, 0.30),
2636 conflicted: error,
2637 },
2638 diff: DiffPalette {
2639 context: Style::default(),
2640 added: Style::new().bg(p.background.blend_toward(success, 0.14)),
2641 removed: Style::new().bg(p.background.blend_toward(error, 0.16)),
2642 empty: Style::new().dim(),
2643 added_word: Style::new().bg(p.background.blend_toward(success, 0.24)),
2644 removed_word: Style::new().bg(p.background.blend_toward(error, 0.28)),
2645 added_marker: Style::new().fg(success),
2646 removed_marker: Style::new().fg(error),
2647 context_line_number: Style::new().fg(p.text.blend_toward(p.background, 0.50)),
2648 added_line_number: Style::default(),
2649 removed_line_number: Style::default(),
2650 context_separator_style: Style::new()
2651 .fg(p.text.blend_toward(p.background, 0.40))
2652 .dim(),
2653 patch_header: Style::new().fg(p.accent.blend_toward(p.text, 0.25)).bold(),
2654 },
2655 document: DocumentPalette {
2656 heading_styles: [
2657 Style::new().bold().fg(p.accent.lighten_by(0.20)),
2658 Style::new().bold().fg(p.accent.lighten_by(0.12)),
2659 Style::new().bold().fg(p.accent),
2660 Style::new().bold().fg(p.text),
2661 Style::new().bold().fg(p.text),
2662 Style::new().bold().fg(p.text).dim(),
2663 ],
2664 code_inline: Style::new().fg(success),
2665 code_block: Style::default(),
2666 emphasis: Style::new().italic(),
2667 strong: Style::new().bold(),
2668 strikethrough: Style::new().strikethrough(),
2669 link: Style::new().fg(p.accent).underline(),
2670 blockquote_bar: Style::new().fg(muted_color).dim(),
2671 table_border: Style::new().fg(border_color).dim(),
2672 table_header: Style::new().bold(),
2673 hr: Style::new().fg(border_color).dim(),
2674 list_item: Style::new().fg(p.accent).bold(),
2675 list_enumeration: Style::new().fg(p.accent).bold(),
2676 diagram_node_fill_style: Style::new().bg(p.background.blend_toward(p.accent, 0.10)),
2677 diagram_node_border_style: Style::new().fg(p.accent.lighten_by(0.08)),
2678 diagram_node_label_style: Style::new().fg(p.text),
2679 diagram_edge_style: Style::new().fg(p.accent.blend_toward(p.text, 0.20)),
2680 diagram_muted_style: Style::new().fg(muted_color).dim(),
2681 },
2682 syntax: SyntaxPalette {
2683 comment: Style::new().fg(muted_color).italic().dim(),
2684 keyword: Style::new().fg(p.accent),
2685 string: Style::new().fg(success.blend_toward(p.accent, 0.15)),
2686 number: Style::new().fg(warning.blend_toward(p.accent, 0.20)),
2687 constant: Style::new().fg(warning.blend_toward(p.text, 0.18)),
2688 function: Style::new().fg(info.blend_toward(p.accent, 0.10)),
2689 builtin: Style::new()
2690 .fg(info.blend_toward(muted_color, 0.22))
2691 .italic(),
2692 type_name: Style::new().fg(p.accent.blend_toward(info, 0.35)),
2693 variable: Style::new().fg(p.text),
2694 parameter: Style::new().fg(p.text).italic(),
2695 operator: Style::new().fg(error.blend_toward(p.accent, 0.45)),
2696 },
2697 input: InputPalette::default(),
2698 text_area: TextAreaPalette::default(),
2699 document_view: DocumentViewPalette::default(),
2700 hex_area: HexAreaPalette {
2701 focus: Style::default(),
2702 cursor: Style::new().fg(p.accent),
2703 },
2704 terminal: TerminalPalette::default(),
2705 scrollbar: ScrollbarPalette {
2706 track: Some(p.background.elevate_by(0.05)),
2707 thumb: scrollbar_thumb,
2708 thumb_focus: Some(p.accent.lighten_by(0.08)),
2709 },
2710 splitter: SplitterPalette {
2711 hover: p.accent.lighten_by(0.08),
2712 active: p.accent.lighten_by(0.18),
2713 },
2714 extensions: ThemeExtensions::default(),
2715 }
2716 }
2717}
2718
2719impl Default for Theme {
2720 fn default() -> Self {
2721 let mut theme: Self = ThemePalette::new(
2722 Color::hex_u24(0xE2E8F0),
2723 Color::hex_u24(0x0B121F),
2724 Color::hex_u24(0x7DCFFF),
2725 )
2726 .success(Color::hex_u24(0x34D399))
2727 .warning(Color::hex_u24(0xFBBF24))
2728 .error(Color::hex_u24(0xF43F5E))
2729 .info(Color::hex_u24(0x38BDF8))
2730 .into();
2731
2732 theme.file_icons = FileIconPalette {
2733 azure: Color::hex_u24(0x7DCFFF),
2734 blue: Color::hex_u24(0x60A5FA),
2735 cyan: Color::hex_u24(0x2DD4BF),
2736 green: Color::hex_u24(0x4ADE80),
2737 grey: Color::hex_u24(0x94A3B8),
2738 orange: Color::hex_u24(0xFB923C),
2739 purple: Color::hex_u24(0xC4B5FD),
2740 red: Color::hex_u24(0xF87171),
2741 yellow: Color::hex_u24(0xFBBF24),
2742 };
2743 theme.git_status = GitStatusPalette {
2744 modified: Color::hex_u24(0xFBBF24),
2745 added: Color::hex_u24(0x34D399),
2746 deleted: Color::hex_u24(0xFB7171),
2747 renamed: Color::hex_u24(0x38BDF8),
2748 untracked: Color::hex_u24(0xA78BFA),
2749 conflicted: Color::hex_u24(0xF43F5E),
2750 };
2751
2752 theme
2753 }
2754}