editor: Add intelligent bracket colorization (#51580)

Closes #50210

Bracket colorization currently runs theme accents through
`ensure_minimum_contrast`, which can significantly alter authored
palette colors in order to satisfy a single foreground-vs-background
target. In practice, that makes rainbow brackets drift away from the
theme colors they are supposed to reflect.

This PR keeps bracket colorization as a single automatic behavior, but
changes how the palette is derived:

- preserve authored accent colors when they already work
- adjust only the specific accents that are too weak against the editor
background
- only reorder the palette when adjacent bracket levels are still too
similar after that

The implementation is staged because bracket colorization has to handle
two separate problems:

- **Bracket-vs-Background Readability**: is an individual bracket
visible against the editor background?
- **Adjacent Bracket Separation**: can you still distinguish one nesting
level from the next?

Those two constraints are also why the implementation cannot simply
defer to whatever accent colors the theme provides. Some themes,
including the bundled One and Ayu, do not define explicit accents at
all, so bracket colorization falls back to Zed's shared default accent
palette. That palette was not designed with any particular theme's
background in mind, which makes the background contrast check especially
important for those cases.

That is why the previous `ensure_minimum_contrast(..., 55.0)` path is
not enough on its own. It addresses foreground-vs-background
readability, but if lightness-only adjustment cannot satisfy that target
it escalates to saturation reduction and then black/white fallback.

This PR instead keeps the bracket-specific background fix bounded and
lightness-only in OKLCH, so the resulting color stays much closer to the
authored accent.

### How it works

- **Background Contrast Correction**: accents that fall below the APCA
floor against the editor background are adjusted by changing OKLCH
lightness only, preserving hue and chroma
- **Adjacency Correction**: if the resulting palette is still too weak
between neighboring nesting levels, the palette is reordered
conservatively to improve adjacent separation

That ordering matters. Doing the reorder first can easily be undone by
the background-fix step.

### Color measurement

- background contrast uses APCA, via the existing `apca_contrast` path
- adjacent bracket separation uses OKLab Euclidean distance

APCA is the right tool for bracket-vs-background readability, but it is
not designed to compare two foreground colors against each other. OKLab
distance is a better fit for detecting when neighboring bracket levels
are perceptually too close.

### Thresholds

- background APCA floor: `30` on dark themes, `35` on light themes
- adjacent OKLab target: `0.08` on dark themes, `0.10` on light themes
- light-theme reorder tolerance band: reorder only triggers below
`0.095`, avoiding threshold-artifact reordering on palettes that are
visually fine but narrowly miss `0.10`

### Validation

- `cargo test -p editor bracket_colorization`
- manual comparison on bundled One, Ayu, and Gruvbox theme families

Release Notes:

- Improved bracket colorization by preserving theme accent colors when
possible and applying targeted contrast fixes only when needed.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Kai KozlovandKirill Bulatov authored and GitHub committed 2026-07-01 17:36:23 +00:00
1 parent 550ddc9405
commit bcd54d0adf
4 files changed
+530 -92

No files matched your search

+444 -91
View File
@@ -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<Editor>) {
@@ -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::<HashMap<Range<text::Anchor>, HashSet<Range<BufferRow>>>>();
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<usize, Vec<Range<Anchor>>> =
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::<Vec<_>>();
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<Hsla> {
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<Hsla> {
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::<String>();
let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::<String>();
@@ -477,8 +817,8 @@ where
let v: Vec<String> = 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«2<String>2» = 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: Result<Option<Foo<'_, «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)
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),
);
@@ -585,9 +925,9 @@ fn process_data«1()1» «1{
let map: Result<Option<Foo«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),
"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: Result<Option«2<Foo«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)
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«2<Option«3<Foo«4<'_, «5()5»>4»>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«1<Vec«2<«1()1»>2»>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}2<Vec\u{00ab}3<\u{00ab}4()4\u{00bb}>3\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),
);
+8 -1
View File
@@ -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,
})
}
+76
View File
@@ -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()
}
+2
View File
@@ -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::*;