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 Opacity(f32),
28 OpacityToward {
30 factor: f32,
32 target: Color,
34 },
35 Tint(Color, f32),
37}
38
39impl PartialEq for ColorTransform {
40 fn eq(&self, other: &Self) -> bool {
41 match (*self, *other) {
42 (Self::Dim(a), Self::Dim(b))
43 | (Self::Lighten(a), Self::Lighten(b))
44 | (Self::Opacity(a), Self::Opacity(b)) => a.to_bits() == b.to_bits(),
45 (
46 Self::OpacityToward {
47 factor: fa,
48 target: ta,
49 },
50 Self::OpacityToward {
51 factor: fb,
52 target: tb,
53 },
54 ) => fa.to_bits() == fb.to_bits() && ta == tb,
55 (Self::Tint(color_a, alpha_a), Self::Tint(color_b, alpha_b)) => {
56 color_a == color_b && alpha_a.to_bits() == alpha_b.to_bits()
57 }
58 _ => false,
59 }
60 }
61}
62
63impl Eq for ColorTransform {}
64
65impl Hash for ColorTransform {
66 fn hash<H: Hasher>(&self, state: &mut H) {
67 match *self {
68 Self::Dim(amount) => {
69 0u8.hash(state);
70 amount.to_bits().hash(state);
71 }
72 Self::Lighten(amount) => {
73 1u8.hash(state);
74 amount.to_bits().hash(state);
75 }
76 Self::Opacity(amount) => {
77 2u8.hash(state);
78 amount.to_bits().hash(state);
79 }
80 Self::OpacityToward { factor, target } => {
81 4u8.hash(state);
82 factor.to_bits().hash(state);
83 target.hash(state);
84 }
85 Self::Tint(color, alpha) => {
86 3u8.hash(state);
87 color.hash(state);
88 alpha.to_bits().hash(state);
89 }
90 }
91 }
92}
93
94impl ColorTransform {
95 pub fn apply(self, color: Color) -> Color {
97 self.apply_with_backdrop(color, None)
98 }
99
100 pub fn apply_with_backdrop(self, color: Color, backdrop: Option<Color>) -> Color {
102 if matches!(color, Color::Transparent | Color::Backdrop) {
103 return color;
104 }
105 match self {
106 Self::Dim(amount) => color.dim_by(amount),
107 Self::Lighten(amount) => color.lighten_by(amount),
108 Self::Opacity(opacity) => backdrop.map_or(color, |bg| {
109 color.blend_toward(bg, (1.0 - opacity).clamp(0.0, 1.0))
110 }),
111 Self::OpacityToward { factor, target } => {
112 color.blend_toward(target, (1.0 - factor).clamp(0.0, 1.0))
113 }
114 Self::Tint(target, alpha) => color.blend_toward(target, alpha),
115 }
116 }
117
118 pub fn apply_paint(self, paint: Paint) -> Paint {
123 self.apply_paint_with_backdrop(paint, None)
124 }
125
126 pub fn apply_paint_with_backdrop(self, paint: Paint, backdrop: Option<Paint>) -> Paint {
128 if matches!(paint, Paint::Solid(Color::Transparent | Color::Backdrop)) {
129 return paint;
130 }
131 if let Self::Opacity(opacity) = self {
132 let alpha = (paint.alpha_u8() as f32 * opacity.clamp(0.0, 1.0))
133 .round()
134 .clamp(0.0, 255.0) as u8;
135 return Paint::from_color_alpha_u8(paint.color(), alpha);
136 }
137 let backdrop = backdrop.map(Paint::color);
138 match paint {
139 Paint::Solid(color) => Paint::Solid(self.apply_with_backdrop(color, backdrop)),
140 Paint::Alpha { color, alpha } => {
141 Paint::from_color_alpha_u8(self.apply_with_backdrop(color, backdrop), alpha)
142 }
143 }
144 }
145
146 pub(crate) fn needs_backdrop(self) -> bool {
147 matches!(self, Self::Opacity(_))
148 }
149
150 fn normalized(self) -> Self {
151 match self {
152 Self::Dim(amount) => Self::Dim(amount.clamp(0.0, 1.0)),
153 Self::Lighten(amount) => Self::Lighten(amount.clamp(0.0, 1.0)),
154 Self::Opacity(opacity) => Self::Opacity(opacity.clamp(0.0, 1.0)),
155 Self::OpacityToward { factor, target } => Self::OpacityToward {
156 factor: factor.clamp(0.0, 1.0),
157 target,
158 },
159 Self::Tint(color, alpha) => Self::Tint(color, alpha.clamp(0.0, 1.0)),
160 }
161 }
162}
163
164pub trait ThemeExtension: Clone + fmt::Debug + PartialEq + 'static {}
170
171impl<T> ThemeExtension for T where T: Clone + fmt::Debug + PartialEq + 'static {}
172
173trait ThemeExtensionValue: Any {
174 fn as_any(&self) -> &dyn Any;
175 fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool;
176}
177
178impl<T> ThemeExtensionValue for T
179where
180 T: ThemeExtension,
181{
182 fn as_any(&self) -> &dyn Any {
183 self
184 }
185
186 fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool {
187 other.as_any().downcast_ref::<T>() == Some(self)
188 }
189}
190
191#[derive(Clone, Default)]
192#[doc(hidden)]
193pub struct ThemeExtensions(HashMap<TypeId, Arc<dyn ThemeExtensionValue>>);
194
195impl ThemeExtensions {
196 fn insert<T>(&mut self, extension: T)
197 where
198 T: ThemeExtension,
199 {
200 self.0.insert(TypeId::of::<T>(), Arc::new(extension));
201 }
202
203 fn get<T>(&self) -> Option<&T>
204 where
205 T: ThemeExtension,
206 {
207 self.0
208 .get(&TypeId::of::<T>())
209 .and_then(|value| value.as_any().downcast_ref::<T>())
210 }
211
212 fn remove<T>(&mut self)
213 where
214 T: ThemeExtension,
215 {
216 self.0.remove(&TypeId::of::<T>());
217 }
218}
219
220impl PartialEq for ThemeExtensions {
221 fn eq(&self, other: &Self) -> bool {
222 self.0.len() == other.0.len()
223 && self.0.iter().all(|(type_id, value)| {
224 other
225 .0
226 .get(type_id)
227 .is_some_and(|other_value| value.eq_value(other_value.as_ref()))
228 })
229 }
230}
231
232impl Eq for ThemeExtensions {}
233
234impl fmt::Debug for ThemeExtensions {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 f.debug_struct("ThemeExtensions")
237 .field("count", &self.0.len())
238 .finish()
239 }
240}
241
242#[cfg_attr(
244 feature = "terminal-serde",
245 derive(serde::Serialize, serde::Deserialize)
246)]
247#[derive(Clone, Copy, Debug, Default)]
248pub struct Style {
249 pub fg: Option<Paint>,
251 pub bg: Option<Paint>,
253 pub fg_transform: Option<ColorTransform>,
255 pub bg_transform: Option<ColorTransform>,
257 pub contrast_policy: Option<ContrastPolicy>,
259 pub bold: Option<bool>,
261 pub dim: Option<bool>,
263 pub italic: Option<bool>,
265 pub underline: Option<bool>,
267 pub reverse: Option<bool>,
269 pub strikethrough: Option<bool>,
271 pub underline_color: Option<Paint>,
273 pub dim_amount: Option<f32>,
280 pub tint: Option<(Color, f32)>,
288}
289
290impl PartialEq for Style {
291 fn eq(&self, other: &Self) -> bool {
292 self.fg == other.fg
293 && self.bg == other.bg
294 && self.fg_transform == other.fg_transform
295 && self.bg_transform == other.bg_transform
296 && self.contrast_policy == other.contrast_policy
297 && self.bold == other.bold
298 && self.dim == other.dim
299 && self.italic == other.italic
300 && self.underline == other.underline
301 && self.reverse == other.reverse
302 && self.strikethrough == other.strikethrough
303 && self.underline_color == other.underline_color
304 && self.dim_amount.map(f32::to_bits) == other.dim_amount.map(f32::to_bits)
305 && self.tint.map(|(c, a)| (c, a.to_bits())) == other.tint.map(|(c, a)| (c, a.to_bits()))
306 }
307}
308
309impl Eq for Style {}
310
311#[cfg(all(test, feature = "terminal-serde"))]
312mod terminal_serde_tests {
313 use super::*;
314
315 #[test]
316 fn color_transform_round_trips() {
317 let transform = ColorTransform::OpacityToward {
318 factor: 0.42,
319 target: Color::rgb(1, 2, 3),
320 };
321 let json = serde_json::to_string(&transform).unwrap();
322 assert_eq!(
323 serde_json::from_str::<ColorTransform>(&json).unwrap(),
324 transform
325 );
326 }
327
328 #[test]
329 fn style_round_trips() {
330 let style = Style::default()
331 .fg(Paint::rgb(20, 30, 40))
332 .bg(Paint::rgba(1, 2, 3, 180))
333 .bold()
334 .underline()
335 .contrast_policy(ContrastPolicy::BlackOrWhite)
336 .tint_by(Color::Cyan, 0.25);
337 let json = serde_json::to_string(&style).unwrap();
338 assert_eq!(serde_json::from_str::<Style>(&json).unwrap(), style);
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
348pub enum StyleSlot {
349 #[default]
351 Inherit,
352 Extend(Style),
354 Replace(Style),
356}
357
358impl StyleSlot {
359 pub fn replace(style: Style) -> Self {
361 Self::Replace(style)
362 }
363
364 pub fn extend(style: Style) -> Self {
366 Self::Extend(style)
367 }
368
369 pub fn explicit_style(self) -> Option<Style> {
371 match self {
372 Self::Inherit => None,
373 Self::Extend(style) | Self::Replace(style) => Some(style),
374 }
375 }
376
377 pub fn has_explicit_style(self) -> bool {
379 self.explicit_style().is_some_and(|style| !style.is_empty())
380 }
381
382 pub fn is_empty(self) -> bool {
384 matches!(self, Self::Replace(style) if style.is_empty())
385 }
386}
387
388impl From<Style> for StyleSlot {
389 fn from(style: Style) -> Self {
390 Self::Replace(style)
391 }
392}
393
394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
396pub enum ThemeRole {
397 Base,
399 Accent,
401 Selection,
403 TextSelection,
405 UnfocusedSelection,
407 Hover,
409 DragSource,
415 DropTarget,
421 DropTargetActive,
427 Focus,
429 Active,
431 ItemHover,
433 Border,
435 Disabled,
437 Muted,
439 Error,
441 InputFocusContent,
443 TextAreaFocusContent,
445 DocumentViewFocusContent,
447 HexAreaFocusContent,
449 HexAreaCursor,
451 TerminalFocusContent,
453 ScrollbarThumb,
455 ScrollbarThumbFocus,
457 ScrollbarTrack,
459 SplitterHover,
461 SplitterActive,
463}
464
465impl Hash for Style {
466 fn hash<H: Hasher>(&self, state: &mut H) {
467 self.fg.hash(state);
468 self.bg.hash(state);
469 self.fg_transform.hash(state);
470 self.bg_transform.hash(state);
471 self.contrast_policy.hash(state);
472 self.bold.hash(state);
473 self.dim.hash(state);
474 self.italic.hash(state);
475 self.underline.hash(state);
476 self.reverse.hash(state);
477 self.strikethrough.hash(state);
478 self.underline_color.hash(state);
479 self.dim_amount.map(f32::to_bits).hash(state);
480 if let Some((c, a)) = self.tint {
481 c.hash(state);
482 a.to_bits().hash(state);
483 }
484 }
485}
486
487impl Style {
488 pub fn new() -> Self {
490 Self::default()
491 }
492
493 pub fn fg(mut self, color: impl Into<Paint>) -> Self {
495 self.fg = Some(color.into());
496 self
497 }
498
499 pub fn bg(mut self, color: impl Into<Paint>) -> Self {
501 self.bg = Some(color.into());
502 self
503 }
504
505 pub fn fg_alpha(mut self, color: Color, alpha: f32) -> Self {
507 self.fg = Some(Paint::from_color_alpha(color, alpha));
508 self
509 }
510
511 pub fn bg_alpha(mut self, color: Color, alpha: f32) -> Self {
513 self.bg = Some(Paint::from_color_alpha(color, alpha));
514 self
515 }
516
517 pub fn transform_fg(mut self, transform: ColorTransform) -> Self {
519 self.fg_transform = Some(transform.normalized());
520 self
521 }
522
523 pub fn transform_bg(mut self, transform: ColorTransform) -> Self {
525 self.bg_transform = Some(transform.normalized());
526 self
527 }
528
529 pub fn contrast_policy(mut self, policy: ContrastPolicy) -> Self {
531 self.contrast_policy = Some(policy);
532 self
533 }
534
535 pub fn bold(mut self) -> Self {
537 self.bold = Some(true);
538 self
539 }
540
541 pub fn not_bold(mut self) -> Self {
547 self.bold = Some(false);
548 self
549 }
550
551 pub fn dim(mut self) -> Self {
553 self.dim = Some(true);
554 self
555 }
556
557 pub fn dim_by(mut self, amount: f32) -> Self {
564 let amount = amount.clamp(0.0, 1.0);
565 self.fg_transform = Some(ColorTransform::Dim(amount));
566 self.bg_transform = Some(ColorTransform::Dim(amount));
567 self.dim_amount = Some(amount);
568 self
569 }
570
571 pub fn tint_by(mut self, color: Color, alpha: f32) -> Self {
584 let alpha = alpha.clamp(0.0, 1.0);
585 self.tint = Some((color, alpha));
586 self
587 }
588
589 pub fn lighten_by(mut self, amount: f32) -> Self {
596 let amount = amount.clamp(0.0, 1.0);
597 self.fg_transform = Some(ColorTransform::Lighten(amount));
598 self.bg_transform = Some(ColorTransform::Lighten(amount));
599 self
600 }
601
602 pub fn italic(mut self) -> Self {
604 self.italic = Some(true);
605 self
606 }
607
608 pub fn underline(mut self) -> Self {
610 self.underline = Some(true);
611 self
612 }
613
614 pub fn reverse(mut self) -> Self {
616 self.reverse = Some(true);
617 self
618 }
619
620 pub fn strikethrough(mut self) -> Self {
622 self.strikethrough = Some(true);
623 self
624 }
625
626 pub fn underline_color(mut self, color: impl Into<Paint>) -> Self {
628 self.underline_color = Some(color.into());
629 self.underline = Some(true);
630 self
631 }
632
633 pub fn is_empty(&self) -> bool {
637 self.fg.is_none()
638 && self.bg.is_none()
639 && self.fg_transform.is_none()
640 && self.bg_transform.is_none()
641 && self.contrast_policy.is_none()
642 && self.bold.is_none()
643 && self.dim.is_none()
644 && self.italic.is_none()
645 && self.underline.is_none()
646 && self.reverse.is_none()
647 && self.strikethrough.is_none()
648 && self.underline_color.is_none()
649 && self.dim_amount.is_none()
650 && self.tint.is_none()
651 }
652
653 pub fn patch(self, other: Style) -> Self {
667 let (bg, bg_transform) = merge_channel(
668 self.bg,
669 self.bg_transform,
670 other.bg,
671 other.bg_transform,
672 None,
673 );
674 let backdrop = bg.or(other.bg).or(self.bg);
675 let (fg, fg_transform) = merge_channel(
676 self.fg,
677 self.fg_transform,
678 other.fg,
679 other.fg_transform,
680 backdrop,
681 );
682
683 Self {
684 fg,
685 bg,
686 fg_transform,
687 bg_transform,
688 contrast_policy: other.contrast_policy.or(self.contrast_policy),
689 bold: other.bold.or(self.bold),
690 dim: other.dim.or(self.dim),
691 italic: other.italic.or(self.italic),
692 underline: other.underline.or(self.underline),
693 reverse: other.reverse.or(self.reverse),
694 strikethrough: other.strikethrough.or(self.strikethrough),
695 underline_color: merge_underline_color(other.underline_color, self.underline_color),
696 dim_amount: other.dim_amount.or(self.dim_amount),
697 tint: other.tint.or(self.tint),
698 }
699 }
700
701 pub(crate) fn resolve_color_transforms(self) -> Self {
702 let bg = resolve_channel(self.bg, self.bg_transform, None);
703 let mut fg = resolve_channel(self.fg, self.fg_transform, bg);
704 let mut fg_transform_remaining = None;
705
706 if matches!(fg, Some(Paint::Solid(Color::Transparent))) {
707 if let Some(c) = bg
708 && !matches!(c, Paint::Solid(Color::Transparent | Color::Backdrop))
709 {
710 fg = if let Some(t) = self.fg_transform {
711 Some(t.apply_paint_with_backdrop(c, bg))
712 } else {
713 Some(c)
714 };
715 } else {
716 fg_transform_remaining = self.fg_transform;
717 }
718 }
719 Self {
720 fg,
721 bg,
722 fg_transform: fg_transform_remaining,
723 bg_transform: None,
724 ..self
725 }
726 }
727}
728
729fn merge_underline_color(overlay: Option<Paint>, base: Option<Paint>) -> Option<Paint> {
730 match overlay {
731 None => base,
732 Some(Paint::Solid(Color::Transparent)) => base,
733 Some(c) => Some(c),
734 }
735}
736
737pub(crate) fn merge_channel(
738 base_color: Option<Paint>,
739 base_transform: Option<ColorTransform>,
740 overlay_color: Option<Paint>,
741 overlay_transform: Option<ColorTransform>,
742 backdrop: Option<Paint>,
743) -> (Option<Paint>, Option<ColorTransform>) {
744 let mut color = resolve_channel(base_color, base_transform, backdrop);
745 let mut transform = None;
746
747 if let Some(overlay_color) = overlay_color
748 && !matches!(overlay_color, Paint::Solid(Color::Transparent))
749 {
750 color = Some(overlay_color);
751 }
752
753 if let Some(overlay_transform) = overlay_transform {
754 if let Some(current) = color
755 && (!overlay_transform.needs_backdrop() || backdrop.is_some())
756 {
757 color = Some(overlay_transform.apply_paint_with_backdrop(current, backdrop));
758 } else {
759 transform = Some(overlay_transform.normalized());
760 }
761 }
762
763 (color, transform)
764}
765
766pub(crate) fn resolve_channel(
767 color: Option<Paint>,
768 transform: Option<ColorTransform>,
769 backdrop: Option<Paint>,
770) -> Option<Paint> {
771 match (color, transform) {
772 (Some(color), Some(transform)) => {
773 Some(transform.apply_paint_with_backdrop(color, backdrop))
774 }
775 (color, None) => color,
776 (None, Some(_)) => None,
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use super::{ColorTransform, Style, Theme, ThemePalette, ThemeRole};
783 use crate::app::ContrastPolicy;
784 use crate::style::{Color, HostTerminalColors, Paint};
785
786 fn p(color: Color) -> Option<Paint> {
787 Some(Paint::Solid(color))
788 }
789
790 #[derive(Clone, Debug, PartialEq)]
791 struct BrandTheme {
792 accent_badge: Color,
793 }
794
795 #[test]
796 fn drag_drop_roles_initially_resolve_to_hover() {
797 let theme = Theme::default().hover(Style::new().fg(Color::White).bg(Color::Blue));
798
799 assert_eq!(theme.role(ThemeRole::DragSource), theme.hover);
800 assert_eq!(theme.role(ThemeRole::DropTarget), theme.hover);
801 assert_eq!(theme.role(ThemeRole::DropTargetActive), theme.hover);
802 }
803
804 #[test]
805 fn text_selection_role_is_distinct_from_item_selection() {
806 let theme = Theme::default()
807 .selection(Style::new().fg(Color::Red))
808 .text_selection(Style::new().fg(Color::Blue));
809
810 assert_eq!(theme.role(ThemeRole::Selection), theme.selection);
811 assert_eq!(theme.role(ThemeRole::TextSelection), theme.text_selection);
812 assert_ne!(
813 theme.role(ThemeRole::Selection),
814 theme.role(ThemeRole::TextSelection)
815 );
816 }
817
818 #[test]
819 fn theme_palette_derives_distinct_selection_colors() {
820 let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue)
821 .selection(Color::Green)
822 .text_selection(Color::Magenta)
823 .into_theme();
824
825 assert_eq!(theme.selection.fg, p(Color::Green));
826 assert_eq!(theme.text_selection.fg, p(Color::Magenta));
827 }
828
829 #[test]
830 fn from_host_colors_uses_host_palette() {
831 let mut ansi = std::array::from_fn(|i| Color::rgb(i as u8, i as u8, i as u8));
832 ansi[1] = Color::rgb(210, 30, 40);
833 ansi[2] = Color::rgb(30, 210, 40);
834 ansi[3] = Color::rgb(210, 180, 40);
835 ansi[4] = Color::rgb(30, 80, 210);
836 let colors = HostTerminalColors {
837 fg: Color::rgb(230, 231, 232),
838 bg: Color::rgb(10, 11, 12),
839 ansi,
840 };
841
842 let theme = Theme::from_host_colors(colors);
843
844 assert_eq!(theme.primary.fg, p(colors.fg));
845 assert_eq!(theme.primary.bg, p(colors.bg));
846 assert_eq!(theme.accent.fg, p(colors.ansi[4]));
847 assert_eq!(theme.status.success, colors.ansi[2]);
848 assert_eq!(theme.status.warning, colors.ansi[3]);
849 assert_eq!(theme.status.error, colors.ansi[1]);
850 assert_eq!(theme.status.info, colors.ansi[4]);
851 }
852
853 #[test]
854 fn transform_fg_dims_inherited_color() {
855 let base = Style::new().fg(Color::rgb(100, 120, 140));
856 let overlay = Style::new().transform_fg(ColorTransform::Dim(0.5));
857
858 assert_eq!(
859 base.patch(overlay).resolve_color_transforms().fg,
860 p(Color::rgb(50, 60, 70))
861 );
862 }
863
864 #[test]
865 fn lower_fg_transform_does_not_affect_overlay_color() {
866 let base = Style::new()
867 .fg(Color::rgb(100, 120, 140))
868 .transform_fg(ColorTransform::Dim(0.5));
869 let overlay = Style::new().fg(Color::rgb(10, 20, 30));
870
871 assert_eq!(
872 base.patch(overlay).resolve_color_transforms().fg,
873 p(Color::rgb(10, 20, 30))
874 );
875 }
876
877 #[test]
878 fn patch_transparent_fg_preserves_base() {
879 let base = Style::new().fg(Color::rgb(10, 20, 30));
880 let overlay = Style::new().fg(Color::Transparent);
881 assert_eq!(
882 base.patch(overlay).resolve_color_transforms().fg,
883 p(Color::rgb(10, 20, 30))
884 );
885 }
886
887 #[test]
888 fn patch_transparent_bg_preserves_base() {
889 let base = Style::new().bg(Color::rgb(40, 50, 60));
890 let overlay = Style::new().bg(Color::Transparent);
891 assert_eq!(
892 base.patch(overlay).resolve_color_transforms().bg,
893 p(Color::rgb(40, 50, 60))
894 );
895 }
896
897 #[test]
898 fn patch_alpha_zero_bg_is_not_transparent_sentinel() {
899 let base = Style::new().bg(Color::rgb(40, 50, 60));
900 let overlay = Style::new().bg_alpha(Color::Red, 0.0);
901 assert_eq!(
902 base.patch(overlay).resolve_color_transforms().bg,
903 Some(Paint::Alpha {
904 color: Color::Red,
905 alpha: 0,
906 })
907 );
908 }
909
910 #[test]
911 fn color_transform_apply_paint_preserves_alpha() {
912 let paint = Paint::Alpha {
913 color: Color::rgb(100, 120, 140),
914 alpha: 128,
915 };
916
917 assert_eq!(
918 ColorTransform::Dim(0.5).apply_paint(paint),
919 Paint::Alpha {
920 color: Color::rgb(50, 60, 70),
921 alpha: 128,
922 }
923 );
924 }
925
926 #[test]
927 fn patch_transparent_underline_color_preserves_base() {
928 let base = Style::new().underline_color(Color::Red);
929 let overlay = Style::new().underline_color(Color::Transparent);
930 let patched = base.patch(overlay);
931 assert_eq!(patched.underline_color, p(Color::Red));
932 }
933
934 #[test]
935 fn builder_order_does_not_change_transform_result() {
936 let a = Style::new()
937 .transform_fg(ColorTransform::Dim(0.5))
938 .fg(Color::rgb(100, 120, 140));
939 let b = Style::new()
940 .fg(Color::rgb(100, 120, 140))
941 .transform_fg(ColorTransform::Dim(0.5));
942
943 assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
944 assert_eq!(a.resolve_color_transforms().fg, p(Color::rgb(50, 60, 70)));
945 }
946
947 #[test]
948 fn transform_chain_applies_in_patch_order() {
949 let style = Style::new()
950 .fg(Color::rgb(100, 120, 140))
951 .patch(Style::new().transform_fg(ColorTransform::Dim(0.5)))
952 .patch(Style::new().transform_fg(ColorTransform::Lighten(0.5)))
953 .resolve_color_transforms();
954
955 assert_eq!(style.fg, p(Color::rgb(153, 158, 163)));
956 }
957
958 #[test]
959 fn state_cascade_stacks_bg_transforms_on_resolved_color() {
960 let style = Style::new()
964 .bg(Color::rgb(100, 100, 100))
965 .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
966 .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
967 .resolve_color_transforms();
968
969 assert_eq!(style.bg, p(Color::rgb(25, 25, 25)));
970 }
971
972 #[test]
973 fn opacity_turns_foreground_into_alpha_paint() {
974 let style = Style::new()
975 .fg(Color::rgb(245, 167, 66))
976 .bg(Color::rgb(255, 255, 255))
977 .transform_fg(ColorTransform::Opacity(0.6))
978 .resolve_color_transforms();
979
980 assert_eq!(
981 style.fg,
982 Some(Paint::Alpha {
983 color: Color::rgb(245, 167, 66),
984 alpha: 153,
985 })
986 );
987 }
988
989 #[test]
990 fn opacity_builder_order_is_independent_when_background_arrives_later() {
991 let a = Style::new()
992 .transform_fg(ColorTransform::Opacity(0.6))
993 .fg(Color::rgb(245, 167, 66))
994 .bg(Color::rgb(255, 255, 255));
995 let b = Style::new()
996 .fg(Color::rgb(245, 167, 66))
997 .bg(Color::rgb(255, 255, 255))
998 .transform_fg(ColorTransform::Opacity(0.6));
999
1000 assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
1001 }
1002
1003 #[test]
1004 fn opacity_toward_uses_fixed_target_not_backdrop() {
1005 let c = Color::rgb(0, 100, 200);
1006 let target = Color::rgb(200, 10, 30);
1007 assert_eq!(
1008 ColorTransform::OpacityToward {
1009 factor: 1.0,
1010 target,
1011 }
1012 .apply_with_backdrop(c, Some(Color::White)),
1013 c
1014 );
1015 assert_eq!(
1016 ColorTransform::OpacityToward {
1017 factor: 0.0,
1018 target,
1019 }
1020 .apply_with_backdrop(c, Some(Color::White)),
1021 target
1022 );
1023 }
1024
1025 #[test]
1026 fn patch_prefers_overlay_contrast_policy() {
1027 let base = Style::new().contrast_policy(ContrastPolicy::Wcag);
1028 let overlay = Style::new().contrast_policy(ContrastPolicy::Off);
1029
1030 assert_eq!(
1031 base.patch(overlay).contrast_policy,
1032 Some(ContrastPolicy::Off)
1033 );
1034 }
1035
1036 #[test]
1037 fn theme_extensions_roundtrip_and_affect_equality() {
1038 let a = Theme::default().with_extension(BrandTheme {
1039 accent_badge: Color::rgb(1, 2, 3),
1040 });
1041 let b = Theme::default().with_extension(BrandTheme {
1042 accent_badge: Color::rgb(1, 2, 3),
1043 });
1044 let c = Theme::default().with_extension(BrandTheme {
1045 accent_badge: Color::rgb(9, 8, 7),
1046 });
1047
1048 assert_eq!(a.extension::<BrandTheme>(), b.extension::<BrandTheme>());
1049 assert_eq!(a, b);
1050 assert_ne!(a, c);
1051 }
1052}
1053
1054#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1056pub enum CaretShape {
1057 #[default]
1059 Block,
1060 Bar,
1062 Underline,
1064}
1065
1066#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1070pub struct BorderGlyphs {
1071 pub top_left: &'static str,
1073 pub top: &'static str,
1075 pub top_right: &'static str,
1077 pub left: &'static str,
1079 pub right: &'static str,
1081 pub bottom_left: &'static str,
1083 pub bottom: &'static str,
1085 pub bottom_right: &'static str,
1087}
1088
1089impl Default for BorderGlyphs {
1090 fn default() -> Self {
1091 Self::PLAIN
1092 }
1093}
1094
1095impl BorderGlyphs {
1096 pub const PLAIN: Self = Self {
1098 top_left: "┌",
1099 top: "─",
1100 top_right: "┐",
1101 left: "│",
1102 right: "│",
1103 bottom_left: "└",
1104 bottom: "─",
1105 bottom_right: "┘",
1106 };
1107
1108 pub fn new(parts: BorderGlyphsParts) -> Self {
1110 Self {
1111 top_left: parts.top_left,
1112 top: parts.top,
1113 top_right: parts.top_right,
1114 left: parts.left,
1115 right: parts.right,
1116 bottom_left: parts.bottom_left,
1117 bottom: parts.bottom,
1118 bottom_right: parts.bottom_right,
1119 }
1120 }
1121}
1122
1123pub struct BorderGlyphsParts {
1125 pub top_left: &'static str,
1127 pub top: &'static str,
1129 pub top_right: &'static str,
1131 pub left: &'static str,
1133 pub right: &'static str,
1135 pub bottom_left: &'static str,
1137 pub bottom: &'static str,
1139 pub bottom_right: &'static str,
1141}
1142
1143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1145pub enum BorderStyle {
1146 #[default]
1148 Plain,
1149 Rounded,
1151 Double,
1153 Thick,
1155 LightDoubleDashed,
1157 HeavyDoubleDashed,
1159 LightTripleDashed,
1161 HeavyTripleDashed,
1163 LightQuadrupleDashed,
1165 HeavyQuadrupleDashed,
1167 Custom {
1169 glyphs: BorderGlyphs,
1171 },
1172}
1173
1174#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1176pub enum ScrollbarVariant {
1177 Integrated,
1180 #[default]
1182 Standalone,
1183}
1184
1185#[derive(Clone, Debug, Default, PartialEq)]
1193pub struct ScrollbarConfig {
1194 pub variant: ScrollbarVariant,
1196 pub gap: u16,
1198 pub thumb: Option<char>,
1200 pub thumb_style: Option<Style>,
1202 pub thumb_focus_style: Option<Style>,
1204 pub track_style: Option<Style>,
1206}
1207
1208impl ScrollbarConfig {
1209 pub fn new() -> Self {
1211 Self::default()
1212 }
1213
1214 pub fn variant(mut self, variant: ScrollbarVariant) -> Self {
1216 self.variant = variant;
1217 self
1218 }
1219
1220 pub fn gap(mut self, gap: u16) -> Self {
1222 self.gap = gap;
1223 self
1224 }
1225
1226 pub fn thumb(mut self, ch: char) -> Self {
1228 self.thumb = Some(ch);
1229 self
1230 }
1231
1232 pub fn thumb_style(mut self, style: Style) -> Self {
1234 self.thumb_style = Some(style);
1235 self
1236 }
1237
1238 pub fn thumb_focus_style(mut self, style: Style) -> Self {
1240 self.thumb_focus_style = Some(style);
1241 self
1242 }
1243
1244 pub fn track_style(mut self, style: Style) -> Self {
1246 self.track_style = Some(style);
1247 self
1248 }
1249}
1250
1251#[derive(Clone, Debug, PartialEq)]
1255pub struct FileIconPalette {
1256 pub azure: Color,
1258 pub blue: Color,
1260 pub cyan: Color,
1262 pub green: Color,
1264 pub grey: Color,
1266 pub orange: Color,
1268 pub purple: Color,
1270 pub red: Color,
1272 pub yellow: Color,
1274}
1275
1276impl Default for FileIconPalette {
1277 fn default() -> Self {
1278 Self {
1279 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), }
1290 }
1291}
1292
1293#[derive(Clone, Copy, Debug, PartialEq)]
1295pub struct GitStatusPalette {
1296 pub modified: Color,
1298 pub added: Color,
1300 pub deleted: Color,
1302 pub renamed: Color,
1304 pub untracked: Color,
1306 pub conflicted: Color,
1308}
1309
1310impl Default for GitStatusPalette {
1311 fn default() -> Self {
1312 Self {
1313 modified: Color::hex_u24(0xE5B767),
1314 added: Color::hex_u24(0x7EC699),
1315 deleted: Color::hex_u24(0xE57E7E),
1316 renamed: Color::hex_u24(0x76C5E5),
1317 untracked: Color::hex_u24(0xC59AE5),
1318 conflicted: Color::hex_u24(0xE57E7E),
1319 }
1320 }
1321}
1322
1323#[derive(Clone, Copy, Debug, PartialEq)]
1325pub struct ScrollbarPalette {
1326 pub track: Option<Color>,
1328 pub thumb: Color,
1330 pub thumb_focus: Option<Color>,
1332}
1333
1334impl Default for ScrollbarPalette {
1335 fn default() -> Self {
1336 Self {
1337 track: None,
1338 thumb: Color::DarkGray,
1339 thumb_focus: Some(Color::Gray),
1340 }
1341 }
1342}
1343
1344#[derive(Clone, Copy, Debug, PartialEq)]
1346pub struct SplitterPalette {
1347 pub hover: Color,
1349 pub active: Color,
1351}
1352
1353impl Default for SplitterPalette {
1354 fn default() -> Self {
1355 Self {
1356 hover: Color::hex_u24(0x2563EB),
1357 active: Color::hex_u24(0x22D3EE),
1358 }
1359 }
1360}
1361
1362#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1364pub struct SurfacePalette {
1365 pub panel: Color,
1367 pub element: Color,
1369 pub menu: Color,
1371 pub backdrop: Color,
1373}
1374
1375#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1377pub struct StatusPalette {
1378 pub success: Color,
1380 pub warning: Color,
1382 pub error: Color,
1384 pub info: Color,
1386}
1387
1388#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1390pub struct DiffPalette {
1391 pub context: Style,
1393 pub added: Style,
1395 pub removed: Style,
1397 pub empty: Style,
1399 pub added_word: Style,
1401 pub removed_word: Style,
1403 pub added_marker: Style,
1405 pub removed_marker: Style,
1407 pub context_line_number: Style,
1409 pub added_line_number: Style,
1411 pub removed_line_number: Style,
1413 pub context_separator_style: Style,
1415 pub patch_header: Style,
1417}
1418
1419#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1421pub struct DocumentPalette {
1422 pub heading_styles: [Style; 6],
1424 pub code_inline: Style,
1426 pub code_block: Style,
1428 pub emphasis: Style,
1430 pub strong: Style,
1432 pub strikethrough: Style,
1434 pub link: Style,
1436 pub blockquote_bar: Style,
1438 pub table_border: Style,
1440 pub table_header: Style,
1442 pub hr: Style,
1444 pub list_item: Style,
1446 pub list_enumeration: Style,
1448 pub diagram_node_fill_style: Style,
1450 pub diagram_node_border_style: Style,
1452 pub diagram_node_label_style: Style,
1454 pub diagram_edge_style: Style,
1456 pub diagram_muted_style: Style,
1458}
1459
1460impl Default for DocumentPalette {
1461 fn default() -> Self {
1462 Self {
1463 heading_styles: [
1464 Style::new().bold().fg(Color::LightBlue),
1465 Style::new().bold().fg(Color::LightBlue),
1466 Style::new().bold().fg(Color::LightBlue),
1467 Style::new().bold(),
1468 Style::new().bold(),
1469 Style::new().bold().dim(),
1470 ],
1471 code_inline: Style::new().fg(Color::Green),
1472 code_block: Style::default(),
1473 emphasis: Style::new().italic(),
1474 strong: Style::new().bold(),
1475 strikethrough: Style::new().strikethrough(),
1476 link: Style::new().fg(Color::LightBlue).underline(),
1477 blockquote_bar: Style::new().fg(Color::DarkGray).dim(),
1478 table_border: Style::new().fg(Color::DarkGray).dim(),
1479 table_header: Style::new().bold(),
1480 hr: Style::new().fg(Color::DarkGray).dim(),
1481 list_item: Style::new().fg(Color::LightBlue).bold(),
1482 list_enumeration: Style::new().fg(Color::LightBlue).bold(),
1483 diagram_node_fill_style: Style::default(),
1484 diagram_node_border_style: Style::default(),
1485 diagram_node_label_style: Style::default(),
1486 diagram_edge_style: Style::default(),
1487 diagram_muted_style: Style::default(),
1488 }
1489 }
1490}
1491
1492#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1494pub struct SyntaxPalette {
1495 pub comment: Style,
1497 pub keyword: Style,
1499 pub string: Style,
1501 pub number: Style,
1503 pub constant: Style,
1505 pub function: Style,
1507 pub builtin: Style,
1509 pub type_name: Style,
1511 pub variable: Style,
1513 pub parameter: Style,
1515 pub operator: Style,
1517}
1518
1519impl Default for SyntaxPalette {
1520 fn default() -> Self {
1521 let number = Style::new().fg(Color::Yellow);
1522 let function = Style::new().fg(Color::Cyan);
1523 let variable = Style::new().fg(Color::White);
1524
1525 Self {
1526 comment: Style::new().fg(Color::DarkGray).italic().dim(),
1527 keyword: Style::new().fg(Color::LightMagenta),
1528 string: Style::new().fg(Color::Green),
1529 number,
1530 constant: number.lighten_by(0.12),
1531 function,
1532 builtin: function.italic(),
1533 type_name: Style::new().fg(Color::LightBlue),
1534 variable,
1535 parameter: variable.italic(),
1536 operator: Style::new().fg(Color::LightRed),
1537 }
1538 }
1539}
1540
1541#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1543pub struct InputPalette {
1544 pub focus: Style,
1549}
1550
1551#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1553pub struct TextAreaPalette {
1554 pub focus: Style,
1559}
1560
1561#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1563pub struct DocumentViewPalette {
1564 pub focus: Style,
1569}
1570
1571#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1573pub struct HexAreaPalette {
1574 pub focus: Style,
1576 pub cursor: Style,
1578}
1579
1580#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1582pub struct TerminalPalette {
1583 pub focus: Style,
1588}
1589
1590#[derive(Clone, Debug, PartialEq)]
1592pub struct Theme {
1593 pub primary: Style,
1595 pub accent: Style,
1600 pub selection: Style,
1602 pub text_selection: Style,
1604 pub focus: Style,
1610 pub hover: Style,
1612 pub border: Style,
1617 pub muted: Style,
1622 pub surface: SurfacePalette,
1624 pub status: StatusPalette,
1626 pub border_active: Color,
1628 pub file_icons: FileIconPalette,
1630 pub git_status: GitStatusPalette,
1632 pub diff: DiffPalette,
1634 pub document: DocumentPalette,
1636 pub syntax: SyntaxPalette,
1638 pub input: InputPalette,
1640 pub text_area: TextAreaPalette,
1642 pub document_view: DocumentViewPalette,
1644 pub hex_area: HexAreaPalette,
1646 pub terminal: TerminalPalette,
1648 pub scrollbar: ScrollbarPalette,
1650 pub splitter: SplitterPalette,
1652 #[doc(hidden)]
1654 pub extensions: ThemeExtensions,
1655}
1656
1657impl Theme {
1658 pub fn from_host_colors(colors: HostTerminalColors) -> Self {
1660 ThemePalette::new(colors.fg, colors.bg, colors.ansi[4])
1661 .success(colors.ansi[2])
1662 .warning(colors.ansi[3])
1663 .error(colors.ansi[1])
1664 .info(colors.ansi[4])
1665 .into_theme()
1666 }
1667
1668 pub fn role(&self, role: ThemeRole) -> Style {
1670 match role {
1671 ThemeRole::Base => self.primary,
1672 ThemeRole::Accent => {
1673 let mut style = self.accent;
1674 if style.fg.is_none() {
1675 style.fg = self.primary.fg;
1676 }
1677 if style.fg_transform.is_none() {
1678 style.fg_transform = self.primary.fg_transform;
1679 }
1680 style
1681 }
1682 ThemeRole::Selection | ThemeRole::UnfocusedSelection => self.selection,
1683 ThemeRole::TextSelection => self.text_selection,
1684 ThemeRole::Hover
1685 | ThemeRole::DragSource
1686 | ThemeRole::DropTarget
1687 | ThemeRole::DropTargetActive
1688 | ThemeRole::ItemHover => self.hover,
1689 ThemeRole::Focus => self.focus,
1690 ThemeRole::Active => self.selection,
1691 ThemeRole::Border => self.primary.patch(self.border),
1692 ThemeRole::Disabled | ThemeRole::Muted => self.primary.patch(self.muted),
1693 ThemeRole::Error => Style::new().fg(self.status.error),
1694 ThemeRole::InputFocusContent => self.input.focus,
1695 ThemeRole::TextAreaFocusContent => self.text_area.focus,
1696 ThemeRole::DocumentViewFocusContent => self.document_view.focus,
1697 ThemeRole::HexAreaFocusContent => self.hex_area.focus,
1698 ThemeRole::HexAreaCursor => self.hex_area.cursor,
1699 ThemeRole::TerminalFocusContent => self.terminal.focus,
1700 ThemeRole::ScrollbarThumb => Style::new().bg(self.scrollbar.thumb),
1701 ThemeRole::ScrollbarThumbFocus => self
1702 .scrollbar
1703 .thumb_focus
1704 .map(|color| Style::new().bg(color))
1705 .unwrap_or_default(),
1706 ThemeRole::ScrollbarTrack => self
1707 .scrollbar
1708 .track
1709 .map(|color| Style::new().bg(color))
1710 .unwrap_or_default(),
1711 ThemeRole::SplitterHover => Style::new().fg(self.splitter.hover),
1712 ThemeRole::SplitterActive => Style::new().fg(self.splitter.active),
1713 }
1714 }
1715
1716 pub fn custom(primary_fg: Color, primary_bg: Color, accent: Color) -> Self {
1723 let success = Color::Green;
1724 let warning = Color::Yellow;
1725 let error = Color::Red;
1726 let info = accent;
1727 let muted = primary_fg.blend_toward(primary_bg, 0.42);
1728 let border_active = accent.lighten_by(0.08);
1729 Self {
1730 primary: Style::new().fg(primary_fg).bg(primary_bg),
1731 accent: Style::new().fg(accent),
1732 selection: Style::new()
1733 .fg(accent)
1734 .bg(primary_bg.blend_toward(accent, 0.22)),
1735 text_selection: Style::new()
1736 .fg(accent)
1737 .bg(primary_bg.blend_toward(accent, 0.22)),
1738 focus: Style::new().fg(border_active),
1739 hover: Style::default(),
1740 border: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.40)),
1741 muted: Style::new().fg(muted),
1742 surface: SurfacePalette {
1743 panel: primary_bg.elevate(0.07),
1744 element: primary_bg.elevate(0.04),
1745 menu: primary_bg.elevate(0.12),
1746 backdrop: primary_bg,
1747 },
1748 status: StatusPalette {
1749 success,
1750 warning,
1751 error,
1752 info,
1753 },
1754 border_active,
1755 file_icons: FileIconPalette::default(),
1756 git_status: GitStatusPalette::default(),
1757 diff: DiffPalette {
1758 context: Style::default(),
1759 added: Style::new().bg(primary_bg.blend_toward(success, 0.14)),
1760 removed: Style::new().bg(primary_bg.blend_toward(error, 0.16)),
1761 empty: Style::new().dim(),
1762 added_word: Style::new().bg(primary_bg.blend_toward(success, 0.24)),
1763 removed_word: Style::new().bg(primary_bg.blend_toward(error, 0.28)),
1764 added_marker: Style::new().fg(success),
1765 removed_marker: Style::new().fg(error),
1766 context_line_number: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.50)),
1767 added_line_number: Style::default(),
1768 removed_line_number: Style::default(),
1769 context_separator_style: Style::new()
1770 .fg(primary_fg.blend_toward(primary_bg, 0.40))
1771 .dim(),
1772 patch_header: Style::new()
1773 .fg(accent.blend_toward(primary_fg, 0.35))
1774 .bold(),
1775 },
1776 document: DocumentPalette {
1777 heading_styles: [
1778 Style::new().bold().fg(accent.lighten_by(0.20)),
1779 Style::new().bold().fg(accent.lighten_by(0.12)),
1780 Style::new().bold().fg(accent),
1781 Style::new().bold().fg(primary_fg),
1782 Style::new().bold().fg(primary_fg),
1783 Style::new().bold().fg(primary_fg).dim(),
1784 ],
1785 code_inline: Style::new().fg(success),
1786 code_block: Style::default(),
1787 emphasis: Style::new().italic(),
1788 strong: Style::new().bold(),
1789 strikethrough: Style::new().strikethrough(),
1790 link: Style::new().fg(accent).underline(),
1791 blockquote_bar: Style::new().fg(muted).dim(),
1792 table_border: Style::new()
1793 .fg(primary_fg.blend_toward(primary_bg, 0.40))
1794 .dim(),
1795 table_header: Style::new().bold(),
1796 hr: Style::new()
1797 .fg(primary_fg.blend_toward(primary_bg, 0.40))
1798 .dim(),
1799 list_item: Style::new().fg(accent).bold(),
1800 list_enumeration: Style::new().fg(accent).bold(),
1801 diagram_node_fill_style: Style::new().bg(primary_bg.blend_toward(accent, 0.10)),
1802 diagram_node_border_style: Style::new().fg(accent.lighten_by(0.08)),
1803 diagram_node_label_style: Style::new().fg(primary_fg),
1804 diagram_edge_style: Style::new().fg(accent.blend_toward(primary_fg, 0.20)),
1805 diagram_muted_style: Style::new().fg(muted).dim(),
1806 },
1807 syntax: SyntaxPalette {
1808 comment: Style::new().fg(muted).italic().dim(),
1809 keyword: Style::new().fg(accent),
1810 string: Style::new().fg(accent.blend_toward(success, 0.55)),
1811 number: Style::new().fg(accent.blend_toward(Color::Yellow, 0.60)),
1812 constant: Style::new()
1813 .fg(accent.blend_toward(Color::Yellow, 0.52).lighten_by(0.10)),
1814 function: Style::new().fg(info.blend_toward(accent, 0.12)),
1815 builtin: Style::new().fg(info.blend_toward(accent, 0.28)).italic(),
1816 type_name: Style::new().fg(accent.blend_toward(info, 0.32)),
1817 variable: Style::new().fg(primary_fg),
1818 parameter: Style::new()
1819 .fg(primary_fg.blend_toward(accent, 0.12))
1820 .italic(),
1821 operator: Style::new().fg(accent.blend_toward(error, 0.45)),
1822 },
1823 input: InputPalette::default(),
1824 text_area: TextAreaPalette::default(),
1825 document_view: DocumentViewPalette::default(),
1826 hex_area: HexAreaPalette {
1827 focus: Style::default(),
1828 cursor: Style::new().fg(accent),
1829 },
1830 terminal: TerminalPalette::default(),
1831 scrollbar: ScrollbarPalette {
1832 track: Some(primary_bg.elevate(0.05)),
1833 thumb: primary_bg.elevate(0.20),
1834 thumb_focus: Some(accent.lighten_by(0.08)),
1835 },
1836 splitter: SplitterPalette {
1837 hover: accent.lighten_by(0.08),
1838 active: accent.lighten_by(0.18),
1839 },
1840 extensions: ThemeExtensions::default(),
1841 }
1842 }
1843
1844 pub fn primary(mut self, style: Style) -> Self {
1857 self.primary = style;
1858 self
1859 }
1860
1861 pub fn accent(mut self, style: Style) -> Self {
1867 self.accent = style;
1868 self
1869 }
1870
1871 pub fn focus(mut self, style: Style) -> Self {
1877 self.focus = style;
1878 self
1879 }
1880
1881 pub fn with_extension<T>(mut self, extension: T) -> Self
1887 where
1888 T: ThemeExtension,
1889 {
1890 self.extensions.insert(extension);
1891 self
1892 }
1893
1894 pub fn without_extension<T>(mut self) -> Self
1896 where
1897 T: ThemeExtension,
1898 {
1899 self.extensions.remove::<T>();
1900 self
1901 }
1902
1903 pub fn extension<T>(&self) -> Option<&T>
1905 where
1906 T: ThemeExtension,
1907 {
1908 self.extensions.get::<T>()
1909 }
1910
1911 pub fn extension_cloned<T>(&self) -> Option<T>
1913 where
1914 T: ThemeExtension,
1915 {
1916 self.extension::<T>().cloned()
1917 }
1918
1919 pub fn selection(mut self, style: Style) -> Self {
1924 self.selection = style;
1925 self
1926 }
1927
1928 pub fn text_selection(mut self, style: Style) -> Self {
1933 self.text_selection = style;
1934 self
1935 }
1936
1937 pub fn hover(mut self, style: Style) -> Self {
1943 self.hover = style;
1944 self
1945 }
1946
1947 pub fn border(mut self, style: Style) -> Self {
1952 self.border = style;
1953 self
1954 }
1955
1956 pub fn muted(mut self, style: Style) -> Self {
1961 self.muted = style;
1962 self
1963 }
1964
1965 pub fn scrollbar(mut self, palette: ScrollbarPalette) -> Self {
1967 self.scrollbar = palette;
1968 self
1969 }
1970
1971 pub fn splitter(mut self, palette: SplitterPalette) -> Self {
1973 self.splitter = palette;
1974 self
1975 }
1976
1977 pub fn file_icons(mut self, palette: FileIconPalette) -> Self {
1979 self.file_icons = palette;
1980 self
1981 }
1982
1983 pub fn git_status(mut self, palette: GitStatusPalette) -> Self {
1985 self.git_status = palette;
1986 self
1987 }
1988
1989 pub fn diff(mut self, palette: DiffPalette) -> Self {
1991 self.diff = palette;
1992 self
1993 }
1994
1995 pub fn document(mut self, palette: DocumentPalette) -> Self {
1997 self.document = palette;
1998 self
1999 }
2000
2001 pub fn syntax(mut self, palette: SyntaxPalette) -> Self {
2003 self.syntax = palette;
2004 self
2005 }
2006
2007 pub fn input(mut self, palette: InputPalette) -> Self {
2009 self.input = palette;
2010 self
2011 }
2012
2013 pub fn text_area(mut self, palette: TextAreaPalette) -> Self {
2015 self.text_area = palette;
2016 self
2017 }
2018
2019 pub fn document_view(mut self, palette: DocumentViewPalette) -> Self {
2021 self.document_view = palette;
2022 self
2023 }
2024
2025 pub fn hex_area(mut self, palette: HexAreaPalette) -> Self {
2027 self.hex_area = palette;
2028 self
2029 }
2030
2031 pub fn terminal(mut self, palette: TerminalPalette) -> Self {
2033 self.terminal = palette;
2034 self
2035 }
2036}
2037
2038#[derive(Clone, Debug)]
2075pub struct ThemePalette {
2076 pub text: Color,
2078 pub background: Color,
2080 pub accent: Color,
2082 pub selection: Option<Color>,
2084 pub text_selection: Option<Color>,
2086 pub border: Option<Color>,
2088 pub muted: Option<Color>,
2091 pub scrollbar: Option<Color>,
2093 pub success: Option<Color>,
2095 pub warning: Option<Color>,
2097 pub error: Option<Color>,
2099 pub info: Option<Color>,
2101}
2102
2103impl ThemePalette {
2104 pub fn new(text: Color, background: Color, accent: Color) -> Self {
2106 Self {
2107 text,
2108 background,
2109 accent,
2110 selection: None,
2111 text_selection: None,
2112 border: None,
2113 muted: None,
2114 scrollbar: None,
2115 success: None,
2116 warning: None,
2117 error: None,
2118 info: None,
2119 }
2120 }
2121
2122 pub fn selection(mut self, color: Color) -> Self {
2124 self.selection = Some(color);
2125 self
2126 }
2127
2128 pub fn text_selection(mut self, color: Color) -> Self {
2130 self.text_selection = Some(color);
2131 self
2132 }
2133
2134 pub fn border(mut self, color: Color) -> Self {
2136 self.border = Some(color);
2137 self
2138 }
2139
2140 pub fn muted(mut self, color: Color) -> Self {
2142 self.muted = Some(color);
2143 self
2144 }
2145
2146 pub fn scrollbar(mut self, color: Color) -> Self {
2148 self.scrollbar = Some(color);
2149 self
2150 }
2151
2152 pub fn success(mut self, color: Color) -> Self {
2154 self.success = Some(color);
2155 self
2156 }
2157
2158 pub fn warning(mut self, color: Color) -> Self {
2160 self.warning = Some(color);
2161 self
2162 }
2163
2164 pub fn error(mut self, color: Color) -> Self {
2166 self.error = Some(color);
2167 self
2168 }
2169
2170 pub fn info(mut self, color: Color) -> Self {
2172 self.info = Some(color);
2173 self
2174 }
2175
2176 pub fn into_theme(self) -> Theme {
2178 Theme::from(self)
2179 }
2180}
2181
2182impl From<ThemePalette> for Theme {
2183 fn from(p: ThemePalette) -> Self {
2184 let border_color = p
2185 .border
2186 .unwrap_or_else(|| p.text.blend_toward(p.background, 0.40));
2187 let muted_color = p
2188 .muted
2189 .unwrap_or_else(|| p.text.blend_toward(p.background, 0.42));
2190 let scrollbar_thumb = p.scrollbar.unwrap_or_else(|| p.background.elevate(0.20));
2191
2192 let success = p.success.unwrap_or(Color::hex_u24(0x34D399));
2193 let warning = p.warning.unwrap_or(Color::hex_u24(0xFBBF24));
2194 let error = p.error.unwrap_or(Color::hex_u24(0xF43F5E));
2195 let info = p.info.unwrap_or(p.accent);
2196 let border_active = p.border.unwrap_or(p.accent).lighten_by(0.08);
2197 let selection = p.selection.unwrap_or(p.accent);
2198 let text_selection = p.text_selection.unwrap_or(p.accent);
2199
2200 Theme {
2201 primary: Style::new().fg(p.text).bg(p.background),
2202 accent: Style::new().fg(p.accent),
2203 selection: Style::new()
2204 .fg(selection)
2205 .bg(p.background.blend_toward(selection, 0.22)),
2206 text_selection: Style::new()
2207 .fg(text_selection)
2208 .bg(p.background.blend_toward(text_selection, 0.22)),
2209 focus: Style::new().fg(border_active),
2210 hover: Style::default(),
2211 border: Style::new().fg(border_color),
2212 muted: Style::new().fg(muted_color),
2213 surface: SurfacePalette {
2214 panel: p.background.elevate(0.07),
2215 element: p.background.elevate(0.04),
2216 menu: p.background.elevate(0.12),
2217 backdrop: p.background,
2218 },
2219 status: StatusPalette {
2220 success,
2221 warning,
2222 error,
2223 info,
2224 },
2225 border_active,
2226 file_icons: FileIconPalette {
2227 green: success,
2228 red: error,
2229 yellow: warning,
2230 azure: info,
2231 blue: p.accent,
2232 cyan: info.lighten_by(0.10),
2233 grey: muted_color,
2234 orange: warning.blend_toward(error, 0.40),
2235 purple: p.accent.blend_toward(error, 0.30),
2236 },
2237 git_status: GitStatusPalette {
2238 modified: warning,
2239 added: success,
2240 deleted: error,
2241 renamed: info,
2242 untracked: p.accent.blend_toward(error, 0.30),
2243 conflicted: error,
2244 },
2245 diff: DiffPalette {
2246 context: Style::default(),
2247 added: Style::new().bg(p.background.blend_toward(success, 0.14)),
2248 removed: Style::new().bg(p.background.blend_toward(error, 0.16)),
2249 empty: Style::new().dim(),
2250 added_word: Style::new().bg(p.background.blend_toward(success, 0.24)),
2251 removed_word: Style::new().bg(p.background.blend_toward(error, 0.28)),
2252 added_marker: Style::new().fg(success),
2253 removed_marker: Style::new().fg(error),
2254 context_line_number: Style::new().fg(p.text.blend_toward(p.background, 0.50)),
2255 added_line_number: Style::default(),
2256 removed_line_number: Style::default(),
2257 context_separator_style: Style::new()
2258 .fg(p.text.blend_toward(p.background, 0.40))
2259 .dim(),
2260 patch_header: Style::new().fg(p.accent.blend_toward(p.text, 0.25)).bold(),
2261 },
2262 document: DocumentPalette {
2263 heading_styles: [
2264 Style::new().bold().fg(p.accent.lighten_by(0.20)),
2265 Style::new().bold().fg(p.accent.lighten_by(0.12)),
2266 Style::new().bold().fg(p.accent),
2267 Style::new().bold().fg(p.text),
2268 Style::new().bold().fg(p.text),
2269 Style::new().bold().fg(p.text).dim(),
2270 ],
2271 code_inline: Style::new().fg(success),
2272 code_block: Style::default(),
2273 emphasis: Style::new().italic(),
2274 strong: Style::new().bold(),
2275 strikethrough: Style::new().strikethrough(),
2276 link: Style::new().fg(p.accent).underline(),
2277 blockquote_bar: Style::new().fg(muted_color).dim(),
2278 table_border: Style::new().fg(border_color).dim(),
2279 table_header: Style::new().bold(),
2280 hr: Style::new().fg(border_color).dim(),
2281 list_item: Style::new().fg(p.accent).bold(),
2282 list_enumeration: Style::new().fg(p.accent).bold(),
2283 diagram_node_fill_style: Style::new().bg(p.background.blend_toward(p.accent, 0.10)),
2284 diagram_node_border_style: Style::new().fg(p.accent.lighten_by(0.08)),
2285 diagram_node_label_style: Style::new().fg(p.text),
2286 diagram_edge_style: Style::new().fg(p.accent.blend_toward(p.text, 0.20)),
2287 diagram_muted_style: Style::new().fg(muted_color).dim(),
2288 },
2289 syntax: SyntaxPalette {
2290 comment: Style::new().fg(muted_color).italic().dim(),
2291 keyword: Style::new().fg(p.accent),
2292 string: Style::new().fg(success.blend_toward(p.accent, 0.15)),
2293 number: Style::new().fg(warning.blend_toward(p.accent, 0.20)),
2294 constant: Style::new().fg(warning.blend_toward(p.text, 0.18)),
2295 function: Style::new().fg(info.blend_toward(p.accent, 0.10)),
2296 builtin: Style::new()
2297 .fg(info.blend_toward(muted_color, 0.22))
2298 .italic(),
2299 type_name: Style::new().fg(p.accent.blend_toward(info, 0.35)),
2300 variable: Style::new().fg(p.text),
2301 parameter: Style::new().fg(p.text).italic(),
2302 operator: Style::new().fg(error.blend_toward(p.accent, 0.45)),
2303 },
2304 input: InputPalette::default(),
2305 text_area: TextAreaPalette::default(),
2306 document_view: DocumentViewPalette::default(),
2307 hex_area: HexAreaPalette {
2308 focus: Style::default(),
2309 cursor: Style::new().fg(p.accent),
2310 },
2311 terminal: TerminalPalette::default(),
2312 scrollbar: ScrollbarPalette {
2313 track: Some(p.background.elevate(0.05)),
2314 thumb: scrollbar_thumb,
2315 thumb_focus: Some(p.accent.lighten_by(0.08)),
2316 },
2317 splitter: SplitterPalette {
2318 hover: p.accent.lighten_by(0.08),
2319 active: p.accent.lighten_by(0.18),
2320 },
2321 extensions: ThemeExtensions::default(),
2322 }
2323 }
2324}
2325
2326impl Default for Theme {
2327 fn default() -> Self {
2328 let mut theme: Self = ThemePalette::new(
2329 Color::hex_u24(0xE2E8F0),
2330 Color::hex_u24(0x0B121F),
2331 Color::hex_u24(0x7DCFFF),
2332 )
2333 .success(Color::hex_u24(0x34D399))
2334 .warning(Color::hex_u24(0xFBBF24))
2335 .error(Color::hex_u24(0xF43F5E))
2336 .info(Color::hex_u24(0x38BDF8))
2337 .into();
2338
2339 theme.file_icons = FileIconPalette {
2340 azure: Color::hex_u24(0x7DCFFF),
2341 blue: Color::hex_u24(0x60A5FA),
2342 cyan: Color::hex_u24(0x2DD4BF),
2343 green: Color::hex_u24(0x4ADE80),
2344 grey: Color::hex_u24(0x94A3B8),
2345 orange: Color::hex_u24(0xFB923C),
2346 purple: Color::hex_u24(0xC4B5FD),
2347 red: Color::hex_u24(0xF87171),
2348 yellow: Color::hex_u24(0xFBBF24),
2349 };
2350 theme.git_status = GitStatusPalette {
2351 modified: Color::hex_u24(0xFBBF24),
2352 added: Color::hex_u24(0x34D399),
2353 deleted: Color::hex_u24(0xFB7171),
2354 renamed: Color::hex_u24(0x38BDF8),
2355 untracked: Color::hex_u24(0xA78BFA),
2356 conflicted: Color::hex_u24(0xF43F5E),
2357 };
2358
2359 theme
2360 }
2361}