diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs index 8c8c3a36e9..3b90d1e2bc 100644 --- a/crates/editor/src/bracket_colorization.rs +++ b/crates/editor/src/bracket_colorization.rs @@ -2,15 +2,18 @@ //! Uses tree-sitter queries from brackets.scm to capture bracket pairs, //! and theme accents to colorize those. +use std::cmp::Ordering; use std::ops::Range; +use std::sync::Arc; use crate::{Editor, HighlightKey}; use collections::{HashMap, HashSet}; -use gpui::{AppContext as _, Context, HighlightStyle}; +use gpui::{AppContext as _, Context, HighlightStyle, Hsla}; use language::{BufferRow, BufferSnapshot, language_settings::LanguageSettings}; use multi_buffer::{Anchor, BufferOffset, ExcerptRange, MultiBufferSnapshot}; use text::OffsetRangeExt as _; -use ui::{ActiveTheme, utils::ensure_minimum_contrast}; +use theme::{Appearance, Oklab, Oklch, hsla_to_oklab, hsla_to_oklch, oklch_to_hsla}; +use ui::utils::apca_contrast; impl Editor { pub(crate) fn colorize_brackets(&mut self, invalidate: bool, cx: &mut Context) { @@ -22,7 +25,10 @@ impl Editor { self.bracket_fetched_tree_sitter_chunks.clear(); } - let accents_count = cx.theme().accents().0.len(); + let Some(accent_data) = self.accent_data.as_ref() else { + return; + }; + let accents = accent_data.colors.0.clone(); let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); let visible_excerpts = self.visible_buffer_ranges(cx); @@ -52,7 +58,12 @@ impl Editor { }) .collect::, HashSet>>>(); + let accents_count = accents.len(); let bracket_matches_by_accent = cx.background_spawn(async move { + if accents_count == 0 { + return (HashMap::default(), fetched_tree_sitter_chunks); + } + let bracket_matches_by_accent: HashMap>> = excerpt_data.into_iter().fold( HashMap::default(), @@ -92,9 +103,6 @@ impl Editor { (bracket_matches_by_accent, fetched_tree_sitter_chunks) }); - let editor_background = cx.theme().colors().editor_background; - let accents = cx.theme().accents().clone(); - self.colorize_brackets_task = cx.spawn(async move |editor, cx| { if invalidate { editor @@ -115,11 +123,11 @@ impl Editor { .bracket_fetched_tree_sitter_chunks .extend(updated_chunks); for (accent_number, bracket_highlights) in bracket_matches_by_accent { - let bracket_color = accents.color_for_index(accent_number as u32); - let adjusted_color = - ensure_minimum_contrast(bracket_color, editor_background, 55.0); + let Some(&bracket_color) = accents.get(accent_number) else { + continue; + }; let style = HighlightStyle { - color: Some(adjusted_color), + color: Some(bracket_color), ..HighlightStyle::default() }; @@ -137,6 +145,194 @@ impl Editor { } } +const BACKGROUND_APCA_LIGHT: f32 = 35.0; +const BACKGROUND_APCA_DARK: f32 = 30.0; +const ADJACENT_OKLAB_LIGHT: f32 = 0.10; +const ADJACENT_OKLAB_DARK: f32 = 0.08; +const ADJACENT_OKLAB_LIGHT_INTERVENTION: f32 = 0.095; +const ADJACENT_OKLAB_DARK_INTERVENTION: f32 = 0.08; +const LIGHTNESS_CLAMP_MIN: f32 = 0.18; +const LIGHTNESS_CLAMP_MAX: f32 = 0.92; + +pub(crate) fn bracket_colorization_accents( + accents: &[Hsla], + appearance: Appearance, + background: Hsla, +) -> Arc<[Hsla]> { + let (intervention_distance, comfortable_distance, min_background_contrast) = match appearance { + Appearance::Light => ( + ADJACENT_OKLAB_LIGHT_INTERVENTION, + ADJACENT_OKLAB_LIGHT, + BACKGROUND_APCA_LIGHT, + ), + Appearance::Dark => ( + ADJACENT_OKLAB_DARK_INTERVENTION, + ADJACENT_OKLAB_DARK, + BACKGROUND_APCA_DARK, + ), + }; + let background_adjusted = accents + .iter() + .copied() + .map(|accent| adjust_color_for_background(accent, background, min_background_contrast)) + .collect::>(); + let adjusted_min_adj = min_adjacent_oklab_distance(&background_adjusted, background); + + if accents.len() < 3 || adjusted_min_adj >= intervention_distance { + return Arc::from(background_adjusted); + } + + let reordered = maximize_adjacent_separation(&background_adjusted, background); + if min_adjacent_oklab_distance(&reordered, background) >= comfortable_distance { + Arc::from(reordered) + } else { + Arc::from(background_adjusted) + } +} + +fn maximize_adjacent_separation(accents: &[Hsla], background: Hsla) -> Vec { + let Some((&first, rest)) = accents.split_first() else { + return Vec::new(); + }; + let mut remaining = rest.to_vec(); + let mut order = Vec::with_capacity(accents.len()); + order.push(first); + let mut last = first; + + while !remaining.is_empty() { + let Some((position, &next)) = + remaining + .iter() + .enumerate() + .max_by(|&(_, &left), &(_, &right)| { + compare_candidates(background, last, first, left, right) + }) + else { + break; + }; + remaining.swap_remove(position); + order.push(next); + last = next; + } + + order +} + +fn compare_candidates( + background: Hsla, + last: Hsla, + first: Hsla, + left: Hsla, + right: Hsla, +) -> Ordering { + adjacent_distance(last, left, background) + .partial_cmp(&adjacent_distance(last, right, background)) + .unwrap_or(Ordering::Equal) + .then_with(|| { + adjacent_distance(first, left, background) + .partial_cmp(&adjacent_distance(first, right, background)) + .unwrap_or(Ordering::Equal) + }) +} + +fn min_adjacent_oklab_distance(accents: &[Hsla], background: Hsla) -> f32 { + if accents.len() < 2 { + return f32::MAX; + } + accents + .iter() + .copied() + .zip(accents.iter().copied().cycle().skip(1)) + .take(accents.len()) + .map(|(left, right)| adjacent_distance(left, right, background)) + .fold(f32::MAX, f32::min) +} + +fn oklab_distance(left: Oklab, right: Oklab) -> f32 { + let dl = left.l - right.l; + let da = left.a - right.a; + let db = left.b - right.b; + (dl * dl + da * da + db * db).sqrt() +} + +fn adjacent_distance(left: Hsla, right: Hsla, background: Hsla) -> f32 { + oklab_distance( + hsla_to_oklab(background.blend(left)), + hsla_to_oklab(background.blend(right)), + ) +} + +fn adjust_color_for_background( + color: Hsla, + background: Hsla, + minimum_background_contrast: f32, +) -> Hsla { + if background_contrast(color, background) >= minimum_background_contrast { + return color; + } + + let original = hsla_to_oklab(color); + let darker_candidate = adjusted_lightness_candidate( + color, + background, + minimum_background_contrast, + LIGHTNESS_CLAMP_MIN, + ); + let lighter_candidate = adjusted_lightness_candidate( + color, + background, + minimum_background_contrast, + LIGHTNESS_CLAMP_MAX, + ); + + match (darker_candidate, lighter_candidate) { + (Some(darker_candidate), Some(lighter_candidate)) => { + let darker_distance = oklab_distance(original, hsla_to_oklab(darker_candidate)); + let lighter_distance = oklab_distance(original, hsla_to_oklab(lighter_candidate)); + if darker_distance <= lighter_distance { + darker_candidate + } else { + lighter_candidate + } + } + (Some(darker_candidate), None) => darker_candidate, + (None, Some(lighter_candidate)) => lighter_candidate, + (None, None) => color, + } +} + +fn adjusted_lightness_candidate( + color: Hsla, + background: Hsla, + minimum_background_contrast: f32, + target_lightness: f32, +) -> Option { + let original = hsla_to_oklch(color); + let lightness_delta = target_lightness - original.l; + + if lightness_delta.abs() <= f32::EPSILON { + return None; + } + + (1..=128).find_map(|step| { + let amount = step as f32 / 128.0; + let candidate = oklch_to_hsla( + Oklch { + l: (original.l + lightness_delta * amount).clamp(0.0, 1.0), + chroma: original.chroma, + hue: original.hue, + }, + color.a, + ); + (background_contrast(candidate, background) >= minimum_background_contrast) + .then_some(candidate) + }) +} + +fn background_contrast(foreground: Hsla, background: Hsla) -> f32 { + apca_contrast(background.blend(foreground), background).abs() +} + fn compute_bracket_ranges( multi_buffer_snapshot: &MultiBufferSnapshot, buffer_snapshot: &BufferSnapshot, @@ -201,7 +397,7 @@ mod tests { }; use collections::HashSet; use fs::FakeFs; - use gpui::UpdateGlobal as _; + use gpui::{Rgba, UpdateGlobal as _, hsla}; use indoc::indoc; use itertools::Itertools; use language::{Capability, markdown_lang}; @@ -213,10 +409,154 @@ mod tests { use serde_json::json; use settings::{AccentContent, SettingsStore}; use text::{Bias, OffsetRangeExt, ToOffset}; + use theme::Appearance; use theme_settings::ThemeStyleContent; + use ui::ActiveTheme; use util::{path, post_inc}; + fn light_editor_background() -> Hsla { + hsla(0.0, 0.0, 0.98, 1.0) + } + + fn dark_editor_background() -> Hsla { + hsla(0.0, 0.0, 0.12, 1.0) + } + + #[test] + fn test_auto_bracket_colorization_mode_reorders_weak_palette() { + let accents = vec![ + hsla(0.0, 1.0, 0.68, 1.0), + hsla(0.02, 1.0, 0.68, 1.0), + hsla(0.34, 1.0, 0.68, 1.0), + hsla(0.36, 1.0, 0.68, 1.0), + ]; + + let original_min_adj = min_adjacent_oklab_distance(&accents, dark_editor_background()); + let reordered = maximize_adjacent_separation(&accents, dark_editor_background()); + let reordered_min_adj = min_adjacent_oklab_distance(&reordered, dark_editor_background()); + + assert_ne!(reordered.as_slice(), accents.as_slice()); + assert!(reordered_min_adj > original_min_adj); + } + + #[test] + fn test_preserves_strong_palette() { + let accents = vec![ + hsla(0.0, 1.0, 0.78, 1.0), + hsla(0.16, 1.0, 0.78, 1.0), + hsla(0.33, 1.0, 0.78, 1.0), + hsla(0.66, 1.0, 0.78, 1.0), + ]; + + let palette = + bracket_colorization_accents(&accents, Appearance::Dark, dark_editor_background()); + + assert_eq!(palette.as_ref(), accents.as_slice()); + } + + #[test] + fn test_adjusts_background_failures_preserving_hue_and_chroma() { + let accents = vec![ + hsla(0.58, 1.0, 0.28, 1.0), + hsla(0.12, 1.0, 0.28, 1.0), + hsla(0.22, 0.9, 0.76, 1.0), + ]; + + let palette = + bracket_colorization_accents(&accents, Appearance::Light, light_editor_background()); + let original = hsla_to_oklch(accents[2]); + let adjusted = hsla_to_oklch(palette[2]); + + assert_ne!(palette.as_ref(), accents.as_slice()); + assert_eq!(palette.len(), accents.len()); + assert_eq!(palette[0], accents[0]); + assert_eq!(palette[1], accents[1]); + assert_ne!(palette[2], accents[2]); + assert!((original.chroma - adjusted.chroma).abs() < 0.0001); + assert!((original.hue - adjusted.hue).abs() < 0.001); + assert_ne!(original.l, adjusted.l); + assert!( + background_contrast(palette[2], light_editor_background()) >= BACKGROUND_APCA_LIGHT + ); + } + + #[test] + fn test_preserves_light_near_miss_palette() { + let accents = vec![ + Hsla::from(Rgba::try_from("#CC241D").expect("valid color")), + Hsla::from(Rgba::try_from("#98971A").expect("valid color")), + Hsla::from(Rgba::try_from("#D79921").expect("valid color")), + Hsla::from(Rgba::try_from("#458588").expect("valid color")), + Hsla::from(Rgba::try_from("#B16286").expect("valid color")), + Hsla::from(Rgba::try_from("#689D6A").expect("valid color")), + Hsla::from(Rgba::try_from("#D65D0E").expect("valid color")), + ]; + let background = Hsla::from(Rgba::try_from("#FBF1C7").expect("valid color")); + + let palette = bracket_colorization_accents(&accents, Appearance::Light, background); + let original_min_adj = min_adjacent_oklab_distance(&accents, background); + + assert_eq!(palette.as_ref(), accents.as_slice()); + assert!(original_min_adj < ADJACENT_OKLAB_LIGHT); + assert!(original_min_adj >= ADJACENT_OKLAB_LIGHT_INTERVENTION); + } + + #[test] + fn test_adjust_color_for_background_prefers_closest_passing_candidate() { + // Verify that when both darker and lighter candidates exist, + // we pick the one with minimum OKLab distance from the original. + let background = hsla(0.0, 0.0, 0.50, 1.0); + let min_contrast = 30.0; + let color = hsla(0.33, 0.7, 0.46, 1.0); + + assert!( + background_contrast(color, background) < min_contrast, + "test color must fail contrast check; got {}", + background_contrast(color, background) + ); + + let original = hsla_to_oklab(color); + let darker = + adjusted_lightness_candidate(color, background, min_contrast, LIGHTNESS_CLAMP_MIN) + .expect("fixture must produce a darker passing candidate"); + let lighter = + adjusted_lightness_candidate(color, background, min_contrast, LIGHTNESS_CLAMP_MAX) + .expect("fixture must produce a lighter passing candidate"); + + let darker_dist = oklab_distance(original, hsla_to_oklab(darker)); + let lighter_dist = oklab_distance(original, hsla_to_oklab(lighter)); + let expected = if darker_dist <= lighter_dist { + darker + } else { + lighter + }; + assert_eq!( + adjust_color_for_background(color, background, min_contrast), + expected + ); + } + + #[test] + fn test_background_adjustment_edge_cases() { + let color = hsla(0.22, 0.9, 0.76, 1.0); + let original_contrast = background_contrast(color, light_editor_background()); + assert!(original_contrast < 20.0); + let palette = + bracket_colorization_accents(&[color], Appearance::Light, light_editor_background()); + assert_ne!(palette.as_ref(), &[color][..]); + assert!( + background_contrast(palette[0], light_editor_background()) >= BACKGROUND_APCA_LIGHT + ); + + let impossible_color = hsla(0.58, 1.0, 0.47, 1.0); + let impossible_bg = hsla(0.0, 0.0, 0.50, 1.0); + assert_eq!( + adjust_color_for_background(impossible_color, impossible_bg, 200.0), + impossible_color + ); + } + #[gpui::test] async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) { init_test(cx, |language_settings| { @@ -291,11 +631,11 @@ where 2 }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) 6 hsla(95.00, 38.00%, 62.00%, 1.00) 7 hsla(39.00, 67.00%, 69.00%, 1.00) "#, @@ -327,7 +667,7 @@ where assert_eq!( "fn main«1()1» «1{}1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) ", editor .update(cx, |editor, window, cx| { @@ -356,7 +696,7 @@ where assert_eq!( r#"«1[LLM-powered features]1»«1(./ai/overview.md)1», «1[bring and configure your own API keys]1»«1(./ai/llm-providers.md#use-your-own-keys)1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) "#, &bracket_colors_markup(&mut cx), "All markdown brackets should be colored based on their depth" @@ -368,8 +708,8 @@ where assert_eq!( r#"«1{«2{}2»}1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) "#, &bracket_colors_markup(&mut cx), "All markdown brackets should be colored based on their depth, again" @@ -384,7 +724,7 @@ where cx.executor().run_until_parked(); assert_eq!( - "«1('')1»«1('')1»\n\n«1(«2('')2»)1»«1('')1»\n\n«1('')1»«1(«2('')2»)1»\n1 hsla(207.80, 16.20%, 69.19%, 1.00)\n2 hsla(29.00, 54.00%, 65.88%, 1.00)\n", + "«1('')1»«1('')1»\n\n«1(«2('')2»)1»«1('')1»\n\n«1('')1»«1(«2('')2»)1»\n1 hsla(207.80, 81.00%, 66.00%, 1.00)\n2 hsla(29.00, 54.00%, 61.00%, 1.00)\n", &bracket_colors_markup(&mut cx), "Markdown quote pairs should not interfere with parenthesis pairing" ); @@ -403,7 +743,7 @@ where .await; let rows = 100; - let footer = "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n"; + let footer = "1 hsla(207.80, 81.00%, 66.00%, 1.00)\n"; let simple_brackets = (0..rows).map(|_| "ˇ[]\n").collect::(); let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::(); @@ -477,8 +817,8 @@ where let v: Vec = vec!«2[]2»; }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) "#, &bracket_colors_markup(&mut cx), "Markdown does not colorize <> brackets" @@ -495,8 +835,8 @@ where let v: Vec«22» = vec!«2[]2»; }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) "#, &bracket_colors_markup(&mut cx), "After switching to Rust, <> brackets are now colorized" @@ -540,9 +880,9 @@ fn process_data«1()1» «1{ let map: Result< }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) "#}, &bracket_colors_markup(&mut cx), "Brackets without pairs should be ignored and not colored" @@ -563,9 +903,9 @@ fn process_data«1()1» «1{ let map: Result2» }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) "#}, &bracket_colors_markup(&mut cx), "When brackets start to get closed, inner brackets are re-colored based on their depth" @@ -608,10 +948,10 @@ fn process_data«1()1» «1{ let map: Result3»>2» }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) "#}, &bracket_colors_markup(&mut cx), ); @@ -631,11 +971,11 @@ fn process_data«1()1» «1{ let map: Result«24»>3», «3()3»>2» = unimplemented!«2()2»; }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#}, &bracket_colors_markup(&mut cx), ); @@ -687,11 +1027,11 @@ mod foo «1{ } }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#}, comment_lines, ), @@ -719,11 +1059,11 @@ mod foo «1{ }2» }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#}, comment_lines, ), @@ -750,11 +1090,11 @@ mod foo «1{ } «3{«4{}4»}3»}2»}1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#}, comment_lines, ), @@ -781,11 +1121,11 @@ mod foo «1{ } «3{«4{}4»}3»}2»}1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#}, comment_lines, ), @@ -842,11 +1182,11 @@ mod foo «1{ } {{}}}}1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#, comment_lines, ), @@ -1353,11 +1693,11 @@ mod foo «1{ }2» }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#,}, &editor_bracket_colors_markup(&editor_snapshot), "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \ @@ -1393,11 +1733,11 @@ mod foo «1{ }2» }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) -4 hsla(187.00, 47.00%, 59.22%, 1.00) -5 hsla(355.00, 65.00%, 75.94%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) +4 hsla(187.00, 47.00%, 55.00%, 1.00) +5 hsla(355.00, 65.00%, 65.00%, 1.00) "#,}, &editor_bracket_colors_markup(&editor_snapshot), ); @@ -1424,7 +1764,18 @@ mod foo «1{ let editor_snapshot = editor .update(cx, |editor, window, cx| editor.snapshot(window, cx)) .unwrap(); - assert_eq!( + let adjusted_palette = cx.update(|cx| { + bracket_colorization_accents( + &[ + Hsla::from(Rgba::try_from("#ff0000").expect("valid override accent")), + Hsla::from(Rgba::try_from("#0000ff").expect("valid override accent")), + ], + cx.theme().appearance, + cx.theme().colors().editor_background, + ) + }); + let expected_markup = format!( + "{}\n1 {}\n2 {}\n", indoc! {r#" @@ -1442,11 +1793,13 @@ mod foo «1{ let other_map: Option«12»>1» = None; }2» }1» - -1 hsla(0.00, 100.00%, 78.12%, 1.00) -2 hsla(240.00, 100.00%, 82.81%, 1.00) "#,}, - &editor_bracket_colors_markup(&editor_snapshot), + adjusted_palette[0], + adjusted_palette[1], + ); + assert_eq!( + expected_markup, + editor_bracket_colors_markup(&editor_snapshot), "After updating theme accents, the editor should update the bracket coloring" ); } @@ -1536,10 +1889,10 @@ mod foo «1{ " let other_map: Option\u{00ab}23\u{00bb}>2\u{00bb} = None;\n", "}1\u{00bb}\n", "\n", - "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n", - "2 hsla(29.00, 54.00%, 65.88%, 1.00)\n", - "3 hsla(286.00, 51.00%, 75.25%, 1.00)\n", - "4 hsla(187.00, 47.00%, 59.22%, 1.00)\n", + "1 hsla(207.80, 81.00%, 66.00%, 1.00)\n", + "2 hsla(29.00, 54.00%, 61.00%, 1.00)\n", + "3 hsla(286.00, 51.00%, 64.00%, 1.00)\n", + "4 hsla(187.00, 47.00%, 55.00%, 1.00)\n", ), &editor_bracket_colors_markup(&editor_snapshot), "Two close excerpts from the same buffer (within same tree-sitter chunk) should both have bracket colors" @@ -1592,9 +1945,9 @@ fn small_function«1()1» «1{ let x = «2(1, «3(2, 3)3»)2»; }1» -1 hsla(207.80, 16.20%, 69.19%, 1.00) -2 hsla(29.00, 54.00%, 65.88%, 1.00) -3 hsla(286.00, 51.00%, 75.25%, 1.00) +1 hsla(207.80, 81.00%, 66.00%, 1.00) +2 hsla(29.00, 54.00%, 61.00%, 1.00) +3 hsla(286.00, 51.00%, 64.00%, 1.00) "#,}, bracket_colors_markup(&mut cx), ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 12138cb7f9..70b4911e17 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9640,6 +9640,13 @@ impl Editor { let theme_settings = theme_settings::ThemeSettings::get_global(cx); let theme = cx.theme(); let accent_colors = theme.accents().clone(); + let editor_background = theme.colors().editor_background; + let auto_accent_colors = + AccentColors(crate::bracket_colorization::bracket_colorization_accents( + &accent_colors.0, + theme.appearance, + editor_background, + )); let accent_overrides = theme_settings .theme_overrides @@ -9659,7 +9666,7 @@ impl Editor { .collect(); Some(AccentData { - colors: accent_colors, + colors: auto_accent_colors, overrides: accent_overrides, }) } diff --git a/crates/theme/src/color_space.rs b/crates/theme/src/color_space.rs new file mode 100644 index 0000000000..125dfa6d9d --- /dev/null +++ b/crates/theme/src/color_space.rs @@ -0,0 +1,76 @@ +//! Conversions between gpui's [`Hsla`] and the OKLab / OKLCh perceptual color +//! spaces, backed by the `palette` crate. +//! +//! These are exposed so consumers can reason about perceptual color distance +//! (e.g. bracket colorization) without taking a direct dependency on `palette`. + +use gpui::{Hsla, Rgba}; +use palette::{ + FromColor, OklabHue, + rgb::{LinSrgba, Srgba}, +}; + +/// A color in the OKLab perceptual color space. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Oklab { + /// Perceptual lightness, in `0.0..=1.0`. + pub l: f32, + /// Green/red opponent axis. + pub a: f32, + /// Blue/yellow opponent axis. + pub b: f32, +} + +/// A color in the OKLCh perceptual color space (the cylindrical form of OKLab). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Oklch { + /// Perceptual lightness, in `0.0..=1.0`. + pub l: f32, + /// Chroma (colorfulness). + pub chroma: f32, + /// Hue, in degrees (`0.0..360.0`). + pub hue: f32, +} + +/// Converts an [`Hsla`] color into the OKLab color space. +pub fn hsla_to_oklab(color: Hsla) -> Oklab { + let oklab = palette::Oklab::from_color(hsla_to_linear(color)); + Oklab { + l: oklab.l, + a: oklab.a, + b: oklab.b, + } +} + +/// Converts an [`Hsla`] color into the OKLCh color space. +pub fn hsla_to_oklch(color: Hsla) -> Oklch { + let oklch = palette::Oklch::from_color(hsla_to_linear(color)); + Oklch { + l: oklch.l, + chroma: oklch.chroma, + hue: oklch.hue.into_positive_degrees(), + } +} + +/// Converts an [`Oklch`] color back into [`Hsla`], using `alpha` for the +/// resulting alpha channel. Channels outside the sRGB gamut are clamped. +pub fn oklch_to_hsla(color: Oklch, alpha: f32) -> Hsla { + let oklch = palette::Oklch { + l: color.l, + chroma: color.chroma, + hue: OklabHue::from_degrees(color.hue), + }; + let rgba: Srgba = Srgba::from_linear(LinSrgba::from_color(oklch)); + let (red, green, blue, _) = rgba.into_components(); + Hsla::from(Rgba { + r: red.clamp(0.0, 1.0), + g: green.clamp(0.0, 1.0), + b: blue.clamp(0.0, 1.0), + a: alpha, + }) +} + +fn hsla_to_linear(color: Hsla) -> LinSrgba { + let rgba = Rgba::from(color); + Srgba::new(rgba.r, rgba.g, rgba.b, rgba.a).into_linear() +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ab49877c6b..b973d8f25c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -8,6 +8,7 @@ //! //! A theme is a collection of colors used to build a consistent appearance for UI components across the application. +mod color_space; mod default_colors; mod fallback_themes; mod font_family_cache; @@ -30,6 +31,7 @@ use gpui::{ }; use serde::Deserialize; +pub use crate::color_space::*; pub use crate::default_colors::*; pub use crate::fallback_themes::{apply_status_color_defaults, apply_theme_color_defaults}; pub use crate::font_family_cache::*;