facet_diff_core/
theme.rs

1//! Color themes for diff rendering.
2
3use owo_colors::Rgb;
4use palette::{FromColor, Lch, LinSrgb, Mix, Srgb};
5
6/// Color theme for diff rendering.
7///
8/// Defines colors for different kinds of changes. The default uses
9/// colorblind-friendly yellow/blue with type-specific value colors.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DiffTheme {
12    /// Foreground color for deleted content (accent color)
13    pub deleted: Rgb,
14
15    /// Foreground color for inserted content (accent color)
16    pub inserted: Rgb,
17
18    /// Foreground color for moved content (accent color)
19    pub moved: Rgb,
20
21    /// Foreground color for unchanged content
22    pub unchanged: Rgb,
23
24    /// Foreground color for keys/field names
25    pub key: Rgb,
26
27    /// Foreground color for structural elements like braces, brackets
28    pub structure: Rgb,
29
30    /// Foreground color for comments and type hints
31    pub comment: Rgb,
32
33    // === Value type base colors ===
34    /// Base color for string values
35    pub string: Rgb,
36
37    /// Base color for numeric values (integers, floats)
38    pub number: Rgb,
39
40    /// Base color for boolean values
41    pub boolean: Rgb,
42
43    /// Base color for null/None values
44    pub null: Rgb,
45
46    /// Subtle background for deleted lines (None = no background)
47    pub deleted_line_bg: Option<Rgb>,
48
49    /// Stronger background highlight for changed values on deleted lines
50    pub deleted_highlight_bg: Option<Rgb>,
51
52    /// Subtle background for inserted lines (None = no background)
53    pub inserted_line_bg: Option<Rgb>,
54
55    /// Stronger background highlight for changed values on inserted lines
56    pub inserted_highlight_bg: Option<Rgb>,
57
58    /// Subtle background for moved lines (None = no background)
59    pub moved_line_bg: Option<Rgb>,
60
61    /// Stronger background highlight for changed values on moved lines
62    pub moved_highlight_bg: Option<Rgb>,
63}
64
65impl Default for DiffTheme {
66    fn default() -> Self {
67        Self::COLORBLIND_WITH_BG
68    }
69}
70
71impl DiffTheme {
72    /// Colorblind-friendly theme - orange vs blue. No backgrounds.
73    pub const COLORBLIND_ORANGE_BLUE: Self = Self {
74        deleted: Rgb(255, 167, 89),    // #ffa759 warm orange
75        inserted: Rgb(97, 175, 239),   // #61afef sky blue
76        moved: Rgb(198, 120, 221),     // #c678dd purple/magenta
77        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
78        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
79        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
80        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
81        string: Rgb(152, 195, 121),    // #98c379 green (like One Dark Pro)
82        number: Rgb(209, 154, 102),    // #d19a66 orange
83        boolean: Rgb(209, 154, 102),   // #d19a66 orange
84        null: Rgb(86, 182, 194),       // #56b6c2 cyan
85        deleted_line_bg: None,
86        deleted_highlight_bg: None,
87        inserted_line_bg: None,
88        inserted_highlight_bg: None,
89        moved_line_bg: None,
90        moved_highlight_bg: None,
91    };
92
93    /// Colorblind-friendly with line + highlight backgrounds (yellow/blue).
94    pub const COLORBLIND_WITH_BG: Self = Self {
95        deleted: Rgb(229, 192, 123),   // #e5c07b warm yellow/gold
96        inserted: Rgb(97, 175, 239),   // #61afef sky blue
97        moved: Rgb(198, 120, 221),     // #c678dd purple/magenta
98        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
99        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
100        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
101        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
102        string: Rgb(152, 195, 121),    // #98c379 green (like One Dark Pro)
103        number: Rgb(209, 154, 102),    // #d19a66 orange
104        boolean: Rgb(209, 154, 102),   // #d19a66 orange
105        null: Rgb(86, 182, 194),       // #56b6c2 cyan
106        // Subtle line backgrounds
107        deleted_line_bg: Some(Rgb(55, 48, 35)), // medium-dark warm yellow
108        inserted_line_bg: Some(Rgb(35, 48, 60)), // medium-dark cool blue
109        moved_line_bg: Some(Rgb(50, 40, 60)),   // medium-dark purple
110        // Stronger highlight backgrounds for changed values
111        deleted_highlight_bg: Some(Rgb(90, 75, 50)), // medium yellow/brown
112        inserted_highlight_bg: Some(Rgb(45, 70, 95)), // medium blue
113        moved_highlight_bg: Some(Rgb(80, 55, 95)),   // medium purple
114    };
115
116    /// Pastel color theme - soft but distinguishable (not colorblind-friendly).
117    pub const PASTEL: Self = Self {
118        deleted: Rgb(255, 138, 128),   // #ff8a80 saturated coral/salmon
119        inserted: Rgb(128, 203, 156),  // #80cb9c saturated mint green
120        moved: Rgb(128, 179, 255),     // #80b3ff saturated sky blue
121        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
122        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
123        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
124        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
125        string: Rgb(152, 195, 121),    // #98c379 green
126        number: Rgb(209, 154, 102),    // #d19a66 orange
127        boolean: Rgb(209, 154, 102),   // #d19a66 orange
128        null: Rgb(86, 182, 194),       // #56b6c2 cyan
129        deleted_line_bg: None,
130        deleted_highlight_bg: None,
131        inserted_line_bg: None,
132        inserted_highlight_bg: None,
133        moved_line_bg: None,
134        moved_highlight_bg: None,
135    };
136
137    /// One Dark Pro color theme.
138    pub const ONE_DARK_PRO: Self = Self {
139        deleted: Rgb(224, 108, 117),   // #e06c75 red
140        inserted: Rgb(152, 195, 121),  // #98c379 green
141        moved: Rgb(97, 175, 239),      // #61afef blue
142        unchanged: Rgb(171, 178, 191), // #abb2bf white (normal text)
143        key: Rgb(171, 178, 191),       // #abb2bf white
144        structure: Rgb(171, 178, 191), // #abb2bf white
145        comment: Rgb(92, 99, 112),     // #5c6370 gray (muted)
146        string: Rgb(152, 195, 121),    // #98c379 green
147        number: Rgb(209, 154, 102),    // #d19a66 orange
148        boolean: Rgb(209, 154, 102),   // #d19a66 orange
149        null: Rgb(86, 182, 194),       // #56b6c2 cyan
150        deleted_line_bg: None,
151        deleted_highlight_bg: None,
152        inserted_line_bg: None,
153        inserted_highlight_bg: None,
154        moved_line_bg: None,
155        moved_highlight_bg: None,
156    };
157
158    /// Tokyo Night color theme.
159    pub const TOKYO_NIGHT: Self = Self {
160        deleted: Rgb(247, 118, 142),   // red
161        inserted: Rgb(158, 206, 106),  // green
162        moved: Rgb(122, 162, 247),     // blue
163        unchanged: Rgb(192, 202, 245), // white (normal text)
164        key: Rgb(192, 202, 245),       // white
165        structure: Rgb(192, 202, 245), // white
166        comment: Rgb(86, 95, 137),     // gray (muted)
167        string: Rgb(158, 206, 106),    // green
168        number: Rgb(255, 158, 100),    // orange
169        boolean: Rgb(255, 158, 100),   // orange
170        null: Rgb(125, 207, 255),      // cyan
171        deleted_line_bg: None,
172        deleted_highlight_bg: None,
173        inserted_line_bg: None,
174        inserted_highlight_bg: None,
175        moved_line_bg: None,
176        moved_highlight_bg: None,
177    };
178
179    /// Get the color for a change kind.
180    pub fn color_for(&self, kind: crate::ChangeKind) -> Rgb {
181        match kind {
182            crate::ChangeKind::Unchanged => self.unchanged,
183            crate::ChangeKind::Deleted => self.deleted,
184            crate::ChangeKind::Inserted => self.inserted,
185            crate::ChangeKind::MovedFrom | crate::ChangeKind::MovedTo => self.moved,
186            crate::ChangeKind::Modified => self.deleted, // old value gets deleted color
187        }
188    }
189
190    /// Blend two colors in linear sRGB space.
191    /// `t` ranges from 0.0 (all `a`) to 1.0 (all `b`).
192    pub fn blend(a: Rgb, b: Rgb, t: f32) -> Rgb {
193        // Convert to linear sRGB for perceptually correct blending
194        let a_lin: LinSrgb =
195            Srgb::new(a.0 as f32 / 255.0, a.1 as f32 / 255.0, a.2 as f32 / 255.0).into_linear();
196        let b_lin: LinSrgb =
197            Srgb::new(b.0 as f32 / 255.0, b.1 as f32 / 255.0, b.2 as f32 / 255.0).into_linear();
198
199        // Mix in linear space
200        let mixed = a_lin.mix(b_lin, t);
201
202        // Convert back to sRGB
203        let result: Srgb = mixed.into();
204        Rgb(
205            (result.red * 255.0).round() as u8,
206            (result.green * 255.0).round() as u8,
207            (result.blue * 255.0).round() as u8,
208        )
209    }
210
211    /// Brighten and saturate a color for use in highlights.
212    /// Increases both lightness and saturation in LCH space.
213    pub fn brighten_saturate(rgb: Rgb, lightness_boost: f32, chroma_boost: f32) -> Rgb {
214        let srgb = Srgb::new(
215            rgb.0 as f32 / 255.0,
216            rgb.1 as f32 / 255.0,
217            rgb.2 as f32 / 255.0,
218        );
219        let mut lch = Lch::from_color(srgb);
220
221        // Increase lightness
222        lch.l = (lch.l + lightness_boost * 100.0).min(100.0);
223
224        // Increase chroma (saturation-like)
225        lch.chroma = (lch.chroma + chroma_boost).min(150.0);
226
227        let result: Srgb = Srgb::from_color(lch);
228        Rgb(
229            (result.red * 255.0).round() as u8,
230            (result.green * 255.0).round() as u8,
231            (result.blue * 255.0).round() as u8,
232        )
233    }
234
235    /// Desaturate a color for use in backgrounds.
236    /// Reduces saturation (chroma) in LCH space.
237    pub fn desaturate(rgb: Rgb, amount: f32) -> Rgb {
238        let srgb = Srgb::new(
239            rgb.0 as f32 / 255.0,
240            rgb.1 as f32 / 255.0,
241            rgb.2 as f32 / 255.0,
242        );
243        let mut lch = Lch::from_color(srgb);
244
245        // Reduce chroma (saturation)
246        lch.chroma *= 1.0 - amount;
247
248        let result: Srgb = Srgb::from_color(lch);
249        Rgb(
250            (result.red * 255.0).round() as u8,
251            (result.green * 255.0).round() as u8,
252            (result.blue * 255.0).round() as u8,
253        )
254    }
255
256    /// Get the key color blended for a deleted context.
257    pub fn deleted_key(&self) -> Rgb {
258        Self::blend(self.key, self.deleted, 0.5)
259    }
260
261    /// Get the key color blended for an inserted context.
262    pub fn inserted_key(&self) -> Rgb {
263        Self::blend(self.key, self.inserted, 0.5)
264    }
265
266    /// Get the structure color blended for a deleted context.
267    pub fn deleted_structure(&self) -> Rgb {
268        Self::blend(self.structure, self.deleted, 0.4)
269    }
270
271    /// Get the structure color blended for an inserted context.
272    pub fn inserted_structure(&self) -> Rgb {
273        Self::blend(self.structure, self.inserted, 0.4)
274    }
275
276    /// Get the comment color blended for a deleted context.
277    pub fn deleted_comment(&self) -> Rgb {
278        Self::blend(self.comment, self.deleted, 0.35)
279    }
280
281    /// Get the comment color blended for an inserted context.
282    pub fn inserted_comment(&self) -> Rgb {
283        Self::blend(self.comment, self.inserted, 0.35)
284    }
285
286    // === Value type blending methods ===
287
288    /// Get the string color blended for a deleted context.
289    pub fn deleted_string(&self) -> Rgb {
290        Self::blend(self.string, self.deleted, 0.7)
291    }
292
293    /// Get the string color blended for an inserted context.
294    pub fn inserted_string(&self) -> Rgb {
295        Self::blend(self.string, self.inserted, 0.7)
296    }
297
298    /// Get the number color blended for a deleted context.
299    pub fn deleted_number(&self) -> Rgb {
300        Self::blend(self.number, self.deleted, 0.7)
301    }
302
303    /// Get the number color blended for an inserted context.
304    pub fn inserted_number(&self) -> Rgb {
305        Self::blend(self.number, self.inserted, 0.7)
306    }
307
308    /// Get the boolean color blended for a deleted context.
309    pub fn deleted_boolean(&self) -> Rgb {
310        Self::blend(self.boolean, self.deleted, 0.7)
311    }
312
313    /// Get the boolean color blended for an inserted context.
314    pub fn inserted_boolean(&self) -> Rgb {
315        Self::blend(self.boolean, self.inserted, 0.7)
316    }
317
318    /// Get the null color blended for a deleted context.
319    pub fn deleted_null(&self) -> Rgb {
320        Self::blend(self.null, self.deleted, 0.7)
321    }
322
323    /// Get the null color blended for an inserted context.
324    pub fn inserted_null(&self) -> Rgb {
325        Self::blend(self.null, self.inserted, 0.7)
326    }
327
328    // === Bright highlight colors for values with highlight backgrounds ===
329
330    /// Get the string color for a deleted highlight (brightened and saturated accent color).
331    pub fn deleted_highlight_string(&self) -> Rgb {
332        Self::brighten_saturate(self.deleted, 0.15, 0.2)
333    }
334
335    /// Get the string color for an inserted highlight (brightened and saturated accent color).
336    pub fn inserted_highlight_string(&self) -> Rgb {
337        Self::brighten_saturate(self.inserted, 0.15, 0.2)
338    }
339
340    /// Get the number color for a deleted highlight (brightened and saturated accent color).
341    pub fn deleted_highlight_number(&self) -> Rgb {
342        Self::brighten_saturate(self.deleted, 0.15, 0.2)
343    }
344
345    /// Get the number color for an inserted highlight (brightened and saturated accent color).
346    pub fn inserted_highlight_number(&self) -> Rgb {
347        Self::brighten_saturate(self.inserted, 0.15, 0.2)
348    }
349
350    /// Get the boolean color for a deleted highlight (brightened and saturated accent color).
351    pub fn deleted_highlight_boolean(&self) -> Rgb {
352        Self::brighten_saturate(self.deleted, 0.15, 0.2)
353    }
354
355    /// Get the boolean color for an inserted highlight (brightened and saturated accent color).
356    pub fn inserted_highlight_boolean(&self) -> Rgb {
357        Self::brighten_saturate(self.inserted, 0.15, 0.2)
358    }
359
360    /// Get the null color for a deleted highlight (brightened and saturated accent color).
361    pub fn deleted_highlight_null(&self) -> Rgb {
362        Self::brighten_saturate(self.deleted, 0.15, 0.2)
363    }
364
365    /// Get the null color for an inserted highlight (brightened and saturated accent color).
366    pub fn inserted_highlight_null(&self) -> Rgb {
367        Self::brighten_saturate(self.inserted, 0.15, 0.2)
368    }
369
370    // === Syntax highlight colors (keys, structure, comments with brightened accents) ===
371
372    /// Get the key color for a deleted highlight (brightened and saturated accent color).
373    pub fn deleted_highlight_key(&self) -> Rgb {
374        Self::brighten_saturate(self.deleted, 0.15, 0.2)
375    }
376
377    /// Get the key color for an inserted highlight (brightened and saturated accent color).
378    pub fn inserted_highlight_key(&self) -> Rgb {
379        Self::brighten_saturate(self.inserted, 0.15, 0.2)
380    }
381
382    /// Get the structure color for a deleted highlight (brightened and saturated accent color).
383    pub fn deleted_highlight_structure(&self) -> Rgb {
384        Self::brighten_saturate(self.deleted, 0.15, 0.2)
385    }
386
387    /// Get the structure color for an inserted highlight (brightened and saturated accent color).
388    pub fn inserted_highlight_structure(&self) -> Rgb {
389        Self::brighten_saturate(self.inserted, 0.15, 0.2)
390    }
391
392    /// Get the comment color for a deleted highlight (brightened and saturated accent color).
393    pub fn deleted_highlight_comment(&self) -> Rgb {
394        Self::brighten_saturate(self.deleted, 0.15, 0.2)
395    }
396
397    /// Get the comment color for an inserted highlight (brightened and saturated accent color).
398    pub fn inserted_highlight_comment(&self) -> Rgb {
399        Self::brighten_saturate(self.inserted, 0.15, 0.2)
400    }
401
402    // === Desaturated background getters ===
403
404    /// Get desaturated deleted line background (more saturated ambient, darker context).
405    pub fn desaturated_deleted_line_bg(&self) -> Option<Rgb> {
406        self.deleted_line_bg.map(|bg| Self::desaturate(bg, 0.2))
407    }
408
409    /// Get desaturated inserted line background (more saturated ambient, darker context).
410    pub fn desaturated_inserted_line_bg(&self) -> Option<Rgb> {
411        self.inserted_line_bg.map(|bg| Self::desaturate(bg, 0.2))
412    }
413
414    /// Get desaturated moved line background (more saturated ambient, darker context).
415    pub fn desaturated_moved_line_bg(&self) -> Option<Rgb> {
416        self.moved_line_bg.map(|bg| Self::desaturate(bg, 0.2))
417    }
418
419    /// Get desaturated deleted highlight background (very desaturated, minimal brightness boost).
420    pub fn desaturated_deleted_highlight_bg(&self) -> Option<Rgb> {
421        self.deleted_highlight_bg.map(|bg| {
422            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
423            Self::desaturate(brightened, 0.75)
424        })
425    }
426
427    /// Get desaturated inserted highlight background (very desaturated, minimal brightness boost).
428    pub fn desaturated_inserted_highlight_bg(&self) -> Option<Rgb> {
429        self.inserted_highlight_bg.map(|bg| {
430            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
431            Self::desaturate(brightened, 0.75)
432        })
433    }
434
435    /// Get desaturated moved highlight background (very desaturated, minimal brightness boost).
436    pub fn desaturated_moved_highlight_bg(&self) -> Option<Rgb> {
437        self.moved_highlight_bg.map(|bg| {
438            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
439            Self::desaturate(brightened, 0.75)
440        })
441    }
442}