Skip to main content

clankerdiff_ratatui/
style.rs

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