use std::sync::Arc;
use gpui::{HighlightStyle, Hsla, Rems, StyleRefinement, rems};
use crate::ColorTokens;
#[derive(Clone)]
pub struct TextViewStyle {
foreground: Hsla,
muted_foreground: Hsla,
link: Hsla,
selection: Hsla,
code_background: Hsla,
border: Hsla,
paragraph_gap: Rems,
heading: Arc<dyn Fn(u8) -> StyleRefinement + Send + Sync + 'static>,
code_block: StyleRefinement,
table: StyleRefinement,
table_head: StyleRefinement,
table_cell: StyleRefinement,
inline_code: HighlightStyle,
table_background: Option<Hsla>,
is_dark: bool,
}
impl PartialEq for TextViewStyle {
fn eq(&self, other: &Self) -> bool {
self.paragraph_gap == other.paragraph_gap
&& self.foreground == other.foreground
&& self.muted_foreground == other.muted_foreground
&& self.link == other.link
&& self.selection == other.selection
&& self.code_background == other.code_background
&& self.border == other.border
&& (1..=6).all(|level| (self.heading)(level) == (other.heading)(level))
&& self.code_block == other.code_block
&& self.table == other.table
&& self.table_head == other.table_head
&& self.table_cell == other.table_cell
&& self.inline_code == other.inline_code
&& self.table_background == other.table_background
&& self.is_dark == other.is_dark
}
}
impl Default for TextViewStyle {
fn default() -> Self {
Self::from_colors(&ColorTokens::light(), false)
}
}
impl TextViewStyle {
pub fn from_theme(theme: &crate::Theme) -> Self {
Self::from_colors(
&theme.tokens.colors,
theme.appearance == crate::ThemeAppearance::Dark,
)
}
fn from_colors(colors: &ColorTokens, is_dark: bool) -> Self {
Self {
foreground: colors.foreground,
muted_foreground: colors.muted_foreground,
link: colors.primary,
selection: colors.selection,
code_background: colors.accent,
border: colors.border,
paragraph_gap: rems(0.75),
heading: Arc::new(|_| StyleRefinement::default()),
code_block: StyleRefinement::default(),
table: StyleRefinement::default(),
table_head: StyleRefinement::default(),
table_cell: StyleRefinement::default(),
inline_code: HighlightStyle {
background_color: Some(colors.accent),
..Default::default()
},
table_background: None,
is_dark,
}
}
pub fn with_foreground(mut self, color: Hsla) -> Self {
self.foreground = color;
self
}
pub fn with_muted_foreground(mut self, color: Hsla) -> Self {
self.muted_foreground = color;
self
}
pub fn with_link(mut self, color: Hsla) -> Self {
self.link = color;
self
}
pub fn with_selection(mut self, color: Hsla) -> Self {
self.selection = color;
self
}
pub fn with_code_background(mut self, color: Hsla) -> Self {
self.code_background = color;
self
}
pub fn with_border(mut self, color: Hsla) -> Self {
self.border = color;
self
}
pub fn with_paragraph_gap(mut self, gap: Rems) -> Self {
self.paragraph_gap = gap;
self
}
pub fn with_heading<F>(mut self, heading: F) -> Self
where
F: Fn(u8) -> StyleRefinement + Send + Sync + 'static,
{
self.heading = Arc::new(heading);
self
}
pub fn with_code_block(mut self, style: StyleRefinement) -> Self {
self.code_block = style;
self
}
pub fn with_inline_code(mut self, style: HighlightStyle) -> Self {
self.inline_code = style;
self
}
pub fn with_table(mut self, style: StyleRefinement) -> Self {
self.table = style;
self
}
pub fn with_table_head(mut self, style: StyleRefinement) -> Self {
self.table_head = style;
self
}
pub fn with_table_cell(mut self, style: StyleRefinement) -> Self {
self.table_cell = style;
self
}
pub fn with_dark(mut self, is_dark: bool) -> Self {
self.is_dark = is_dark;
self
}
pub fn foreground(&self) -> Hsla {
self.foreground
}
pub fn muted_foreground(&self) -> Hsla {
self.muted_foreground
}
pub fn link(&self) -> Hsla {
self.link
}
pub fn selection(&self) -> Hsla {
self.selection
}
pub fn code_background(&self) -> Hsla {
self.code_background
}
pub fn border(&self) -> Hsla {
self.border
}
pub fn paragraph_gap(&self) -> Rems {
self.paragraph_gap
}
pub fn heading(&self, level: u8) -> StyleRefinement {
(self.heading)(level)
}
pub fn code_block(&self) -> &StyleRefinement {
&self.code_block
}
pub fn table(&self) -> &StyleRefinement {
&self.table
}
pub fn table_head(&self) -> &StyleRefinement {
&self.table_head
}
pub fn table_cell(&self) -> &StyleRefinement {
&self.table_cell
}
pub fn inline_code(&self) -> HighlightStyle {
self.inline_code
}
pub fn is_dark(&self) -> bool {
self.is_dark
}
pub(crate) fn table_background(&self) -> Option<Hsla> {
self.table_background
}
pub(crate) fn inline_code_highlight(&self) -> HighlightStyle {
let mut style = self.inline_code;
if style.background_color.is_none() {
style.background_color = Some(self.code_background);
}
style
}
pub(crate) fn on_text_color(&self, color: Hsla) -> Self {
let style = self.clone().with_foreground(color);
if !self.is_inverted_by(color) {
return style;
}
let code_background = color.opacity(0.12);
let mut table_head = self.table_head.clone();
table_head.background = Some(code_background.into());
table_head.text.color = Some(color);
let mut style = style
.with_muted_foreground(color.opacity(0.7))
.with_link(color)
.with_selection(color.opacity(0.25))
.with_code_background(code_background)
.with_border(color.opacity(0.2))
.with_inline_code(HighlightStyle {
background_color: Some(code_background),
..self.inline_code
})
.with_table_head(table_head)
.with_dark(!self.is_dark);
style.table_background = Some(gpui::transparent_black());
style
}
pub(crate) fn is_inverted_by(&self, color: Hsla) -> bool {
const INVERTED_LIGHTNESS_GAP: f32 = 0.6;
(oklab_lightness(color) - oklab_lightness(self.foreground)).abs() > INVERTED_LIGHTNESS_GAP
}
}
fn oklab_lightness(color: Hsla) -> f32 {
let rgb = color.to_rgb();
let linear = |c: f32| {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
};
let (r, g, b) = (linear(rgb.r), linear(rgb.g), linear(rgb.b));
let l = (0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt();
let m = (0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b).cbrt();
let s = (0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b).cbrt();
0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s
}
#[cfg(test)]
mod tests {
use gpui::{Styled as _, px};
use super::*;
#[test]
fn selection_layout_fingerprint_covers_callback_table_and_theme_fields() {
let base = TextViewStyle::default();
let heading = base
.clone()
.with_heading(|_| StyleRefinement::default().text_size(px(14.)));
assert!(
heading
== base
.clone()
.with_heading(|_| StyleRefinement::default().text_size(px(14.)))
);
assert!(
heading
!= base
.clone()
.with_heading(|_| StyleRefinement::default().text_size(px(28.)))
);
let mut table = StyleRefinement::default();
table.text.white_space = Some(gpui::WhiteSpace::Nowrap);
assert!(base != base.clone().with_table_cell(table));
assert!(base != base.clone().with_dark(true));
}
#[test]
fn cloning_preserves_the_same_heading_callback_fingerprint() {
let style = TextViewStyle::default()
.with_heading(|_| StyleRefinement::default().text_size(px(14.)));
assert!(style == style.clone());
}
#[test]
fn default_style_is_readable_without_an_application_theme() {
let style = TextViewStyle::default();
assert_eq!(style.foreground().a, 1.0);
assert_eq!(style.link().a, 1.0);
assert!(style.selection().a > 0.0);
assert!(style.inline_code().background_color.is_some());
assert!(style.code_background().a > 0.0);
assert!(style.border().a > 0.0);
assert_eq!(style.code_block().corner_radii.top_left, None);
assert_eq!(style.code_block().corner_radii.top_right, None);
assert_eq!(style.code_block().corner_radii.bottom_left, None);
assert_eq!(style.code_block().corner_radii.bottom_right, None);
}
#[test]
fn heading_refinement_defaults_empty_and_resolves_by_level() {
let base = TextViewStyle::default();
assert_eq!(base.heading(1), StyleRefinement::default());
let style = base.clone().with_heading(|level| match level {
1 => StyleRefinement::default().pt(rems(1.)).pb(rems(0.5)),
_ => StyleRefinement::default().pb(rems(0.25)),
});
assert_eq!(
style.heading(1),
StyleRefinement::default().pt(rems(1.)).pb(rems(0.5))
);
assert_eq!(style.heading(2), StyleRefinement::default().pb(rems(0.25)));
assert!(style != base);
}
#[test]
fn inline_code_falls_back_to_the_code_background() {
let style = TextViewStyle::default()
.with_code_background(gpui::rgb(0x123456).into())
.with_inline_code(HighlightStyle::default());
assert_eq!(
style.inline_code_highlight().background_color,
Some(gpui::rgb(0x123456).into())
);
}
#[test]
fn text_color_of_a_matching_surface_only_replaces_the_body_text() {
let style = TextViewStyle::from_colors(&ColorTokens::light(), false);
let destructive = ColorTokens::light().destructive;
let adapted = style.on_text_color(destructive);
assert_eq!(adapted.foreground(), destructive);
assert_eq!(adapted.link(), style.link());
assert_eq!(adapted.muted_foreground(), style.muted_foreground());
assert_eq!(adapted.code_background(), style.code_background());
assert_eq!(adapted.table_background(), None);
assert!(!adapted.is_dark());
}
#[test]
fn text_color_of_an_inverted_surface_derives_every_color_from_it() {
for (colors, is_dark) in [(ColorTokens::light(), false), (ColorTokens::dark(), true)] {
let style = TextViewStyle::from_colors(&colors, is_dark);
let text = colors.primary_foreground;
let adapted = style.on_text_color(text);
assert_eq!(adapted.foreground(), text);
assert_eq!(adapted.link(), text);
assert_eq!(adapted.muted_foreground(), text.opacity(0.7));
assert_eq!(adapted.code_background(), text.opacity(0.12));
assert_eq!(
adapted.inline_code_highlight().background_color,
Some(text.opacity(0.12))
);
assert_eq!(adapted.border(), text.opacity(0.2));
assert_eq!(adapted.selection(), text.opacity(0.25));
assert_eq!(adapted.table_background(), Some(gpui::transparent_black()));
assert_eq!(adapted.table_head().text.color, Some(text));
assert_eq!(adapted.is_dark(), !is_dark);
}
}
#[test]
fn from_theme_maps_base_semantic_tokens() {
let mut theme = crate::Theme::default();
theme.tokens.colors.foreground = gpui::rgb(0x112233).into();
theme.tokens.colors.muted_foreground = gpui::rgb(0x445566).into();
theme.tokens.colors.primary = gpui::rgb(0x3366ff).into();
theme.tokens.colors.accent = gpui::rgb(0xddeeff).into();
theme.tokens.colors.border = gpui::rgb(0x778899).into();
theme.tokens.colors.selection = gpui::rgb(0x55a0fc).into();
let style = TextViewStyle::from_theme(&theme);
assert_eq!(style.foreground(), theme.tokens.colors.foreground);
assert_eq!(style.link(), theme.tokens.colors.primary);
assert_eq!(style.selection(), theme.tokens.colors.selection);
assert_eq!(style.code_background(), theme.tokens.colors.accent);
assert_eq!(style.border(), theme.tokens.colors.border);
}
}