Skip to main content

markdown/
style.rs

1//! No `theme_settings` crate exists in this workspace, so font
2//! family/size/weight are not read from a settings global; see
3//! `code_editor.rs`'s `CODE_FONT_FAMILY`/`CODE_FONT_SIZE` consts for the
4//! established alternative pattern here: stateless, explicit config instead
5//! of a settings global. [`MarkdownFontConfig`] carries that config
6//! explicitly at construction time; theme *colors*/*syntax* still come from
7//! `cx.theme()` (`boltz-theme`).
8//!
9//! `CodeBlockRenderer` is intentionally not defined here even though it's
10//! visually a "style" concern: its `Custom` variant's
11//! `CodeBlockRenderFn`/`CodeBlockTransformFn` types reference
12//! `ParsedMarkdown`/`AnyDiv`/`Div`, which live in `entity.rs`/`element.rs`.
13
14use std::rc::Rc;
15use std::sync::Arc;
16
17use gpui::{
18    AbsoluteLength, App, BorderStyle, DefiniteLength, EdgesRefinement, FontFallbacks, FontFeatures,
19    FontWeight, Hsla, Length, Pixels, Refineable as _, SharedString, StyleRefinement, TextStyle,
20    TextStyleRefinement, UnderlineStyle, Window, px, rems,
21};
22use pulldown_cmark::BlockQuoteKind;
23use syntax_theme::SyntaxTheme;
24use theme::{ActiveTheme, ThemeColors};
25
26/// A callback function that can be used to customize the style of links
27/// based on the destination URL. If the callback returns `None`, the
28/// default link style will be used.
29type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
30
31#[derive(Clone, Copy, Default)]
32pub struct BlockQuoteKindColors {
33    pub note: Hsla,
34    pub tip: Hsla,
35    pub important: Hsla,
36    pub warning: Hsla,
37    pub caution: Hsla,
38}
39
40impl BlockQuoteKindColors {
41    pub(crate) fn for_kind(&self, kind: Option<BlockQuoteKind>, default: Hsla) -> Hsla {
42        match kind {
43            Some(BlockQuoteKind::Note) => self.note,
44            Some(BlockQuoteKind::Tip) => self.tip,
45            Some(BlockQuoteKind::Important) => self.important,
46            Some(BlockQuoteKind::Warning) => self.warning,
47            Some(BlockQuoteKind::Caution) => self.caution,
48            None => default,
49        }
50    }
51}
52
53#[derive(Clone, Default)]
54pub struct HeadingLevelStyles {
55    pub h1: Option<TextStyleRefinement>,
56    pub h2: Option<TextStyleRefinement>,
57    pub h3: Option<TextStyleRefinement>,
58    pub h4: Option<TextStyleRefinement>,
59    pub h5: Option<TextStyleRefinement>,
60    pub h6: Option<TextStyleRefinement>,
61}
62
63#[derive(Clone)]
64pub struct MarkdownStyle {
65    pub base_text_style: TextStyle,
66    pub container_style: StyleRefinement,
67    pub code_block: StyleRefinement,
68    pub code_block_overflow_x_scroll: bool,
69    pub inline_code: TextStyleRefinement,
70    pub block_quote: TextStyleRefinement,
71    pub link: TextStyleRefinement,
72    pub link_callback: Option<LinkStyleCallback>,
73    pub rule_color: Hsla,
74    pub block_quote_border_color: Hsla,
75    pub block_quote_kind_colors: BlockQuoteKindColors,
76    pub syntax: Arc<SyntaxTheme>,
77    pub selection_background_color: Hsla,
78    pub heading: StyleRefinement,
79    pub heading_level_styles: Option<HeadingLevelStyles>,
80    pub heading_border_color: Option<Hsla>,
81    pub height_is_multiple_of_line_height: bool,
82    pub prevent_mouse_interaction: bool,
83    pub table_columns_min_size: bool,
84    pub soft_break_as_hard_break: bool,
85}
86
87impl Default for MarkdownStyle {
88    fn default() -> Self {
89        Self {
90            base_text_style: Default::default(),
91            container_style: Default::default(),
92            code_block: Default::default(),
93            code_block_overflow_x_scroll: false,
94            inline_code: Default::default(),
95            block_quote: Default::default(),
96            link: Default::default(),
97            link_callback: None,
98            rule_color: Default::default(),
99            block_quote_border_color: Default::default(),
100            block_quote_kind_colors: Default::default(),
101            syntax: Arc::new(SyntaxTheme::default()),
102            selection_background_color: Default::default(),
103            heading: Default::default(),
104            heading_level_styles: None,
105            heading_border_color: None,
106            height_is_multiple_of_line_height: false,
107            prevent_mouse_interaction: false,
108            table_columns_min_size: false,
109            soft_break_as_hard_break: false,
110        }
111    }
112}
113
114#[derive(Clone, Copy)]
115pub enum MarkdownFont {
116    Agent,
117    Editor,
118    Preview,
119}
120
121/// Explicit font configuration, passed in by the caller instead of read from
122/// a global `ThemeSettings` (no `theme_settings` crate exists in this
123/// workspace — see module docs above).
124#[derive(Clone)]
125pub struct MarkdownFontConfig {
126    pub ui_font_family: SharedString,
127    pub ui_font_fallbacks: Option<FontFallbacks>,
128    pub ui_font_features: FontFeatures,
129    pub ui_font_size: Pixels,
130    pub buffer_font_family: SharedString,
131    pub buffer_font_fallbacks: Option<FontFallbacks>,
132    pub buffer_font_features: FontFeatures,
133    pub buffer_font_weight: FontWeight,
134    pub buffer_font_size: Pixels,
135    pub agent_buffer_font_size: Pixels,
136    pub agent_ui_font_size: Pixels,
137    pub markdown_preview_font_size: Pixels,
138    pub markdown_preview_font_family: SharedString,
139    pub markdown_preview_code_font_family: SharedString,
140}
141
142impl MarkdownStyle {
143    pub fn themed(
144        font: MarkdownFont,
145        fonts: &MarkdownFontConfig,
146        window: &Window,
147        cx: &App,
148    ) -> Self {
149        let colors = cx.theme().colors();
150        let syntax = cx.theme().syntax().clone();
151        Self::themed_with_overrides(font, colors, &syntax, fonts, window, cx)
152    }
153
154    /// Like [`Self::themed`], but takes explicit [`ThemeColors`] and
155    /// [`SyntaxTheme`] so callers (e.g. the markdown preview) can render the
156    /// markdown using a theme other than the active editor theme.
157    pub fn themed_with_overrides(
158        font: MarkdownFont,
159        colors: &ThemeColors,
160        syntax: &Arc<SyntaxTheme>,
161        fonts: &MarkdownFontConfig,
162        window: &Window,
163        cx: &App,
164    ) -> Self {
165        let is_preview = matches!(font, MarkdownFont::Preview);
166
167        let buffer_font_weight = fonts.buffer_font_weight;
168        let (buffer_font_size, ui_font_size) = match font {
169            MarkdownFont::Agent => (fonts.agent_buffer_font_size, fonts.agent_ui_font_size),
170            MarkdownFont::Editor => (fonts.buffer_font_size, fonts.ui_font_size),
171            MarkdownFont::Preview => (fonts.markdown_preview_font_size, fonts.ui_font_size),
172        };
173
174        let body_font_family = if is_preview {
175            fonts.markdown_preview_font_family.clone()
176        } else {
177            fonts.ui_font_family.clone()
178        };
179        let code_font_family = if is_preview {
180            fonts.markdown_preview_code_font_family.clone()
181        } else {
182            fonts.buffer_font_family.clone()
183        };
184
185        let mut text_style = window.text_style();
186        let line_height = buffer_font_size * 1.5;
187
188        text_style.refine(&TextStyleRefinement {
189            font_family: Some(body_font_family),
190            font_fallbacks: fonts.ui_font_fallbacks.clone(),
191            font_features: Some(fonts.ui_font_features.clone()),
192            font_size: Some(if is_preview {
193                rems(1.0).into()
194            } else {
195                ui_font_size.into()
196            }),
197            line_height: Some(line_height.into()),
198            color: Some(colors.text),
199            ..Default::default()
200        });
201
202        let style = MarkdownStyle {
203            base_text_style: text_style.clone(),
204            syntax: syntax.clone(),
205            selection_background_color: colors.element_selection_background,
206            rule_color: colors.border,
207            block_quote_border_color: colors.border,
208            block_quote_kind_colors: {
209                let status = cx.theme().status();
210                BlockQuoteKindColors {
211                    note: status.info,
212                    tip: status.success,
213                    important: status.info,
214                    warning: status.warning,
215                    caution: status.error,
216                }
217            },
218            code_block_overflow_x_scroll: true,
219            code_block: StyleRefinement {
220                padding: EdgesRefinement {
221                    top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
222                    left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
223                    right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
224                    bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
225                },
226                margin: EdgesRefinement {
227                    top: Some(Length::Definite(px(8.).into())),
228                    left: Some(Length::Definite(px(0.).into())),
229                    right: Some(Length::Definite(px(0.).into())),
230                    bottom: Some(Length::Definite(px(12.).into())),
231                },
232                border_style: Some(BorderStyle::Solid),
233                border_widths: EdgesRefinement {
234                    top: Some(AbsoluteLength::Pixels(px(1.))),
235                    left: Some(AbsoluteLength::Pixels(px(1.))),
236                    right: Some(AbsoluteLength::Pixels(px(1.))),
237                    bottom: Some(AbsoluteLength::Pixels(px(1.))),
238                },
239                border_color: Some(colors.border_variant),
240                background: Some(colors.editor_background.into()),
241                text: TextStyleRefinement {
242                    font_family: Some(code_font_family.clone()),
243                    font_fallbacks: fonts.buffer_font_fallbacks.clone(),
244                    font_features: Some(fonts.buffer_font_features.clone()),
245                    font_size: Some(buffer_font_size.into()),
246                    font_weight: Some(buffer_font_weight),
247                    ..Default::default()
248                },
249                ..Default::default()
250            },
251            inline_code: TextStyleRefinement {
252                font_family: Some(code_font_family),
253                font_fallbacks: fonts.buffer_font_fallbacks.clone(),
254                font_features: Some(fonts.buffer_font_features.clone()),
255                font_size: Some(buffer_font_size.into()),
256                font_weight: Some(buffer_font_weight),
257                background_color: Some(colors.editor_foreground.opacity(0.08)),
258                ..Default::default()
259            },
260            link: TextStyleRefinement {
261                background_color: Some(colors.editor_foreground.opacity(0.025)),
262                color: Some(colors.text_accent),
263                underline: Some(UnderlineStyle {
264                    color: Some(colors.text_accent.opacity(0.5)),
265                    thickness: px(1.),
266                    ..Default::default()
267                }),
268                ..Default::default()
269            },
270            soft_break_as_hard_break: matches!(font, MarkdownFont::Agent),
271            heading_level_styles: matches!(font, MarkdownFont::Agent).then_some(
272                HeadingLevelStyles {
273                    h1: Some(TextStyleRefinement {
274                        font_size: Some(rems(1.15).into()),
275                        font_weight: Some(FontWeight::SEMIBOLD),
276                        ..Default::default()
277                    }),
278                    h2: Some(TextStyleRefinement {
279                        font_size: Some(rems(1.1).into()),
280                        font_weight: Some(FontWeight::SEMIBOLD),
281                        ..Default::default()
282                    }),
283                    h3: Some(TextStyleRefinement {
284                        font_size: Some(rems(1.05).into()),
285                        font_weight: Some(FontWeight::SEMIBOLD),
286                        ..Default::default()
287                    }),
288                    h4: Some(TextStyleRefinement {
289                        font_size: Some(rems(1.).into()),
290                        font_weight: Some(FontWeight::MEDIUM),
291                        ..Default::default()
292                    }),
293                    h5: Some(TextStyleRefinement {
294                        font_size: Some(rems(0.95).into()),
295                        font_weight: Some(FontWeight::MEDIUM),
296                        ..Default::default()
297                    }),
298                    h6: Some(TextStyleRefinement {
299                        font_size: Some(rems(0.875).into()),
300                        font_weight: Some(FontWeight::MEDIUM),
301                        ..Default::default()
302                    }),
303                },
304            ),
305            ..Default::default()
306        };
307
308        if is_preview {
309            style.with_preview_overrides(colors)
310        } else {
311            style
312        }
313    }
314
315    fn with_preview_overrides(mut self, colors: &ThemeColors) -> Self {
316        let body_font_size = rems(0.92);
317        self.base_text_style.font_size = body_font_size.into();
318        self.container_style.text.font_size = Some(body_font_size.into());
319
320        self.base_text_style.color = colors.text_muted.blend(colors.text.opacity(0.25));
321        self.inline_code.color = Some(colors.text);
322        self.heading.text.color = Some(colors.text);
323
324        self.heading_level_styles = Some(HeadingLevelStyles {
325            h1: Some(TextStyleRefinement {
326                font_size: Some(rems(1.45).into()),
327                ..Default::default()
328            }),
329            h2: Some(TextStyleRefinement {
330                font_size: Some(rems(1.3).into()),
331                ..Default::default()
332            }),
333            h3: Some(TextStyleRefinement {
334                font_size: Some(rems(1.1).into()),
335                ..Default::default()
336            }),
337            h4: Some(TextStyleRefinement {
338                font_size: Some(rems(1.01).into()),
339                ..Default::default()
340            }),
341            h5: Some(TextStyleRefinement {
342                font_size: Some(rems(0.95).into()),
343                ..Default::default()
344            }),
345            h6: Some(TextStyleRefinement {
346                font_size: Some(rems(0.85).into()),
347                ..Default::default()
348            }),
349        });
350
351        self.heading_border_color = Some(colors.border_variant);
352
353        self
354    }
355
356    pub fn with_buffer_font(mut self, fonts: &MarkdownFontConfig) -> Self {
357        self.base_text_style.font_family = fonts.buffer_font_family.clone();
358        self.base_text_style.font_fallbacks = fonts.buffer_font_fallbacks.clone();
359        self.base_text_style.font_features = fonts.buffer_font_features.clone();
360        self.base_text_style.font_weight = fonts.buffer_font_weight;
361        self
362    }
363
364    pub fn with_muted_text(mut self, cx: &App) -> Self {
365        let colors = cx.theme().colors();
366        self.base_text_style.color = colors.text_muted;
367        self
368    }
369}
370
371#[derive(Clone, Copy, PartialEq, Eq)]
372pub enum CopyButtonVisibility {
373    Hidden,
374    AlwaysVisible,
375    VisibleOnHover,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub enum WrapButtonVisibility {
380    Hidden,
381    AlwaysVisible,
382    VisibleOnHover,
383}