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