Skip to main content

clankerdiff_ratatui/
style.rs

1//! Ratatui conversions for renderer-neutral diff themes.
2
3use crate::color::{composite_color, native_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::style::{Color, Modifier, Style};
11
12/// Ratatui adapter for shared semantic component states.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct RatatuiUiTheme {
15    palette: UiPalette,
16    pub canvas: Color,
17    pub surface: Color,
18    pub surface_hover: Color,
19    pub surface_selected: Color,
20    pub text: Color,
21    pub text_secondary: Color,
22    pub text_muted: Color,
23    pub border: Color,
24    pub accent: Color,
25    pub accent_foreground: Color,
26    pub info: Color,
27    pub positive: Color,
28    pub warning: Color,
29    pub destructive: Color,
30    pub destructive_foreground: Color,
31}
32
33impl RatatuiUiTheme {
34    #[must_use]
35    pub fn control_style(self, variant: ButtonVariant, state: ControlState) -> Style {
36        semantic_style(self.palette.control_style(variant, state), self.canvas)
37    }
38
39    #[must_use]
40    pub fn selection_style(self, state: SelectionState) -> Style {
41        semantic_style(self.palette.selection_style(state), self.canvas)
42    }
43
44    #[must_use]
45    pub fn notice_style(self, tone: NoticeTone) -> Style {
46        semantic_style(self.palette.notice_style(tone), self.canvas)
47    }
48
49    #[must_use]
50    pub fn on_background(self, background: Color) -> Self {
51        Self::from_palette(self.palette, background)
52    }
53
54    #[must_use]
55    pub const fn modal_size(size: ModalSize) -> (u16, u16) {
56        match size {
57            ModalSize::Compact => (40, 12),
58            ModalSize::Medium => (58, 18),
59            ModalSize::Wide => (72, 22),
60        }
61    }
62
63    fn from_palette(palette: UiPalette, canvas: Color) -> Self {
64        let color = |value| native_color(value, canvas);
65        Self {
66            palette,
67            canvas,
68            surface: color(palette.surface),
69            surface_hover: color(palette.surface_hover),
70            surface_selected: color(palette.surface_selected),
71            text: color(palette.text),
72            text_secondary: color(palette.text_secondary),
73            text_muted: color(palette.text_muted),
74            border: color(palette.border),
75            accent: color(palette.accent),
76            accent_foreground: native_color(palette.accent_foreground, color(palette.accent)),
77            info: color(palette.info),
78            positive: color(palette.positive),
79            warning: color(palette.warning),
80            destructive: color(palette.destructive),
81            destructive_foreground: native_color(
82                palette.destructive_foreground,
83                color(palette.destructive),
84            ),
85        }
86    }
87}
88
89impl From<&UiPalette> for RatatuiUiTheme {
90    fn from(palette: &UiPalette) -> Self {
91        Self::from_palette(
92            *palette,
93            composite_color(palette.canvas, Rgba::new(0, 0, 0, 255)),
94        )
95    }
96}
97
98fn semantic_style(style: SemanticStyle, canvas: Color) -> Style {
99    let background = style.background.map(|color| native_color(color, canvas));
100    let mut native = Style::new().fg(native_color(style.foreground, background.unwrap_or(canvas)));
101    if let Some(background) = background {
102        native = native.bg(background);
103    }
104    if style.emphasized {
105        native = native.add_modifier(Modifier::BOLD);
106    }
107    native
108}
109
110/// Ratatui colors derived from a shared [`ReviewTheme`]. Application colors live
111/// in [`RatatuiUiTheme`]; only diff-specific colors are kept here.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct RatatuiTheme {
114    /// Semantic application colors.
115    pub ui: RatatuiUiTheme,
116    pub foreground: Color,
117    pub background: Color,
118    /// Line-number and secondary text.
119    pub gutter: Color,
120    /// Added-line foreground.
121    pub addition: Color,
122    /// Removed-line foreground.
123    pub deletion: Color,
124    /// Added-line background.
125    pub addition_background: Color,
126    /// Removed-line background.
127    pub deletion_background: Color,
128}
129
130impl RatatuiTheme {
131    #[must_use]
132    pub const fn tone(&self, tone: DiffTone) -> (Color, Color) {
133        match tone {
134            DiffTone::Added => (self.addition, self.addition_background),
135            DiffTone::Removed => (self.deletion, self.deletion_background),
136            DiffTone::Context | DiffTone::Meta => (self.foreground, self.background),
137        }
138    }
139}
140
141impl From<&ReviewTheme> for RatatuiTheme {
142    fn from(theme: &ReviewTheme) -> Self {
143        let palette = &theme.diff;
144        let color = |value| composite_color(value, palette.background);
145        Self {
146            ui: RatatuiUiTheme::from(&theme.ui),
147            foreground: color(palette.foreground),
148            background: color(palette.background),
149            gutter: color(palette.gutter),
150            addition: color(palette.addition),
151            deletion: color(palette.deletion),
152            addition_background: color(palette.addition_background),
153            deletion_background: color(palette.deletion_background),
154        }
155    }
156}
157
158pub(crate) fn syntax_style(foreground: Rgba, font: FontStyle, background: Color) -> Style {
159    let mut modifiers = Modifier::empty();
160    modifiers.set(Modifier::BOLD, font.bold);
161    modifiers.set(Modifier::ITALIC, font.italic);
162    modifiers.set(Modifier::UNDERLINED, font.underline);
163    Style::new()
164        .fg(native_color(foreground, background))
165        .bg(background)
166        .add_modifier(modifiers)
167}