1pub use vtcode_commons::diff_theme::{
8 DiffColorLevel, DiffTheme, diff_add_bg, diff_add_fg, diff_add_word_bg, diff_del_bg, diff_del_fg, diff_del_word_bg,
9 diff_gutter_bg_add_light, diff_gutter_bg_del_light, diff_gutter_fg, diff_gutter_fg_light, diff_hunk_bg,
10};
11pub use vtcode_commons::styling::DiffColorPalette;
12
13use crate::tui::ui::syntax_highlight::{DiffScopeBackgroundRgbs, diff_scope_background_rgbs};
14use ratatui::style::{Color as RatatuiColor, Modifier, Style as RatatuiStyle};
15use vtcode_commons::color256_theme::rgb_to_ansi256_for_theme;
16
17use anstyle::{Color as AnstyleColor, Style as AnstyleStyle};
18
19fn ratatui_color_from_anstyle(color: anstyle::Color) -> RatatuiColor {
23 crate::design::color::anstyle_to_ratatui_color(color)
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum DiffLineType {
31 Insert,
32 Delete,
33 Context,
34}
35
36#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37struct ResolvedDiffBackgrounds {
38 add: Option<RatatuiColor>,
39 del: Option<RatatuiColor>,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct DiffRenderStyleContext {
45 theme: DiffTheme,
46 level: DiffColorLevel,
47 backgrounds: ResolvedDiffBackgrounds,
48}
49
50impl DiffRenderStyleContext {
51 pub fn theme(self) -> DiffTheme {
52 self.theme
53 }
54
55 pub fn level(self) -> DiffColorLevel {
56 self.level
57 }
58
59 pub fn add_word_bg(self) -> Option<AnstyleColor> {
61 (self.level != DiffColorLevel::Ansi16).then(|| diff_add_word_bg(self.theme, self.level))
62 }
63
64 pub fn del_word_bg(self) -> Option<AnstyleColor> {
66 (self.level != DiffColorLevel::Ansi16).then(|| diff_del_word_bg(self.theme, self.level))
67 }
68}
69
70pub(crate) fn current_diff_render_style_context() -> DiffRenderStyleContext {
72 let theme = DiffTheme::detect();
73 let level = DiffColorLevel::detect();
74 diff_render_style_context_for(theme, level, scope_backgrounds_for_level(level))
75}
76
77pub(crate) fn diff_render_style_context_for(
78 theme: DiffTheme,
79 level: DiffColorLevel,
80 scope_backgrounds: DiffScopeBackgroundRgbs,
81) -> DiffRenderStyleContext {
82 DiffRenderStyleContext {
83 theme,
84 level,
85 backgrounds: resolve_diff_backgrounds_for(theme, level, scope_backgrounds),
86 }
87}
88
89fn resolve_diff_backgrounds_for(
90 theme: DiffTheme,
91 level: DiffColorLevel,
92 scope_backgrounds: DiffScopeBackgroundRgbs,
93) -> ResolvedDiffBackgrounds {
94 let mut resolved = fallback_diff_backgrounds(theme, level);
95 if level == DiffColorLevel::Ansi16 {
96 return resolved;
97 }
98
99 if let Some(rgb) = scope_backgrounds.inserted
100 && let Some(color) = color_from_rgb_for_level(rgb, theme, level)
101 {
102 resolved.add = Some(color);
103 }
104
105 if let Some(rgb) = scope_backgrounds.deleted
106 && let Some(color) = color_from_rgb_for_level(rgb, theme, level)
107 {
108 resolved.del = Some(color);
109 }
110
111 resolved
112}
113
114fn fallback_diff_backgrounds(theme: DiffTheme, level: DiffColorLevel) -> ResolvedDiffBackgrounds {
115 match level {
116 DiffColorLevel::Ansi16 => ResolvedDiffBackgrounds::default(),
117 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => ResolvedDiffBackgrounds {
118 add: Some(ratatui_color_from_anstyle(diff_add_bg(theme, level))),
119 del: Some(ratatui_color_from_anstyle(diff_del_bg(theme, level))),
120 },
121 }
122}
123
124fn color_from_rgb_for_level(rgb: (u8, u8, u8), theme: DiffTheme, level: DiffColorLevel) -> Option<RatatuiColor> {
125 match level {
126 DiffColorLevel::TrueColor => Some(RatatuiColor::Rgb(rgb.0, rgb.1, rgb.2)),
127 DiffColorLevel::Ansi256 => {
128 Some(RatatuiColor::Indexed(rgb_to_ansi256_for_theme(rgb.0, rgb.1, rgb.2, theme.is_light())))
129 }
130 DiffColorLevel::Ansi16 => None,
131 }
132}
133
134fn content_background(kind: DiffLineType, style_context: DiffRenderStyleContext) -> Option<RatatuiColor> {
135 match kind {
136 DiffLineType::Insert => style_context.backgrounds.add,
137 DiffLineType::Delete => style_context.backgrounds.del,
138 DiffLineType::Context => None,
139 }
140}
141
142fn word_background(kind: DiffLineType, style_context: DiffRenderStyleContext) -> Option<RatatuiColor> {
143 let color = match kind {
144 DiffLineType::Insert => style_context.add_word_bg(),
145 DiffLineType::Delete => style_context.del_word_bg(),
146 DiffLineType::Context => None,
147 }?;
148 Some(ratatui_color_from_anstyle(color))
149}
150
151pub(crate) fn style_line_bg(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
154 content_background(kind, style_context).map_or_else(RatatuiStyle::default, |bg| RatatuiStyle::default().bg(bg))
155}
156
157pub(crate) fn style_word_bg(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
159 word_background(kind, style_context).map_or_else(RatatuiStyle::default, |bg| RatatuiStyle::default().bg(bg))
160}
161
162fn scope_backgrounds_for_level(level: DiffColorLevel) -> DiffScopeBackgroundRgbs {
163 match level {
164 DiffColorLevel::Ansi16 => DiffScopeBackgroundRgbs::default(),
165 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => diff_scope_background_rgbs(),
166 }
167}
168
169fn indicator_fg(kind: DiffLineType, style_context: DiffRenderStyleContext) -> Option<RatatuiColor> {
173 let color = match kind {
174 DiffLineType::Insert => diff_add_fg(style_context.theme, style_context.level),
175 DiffLineType::Delete => diff_del_fg(style_context.theme, style_context.level),
176 DiffLineType::Context => return None,
177 };
178 Some(ratatui_color_from_anstyle(color))
179}
180
181fn gutter_fg(style_context: DiffRenderStyleContext) -> RatatuiColor {
184 ratatui_color_from_anstyle(diff_gutter_fg(style_context.theme, style_context.level))
185}
186
187pub(crate) fn style_gutter(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
193 let mut style = RatatuiStyle::default().fg(gutter_fg(style_context));
194 if let Some(bg) = content_background(kind, style_context) {
195 style = style.bg(bg);
196 }
197 style
198}
199
200pub(crate) fn style_sign(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
202 let mut style = RatatuiStyle::default().add_modifier(Modifier::BOLD);
203 if let Some(color) = indicator_fg(kind, style_context) {
204 style = style.fg(color);
205 }
206 if let Some(bg) = content_background(kind, style_context) {
207 style = style.bg(bg);
208 }
209 style
210}
211
212pub(crate) fn style_hunk_header(style_context: DiffRenderStyleContext) -> RatatuiStyle {
214 let _ = style_context;
215 RatatuiStyle::default().fg(RatatuiColor::Cyan).add_modifier(Modifier::BOLD)
216}
217
218fn style_file_header(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
221 let mut style = RatatuiStyle::default().add_modifier(Modifier::BOLD);
222 if let Some(color) = indicator_fg(kind, style_context) {
223 style = style.fg(color);
224 }
225 style
226}
227
228pub(crate) fn style_file_header_old(style_context: DiffRenderStyleContext) -> RatatuiStyle {
230 style_file_header(DiffLineType::Delete, style_context)
231}
232
233pub(crate) fn style_file_header_new(style_context: DiffRenderStyleContext) -> RatatuiStyle {
235 style_file_header(DiffLineType::Insert, style_context)
236}
237
238pub(crate) fn style_content(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
244 let bg = content_background(kind, style_context);
245 let fg = indicator_fg(kind, style_context);
246 match (kind, style_context.level, bg) {
247 (DiffLineType::Context, _, _) => RatatuiStyle::default(),
248 (_, DiffColorLevel::Ansi16, _) => fg.map(|c| RatatuiStyle::default().fg(c)).unwrap_or_default(),
249 (_, _, Some(bg)) => RatatuiStyle::default().bg(bg),
250 (_, _, None) => RatatuiStyle::default(),
251 }
252}
253
254pub(crate) fn style_content_ansi(kind: DiffLineType, style_context: DiffRenderStyleContext) -> AnstyleStyle {
256 let background = content_background(kind, style_context).map(ratatui_color_to_anstyle);
257 let foreground = indicator_fg(kind, style_context).map(ratatui_color_to_anstyle);
258 match (kind, style_context.level, background) {
259 (DiffLineType::Context, _, _) => AnstyleStyle::new(),
260 (_, DiffColorLevel::Ansi16, _) => AnstyleStyle::new().fg_color(foreground),
261 (_, _, background) => AnstyleStyle::new().bg_color(background),
262 }
263}
264
265pub(crate) fn style_sign_ansi(kind: DiffLineType, style_context: DiffRenderStyleContext) -> AnstyleStyle {
270 let background = content_background(kind, style_context).map(ratatui_color_to_anstyle);
271 let foreground = indicator_fg(kind, style_context).map(ratatui_color_to_anstyle);
272 let mut style = AnstyleStyle::new().fg_color(foreground).bg_color(background);
273 if !matches!(kind, DiffLineType::Context) {
274 style = style.effects(anstyle::Effects::BOLD);
275 }
276 style
277}
278
279pub(crate) fn style_gutter_ansi(kind: DiffLineType, style_context: DiffRenderStyleContext) -> AnstyleStyle {
282 let background = content_background(kind, style_context).map(ratatui_color_to_anstyle);
283 let foreground = Some(ratatui_color_to_anstyle(gutter_fg(style_context)));
284 AnstyleStyle::new().fg_color(foreground).bg_color(background)
285}
286
287pub(crate) fn style_hunk_header_ansi(style_context: DiffRenderStyleContext) -> AnstyleStyle {
290 let _ = style_context;
291 AnstyleStyle::new()
292 .fg_color(Some(AnstyleColor::Ansi(anstyle::AnsiColor::Cyan)))
293 .effects(anstyle::Effects::BOLD)
294}
295
296fn style_file_header_ansi(kind: DiffLineType, style_context: DiffRenderStyleContext) -> AnstyleStyle {
299 let foreground = indicator_fg(kind, style_context).map(ratatui_color_to_anstyle);
300 AnstyleStyle::new().fg_color(foreground).effects(anstyle::Effects::BOLD)
301}
302
303pub(crate) fn style_file_header_old_ansi(style_context: DiffRenderStyleContext) -> AnstyleStyle {
305 style_file_header_ansi(DiffLineType::Delete, style_context)
306}
307
308pub(crate) fn style_file_header_new_ansi(style_context: DiffRenderStyleContext) -> AnstyleStyle {
310 style_file_header_ansi(DiffLineType::Insert, style_context)
311}
312
313fn ratatui_color_to_anstyle(color: RatatuiColor) -> AnstyleColor {
314 match color {
315 RatatuiColor::Reset => AnstyleColor::Ansi(anstyle::AnsiColor::Black),
316 RatatuiColor::Black => AnstyleColor::Ansi(anstyle::AnsiColor::Black),
317 RatatuiColor::Red => AnstyleColor::Ansi(anstyle::AnsiColor::Red),
318 RatatuiColor::Green => AnstyleColor::Ansi(anstyle::AnsiColor::Green),
319 RatatuiColor::Yellow => AnstyleColor::Ansi(anstyle::AnsiColor::Yellow),
320 RatatuiColor::Blue => AnstyleColor::Ansi(anstyle::AnsiColor::Blue),
321 RatatuiColor::Magenta => AnstyleColor::Ansi(anstyle::AnsiColor::Magenta),
322 RatatuiColor::Cyan => AnstyleColor::Ansi(anstyle::AnsiColor::Cyan),
323 RatatuiColor::Gray => AnstyleColor::Ansi(anstyle::AnsiColor::White),
324 RatatuiColor::DarkGray => AnstyleColor::Ansi(anstyle::AnsiColor::BrightBlack),
325 RatatuiColor::LightRed => AnstyleColor::Ansi(anstyle::AnsiColor::BrightRed),
326 RatatuiColor::LightGreen => AnstyleColor::Ansi(anstyle::AnsiColor::BrightGreen),
327 RatatuiColor::LightYellow => AnstyleColor::Ansi(anstyle::AnsiColor::BrightYellow),
328 RatatuiColor::LightBlue => AnstyleColor::Ansi(anstyle::AnsiColor::BrightBlue),
329 RatatuiColor::LightMagenta => AnstyleColor::Ansi(anstyle::AnsiColor::BrightMagenta),
330 RatatuiColor::LightCyan => AnstyleColor::Ansi(anstyle::AnsiColor::BrightCyan),
331 RatatuiColor::White => AnstyleColor::Ansi(anstyle::AnsiColor::BrightWhite),
332 RatatuiColor::Indexed(index) => AnstyleColor::Ansi256(anstyle::Ansi256Color(index)),
333 RatatuiColor::Rgb(red, green, blue) => AnstyleColor::Rgb(anstyle::RgbColor(red, green, blue)),
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn test_style_context(theme: DiffTheme, level: DiffColorLevel) -> DiffRenderStyleContext {
342 diff_render_style_context_for(theme, level, DiffScopeBackgroundRgbs::default())
343 }
344
345 #[test]
346 fn dark_add_bg_is_subtle_green_tint() {
347 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
348 assert_eq!(bg, anstyle::Color::Rgb(anstyle::RgbColor(20, 58, 45)));
349 }
350
351 #[test]
352 fn dark_del_bg_is_subtle_red_tint() {
353 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
354 assert_eq!(bg, anstyle::Color::Rgb(anstyle::RgbColor(70, 38, 42)));
355 }
356
357 #[test]
358 fn light_add_bg_is_subtle_green_tint() {
359 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
360 assert_eq!(bg, anstyle::Color::Rgb(anstyle::RgbColor(218, 246, 225)));
361 }
362
363 #[test]
364 fn light_del_bg_is_subtle_red_tint() {
365 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
366 assert_eq!(bg, anstyle::Color::Rgb(anstyle::RgbColor(255, 224, 224)));
367 }
368
369 #[test]
370 fn backgrounds_follow_terminal_capability() {
371 assert_eq!(
372 diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor),
373 anstyle::Color::Rgb(anstyle::RgbColor(20, 58, 45))
374 );
375 assert_eq!(
376 diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor),
377 anstyle::Color::Rgb(anstyle::RgbColor(70, 38, 42))
378 );
379 assert!(matches!(diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256), anstyle::Color::Ansi256(_)));
380 assert!(matches!(diff_del_word_bg(DiffTheme::Light, DiffColorLevel::Ansi256), anstyle::Color::Ansi256(_)));
381 }
382
383 #[test]
384 fn context_line_bg_is_default() {
385 let style =
386 style_line_bg(DiffLineType::Context, test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor));
387 assert_eq!(style, RatatuiStyle::default());
388 }
389
390 #[test]
391 fn dark_gutter_context_is_contrast_safe_without_bg() {
392 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
393 let style = style_gutter(DiffLineType::Context, ctx);
394 assert_eq!(style.fg, Some(RatatuiColor::Rgb(165, 175, 170)));
395 assert!(!style.add_modifier.contains(Modifier::DIM));
396 assert_eq!(style.bg, None);
397 }
398
399 #[test]
400 fn insert_gutter_is_contrast_safe_on_add_bg() {
401 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
402 let style = style_gutter(DiffLineType::Insert, ctx);
403 assert_eq!(style.fg, Some(RatatuiColor::Rgb(165, 175, 170)));
404 assert!(!style.add_modifier.contains(Modifier::DIM));
405 assert_eq!(style.bg, Some(RatatuiColor::Rgb(20, 58, 45)));
406 }
407
408 #[test]
409 fn delete_gutter_is_contrast_safe_on_delete_bg() {
410 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
411 let style = style_gutter(DiffLineType::Delete, ctx);
412 assert_eq!(style.fg, Some(RatatuiColor::Rgb(165, 175, 170)));
413 assert!(!style.add_modifier.contains(Modifier::DIM));
414 assert_eq!(style.bg, Some(RatatuiColor::Rgb(70, 38, 42)));
415 }
416
417 #[test]
418 fn dark_ansi16_content_uses_foreground_only() {
419 let style = style_content(DiffLineType::Insert, test_style_context(DiffTheme::Dark, DiffColorLevel::Ansi16));
420 assert_eq!(style.fg, Some(RatatuiColor::LightGreen));
421 assert_eq!(style.bg, None);
422 }
423
424 #[test]
425 fn truecolor_content_uses_row_tint_and_keeps_default_foreground() {
426 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
427 let add = style_content(DiffLineType::Insert, ctx);
428 let del = style_content(DiffLineType::Delete, ctx);
429 assert_eq!(add.fg, None);
430 assert_eq!(del.fg, None);
431 assert_eq!(add.bg, Some(RatatuiColor::Rgb(20, 58, 45)));
432 assert_eq!(del.bg, Some(RatatuiColor::Rgb(70, 38, 42)));
433 let sign = style_sign(DiffLineType::Insert, ctx);
434 assert_eq!(sign.fg, Some(RatatuiColor::Rgb(85, 255, 85)));
435 assert_eq!(sign.bg, add.bg);
436 }
437
438 #[test]
439 fn sign_style_dark_uses_light_green_and_custom_red_with_bold_on_row_bg() {
440 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
441 let add_sign = style_sign(DiffLineType::Insert, ctx);
442 let del_sign = style_sign(DiffLineType::Delete, ctx);
443 assert_eq!(add_sign.fg, Some(RatatuiColor::Rgb(85, 255, 85)));
444 assert_eq!(del_sign.fg, Some(RatatuiColor::Rgb(255, 180, 180)));
445 assert!(add_sign.add_modifier.contains(Modifier::BOLD));
446 assert!(del_sign.add_modifier.contains(Modifier::BOLD));
447 assert_eq!(add_sign.bg, Some(RatatuiColor::Rgb(20, 58, 45)));
448 assert_eq!(del_sign.bg, Some(RatatuiColor::Rgb(70, 38, 42)));
449 }
450
451 #[test]
452 fn sign_style_light_stays_bright_bold_on_row_bg() {
453 let ctx = test_style_context(DiffTheme::Light, DiffColorLevel::TrueColor);
454 let add_sign = style_sign(DiffLineType::Insert, ctx);
455 let del_sign = style_sign(DiffLineType::Delete, ctx);
456 assert_eq!(add_sign.fg, Some(RatatuiColor::Rgb(0, 92, 43)));
457 assert_eq!(del_sign.fg, Some(RatatuiColor::Rgb(140, 20, 25)));
458 assert!(add_sign.add_modifier.contains(Modifier::BOLD));
459 assert!(!add_sign.add_modifier.contains(Modifier::DIM));
460 assert!(!del_sign.add_modifier.contains(Modifier::DIM));
461 assert_eq!(add_sign.bg, Some(RatatuiColor::Rgb(218, 246, 225)));
462 assert_eq!(del_sign.bg, Some(RatatuiColor::Rgb(255, 224, 224)));
463 }
464
465 #[test]
466 fn hunk_header_uses_cyan_bold_without_background() {
467 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
468 let style = style_hunk_header(ctx);
469 assert_eq!(style.fg, Some(RatatuiColor::Cyan));
470 assert!(style.add_modifier.contains(Modifier::BOLD));
471 assert_eq!(style.bg, None);
472 }
473
474 #[test]
475 fn file_headers_use_red_green_bold_without_background() {
476 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
477 let old = style_file_header_old(ctx);
478 let new = style_file_header_new(ctx);
479 assert!(old.add_modifier.contains(Modifier::BOLD));
480 assert!(new.add_modifier.contains(Modifier::BOLD));
481 assert_eq!(old.bg, None);
482 assert_eq!(new.bg, None);
483 assert_ne!(old.fg, new.fg);
484 }
485
486 #[test]
487 fn line_backgrounds_use_scope_colors_when_available() {
488 let style_context = diff_render_style_context_for(
489 DiffTheme::Dark,
490 DiffColorLevel::TrueColor,
491 DiffScopeBackgroundRgbs {
492 inserted: Some((1, 2, 3)),
493 deleted: Some((4, 5, 6)),
494 },
495 );
496
497 assert_eq!(style_line_bg(DiffLineType::Insert, style_context).bg, Some(RatatuiColor::Rgb(1, 2, 3)));
498 assert_eq!(style_line_bg(DiffLineType::Delete, style_context).bg, Some(RatatuiColor::Rgb(4, 5, 6)));
499 }
500
501 #[test]
502 fn ansi256_maps_row_and_word_backgrounds() {
503 let style_context = diff_render_style_context_for(
504 DiffTheme::Dark,
505 DiffColorLevel::Ansi256,
506 DiffScopeBackgroundRgbs { inserted: Some((0, 95, 0)), deleted: None },
507 );
508 assert!(style_line_bg(DiffLineType::Insert, style_context).bg.is_some());
509 assert!(style_line_bg(DiffLineType::Delete, style_context).bg.is_some());
510 assert!(style_word_bg(DiffLineType::Insert, style_context).bg.is_some());
511 }
512
513 #[test]
514 fn ansi16_disables_line_backgrounds_even_with_scope_colors() {
515 let style_context = diff_render_style_context_for(
516 DiffTheme::Dark,
517 DiffColorLevel::Ansi16,
518 DiffScopeBackgroundRgbs {
519 inserted: Some((8, 9, 10)),
520 deleted: Some((11, 12, 13)),
521 },
522 );
523 assert_eq!(style_line_bg(DiffLineType::Insert, style_context), RatatuiStyle::default());
524 assert_eq!(style_line_bg(DiffLineType::Delete, style_context), RatatuiStyle::default());
525 }
526
527 #[test]
528 fn ansi16_content_has_no_background() {
529 let style_context =
530 diff_render_style_context_for(DiffTheme::Dark, DiffColorLevel::Ansi16, DiffScopeBackgroundRgbs::default());
531 let add = style_content(DiffLineType::Insert, style_context);
532 let del = style_content(DiffLineType::Delete, style_context);
533 assert_eq!(add.fg, Some(RatatuiColor::LightGreen));
534 assert_eq!(add.bg, None);
535 assert_eq!(del.fg, Some(RatatuiColor::LightRed));
536 assert_eq!(del.bg, None);
537 }
538
539 #[test]
540 fn partial_scope_override_keeps_palette_for_missing_side() {
541 let style_context = diff_render_style_context_for(
542 DiffTheme::Dark,
543 DiffColorLevel::TrueColor,
544 DiffScopeBackgroundRgbs { inserted: Some((12, 34, 56)), deleted: None },
545 );
546 assert_eq!(style_line_bg(DiffLineType::Insert, style_context).bg, Some(RatatuiColor::Rgb(12, 34, 56)));
547 assert_eq!(style_content(DiffLineType::Insert, style_context).bg, Some(RatatuiColor::Rgb(12, 34, 56)));
548 assert_eq!(style_content(DiffLineType::Delete, style_context).bg, Some(RatatuiColor::Rgb(70, 38, 42)));
549 }
550
551 #[test]
552 fn word_background_is_stronger_and_ansi16_stays_foreground_only() {
553 let ctx = test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor);
554 assert_eq!(style_word_bg(DiffLineType::Insert, ctx).bg, Some(RatatuiColor::Rgb(36, 100, 70)));
555 assert_eq!(style_word_bg(DiffLineType::Delete, ctx).bg, Some(RatatuiColor::Rgb(140, 52, 58)));
556
557 let ansi16 = test_style_context(DiffTheme::Dark, DiffColorLevel::Ansi16);
558 assert_eq!(style_line_bg(DiffLineType::Insert, ansi16).bg, None);
559 assert_eq!(style_word_bg(DiffLineType::Insert, ansi16).bg, None);
560 assert_eq!(style_content_ansi(DiffLineType::Insert, ansi16).get_bg_color(), None);
561 assert_eq!(style_sign_ansi(DiffLineType::Insert, ansi16).get_bg_color(), None);
562 }
563
564 fn relative_luminance(color: (u8, u8, u8)) -> f32 {
565 let linear = |component: u8| {
566 let value = f32::from(component) / 255.0;
567 if value <= 0.04045 {
568 value / 12.92
569 } else {
570 ((value + 0.055) / 1.055).powf(2.4)
571 }
572 };
573 let [red, green, blue] = [linear(color.0), linear(color.1), linear(color.2)];
574 0.2126 * red + 0.7152 * green + 0.0722 * blue
575 }
576
577 fn contrast_ratio(foreground: (u8, u8, u8), background: (u8, u8, u8)) -> f32 {
578 let foreground = relative_luminance(foreground);
579 let background = relative_luminance(background);
580 (foreground.max(background) + 0.05) / (foreground.min(background) + 0.05)
581 }
582
583 fn test_color_rgb(color: RatatuiColor) -> (u8, u8, u8) {
584 match color {
585 RatatuiColor::Rgb(red, green, blue) => (red, green, blue),
586 RatatuiColor::LightGreen => (85, 255, 85),
587 other => panic!("test only covers RGB and bright-green marker colors, got {other:?}"),
588 }
589 }
590
591 #[test]
592 fn marker_foregrounds_meet_wcag_against_both_diff_background_levels() {
593 for theme in [DiffTheme::Dark, DiffTheme::Light] {
594 let context = test_style_context(theme, DiffColorLevel::TrueColor);
595 for kind in [DiffLineType::Insert, DiffLineType::Delete] {
596 let foreground = test_color_rgb(style_sign(kind, context).fg.expect("marker foreground"));
597 let row = test_color_rgb(style_line_bg(kind, context).bg.expect("row background"));
598 let word = test_color_rgb(style_word_bg(kind, context).bg.expect("word background"));
599 let gutter = test_color_rgb(style_gutter(kind, context).fg.expect("gutter foreground"));
600 assert!(
601 contrast_ratio(foreground, row) >= 4.5,
602 "{theme:?} {kind:?} marker fails row contrast: {foreground:?} on {row:?}"
603 );
604 assert!(
605 contrast_ratio(foreground, word) >= 4.5,
606 "{theme:?} {kind:?} marker fails word contrast: {foreground:?} on {word:?}"
607 );
608 assert!(
609 contrast_ratio(gutter, row) >= 4.5,
610 "{theme:?} {kind:?} gutter fails row contrast: {gutter:?} on {row:?}"
611 );
612 }
613 }
614 }
615}