1use gpui::prelude::*;
18use gpui::{
19 div, px, App, Font, FontStyle, FontWeight, IntoElement, SharedString, StrikethroughStyle,
20 StyledText, TextRun, UnderlineStyle, Window,
21};
22
23use super::block::{classify, DocState};
24use super::layout::{metrics, plan, RowKind, RowPlan};
25use crate::devtools::Probed;
26use crate::style::MONO_FAMILY;
27use crate::theme::{theme, ColorName, Size};
28
29#[derive(IntoElement)]
31pub struct Markdown {
32 source: SharedString,
33 size: Size,
34 accent: Option<ColorName>,
36 max_lines: Option<usize>,
39}
40
41impl Markdown {
42 pub fn new(source: impl Into<SharedString>) -> Self {
43 Markdown {
44 source: source.into(),
45 size: Size::Sm,
46 accent: None,
47 max_lines: None,
48 }
49 }
50
51 pub fn size(mut self, size: Size) -> Self {
53 self.size = size;
54 self
55 }
56
57 pub fn accent(mut self, accent: ColorName) -> Self {
59 self.accent = Some(accent);
60 self
61 }
62
63 pub fn max_lines(mut self, lines: usize) -> Self {
65 self.max_lines = Some(lines);
66 self
67 }
68}
69
70impl RenderOnce for Markdown {
71 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
72 let t = theme(cx);
73 let prose = window.text_style().font();
77 let base = t.font_size(self.size);
78 let text_color = t.text().hsla();
79 let dimmed = t.dimmed().hsla();
80 let border = t.border().hsla();
81 let accent = self
82 .accent
83 .map_or_else(|| t.primary(), |name| t.color(name, 6))
84 .hsla();
85 let code_bg = t.surface_hover().alpha(0.7);
86 let mut highlight_bg = t.color(ColorName::Yellow, 3).hsla();
87 highlight_bg.a = 0.45;
88
89 let prose_family = prose.family.clone();
93 let mono_family: SharedString = MONO_FAMILY.into();
94 let colors = RowColors {
95 text: text_color,
96 dimmed,
97 border,
98 code_bg,
99 };
100
101 let mut state = DocState::default();
102 let mut column = div().flex().flex_col().w_full();
103 let lines = self
104 .source
105 .lines()
106 .take(self.max_lines.unwrap_or(usize::MAX));
107
108 for line in lines {
109 let block = classify(line, &mut state);
110 let row = plan(line, &block, None, false);
116 let row_metrics = metrics(&row.kind);
117 let size = base * row_metrics.scale;
118 let weight = if matches!(row.kind, RowKind::Heading(_)) {
119 FontWeight::BOLD
120 } else {
121 prose.weight
122 };
123
124 let runs: Vec<TextRun> = row
125 .runs
126 .iter()
127 .filter(|run| run.len > 0)
128 .map(|run| {
129 let s = run.style;
130 let mono = s.code || matches!(row.kind, RowKind::Code { .. });
131 TextRun {
132 len: run.len,
133 font: Font {
134 family: if mono {
135 mono_family.clone()
136 } else {
137 prose_family.clone()
138 },
139 weight: if s.bold { FontWeight::BOLD } else { weight },
140 style: if s.italic {
141 FontStyle::Italic
142 } else {
143 prose.style
144 },
145 ..prose.clone()
146 },
147 color: if run.marker || run.dim {
148 dimmed
149 } else if s.link {
150 accent
151 } else {
152 text_color
153 },
154 background_color: if s.highlight {
155 Some(highlight_bg)
156 } else if s.code && !mono_block(&row.kind) {
157 Some(code_bg)
158 } else {
159 None
160 },
161 underline: (s.link && !run.marker).then(|| UnderlineStyle {
162 thickness: px(1.0),
163 color: Some(accent),
164 wavy: false,
165 }),
166 strikethrough: s.strike.then(|| StrikethroughStyle {
167 thickness: px(1.0),
168 color: Some(dimmed),
169 }),
170 }
171 })
172 .collect();
173
174 column = column.child(row_element(row, runs, size, base, &colors));
175 }
176 column.probe("Markdown")
177 }
178}
179
180struct RowColors {
183 text: gpui::Hsla,
184 dimmed: gpui::Hsla,
185 border: gpui::Hsla,
186 code_bg: gpui::Hsla,
187}
188
189fn row_element(
192 row: RowPlan,
193 runs: Vec<TextRun>,
194 size: f32,
195 base: f32,
196 colors: &RowColors,
197) -> gpui::Div {
198 let m = metrics(&row.kind);
199 let text = StyledText::new(SharedString::from(row.visible)).with_runs(runs);
202
203 let mut line = div()
204 .flex()
205 .flex_row()
206 .items_start()
207 .w_full()
208 .text_size(px(size))
209 .pt(px(base * m.pad_top))
210 .pb(px(base * m.pad_bottom));
211
212 match &row.kind {
213 RowKind::Blank => return div().h(px(base * 0.6)),
216 RowKind::Rule => {
217 return div()
218 .py(px(base * 0.5))
219 .child(div().w_full().h(px(1.0)).bg(colors.border))
220 }
221 RowKind::Fence { .. } | RowKind::FrontMatter => return div(),
223 RowKind::Code { .. } => {
224 return div()
225 .w_full()
226 .px(px(base * 0.6))
227 .bg(colors.code_bg)
228 .text_size(px(size))
229 .child(text);
230 }
231 RowKind::Quote { depth } => {
232 for _ in 0..(*depth).max(1) {
233 line = line.child(
234 div()
235 .flex_none()
236 .w(px(2.0))
237 .h(px(base * 1.4))
238 .mr(px(base * 0.6))
239 .bg(colors.border),
240 );
241 }
242 }
243 RowKind::Bullet { cols } => {
244 line = line
245 .pl(px(*cols as f32 * base * 0.5))
246 .child(marker(base, colors.dimmed, "\u{2022}"));
247 }
248 RowKind::Ordered { cols, number } => {
249 line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
250 base,
251 colors.dimmed,
252 format!("{number}."),
253 ));
254 }
255 RowKind::Task { cols, checked } => {
256 let glyph = if *checked { "\u{2611}" } else { "\u{2610}" };
257 line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
258 base,
259 if *checked { colors.dimmed } else { colors.text },
260 glyph,
261 ));
262 }
263 RowKind::Heading(_) | RowKind::Paragraph | RowKind::Table => {}
264 }
265
266 line.child(div().flex_1().min_w(px(0.0)).child(text))
267}
268
269fn marker(base: f32, color: gpui::Hsla, glyph: impl Into<SharedString>) -> gpui::Div {
271 div()
272 .flex_none()
273 .min_w(px(base * 1.1))
274 .mr(px(base * 0.4))
275 .text_color(color)
276 .child(glyph.into())
277}
278
279fn mono_block(kind: &RowKind) -> bool {
282 matches!(kind, RowKind::Code { .. } | RowKind::Fence { .. })
283}