1use 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
26type 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#[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 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}