Skip to main content

clankerdiff_ratatui/
style.rs

1//! Ratatui conversions for renderer-neutral diff themes.
2
3use 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/// Ratatui adapter for shared semantic component states.
16#[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/// Ratatui colors derived from a shared [`ReviewTheme`]. Application colors live
114/// in [`RatatuiUiTheme`]; only diff-specific colors are kept here.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct RatatuiTheme {
117    /// Semantic application colors.
118    pub ui: RatatuiUiTheme,
119    pub foreground: Color,
120    pub background: Color,
121    /// Line-number and secondary text.
122    pub gutter: Color,
123    /// Added-line foreground.
124    pub addition: Color,
125    /// Removed-line foreground.
126    pub deletion: Color,
127    /// Added-line background.
128    pub addition_background: Color,
129    /// Removed-line background.
130    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}