1use std::sync::Arc;
2
3use gpui::{HighlightStyle, Hsla, Rems, StyleRefinement, 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: Arc<dyn Fn(u8) -> StyleRefinement + Send + Sync + 'static>,
23 code_block: StyleRefinement,
24 table: StyleRefinement,
25 table_head: StyleRefinement,
26 table_cell: StyleRefinement,
27 inline_code: HighlightStyle,
28 is_dark: bool,
29}
30
31impl PartialEq for TextViewStyle {
32 fn eq(&self, other: &Self) -> bool {
33 self.paragraph_gap == other.paragraph_gap
34 && self.foreground == other.foreground
35 && self.muted_foreground == other.muted_foreground
36 && self.link == other.link
37 && self.selection == other.selection
38 && self.code_background == other.code_background
39 && self.border == other.border
40 && (1..=6).all(|level| (self.heading)(level) == (other.heading)(level))
41 && self.code_block == other.code_block
42 && self.table == other.table
43 && self.table_head == other.table_head
44 && self.table_cell == other.table_cell
45 && self.inline_code == other.inline_code
46 && self.is_dark == other.is_dark
47 }
48}
49
50impl Default for TextViewStyle {
51 fn default() -> Self {
52 Self::from_colors(&ColorTokens::light(), false)
53 }
54}
55
56impl TextViewStyle {
57 pub fn from_theme(theme: &crate::Theme) -> Self {
59 Self::from_colors(
60 &theme.tokens.colors,
61 theme.appearance == crate::ThemeAppearance::Dark,
62 )
63 }
64
65 fn from_colors(colors: &ColorTokens, is_dark: bool) -> Self {
71 Self {
72 foreground: colors.foreground,
73 muted_foreground: colors.muted_foreground,
74 link: colors.primary,
75 selection: colors.selection,
76 code_background: colors.accent,
77 border: colors.border,
78 paragraph_gap: rems(1.),
79 heading: Arc::new(|_| StyleRefinement::default()),
80 code_block: StyleRefinement::default(),
81 table: StyleRefinement::default(),
82 table_head: StyleRefinement::default(),
83 table_cell: StyleRefinement::default(),
84 inline_code: HighlightStyle {
85 background_color: Some(colors.accent),
86 ..Default::default()
87 },
88 is_dark,
89 }
90 }
91
92 pub fn with_foreground(mut self, color: Hsla) -> Self {
94 self.foreground = color;
95 self
96 }
97
98 pub fn with_muted_foreground(mut self, color: Hsla) -> Self {
100 self.muted_foreground = color;
101 self
102 }
103
104 pub fn with_link(mut self, color: Hsla) -> Self {
106 self.link = color;
107 self
108 }
109
110 pub fn with_selection(mut self, color: Hsla) -> Self {
115 self.selection = color;
116 self
117 }
118
119 pub fn with_code_background(mut self, color: Hsla) -> Self {
121 self.code_background = color;
122 self
123 }
124
125 pub fn with_border(mut self, color: Hsla) -> Self {
127 self.border = color;
128 self
129 }
130
131 pub fn with_paragraph_gap(mut self, gap: Rems) -> Self {
133 self.paragraph_gap = gap;
134 self
135 }
136
137 pub fn with_heading<F>(mut self, heading: F) -> Self
139 where
140 F: Fn(u8) -> StyleRefinement + Send + Sync + 'static,
141 {
142 self.heading = Arc::new(heading);
143 self
144 }
145
146 pub fn with_code_block(mut self, style: StyleRefinement) -> Self {
148 self.code_block = style;
149 self
150 }
151
152 pub fn with_inline_code(mut self, style: HighlightStyle) -> Self {
157 self.inline_code = style;
158 self
159 }
160
161 pub fn with_table(mut self, style: StyleRefinement) -> Self {
169 self.table = style;
170 self
171 }
172
173 pub fn with_table_head(mut self, style: StyleRefinement) -> Self {
176 self.table_head = style;
177 self
178 }
179
180 pub fn with_table_cell(mut self, style: StyleRefinement) -> Self {
186 self.table_cell = style;
187 self
188 }
189
190 pub fn with_dark(mut self, is_dark: bool) -> Self {
192 self.is_dark = is_dark;
193 self
194 }
195
196 pub fn foreground(&self) -> Hsla {
198 self.foreground
199 }
200
201 pub fn muted_foreground(&self) -> Hsla {
203 self.muted_foreground
204 }
205
206 pub fn link(&self) -> Hsla {
208 self.link
209 }
210
211 pub fn selection(&self) -> Hsla {
213 self.selection
214 }
215
216 pub fn code_background(&self) -> Hsla {
218 self.code_background
219 }
220
221 pub fn border(&self) -> Hsla {
223 self.border
224 }
225
226 pub fn paragraph_gap(&self) -> Rems {
228 self.paragraph_gap
229 }
230
231 pub fn heading(&self, level: u8) -> StyleRefinement {
233 (self.heading)(level)
234 }
235
236 pub fn code_block(&self) -> &StyleRefinement {
238 &self.code_block
239 }
240
241 pub fn table(&self) -> &StyleRefinement {
243 &self.table
244 }
245
246 pub fn table_head(&self) -> &StyleRefinement {
248 &self.table_head
249 }
250
251 pub fn table_cell(&self) -> &StyleRefinement {
253 &self.table_cell
254 }
255
256 pub fn inline_code(&self) -> HighlightStyle {
259 self.inline_code
260 }
261
262 pub fn is_dark(&self) -> bool {
264 self.is_dark
265 }
266
267 pub(crate) fn inline_code_highlight(&self) -> HighlightStyle {
270 let mut style = self.inline_code;
271 if style.background_color.is_none() {
272 style.background_color = Some(self.code_background);
273 }
274 style
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use gpui::{Styled as _, px};
281
282 use super::*;
283
284 #[test]
285 fn selection_layout_fingerprint_covers_callback_table_and_theme_fields() {
286 let base = TextViewStyle::default();
287 let heading = base
288 .clone()
289 .with_heading(|_| StyleRefinement::default().text_size(px(14.)));
290 assert!(
291 heading
292 == base
293 .clone()
294 .with_heading(|_| StyleRefinement::default().text_size(px(14.)))
295 );
296 assert!(
297 heading
298 != base
299 .clone()
300 .with_heading(|_| StyleRefinement::default().text_size(px(28.)))
301 );
302
303 let mut table = StyleRefinement::default();
304 table.text.white_space = Some(gpui::WhiteSpace::Nowrap);
305 assert!(base != base.clone().with_table_cell(table));
306
307 assert!(base != base.clone().with_dark(true));
308 }
309
310 #[test]
311 fn cloning_preserves_the_same_heading_callback_fingerprint() {
312 let style = TextViewStyle::default()
313 .with_heading(|_| StyleRefinement::default().text_size(px(14.)));
314 assert!(style == style.clone());
315 }
316
317 #[test]
318 fn default_style_is_readable_without_an_application_theme() {
319 let style = TextViewStyle::default();
320
321 assert_eq!(style.foreground().a, 1.0);
322 assert_eq!(style.link().a, 1.0);
323 assert!(style.selection().a > 0.0);
324 assert!(style.inline_code().background_color.is_some());
325 assert!(style.code_background().a > 0.0);
326 assert!(style.border().a > 0.0);
327 assert_eq!(style.code_block().corner_radii.top_left, None);
328 assert_eq!(style.code_block().corner_radii.top_right, None);
329 assert_eq!(style.code_block().corner_radii.bottom_left, None);
330 assert_eq!(style.code_block().corner_radii.bottom_right, None);
331 }
332
333 #[test]
334 fn heading_refinement_defaults_empty_and_resolves_by_level() {
335 let base = TextViewStyle::default();
336 assert_eq!(base.heading(1), StyleRefinement::default());
337
338 let style = base.clone().with_heading(|level| match level {
339 1 => StyleRefinement::default().pt(rems(1.)).pb(rems(0.5)),
340 _ => StyleRefinement::default().pb(rems(0.25)),
341 });
342
343 assert_eq!(
344 style.heading(1),
345 StyleRefinement::default().pt(rems(1.)).pb(rems(0.5))
346 );
347 assert_eq!(style.heading(2), StyleRefinement::default().pb(rems(0.25)));
348 assert!(style != base);
349 }
350
351 #[test]
352 fn inline_code_falls_back_to_the_code_background() {
353 let style = TextViewStyle::default()
354 .with_code_background(gpui::rgb(0x123456).into())
355 .with_inline_code(HighlightStyle::default());
356
357 assert_eq!(
358 style.inline_code_highlight().background_color,
359 Some(gpui::rgb(0x123456).into())
360 );
361 }
362
363 #[test]
364 fn from_theme_maps_base_semantic_tokens() {
365 let mut theme = crate::Theme::default();
366 theme.tokens.colors.foreground = gpui::rgb(0x112233).into();
367 theme.tokens.colors.muted_foreground = gpui::rgb(0x445566).into();
368 theme.tokens.colors.primary = gpui::rgb(0x3366ff).into();
369 theme.tokens.colors.accent = gpui::rgb(0xddeeff).into();
370 theme.tokens.colors.border = gpui::rgb(0x778899).into();
371 theme.tokens.colors.selection = gpui::rgb(0x55a0fc).into();
372
373 let style = TextViewStyle::from_theme(&theme);
374 assert_eq!(style.foreground(), theme.tokens.colors.foreground);
375 assert_eq!(style.link(), theme.tokens.colors.primary);
376 assert_eq!(style.selection(), theme.tokens.colors.selection);
377 assert_eq!(style.code_background(), theme.tokens.colors.accent);
378 assert_eq!(style.border(), theme.tokens.colors.border);
379 }
380}