1use std::sync::Arc;
2
3use gpui::{HighlightStyle, Hsla, Pixels, Rems, StyleRefinement, px, rems};
4
5use crate::ColorTokens;
6
7#[derive(Clone)]
14pub struct TextViewStyle {
15 foreground: Hsla,
16 muted_foreground: Hsla,
17 link: Hsla,
18 selection: Hsla,
19 code_background: Hsla,
20 border: Hsla,
21 paragraph_gap: Rems,
22 heading_base_font_size: Pixels,
23 heading_font_size: Option<Arc<dyn Fn(u8, Pixels) -> Pixels + Send + Sync + 'static>>,
24 code_block: StyleRefinement,
25 table: StyleRefinement,
26 table_head: StyleRefinement,
27 table_cell: StyleRefinement,
28 inline_code: HighlightStyle,
29 is_dark: bool,
30}
31
32impl PartialEq for TextViewStyle {
33 fn eq(&self, other: &Self) -> bool {
34 self.paragraph_gap == other.paragraph_gap
35 && self.foreground == other.foreground
36 && self.muted_foreground == other.muted_foreground
37 && self.link == other.link
38 && self.selection == other.selection
39 && self.code_background == other.code_background
40 && self.border == other.border
41 && self.heading_base_font_size == other.heading_base_font_size
42 && match (&self.heading_font_size, &other.heading_font_size) {
43 (Some(left), Some(right)) => (1..=6).all(|level| {
44 left(level, self.heading_base_font_size)
45 == right(level, other.heading_base_font_size)
46 }),
47 (None, None) => true,
48 _ => false,
49 }
50 && self.code_block == other.code_block
51 && self.table == other.table
52 && self.table_head == other.table_head
53 && self.table_cell == other.table_cell
54 && self.inline_code == other.inline_code
55 && self.is_dark == other.is_dark
56 }
57}
58
59impl Default for TextViewStyle {
60 fn default() -> Self {
61 Self::from_colors(&ColorTokens::light(), false)
62 }
63}
64
65impl TextViewStyle {
66 pub fn from_theme(theme: &crate::Theme) -> Self {
68 Self::from_colors(
69 &theme.tokens.colors,
70 theme.appearance == crate::ThemeAppearance::Dark,
71 )
72 }
73
74 fn from_colors(colors: &ColorTokens, is_dark: bool) -> Self {
80 Self {
81 foreground: colors.foreground,
82 muted_foreground: colors.muted_foreground,
83 link: colors.primary,
84 selection: colors.selection,
85 code_background: colors.accent,
86 border: colors.border,
87 paragraph_gap: rems(1.),
88 heading_base_font_size: px(14.),
89 heading_font_size: None,
90 code_block: StyleRefinement::default(),
91 table: StyleRefinement::default(),
92 table_head: StyleRefinement::default(),
93 table_cell: StyleRefinement::default(),
94 inline_code: HighlightStyle {
95 background_color: Some(colors.accent),
96 ..Default::default()
97 },
98 is_dark,
99 }
100 }
101
102 pub fn with_foreground(mut self, color: Hsla) -> Self {
104 self.foreground = color;
105 self
106 }
107
108 pub fn with_muted_foreground(mut self, color: Hsla) -> Self {
110 self.muted_foreground = color;
111 self
112 }
113
114 pub fn with_link(mut self, color: Hsla) -> Self {
116 self.link = color;
117 self
118 }
119
120 pub fn with_selection(mut self, color: Hsla) -> Self {
125 self.selection = color;
126 self
127 }
128
129 pub fn with_code_background(mut self, color: Hsla) -> Self {
131 self.code_background = color;
132 self
133 }
134
135 pub fn with_border(mut self, color: Hsla) -> Self {
137 self.border = color;
138 self
139 }
140
141 pub fn with_paragraph_gap(mut self, gap: Rems) -> Self {
143 self.paragraph_gap = gap;
144 self
145 }
146
147 pub fn with_heading_base_font_size(mut self, size: Pixels) -> Self {
149 self.heading_base_font_size = size;
150 self
151 }
152
153 pub fn with_heading_font_size<F>(mut self, f: F) -> Self
158 where
159 F: Fn(u8, Pixels) -> Pixels + Send + Sync + 'static,
160 {
161 self.heading_font_size = Some(Arc::new(f));
162 self
163 }
164
165 pub fn with_code_block(mut self, style: StyleRefinement) -> Self {
167 self.code_block = style;
168 self
169 }
170
171 pub fn with_inline_code(mut self, style: HighlightStyle) -> Self {
176 self.inline_code = style;
177 self
178 }
179
180 pub fn with_table(mut self, style: StyleRefinement) -> Self {
188 self.table = style;
189 self
190 }
191
192 pub fn with_table_head(mut self, style: StyleRefinement) -> Self {
195 self.table_head = style;
196 self
197 }
198
199 pub fn with_table_cell(mut self, style: StyleRefinement) -> Self {
205 self.table_cell = style;
206 self
207 }
208
209 pub fn with_dark(mut self, is_dark: bool) -> Self {
211 self.is_dark = is_dark;
212 self
213 }
214
215 pub fn foreground(&self) -> Hsla {
217 self.foreground
218 }
219
220 pub fn muted_foreground(&self) -> Hsla {
222 self.muted_foreground
223 }
224
225 pub fn link(&self) -> Hsla {
227 self.link
228 }
229
230 pub fn selection(&self) -> Hsla {
232 self.selection
233 }
234
235 pub fn code_background(&self) -> Hsla {
237 self.code_background
238 }
239
240 pub fn border(&self) -> Hsla {
242 self.border
243 }
244
245 pub fn paragraph_gap(&self) -> Rems {
247 self.paragraph_gap
248 }
249
250 pub fn heading_base_font_size(&self) -> Pixels {
252 self.heading_base_font_size
253 }
254
255 pub fn heading_font_size(&self, level: u8) -> Option<Pixels> {
261 self.heading_font_size
262 .as_ref()
263 .map(|f| f(level, self.heading_base_font_size))
264 }
265
266 pub fn code_block(&self) -> &StyleRefinement {
268 &self.code_block
269 }
270
271 pub fn table(&self) -> &StyleRefinement {
273 &self.table
274 }
275
276 pub fn table_head(&self) -> &StyleRefinement {
278 &self.table_head
279 }
280
281 pub fn table_cell(&self) -> &StyleRefinement {
283 &self.table_cell
284 }
285
286 pub fn inline_code(&self) -> HighlightStyle {
289 self.inline_code
290 }
291
292 pub fn is_dark(&self) -> bool {
294 self.is_dark
295 }
296
297 pub(crate) fn inline_code_highlight(&self) -> HighlightStyle {
300 let mut style = self.inline_code;
301 if style.background_color.is_none() {
302 style.background_color = Some(self.code_background);
303 }
304 style
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn selection_layout_fingerprint_covers_callback_table_and_theme_fields() {
314 let base = TextViewStyle::default();
315 let heading = base.clone().with_heading_font_size(|_, size| size);
316 assert!(heading == base.clone().with_heading_font_size(|_, size| size));
317 assert!(heading != base.clone().with_heading_font_size(|_, size| size * 2.));
318
319 let mut table = StyleRefinement::default();
320 table.text.white_space = Some(gpui::WhiteSpace::Nowrap);
321 assert!(base != base.clone().with_table_cell(table));
322
323 assert!(base != base.clone().with_dark(true));
324 }
325
326 #[test]
327 fn cloning_preserves_the_same_heading_callback_fingerprint() {
328 let style = TextViewStyle::default().with_heading_font_size(|_, size| size);
329 assert!(style == style.clone());
330 }
331
332 #[test]
333 fn default_style_is_readable_without_an_application_theme() {
334 let style = TextViewStyle::default();
335
336 assert_eq!(style.foreground().a, 1.0);
337 assert_eq!(style.link().a, 1.0);
338 assert!(style.selection().a > 0.0);
339 assert!(style.inline_code().background_color.is_some());
340 assert!(style.code_background().a > 0.0);
341 assert!(style.border().a > 0.0);
342 assert_eq!(style.code_block().corner_radii.top_left, None);
343 assert_eq!(style.code_block().corner_radii.top_right, None);
344 assert_eq!(style.code_block().corner_radii.bottom_left, None);
345 assert_eq!(style.code_block().corner_radii.bottom_right, None);
346 }
347
348 #[test]
349 fn heading_font_size_resolves_through_the_installed_callback() {
350 let style = TextViewStyle::default();
351 assert_eq!(style.heading_font_size(1), None);
352
353 let style = style.with_heading_font_size(|level, base| base * (7. - level as f32));
354 assert_eq!(style.heading_font_size(1), Some(px(14.) * 6.));
355 assert_eq!(style.heading_font_size(6), Some(px(14.)));
356 }
357
358 #[test]
359 fn inline_code_falls_back_to_the_code_background() {
360 let style = TextViewStyle::default()
361 .with_code_background(gpui::rgb(0x123456).into())
362 .with_inline_code(HighlightStyle::default());
363
364 assert_eq!(
365 style.inline_code_highlight().background_color,
366 Some(gpui::rgb(0x123456).into())
367 );
368 }
369
370 #[test]
371 fn from_theme_maps_base_semantic_tokens() {
372 let mut theme = crate::Theme::default();
373 theme.tokens.colors.foreground = gpui::rgb(0x112233).into();
374 theme.tokens.colors.muted_foreground = gpui::rgb(0x445566).into();
375 theme.tokens.colors.primary = gpui::rgb(0x3366ff).into();
376 theme.tokens.colors.accent = gpui::rgb(0xddeeff).into();
377 theme.tokens.colors.border = gpui::rgb(0x778899).into();
378 theme.tokens.colors.selection = gpui::rgb(0x55a0fc).into();
379
380 let style = TextViewStyle::from_theme(&theme);
381 assert_eq!(style.foreground(), theme.tokens.colors.foreground);
382 assert_eq!(style.link(), theme.tokens.colors.primary);
383 assert_eq!(style.selection(), theme.tokens.colors.selection);
384 assert_eq!(style.code_background(), theme.tokens.colors.accent);
385 assert_eq!(style.border(), theme.tokens.colors.border);
386 }
387}