1use crate::color::{composite_color, native_color, page_color};
4use clankerdiff_core::DiffTone;
5use clankerdiff_theme::FontStyle;
6use clankerdiff_theme::{
7 ButtonVariant, ControlState, ModalSize, NoticeTone, ReviewTheme, Rgba, SelectionState,
8 SemanticStyle, UiPalette,
9};
10use ratatui::{
11 style::{Color, Modifier, Style},
12 text::Span,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct RatatuiUiTheme {
18 palette: UiPalette,
19 pub canvas: Color,
20 pub surface: Color,
21 pub surface_hover: Color,
22 pub surface_selected: Color,
23 pub text: Color,
24 pub text_secondary: Color,
25 pub text_muted: Color,
26 pub border: Color,
27 pub accent: Color,
28 pub accent_foreground: Color,
29 pub info: Color,
30 pub positive: Color,
31 pub warning: Color,
32 pub destructive: Color,
33 pub destructive_foreground: Color,
34}
35
36impl RatatuiUiTheme {
37 #[must_use]
38 pub fn control_style(self, variant: ButtonVariant, state: ControlState) -> Style {
39 semantic_style(self.palette.control_style(variant, state), self.canvas)
40 }
41
42 #[must_use]
43 pub fn selection_style(self, state: SelectionState) -> Style {
44 semantic_style(self.palette.selection_style(state), self.canvas)
45 }
46
47 #[must_use]
48 pub fn notice_style(self, tone: NoticeTone) -> Style {
49 semantic_style(self.palette.notice_style(tone), self.canvas)
50 }
51
52 #[must_use]
53 pub fn on_background(self, background: Color) -> Self {
54 Self::from_palette(self.palette, background)
55 }
56
57 #[must_use]
58 pub const fn modal_size(size: ModalSize) -> (u16, u16) {
59 match size {
60 ModalSize::Compact => (40, 12),
61 ModalSize::Medium => (58, 18),
62 ModalSize::Wide => (72, 22),
63 }
64 }
65
66 fn from_palette(palette: UiPalette, canvas: Color) -> Self {
67 let color = |value| native_color(value, canvas);
68 Self {
69 palette,
70 canvas,
71 surface: color(palette.surface),
72 surface_hover: color(palette.surface_hover),
73 surface_selected: color(palette.surface_selected),
74 text: color(palette.text),
75 text_secondary: color(palette.text_secondary),
76 text_muted: color(palette.text_muted),
77 border: color(palette.border),
78 accent: color(palette.accent),
79 accent_foreground: native_color(palette.accent_foreground, color(palette.accent)),
80 info: color(palette.info),
81 positive: color(palette.positive),
82 warning: color(palette.warning),
83 destructive: color(palette.destructive),
84 destructive_foreground: native_color(
85 palette.destructive_foreground,
86 color(palette.destructive),
87 ),
88 }
89 }
90}
91
92impl From<&UiPalette> for RatatuiUiTheme {
93 fn from(palette: &UiPalette) -> Self {
94 Self::from_palette(
95 *palette,
96 composite_color(palette.canvas, Rgba::new(0, 0, 0, 255)),
97 )
98 }
99}
100
101fn semantic_style(style: SemanticStyle, canvas: Color) -> Style {
102 let background = style.background.map(|color| native_color(color, canvas));
103 let mut native = Style::new().fg(native_color(style.foreground, background.unwrap_or(canvas)));
104 if let Some(background) = background {
105 native = native.bg(background);
106 }
107 if style.emphasized {
108 native = native.add_modifier(Modifier::BOLD);
109 }
110 native
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct RatatuiTheme {
117 pub ui: RatatuiUiTheme,
119 pub foreground: Color,
120 pub background: Color,
121 pub gutter: Color,
123 pub addition: Color,
125 pub deletion: Color,
127 pub addition_background: Color,
129 pub deletion_background: Color,
131}
132
133impl RatatuiTheme {
134 #[must_use]
135 pub const fn tone(&self, tone: DiffTone) -> (Color, Color) {
136 match tone {
137 DiffTone::Added => (self.addition, self.addition_background),
138 DiffTone::Removed => (self.deletion, self.deletion_background),
139 DiffTone::Context | DiffTone::Meta => (self.foreground, self.background),
140 }
141 }
142}
143
144impl From<&ReviewTheme> for RatatuiTheme {
145 fn from(theme: &ReviewTheme) -> Self {
146 let palette = &theme.diff;
147 let color = |value| composite_color(value, palette.background);
148 Self {
149 ui: RatatuiUiTheme::from(&theme.ui),
150 foreground: color(palette.foreground),
151 background: color(palette.background),
152 gutter: color(palette.gutter),
153 addition: color(palette.addition),
154 deletion: color(palette.deletion),
155 addition_background: color(palette.addition_background),
156 deletion_background: color(palette.deletion_background),
157 }
158 }
159}
160
161pub(crate) fn diff_indicator(tone: DiffTone, theme: &ReviewTheme, base: Style) -> Span<'static> {
162 let (symbol, foreground) = match tone {
163 DiffTone::Added => ("▌", theme.diff.addition),
164 DiffTone::Removed => ("▌", theme.diff.deletion),
165 DiffTone::Context | DiffTone::Meta => (" ", theme.diff.gutter),
166 };
167 Span::styled(symbol, base.fg(page_color(theme, foreground)))
168}
169
170pub(crate) fn syntax_style(foreground: Rgba, font: FontStyle, background: Color) -> Style {
171 let mut modifiers = Modifier::empty();
172 modifiers.set(Modifier::BOLD, font.bold);
173 modifiers.set(Modifier::ITALIC, font.italic);
174 modifiers.set(Modifier::UNDERLINED, font.underline);
175 Style::new()
176 .fg(native_color(foreground, background))
177 .bg(background)
178 .add_modifier(modifiers)
179}