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,
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.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(1.),
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()
},
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 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
}
}
#[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 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);
}
}