1mod selection;
4mod ui;
5pub use selection::{ThemeChoice, ThemeSelection};
6
7use arborium_theme::{HIGHLIGHTS, ThemeSlot, builtin, slot_to_highlight_index};
8pub use clankerdiff_fingerprint::{Fingerprint, FingerprintError};
9use serde::{Deserialize, Serialize};
10use std::{collections::BTreeMap, fmt, sync::LazyLock};
11
12const THEME_VERSION: u32 = 3;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct Rgba {
17 pub r: u8,
18 pub g: u8,
19 pub b: u8,
20 pub a: u8,
21}
22
23impl Rgba {
24 #[must_use]
25 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
26 Self { r, g, b, a }
27 }
28
29 #[must_use]
31 pub const fn over(self, background: Self) -> Self {
32 #[expect(
33 clippy::cast_possible_truncation,
34 reason = "weighted u8 average fits in u8"
35 )]
36 const fn channel(foreground: u8, background: u8, alpha: u8) -> u8 {
37 let foreground = foreground as u32 * alpha as u32;
38 let background = background as u32 * (u8::MAX - alpha) as u32;
39 ((foreground + background + 127) / 255) as u8
40 }
41 Self::new(
42 channel(self.r, background.r, self.a),
43 channel(self.g, background.g, self.a),
44 channel(self.b, background.b, self.a),
45 u8::MAX,
46 )
47 }
48
49 #[must_use]
50 pub const fn to_bytes(self) -> [u8; 4] {
51 [self.r, self.g, self.b, self.a]
52 }
53}
54
55impl Default for Rgba {
56 fn default() -> Self {
57 Self::new(212, 221, 214, 255)
58 }
59}
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
63pub struct FontStyle {
64 #[serde(default)]
65 pub bold: bool,
66 #[serde(default)]
67 pub italic: bool,
68 #[serde(default)]
69 pub underline: bool,
70}
71
72impl FontStyle {
73 #[must_use]
74 pub const fn none() -> Self {
75 Self {
76 bold: false,
77 italic: false,
78 underline: false,
79 }
80 }
81
82 #[must_use]
83 pub const fn is_plain(self) -> bool {
84 !self.bold && !self.italic && !self.underline
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct HighlightSpan {
91 pub range: std::ops::Range<usize>,
92 pub foreground: Rgba,
93 pub font_style: FontStyle,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98pub struct SyntaxStyle {
99 pub foreground: Rgba,
100 #[serde(default)]
101 pub font_style: FontStyle,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106pub enum DiffTone {
107 Context,
108 Added,
109 Removed,
110 Meta,
111}
112
113impl DiffTone {
114 #[must_use]
115 pub const fn marker(self) -> char {
116 match self {
117 Self::Added => '+',
118 Self::Removed => '-',
119 Self::Context | Self::Meta => ' ',
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct ToneColors {
127 pub foreground: Rgba,
128 pub background: Rgba,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct DiffPalette {
134 pub background: Rgba,
135 pub foreground: Rgba,
136 pub gutter: Rgba,
137 pub addition: Rgba,
138 pub deletion: Rgba,
139 pub addition_background: Rgba,
140 pub deletion_background: Rgba,
141 pub selection: Rgba,
142 pub accent: Rgba,
143 pub muted: Rgba,
144 pub border: Rgba,
145}
146
147impl DiffPalette {
148 #[must_use]
149 pub const fn tone(&self, tone: DiffTone) -> ToneColors {
150 match tone {
151 DiffTone::Added => ToneColors {
152 foreground: self.addition,
153 background: self.addition_background,
154 },
155 DiffTone::Removed => ToneColors {
156 foreground: self.deletion,
157 background: self.deletion_background,
158 },
159 DiffTone::Context | DiffTone::Meta => ToneColors {
160 foreground: self.foreground,
161 background: self.background,
162 },
163 }
164 }
165
166 #[must_use]
167 pub const fn colors(&self) -> [Rgba; 11] {
168 [
169 self.background,
170 self.foreground,
171 self.gutter,
172 self.addition,
173 self.deletion,
174 self.addition_background,
175 self.deletion_background,
176 self.selection,
177 self.accent,
178 self.muted,
179 self.border,
180 ]
181 }
182}
183
184pub const SCRIM_ALPHA: u8 = 184;
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(deny_unknown_fields)]
189pub struct UiPalette {
190 pub canvas: Rgba,
191 pub surface: Rgba,
192 pub surface_hover: Rgba,
193 pub surface_selected: Rgba,
194 pub text: Rgba,
195 pub text_secondary: Rgba,
196 pub text_muted: Rgba,
197 pub border: Rgba,
198 pub accent: Rgba,
199 pub accent_foreground: Rgba,
200 pub info: Rgba,
201 pub positive: Rgba,
202 pub warning: Rgba,
203 pub destructive: Rgba,
204 pub destructive_foreground: Rgba,
205 pub scrim: Rgba,
206}
207
208#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
210pub enum ButtonVariant {
211 Primary,
212 Secondary,
213 Destructive,
214 #[default]
215 Ghost,
216}
217
218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
220pub enum InteractionState {
221 #[default]
222 Rest,
223 Hovered,
224 Disabled,
225}
226
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
229pub struct ControlState {
230 pub interaction: InteractionState,
231 pub selected: bool,
232}
233
234impl ControlState {
235 #[must_use]
236 pub const fn new(interaction: InteractionState) -> Self {
237 Self {
238 interaction,
239 selected: false,
240 }
241 }
242
243 #[must_use]
244 pub const fn selected(mut self, selected: bool) -> Self {
245 self.selected = selected;
246 self
247 }
248}
249
250#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
252pub enum ControlSize {
253 Small,
254 #[default]
255 Medium,
256}
257
258#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
260pub enum SelectionState {
261 #[default]
262 None,
263 Selected,
264 Focused,
265 Disabled,
266}
267
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
270pub enum NoticeTone {
271 Neutral,
272 #[default]
273 Info,
274 Positive,
275 Warning,
276 Error,
277}
278
279#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
281pub enum ModalSize {
282 Compact,
283 #[default]
284 Medium,
285 Wide,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub struct SemanticStyle {
291 pub foreground: Rgba,
292 pub background: Option<Rgba>,
293 pub emphasized: bool,
294}
295
296impl UiPalette {
297 #[must_use]
299 pub const fn control_style(self, variant: ButtonVariant, state: ControlState) -> SemanticStyle {
300 if matches!(state.interaction, InteractionState::Disabled) {
301 return SemanticStyle {
302 foreground: self.text_muted,
303 background: Some(self.surface_selected),
304 emphasized: false,
305 };
306 }
307 let (foreground, background) = match (variant, state.interaction, state.selected) {
308 (ButtonVariant::Primary, _, _) => (self.accent_foreground, Some(self.accent)),
309 (ButtonVariant::Destructive, _, _) => {
310 (self.destructive_foreground, Some(self.destructive))
311 }
312 (ButtonVariant::Ghost, _, true) => (self.accent, Some(self.surface_selected)),
313 (ButtonVariant::Secondary | ButtonVariant::Ghost, InteractionState::Hovered, _) => {
314 (self.text, Some(self.surface_hover))
315 }
316 (ButtonVariant::Secondary, _, _) => (self.text, Some(self.surface)),
317 (ButtonVariant::Ghost, _, false) => (self.text_muted, None),
318 };
319 SemanticStyle {
320 foreground,
321 background,
322 emphasized: state.selected,
323 }
324 }
325
326 #[must_use]
328 pub const fn selection_style(self, state: SelectionState) -> SemanticStyle {
329 match state {
330 SelectionState::None => SemanticStyle {
331 foreground: self.text,
332 background: Some(self.surface),
333 emphasized: false,
334 },
335 SelectionState::Selected => SemanticStyle {
336 foreground: self.accent,
337 background: Some(self.surface_selected),
338 emphasized: false,
339 },
340 SelectionState::Focused => SemanticStyle {
341 foreground: self.accent_foreground,
342 background: Some(self.accent),
343 emphasized: true,
344 },
345 SelectionState::Disabled => SemanticStyle {
346 foreground: self.text_muted,
347 background: Some(self.surface),
348 emphasized: false,
349 },
350 }
351 }
352
353 #[must_use]
355 pub const fn notice_style(self, tone: NoticeTone) -> SemanticStyle {
356 let foreground = match tone {
357 NoticeTone::Neutral => self.text_secondary,
358 NoticeTone::Info => self.info,
359 NoticeTone::Positive => self.positive,
360 NoticeTone::Warning => self.warning,
361 NoticeTone::Error => self.destructive,
362 };
363 SemanticStyle {
364 foreground,
365 background: None,
366 emphasized: matches!(tone, NoticeTone::Warning | NoticeTone::Error),
367 }
368 }
369}
370
371impl UiPalette {
372 #[must_use]
373 pub const fn colors(&self) -> [Rgba; 16] {
374 [
375 self.canvas,
376 self.surface,
377 self.surface_hover,
378 self.surface_selected,
379 self.text,
380 self.text_secondary,
381 self.text_muted,
382 self.border,
383 self.accent,
384 self.accent_foreground,
385 self.info,
386 self.positive,
387 self.warning,
388 self.destructive,
389 self.destructive_foreground,
390 self.scrim,
391 ]
392 }
393}
394
395impl Default for DiffPalette {
396 fn default() -> Self {
397 let background = Rgba::new(21, 29, 31, 255);
398 let addition = Rgba::new(179, 215, 98, 255);
399 let deletion = Rgba::new(223, 120, 122, 255);
400 Self {
401 background,
402 foreground: Rgba::default(),
403 gutter: Rgba::new(80, 96, 91, 255),
404 addition,
405 deletion,
406 addition_background: diff_background(addition, background),
407 deletion_background: diff_background(deletion, background),
408 selection: Rgba::new(143, 188, 176, 45),
409 accent: Rgba::new(143, 188, 176, 255),
410 muted: Rgba::new(125, 143, 136, 255),
411 border: Rgba::new(57, 73, 73, 255),
412 }
413 }
414}
415
416#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
418pub enum ThemeId {
419 #[default]
420 Sage,
421 Ayu,
422 Builtin(String),
423 Custom(String),
424}
425
426impl fmt::Display for ThemeId {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 match self {
429 Self::Sage => f.write_str("sage"),
430 Self::Ayu => f.write_str("ayu-dark"),
431 Self::Builtin(name) | Self::Custom(name) => f.write_str(name),
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct ThemeDescriptor {
439 pub id: String,
440 pub name: String,
441 pub is_dark: bool,
442 pub source_url: Option<String>,
443}
444
445#[derive(Debug, Serialize, Deserialize)]
446#[serde(deny_unknown_fields)]
447struct ThemeDocument {
448 version: u32,
449 palette: DiffPalette,
450 ui: UiPalette,
451 syntax: BTreeMap<String, SyntaxStyle>,
452 markdown: MarkdownPalette,
453}
454
455#[derive(Debug, Clone)]
456pub struct ReviewTheme {
457 id: ThemeId,
458 pub diff: DiffPalette,
459 pub ui: UiPalette,
460 pub syntax: SyntaxTheme,
461 pub markdown: MarkdownPalette,
462}
463
464impl ReviewTheme {
465 pub fn builder(id: impl Into<String>) -> ReviewThemeBuilder {
466 ReviewThemeBuilder {
467 id: ThemeId::Custom(id.into()),
468 diff: DiffPalette::default(),
469 ui: UiPalette::default(),
470 syntax: BTreeMap::new(),
471 markdown: None,
472 }
473 }
474
475 pub fn from_bytes(id: ThemeId, bytes: &[u8]) -> Result<Self, ThemeError> {
483 let document: ThemeDocument =
484 serde_json::from_slice(bytes).map_err(|source| ThemeError::Parse {
485 message: source.to_string(),
486 })?;
487 if document.version != THEME_VERSION {
488 return Err(ThemeError::UnsupportedVersion {
489 version: document.version,
490 });
491 }
492 Ok(Self {
493 id,
494 ui: document.ui,
495 diff: document.palette,
496 syntax: SyntaxTheme::new(document.syntax),
497 markdown: document.markdown,
498 })
499 }
500
501 #[must_use]
502 pub const fn id(&self) -> &ThemeId {
503 &self.id
504 }
505
506 #[must_use]
507 pub fn revision(&self) -> Fingerprint {
508 let diff = self.diff.colors().map(Rgba::to_bytes);
509 let markdown = self.markdown.colors().map(Rgba::to_bytes);
510 let ui = self.ui.colors().map(Rgba::to_bytes);
511 Fingerprint::of(
512 std::iter::once(self.syntax.revision().as_bytes().as_slice())
513 .chain(diff.iter().map(<[u8; 4]>::as_slice))
514 .chain(markdown.iter().map(<[u8; 4]>::as_slice))
515 .chain(ui.iter().map(<[u8; 4]>::as_slice)),
516 )
517 }
518
519 pub fn to_bytes(&self) -> Result<Vec<u8>, ThemeError> {
520 serde_json::to_vec(&ThemeDocument {
521 version: THEME_VERSION,
522 palette: self.diff.clone(),
523 ui: self.ui,
524 syntax: self.syntax.captures.clone(),
525 markdown: self.markdown.clone(),
526 })
527 .map_err(|error| ThemeError::Parse {
528 message: error.to_string(),
529 })
530 }
531
532 #[must_use]
534 pub fn catalog() -> Vec<ThemeDescriptor> {
535 let mut themes = vec![ThemeDescriptor {
536 id: "sage".to_owned(),
537 name: "Sage".to_owned(),
538 is_dark: true,
539 source_url: None,
540 }];
541 themes.extend(builtin::all().into_iter().map(|theme| ThemeDescriptor {
542 id: theme_slug(&theme.name),
543 name: theme.name,
544 is_dark: theme.is_dark,
545 source_url: theme.source_url,
546 }));
547 themes.sort_by(|left, right| {
548 left.is_dark
549 .cmp(&right.is_dark)
550 .reverse()
551 .then_with(|| left.name.cmp(&right.name))
552 });
553 themes
554 }
555
556 pub fn builtin(name: &str) -> Result<Self, ThemeError> {
564 let id = theme_slug(name);
565 if id == "sage" {
566 return Self::sage();
567 }
568 if id == "ayu" || id == "ayu-dark" {
569 return Self::ayu();
570 }
571 let source = builtin::all()
572 .into_iter()
573 .find(|theme| theme_slug(&theme.name) == id)
574 .ok_or_else(|| ThemeError::UnknownTheme {
575 name: name.to_owned(),
576 })?;
577 let ui = ui::builtin(&id)?;
578 Ok(Self::from_arborium(ThemeId::Builtin(id), &source, ui))
579 }
580
581 fn from_arborium(id: ThemeId, source: &arborium_theme::Theme, ui: UiPalette) -> Self {
582 let palette = palette_from_arborium(source);
583 let syntax = HIGHLIGHTS
584 .iter()
585 .zip(source.styles.iter())
586 .filter_map(|(highlight, style)| {
587 let foreground = style.fg?;
588 Some((
589 highlight.name.to_owned(),
590 SyntaxStyle {
591 foreground: rgba(foreground),
592 font_style: FontStyle {
593 bold: style.modifiers.bold,
594 italic: style.modifiers.italic,
595 underline: style.modifiers.underline,
596 },
597 },
598 ))
599 })
600 .collect::<BTreeMap<_, _>>();
601 let syntax = SyntaxTheme::new(syntax);
602 let markdown = MarkdownPalette::from_syntax(&syntax, &palette);
603 Self {
604 id,
605 diff: palette,
606 ui,
607 syntax,
608 markdown,
609 }
610 }
611
612 pub fn sage() -> Result<Self, ThemeError> {
617 Self::from_bytes(ThemeId::Sage, include_bytes!("../assets/themes/sage.json"))
618 }
619 pub fn ayu() -> Result<Self, ThemeError> {
624 Self::from_bytes(
625 ThemeId::Ayu,
626 include_bytes!("../assets/themes/ayu-dark.json"),
627 )
628 }
629}
630
631fn theme_slug(name: &str) -> String {
632 name.trim()
633 .chars()
634 .map(|character| match character {
635 ' ' | '_' => '-',
636 character => character.to_ascii_lowercase(),
637 })
638 .collect()
639}
640
641const fn rgba(color: arborium_theme::Color) -> Rgba {
642 Rgba::new(color.r, color.g, color.b, u8::MAX)
643}
644
645fn palette_from_arborium(theme: &arborium_theme::Theme) -> DiffPalette {
646 let background = theme.background.map_or_else(
647 || {
648 if theme.is_dark {
649 Rgba::new(21, 29, 31, 255)
650 } else {
651 Rgba::new(250, 250, 250, 255)
652 }
653 },
654 rgba,
655 );
656 let foreground = theme.foreground.map_or_else(
657 || {
658 if theme.is_dark {
659 Rgba::new(220, 220, 220, 255)
660 } else {
661 Rgba::new(35, 35, 35, 255)
662 }
663 },
664 rgba,
665 );
666 let style_color = |slot| {
667 slot_to_highlight_index(slot)
668 .and_then(|index| theme.style(index))
669 .and_then(|style| style.fg)
670 .map(rgba)
671 };
672 let addition = style_color(ThemeSlot::DiffAdd).unwrap_or_else(|| {
673 if theme.is_dark {
674 Rgba::new(128, 190, 120, 255)
675 } else {
676 Rgba::new(40, 125, 55, 255)
677 }
678 });
679 let deletion = style_color(ThemeSlot::DiffDelete).unwrap_or_else(|| {
680 if theme.is_dark {
681 Rgba::new(225, 115, 115, 255)
682 } else {
683 Rgba::new(185, 45, 45, 255)
684 }
685 });
686 let accent = style_color(ThemeSlot::Link)
687 .or_else(|| style_color(ThemeSlot::Function))
688 .or_else(|| style_color(ThemeSlot::Keyword))
689 .unwrap_or_else(|| mix(foreground, background, 72));
690 DiffPalette {
691 background,
692 foreground,
693 gutter: mix(foreground, background, 45),
694 addition,
695 deletion,
696 addition_background: diff_background(addition, background),
697 deletion_background: diff_background(deletion, background),
698 selection: mix(accent, background, 28),
699 accent,
700 muted: mix(foreground, background, 62),
701 border: mix(foreground, background, 25),
702 }
703}
704
705const fn mix(foreground: Rgba, background: Rgba, amount: u8) -> Rgba {
706 Rgba::new(foreground.r, foreground.g, foreground.b, amount).over(background)
707}
708
709const DIFF_BACKGROUND_ALPHA: u8 = 31;
710const fn diff_background(foreground: Rgba, background: Rgba) -> Rgba {
711 Rgba::new(
712 foreground.r,
713 foreground.g,
714 foreground.b,
715 DIFF_BACKGROUND_ALPHA,
716 )
717 .over(background)
718}
719
720static DEFAULT_THEME: LazyLock<ReviewTheme> =
721 LazyLock::new(|| ReviewTheme::sage().expect("bundled Sage theme JSON must parse"));
722
723impl Default for ReviewTheme {
724 fn default() -> Self {
725 DEFAULT_THEME.clone()
726 }
727}
728
729impl Default for UiPalette {
730 fn default() -> Self {
731 DEFAULT_THEME.ui
732 }
733}
734
735pub struct ReviewThemeBuilder {
736 id: ThemeId,
737 diff: DiffPalette,
738 ui: UiPalette,
739 syntax: BTreeMap<String, SyntaxStyle>,
740 markdown: Option<MarkdownPalette>,
741}
742
743impl ReviewThemeBuilder {
744 #[must_use]
745 pub fn capture(mut self, name: impl Into<String>, style: SyntaxStyle) -> Self {
746 self.syntax.insert(name.into(), style);
747 self
748 }
749
750 #[must_use]
751 pub fn diff(mut self, palette: DiffPalette) -> Self {
752 self.diff = palette;
753 self
754 }
755
756 #[must_use]
757 pub fn ui(mut self, palette: UiPalette) -> Self {
758 self.ui = palette;
759 self
760 }
761
762 #[must_use]
763 pub fn markdown(mut self, palette: MarkdownPalette) -> Self {
764 self.markdown = Some(palette);
765 self
766 }
767
768 #[must_use]
769 pub fn build(self) -> ReviewTheme {
770 let syntax = SyntaxTheme::new(self.syntax);
771 let markdown = self
772 .markdown
773 .unwrap_or_else(|| MarkdownPalette::from_syntax(&syntax, &self.diff));
774 ReviewTheme {
775 id: self.id,
776 ui: self.ui,
777 diff: self.diff,
778 syntax,
779 markdown,
780 }
781 }
782}
783
784#[derive(Debug, Clone, PartialEq, Eq)]
785pub struct SyntaxTheme {
786 captures: BTreeMap<String, SyntaxStyle>,
787 revision: Fingerprint,
788}
789
790impl SyntaxTheme {
791 #[must_use]
792 pub fn new(captures: BTreeMap<String, SyntaxStyle>) -> Self {
793 let mut fields = Vec::with_capacity(captures.len() * 3);
794 for (capture, style) in &captures {
795 fields.push(capture.as_bytes().to_vec());
796 fields.push(style.foreground.to_bytes().to_vec());
797 fields.push(vec![
798 u8::from(style.font_style.bold),
799 u8::from(style.font_style.italic),
800 u8::from(style.font_style.underline),
801 ]);
802 }
803 Self {
804 captures,
805 revision: Fingerprint::of(fields),
806 }
807 }
808
809 #[must_use]
810 pub const fn revision(&self) -> Fingerprint {
811 self.revision
812 }
813
814 #[must_use]
815 pub fn style(&self, capture: &str) -> Option<SyntaxStyle> {
816 let mut candidate = capture;
817 loop {
818 if let Some(style) = self.captures.get(candidate) {
819 return Some(*style);
820 }
821 let (parent, _) = candidate.rsplit_once('.')?;
822 candidate = parent;
823 }
824 }
825}
826
827impl Default for SyntaxTheme {
828 fn default() -> Self {
829 DEFAULT_THEME.syntax.clone()
830 }
831}
832
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
834#[serde(deny_unknown_fields)]
835pub struct MarkdownPalette {
836 pub heading: Rgba,
837 pub link: Rgba,
838 pub quote: Rgba,
839 pub inline_code: Rgba,
840 pub inline_code_background: Rgba,
841 pub code: Rgba,
842 pub code_background: Rgba,
843}
844
845impl MarkdownPalette {
846 #[must_use]
847 pub fn from_syntax(syntax: &SyntaxTheme, palette: &DiffPalette) -> Self {
848 let capture = |name, fallback| {
849 syntax
850 .style(name)
851 .map_or(fallback, |style| style.foreground)
852 };
853 Self {
854 heading: capture("markup.heading", palette.accent),
855 link: capture("markup.link", palette.accent),
856 quote: capture("markup.quote", palette.muted),
857 inline_code: capture("markup.raw.inline", palette.foreground),
858 inline_code_background: palette.border,
859 code: capture("markup.raw.block", palette.foreground),
860 code_background: palette.background,
861 }
862 }
863
864 #[must_use]
865 pub const fn colors(&self) -> [Rgba; 7] {
866 [
867 self.heading,
868 self.link,
869 self.quote,
870 self.inline_code,
871 self.inline_code_background,
872 self.code,
873 self.code_background,
874 ]
875 }
876}
877
878impl Default for MarkdownPalette {
879 fn default() -> Self {
880 DEFAULT_THEME.markdown.clone()
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
886pub enum ThemeError {
887 #[error("unknown built-in theme `{name}`")]
888 UnknownTheme { name: String },
889 #[error("failed to parse theme JSON: {message}")]
890 Parse { message: String },
891 #[error("unsupported theme JSON version {version}")]
892 UnsupportedVersion { version: u32 },
893}