Skip to main content

gpui_base/text/
style.rs

1use std::sync::Arc;
2
3use gpui::{HighlightStyle, Hsla, Rems, StyleRefinement, rems};
4
5use crate::ColorTokens;
6
7/// TextViewStyle used to customize the style for [`super::TextView`].
8///
9/// The fields are private because this type crosses the `gpui-base` seam:
10/// build one with the `with_*` methods and read it back through the accessors
11/// of the same name, so a later field is an additive change rather than a
12/// breaking one.
13#[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    /// Explicit inline code refinements, applied after theme and surface colors.
29    inline_code_style: StyleRefinement,
30    /// Explicit list marker refinements, applied after theme and surface colors.
31    list_style: StyleRefinement,
32    /// Explicit blockquote refinements, applied after theme and surface colors.
33    blockquote_style: StyleRefinement,
34    /// The table body background, or the theme surface when `None`. Only
35    /// [`Self::on_text_color`] sets it, to let a table on an inverted surface
36    /// show that surface.
37    table_background: Option<Hsla>,
38    /// The background of a hovered table body row, or no hover highlight
39    /// when `None`.
40    table_row_hover: Option<Hsla>,
41    is_dark: bool,
42}
43
44impl PartialEq for TextViewStyle {
45    fn eq(&self, other: &Self) -> bool {
46        self.paragraph_gap == other.paragraph_gap
47            && self.foreground == other.foreground
48            && self.muted_foreground == other.muted_foreground
49            && self.link == other.link
50            && self.selection == other.selection
51            && self.code_background == other.code_background
52            && self.border == other.border
53            && (Arc::ptr_eq(&self.heading, &other.heading)
54                || (1..=6).all(|level| (self.heading)(level) == (other.heading)(level)))
55            && self.code_block == other.code_block
56            && self.table == other.table
57            && self.table_head == other.table_head
58            && self.table_cell == other.table_cell
59            && self.inline_code == other.inline_code
60            && self.inline_code_style == other.inline_code_style
61            && self.list_style == other.list_style
62            && self.blockquote_style == other.blockquote_style
63            && self.table_background == other.table_background
64            && self.table_row_hover == other.table_row_hover
65            && self.is_dark == other.is_dark
66    }
67}
68
69impl Default for TextViewStyle {
70    fn default() -> Self {
71        Self::from_colors(&ColorTokens::light(), false)
72    }
73}
74
75impl TextViewStyle {
76    /// Derives rich-text colors from Base semantic theme tokens.
77    pub fn from_theme(theme: &crate::Theme) -> Self {
78        Self::from_colors(
79            &theme.tokens.colors,
80            theme.appearance == crate::ThemeAppearance::Dark,
81        )
82    }
83
84    /// Derives rich-text colors from one palette.
85    ///
86    /// Rich text needs a handful of roles the palette does not name directly —
87    /// a code background, a link color — so they are mapped here once instead
88    /// of at every call site.
89    fn from_colors(colors: &ColorTokens, is_dark: bool) -> Self {
90        Self {
91            foreground: colors.foreground,
92            muted_foreground: colors.muted_foreground,
93            link: colors.primary,
94            selection: colors.selection,
95            code_background: colors.accent,
96            border: colors.border,
97            paragraph_gap: rems(0.75),
98            heading: Arc::new(|_| StyleRefinement::default()),
99            code_block: StyleRefinement::default(),
100            table: StyleRefinement::default(),
101            table_head: StyleRefinement::default(),
102            table_cell: StyleRefinement::default(),
103            inline_code: HighlightStyle {
104                background_color: Some(colors.accent),
105                ..Default::default()
106            },
107            inline_code_style: StyleRefinement::default(),
108            list_style: StyleRefinement::default(),
109            blockquote_style: StyleRefinement::default(),
110            table_background: None,
111            table_row_hover: None,
112            is_dark,
113        }
114    }
115
116    /// Sets the default body-text color.
117    pub fn with_foreground(mut self, color: Hsla) -> Self {
118        self.foreground = color;
119        self
120    }
121
122    /// Sets the secondary text color.
123    pub fn with_muted_foreground(mut self, color: Hsla) -> Self {
124        self.muted_foreground = color;
125        self
126    }
127
128    /// Sets the link text color.
129    pub fn with_link(mut self, color: Hsla) -> Self {
130        self.link = color;
131        self
132    }
133
134    /// Sets the background painted behind selected text.
135    ///
136    /// Selection quads are painted under the glyphs, so this is normally a
137    /// translucent wash rather than a solid fill.
138    pub fn with_selection(mut self, color: Hsla) -> Self {
139        self.selection = color;
140        self
141    }
142
143    /// Sets the background of fenced code blocks and table header rows.
144    pub fn with_code_background(mut self, color: Hsla) -> Self {
145        self.code_background = color;
146        self
147    }
148
149    /// Sets the color of borders and horizontal rules.
150    pub fn with_border(mut self, color: Hsla) -> Self {
151        self.border = color;
152        self
153    }
154
155    /// Sets the gap between paragraphs. Defaults to 0.75 rem.
156    pub fn with_paragraph_gap(mut self, gap: Rems) -> Self {
157        self.paragraph_gap = gap;
158        self
159    }
160
161    /// Sets the style refinement for headings, selected by heading level (1-6).
162    pub fn with_heading<F>(mut self, heading: F) -> Self
163    where
164        F: Fn(u8) -> StyleRefinement + Send + Sync + 'static,
165    {
166        self.heading = Arc::new(heading);
167        self
168    }
169
170    /// Sets the style refinement for code blocks.
171    ///
172    /// Set `overflow.y` to `Overflow::Scroll` together with a max height to
173    /// scroll long code inside the block: it gets its own scrollbar, and wheel
174    /// input over it no longer scrolls an ancestor list until the code reaches
175    /// its edge.
176    pub fn with_code_block(mut self, style: StyleRefinement) -> Self {
177        self.code_block = style;
178        self
179    }
180
181    /// Sets the highlight style for inline code spans.
182    ///
183    /// When `background_color` is `None`, the neutral code background is used,
184    /// which keeps [`TextViewStyle::default`] usable without a theme.
185    pub fn with_inline_code(mut self, style: HighlightStyle) -> Self {
186        self.inline_code = style;
187        self
188    }
189
190    /// Overrides inline code typography and its box presentation.
191    ///
192    /// Supports font family, size, features, fallbacks, weight, style, color, underline and
193    /// strikethrough, background, border widths/color/style, corner radii, and
194    /// padding. Sizes and padding resolve pixels/rem against the window.
195    /// Unspecified properties keep the existing defaults. Box layout, margin,
196    /// positioning, overflow, and interaction properties are not used.
197    /// Line height remains owned by the paragraph. Non-finite or non-positive
198    /// font sizes are ignored. Explicit refinements also take precedence on
199    /// inverted surfaces.
200    pub fn with_inline_code_style(mut self, style: StyleRefinement) -> Self {
201        self.inline_code_style = normalize_inline_code_style(style);
202        self
203    }
204
205    /// Refines the marker of a bullet or numbered list item.
206    ///
207    /// Applied to the marker column after its defaults: the muted text color,
208    /// a minimum width of the list indent, right-aligned text, and a right
209    /// padding of 0.3 times that indent. Text properties such as color, font
210    /// family, size, and weight style the bullet or number. The text color
211    /// also outlines an unchecked task-list checkbox; a checked one keeps the
212    /// link color. Unspecified properties keep the existing defaults, and
213    /// explicit refinements also take precedence on inverted surfaces.
214    pub fn with_list_style(mut self, style: StyleRefinement) -> Self {
215        self.list_style = style;
216        self
217    }
218
219    /// Refines the blockquote container.
220    ///
221    /// Applied after its defaults: the muted text color, a 2px left border in
222    /// the border color, and a left padding of 0.9 times the body font size.
223    /// Border widths and color style the quote bar; text properties,
224    /// background, padding, and corner radii style the quote and its
225    /// contents. Child blocks keep properties they set explicitly, including
226    /// heading and table font sizes. Unspecified properties keep the existing
227    /// defaults, and explicit refinements also take precedence on inverted surfaces.
228    pub fn with_blockquote_style(mut self, style: StyleRefinement) -> Self {
229        self.blockquote_style = style;
230        self
231    }
232
233    /// Sets the style refinement for the table container (the bordered wrapper
234    /// in wrap mode, the scroll viewport in horizontal-scroll mode).
235    ///
236    /// Set `overflow_x: scroll` on the refinement for adaptive table layout:
237    /// columns fit their content when space allows, shrink (wrapping cell
238    /// text) down to a per-column floor when the frame is narrower, and below
239    /// that the table scrolls horizontally instead of squeezing further.
240    pub fn with_table(mut self, style: StyleRefinement) -> Self {
241        self.table = style;
242        self
243    }
244
245    /// Sets the style refinement for the header row (the first row) of a
246    /// table, applied on top of the header background and foreground.
247    pub fn with_table_head(mut self, style: StyleRefinement) -> Self {
248        self.table_head = style;
249        self
250    }
251
252    /// Sets the style refinement for each table cell.
253    ///
254    /// With the scroll table layout, `white_space: nowrap` here keeps cells on
255    /// a single line — columns then never shrink and the table scrolls as soon
256    /// as the content is wider than the frame.
257    pub fn with_table_cell(mut self, style: StyleRefinement) -> Self {
258        self.table_cell = style;
259        self
260    }
261
262    /// Sets the background of a table body row while the pointer is over it.
263    ///
264    /// The header row does not highlight. Without this, table rows have no
265    /// hover highlight.
266    pub fn with_table_row_hover(mut self, color: Hsla) -> Self {
267        self.table_row_hover = Some(color);
268        self
269    }
270
271    /// Sets whether content-specific assets should use their dark variant.
272    pub fn with_dark(mut self, is_dark: bool) -> Self {
273        self.is_dark = is_dark;
274        self
275    }
276
277    /// The default body-text color.
278    pub fn foreground(&self) -> Hsla {
279        self.foreground
280    }
281
282    /// The secondary text color.
283    pub fn muted_foreground(&self) -> Hsla {
284        self.muted_foreground
285    }
286
287    /// The link text color.
288    pub fn link(&self) -> Hsla {
289        self.link
290    }
291
292    /// The background painted behind selected text.
293    pub fn selection(&self) -> Hsla {
294        self.selection
295    }
296
297    /// The background of fenced code blocks and table header rows.
298    pub fn code_background(&self) -> Hsla {
299        self.code_background
300    }
301
302    /// The color of borders and horizontal rules.
303    pub fn border(&self) -> Hsla {
304        self.border
305    }
306
307    /// The gap between paragraphs.
308    pub fn paragraph_gap(&self) -> Rems {
309        self.paragraph_gap
310    }
311
312    /// The style refinement for a heading at `level` (1-6).
313    pub fn heading(&self, level: u8) -> StyleRefinement {
314        (self.heading)(level)
315    }
316
317    /// The style refinement for code blocks.
318    pub fn code_block(&self) -> &StyleRefinement {
319        &self.code_block
320    }
321
322    /// The style refinement for the table container.
323    pub fn table(&self) -> &StyleRefinement {
324        &self.table
325    }
326
327    /// The style refinement for table header rows.
328    pub fn table_head(&self) -> &StyleRefinement {
329        &self.table_head
330    }
331
332    /// The style refinement for table cells.
333    pub fn table_cell(&self) -> &StyleRefinement {
334        &self.table_cell
335    }
336
337    /// The background of a hovered table body row, if rows highlight on hover.
338    pub fn table_row_hover(&self) -> Option<Hsla> {
339        self.table_row_hover
340    }
341
342    /// The highlight style for inline code, before the code-background
343    /// fallback in [`Self::inline_code_highlight`] applies.
344    pub fn inline_code(&self) -> HighlightStyle {
345        self.inline_code
346    }
347
348    /// Explicit inline code typography and box refinements.
349    pub fn inline_code_style(&self) -> &StyleRefinement {
350        &self.inline_code_style
351    }
352
353    /// Explicit list marker refinements.
354    pub fn list_style(&self) -> &StyleRefinement {
355        &self.list_style
356    }
357
358    /// Explicit blockquote refinements.
359    pub fn blockquote_style(&self) -> &StyleRefinement {
360        &self.blockquote_style
361    }
362
363    /// Whether content-specific assets should use their dark variant.
364    pub fn is_dark(&self) -> bool {
365        self.is_dark
366    }
367
368    /// The table body background, when it is not the theme surface.
369    pub(crate) fn table_background(&self) -> Option<Hsla> {
370        self.table_background
371    }
372
373    /// Returns the [`HighlightStyle`] to use for inline code, falling back to
374    /// the code background when no custom background was supplied.
375    pub(crate) fn inline_code_highlight(&self) -> HighlightStyle {
376        let mut style = self.inline_code;
377        if style.background_color.is_none() {
378            style.background_color = Some(self.code_background);
379        }
380        style
381    }
382
383    /// This style adapted to body text drawn in `color`, the text color a
384    /// container sets for its surface.
385    ///
386    /// The body text always takes `color`. When `color` is far from this
387    /// style's foreground in lightness, the surface is inverted from the one
388    /// this style was made for (a `primary` fill, say): the link, muted text,
389    /// code, border, selection and table row hover colors would vanish on it,
390    /// so they are all derived from `color`, and [`Self::is_dark`] flips.
391    pub(crate) fn on_text_color(&self, color: Hsla) -> Self {
392        let style = self.clone().with_foreground(color);
393        if !self.is_inverted_by(color) {
394            return style;
395        }
396
397        let code_background = color.opacity(0.12);
398        let mut table_head = self.table_head.clone();
399        table_head.background = Some(code_background.into());
400        table_head.text.color = Some(color);
401        let mut style = style
402            .with_muted_foreground(color.opacity(0.7))
403            .with_link(color)
404            .with_selection(color.opacity(0.25))
405            .with_code_background(code_background)
406            .with_border(color.opacity(0.2))
407            .with_inline_code(HighlightStyle {
408                background_color: Some(code_background),
409                ..self.inline_code
410            })
411            .with_table_head(table_head)
412            .with_dark(!self.is_dark);
413        style.table_background = Some(gpui::transparent_black());
414        style.table_row_hover = self.table_row_hover.map(|_| color.opacity(0.08));
415        style
416    }
417
418    /// Whether body text in `color` sits on a surface inverted from the one
419    /// this style was made for.
420    ///
421    /// Mid-tone text such as a destructive red reads on either kind of surface,
422    /// so only a lightness gap wider than that counts as inverted.
423    pub(crate) fn is_inverted_by(&self, color: Hsla) -> bool {
424        const INVERTED_LIGHTNESS_GAP: f32 = 0.6;
425        (oklab_lightness(color) - oklab_lightness(self.foreground)).abs() > INVERTED_LIGHTNESS_GAP
426    }
427}
428
429/// The perceptual (Oklab) lightness of `color`, from 0 (black) to 1 (white).
430fn oklab_lightness(color: Hsla) -> f32 {
431    let rgb = color.to_rgb();
432    let linear = |c: f32| {
433        if c <= 0.04045 {
434            c / 12.92
435        } else {
436            ((c + 0.055) / 1.055).powf(2.4)
437        }
438    };
439    let (r, g, b) = (linear(rgb.r), linear(rgb.g), linear(rgb.b));
440    let l = (0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt();
441    let m = (0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b).cbrt();
442    let s = (0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b).cbrt();
443    0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s
444}
445
446/// Drop an invalid font size before it can replace an inherited value or
447/// make retained style equality fail every frame (NaN is never equal).
448pub(super) fn normalize_inline_code_style(mut style: StyleRefinement) -> StyleRefinement {
449    if style.text.font_size.is_some_and(|size| {
450        let size = size.to_pixels(gpui::px(1.)).as_f32();
451        !size.is_finite() || size <= 0.
452    }) {
453        style.text.font_size = None;
454    }
455    style
456}
457
458#[cfg(test)]
459mod tests {
460    use gpui::{Styled as _, px};
461
462    use super::*;
463
464    #[test]
465    fn selection_layout_fingerprint_covers_callback_table_and_theme_fields() {
466        let base = TextViewStyle::default();
467        let heading = base
468            .clone()
469            .with_heading(|_| StyleRefinement::default().text_size(px(14.)));
470        assert!(
471            heading
472                == base
473                    .clone()
474                    .with_heading(|_| StyleRefinement::default().text_size(px(14.)))
475        );
476        assert!(
477            heading
478                != base
479                    .clone()
480                    .with_heading(|_| StyleRefinement::default().text_size(px(28.)))
481        );
482
483        let mut table = StyleRefinement::default();
484        table.text.white_space = Some(gpui::WhiteSpace::Nowrap);
485        assert!(base != base.clone().with_table_cell(table));
486        assert!(base != base.clone().with_table_row_hover(gpui::red()));
487
488        assert!(base != base.clone().with_dark(true));
489        assert!(
490            base != base
491                .clone()
492                .with_inline_code_style(StyleRefinement::default().border_color(gpui::red()))
493        );
494        assert!(
495            base != base
496                .clone()
497                .with_list_style(StyleRefinement::default().text_color(gpui::red()))
498        );
499        assert!(
500            base != base
501                .clone()
502                .with_blockquote_style(StyleRefinement::default().border_color(gpui::red()))
503        );
504    }
505
506    #[test]
507    fn heading_equality_skips_shared_callbacks_and_compares_distinct_ones() {
508        use std::sync::atomic::{AtomicUsize, Ordering};
509
510        let calls = Arc::new(AtomicUsize::new(0));
511        let calls_for_heading = calls.clone();
512        let style = TextViewStyle::default().with_heading(move |_| {
513            calls_for_heading.fetch_add(1, Ordering::Relaxed);
514            StyleRefinement::default().text_size(px(14.))
515        });
516
517        assert!(style == style.clone());
518        assert_eq!(calls.load(Ordering::Relaxed), 0);
519        assert!(style != style.clone().with_paragraph_gap(rems(1.)));
520        assert!(style != style.clone().with_dark(!style.is_dark()));
521        assert_eq!(calls.load(Ordering::Relaxed), 0);
522
523        let equal = style
524            .clone()
525            .with_heading(|_| StyleRefinement::default().text_size(px(14.)));
526        assert!(style == equal);
527        assert_eq!(calls.load(Ordering::Relaxed), 6);
528
529        calls.store(0, Ordering::Relaxed);
530        let different = style.clone().with_heading(|level| {
531            StyleRefinement::default().text_size(px(if level == 6 { 28. } else { 14. }))
532        });
533        assert!(style != different);
534        assert_eq!(calls.load(Ordering::Relaxed), 6);
535    }
536
537    #[test]
538    fn default_style_is_readable_without_an_application_theme() {
539        let style = TextViewStyle::default();
540
541        assert_eq!(style.foreground().a, 1.0);
542        assert_eq!(style.link().a, 1.0);
543        assert!(style.selection().a > 0.0);
544        assert!(style.inline_code().background_color.is_some());
545        assert!(style.code_background().a > 0.0);
546        assert!(style.border().a > 0.0);
547        assert_eq!(style.table_row_hover(), None);
548        assert_eq!(style.code_block().corner_radii.top_left, None);
549        assert_eq!(style.code_block().corner_radii.top_right, None);
550        assert_eq!(style.code_block().corner_radii.bottom_left, None);
551        assert_eq!(style.code_block().corner_radii.bottom_right, None);
552    }
553
554    #[test]
555    fn heading_refinement_defaults_empty_and_resolves_by_level() {
556        let base = TextViewStyle::default();
557        assert_eq!(base.heading(1), StyleRefinement::default());
558
559        let style = base.clone().with_heading(|level| match level {
560            1 => StyleRefinement::default().pt(rems(1.)).pb(rems(0.5)),
561            _ => StyleRefinement::default().pb(rems(0.25)),
562        });
563
564        assert_eq!(
565            style.heading(1),
566            StyleRefinement::default().pt(rems(1.)).pb(rems(0.5))
567        );
568        assert_eq!(style.heading(2), StyleRefinement::default().pb(rems(0.25)));
569        assert!(style != base);
570    }
571
572    #[test]
573    fn inline_code_falls_back_to_the_code_background() {
574        let style = TextViewStyle::default()
575            .with_code_background(gpui::rgb(0x123456).into())
576            .with_inline_code(HighlightStyle::default());
577
578        assert_eq!(
579            style.inline_code_highlight().background_color,
580            Some(gpui::rgb(0x123456).into())
581        );
582    }
583
584    #[test]
585    fn inline_code_refinements_remain_explicit_on_an_inverted_surface() {
586        let colors = ColorTokens::light();
587        let plain = TextViewStyle::from_colors(&colors, false);
588        assert_eq!(plain.inline_code_style(), &StyleRefinement::default());
589        for size in [f32::NAN, f32::INFINITY, 0., -1.] {
590            assert!(
591                plain
592                    .clone()
593                    .with_inline_code_style(StyleRefinement::default().text_size(px(size)))
594                    .inline_code_style()
595                    .text
596                    .font_size
597                    .is_none()
598            );
599        }
600        let refinement = StyleRefinement::default()
601            .text_size(gpui::rems(1.))
602            .bg(gpui::red())
603            .border_1()
604            .border_color(gpui::blue());
605        let style = plain.with_inline_code_style(refinement.clone());
606        assert_eq!(style.inline_code_style(), &refinement);
607        assert_eq!(
608            style
609                .on_text_color(colors.primary_foreground)
610                .inline_code_style(),
611            &refinement
612        );
613    }
614
615    #[test]
616    fn list_and_blockquote_refinements_remain_explicit_on_an_inverted_surface() {
617        let colors = ColorTokens::light();
618        let plain = TextViewStyle::from_colors(&colors, false);
619        assert_eq!(plain.list_style(), &StyleRefinement::default());
620        assert_eq!(plain.blockquote_style(), &StyleRefinement::default());
621
622        let marker = StyleRefinement::default()
623            .text_color(gpui::red())
624            .font_weight(gpui::FontWeight::BOLD);
625        let quote = StyleRefinement::default()
626            .border_l_4()
627            .border_color(gpui::blue())
628            .bg(gpui::green());
629        let style = plain
630            .with_list_style(marker.clone())
631            .with_blockquote_style(quote.clone());
632        assert_eq!(style.list_style(), &marker);
633        assert_eq!(style.blockquote_style(), &quote);
634
635        let inverted = style.on_text_color(colors.primary_foreground);
636        assert_eq!(inverted.list_style(), &marker);
637        assert_eq!(inverted.blockquote_style(), &quote);
638    }
639
640    #[test]
641    fn text_color_of_a_matching_surface_only_replaces_the_body_text() {
642        let style = TextViewStyle::from_colors(&ColorTokens::light(), false)
643            .with_table_row_hover(ColorTokens::light().muted);
644        let destructive = ColorTokens::light().destructive;
645
646        let adapted = style.on_text_color(destructive);
647        assert_eq!(adapted.foreground(), destructive);
648        assert_eq!(adapted.link(), style.link());
649        assert_eq!(adapted.muted_foreground(), style.muted_foreground());
650        assert_eq!(adapted.code_background(), style.code_background());
651        assert_eq!(adapted.table_background(), None);
652        assert_eq!(adapted.table_row_hover(), style.table_row_hover());
653        assert!(!adapted.is_dark());
654    }
655
656    #[test]
657    fn text_color_of_an_inverted_surface_derives_every_color_from_it() {
658        for (colors, is_dark) in [(ColorTokens::light(), false), (ColorTokens::dark(), true)] {
659            let style = TextViewStyle::from_colors(&colors, is_dark);
660            let text = colors.primary_foreground;
661            // Rows without a hover highlight stay without one.
662            assert_eq!(style.on_text_color(text).table_row_hover(), None);
663
664            let style = style.with_table_row_hover(colors.muted);
665            let adapted = style.on_text_color(text);
666            assert_eq!(adapted.foreground(), text);
667            assert_eq!(adapted.link(), text);
668            assert_eq!(adapted.muted_foreground(), text.opacity(0.7));
669            assert_eq!(adapted.code_background(), text.opacity(0.12));
670            assert_eq!(
671                adapted.inline_code_highlight().background_color,
672                Some(text.opacity(0.12))
673            );
674            assert_eq!(adapted.border(), text.opacity(0.2));
675            assert_eq!(adapted.selection(), text.opacity(0.25));
676            assert_eq!(adapted.table_background(), Some(gpui::transparent_black()));
677            assert_eq!(adapted.table_row_hover(), Some(text.opacity(0.08)));
678            assert_eq!(adapted.table_head().text.color, Some(text));
679            assert_eq!(adapted.is_dark(), !is_dark);
680        }
681    }
682
683    #[test]
684    fn from_theme_maps_base_semantic_tokens() {
685        let mut theme = crate::Theme::default();
686        theme.tokens.colors.foreground = gpui::rgb(0x112233).into();
687        theme.tokens.colors.muted_foreground = gpui::rgb(0x445566).into();
688        theme.tokens.colors.primary = gpui::rgb(0x3366ff).into();
689        theme.tokens.colors.accent = gpui::rgb(0xddeeff).into();
690        theme.tokens.colors.border = gpui::rgb(0x778899).into();
691        theme.tokens.colors.selection = gpui::rgb(0x55a0fc).into();
692
693        let style = TextViewStyle::from_theme(&theme);
694        assert_eq!(style.foreground(), theme.tokens.colors.foreground);
695        assert_eq!(style.link(), theme.tokens.colors.primary);
696        assert_eq!(style.selection(), theme.tokens.colors.selection);
697        assert_eq!(style.code_background(), theme.tokens.colors.accent);
698        assert_eq!(style.border(), theme.tokens.colors.border);
699    }
700}