use gpui::prelude::*;
use gpui::{
div, px, App, Font, FontStyle, FontWeight, IntoElement, SharedString, StrikethroughStyle,
StyledText, TextRun, UnderlineStyle, Window,
};
use super::block::{classify, DocState};
use super::layout::{metrics, plan, RowKind, RowPlan};
use crate::devtools::Probed;
use crate::style::MONO_FAMILY;
use crate::theme::{theme, ColorName, Size};
#[derive(IntoElement)]
pub struct Markdown {
source: SharedString,
size: Size,
accent: Option<ColorName>,
max_lines: Option<usize>,
}
impl Markdown {
pub fn new(source: impl Into<SharedString>) -> Self {
Markdown {
source: source.into(),
size: Size::Sm,
accent: None,
max_lines: None,
}
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn accent(mut self, accent: ColorName) -> Self {
self.accent = Some(accent);
self
}
pub fn max_lines(mut self, lines: usize) -> Self {
self.max_lines = Some(lines);
self
}
}
impl RenderOnce for Markdown {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let prose = window.text_style().font();
let base = t.font_size(self.size);
let text_color = t.text().hsla();
let dimmed = t.dimmed().hsla();
let border = t.border().hsla();
let accent = self
.accent
.map_or_else(|| t.primary(), |name| t.color(name, 6))
.hsla();
let code_bg = t.surface_hover().alpha(0.7);
let mut highlight_bg = t.color(ColorName::Yellow, 3).hsla();
highlight_bg.a = 0.45;
let prose_family = prose.family.clone();
let mono_family: SharedString = MONO_FAMILY.into();
let colors = RowColors {
text: text_color,
dimmed,
border,
code_bg,
};
let mut state = DocState::default();
let mut column = div().flex().flex_col().w_full();
let lines = self
.source
.lines()
.take(self.max_lines.unwrap_or(usize::MAX));
for line in lines {
let block = classify(line, &mut state);
let row = plan(line, &block, None, false);
let row_metrics = metrics(&row.kind);
let size = base * row_metrics.scale;
let weight = if matches!(row.kind, RowKind::Heading(_)) {
FontWeight::BOLD
} else {
prose.weight
};
let runs: Vec<TextRun> = row
.runs
.iter()
.filter(|run| run.len > 0)
.map(|run| {
let s = run.style;
let mono = s.code || matches!(row.kind, RowKind::Code { .. });
TextRun {
len: run.len,
font: Font {
family: if mono {
mono_family.clone()
} else {
prose_family.clone()
},
weight: if s.bold { FontWeight::BOLD } else { weight },
style: if s.italic {
FontStyle::Italic
} else {
prose.style
},
..prose.clone()
},
color: if run.marker || run.dim {
dimmed
} else if s.link {
accent
} else {
text_color
},
background_color: if s.highlight {
Some(highlight_bg)
} else if s.code && !mono_block(&row.kind) {
Some(code_bg)
} else {
None
},
underline: (s.link && !run.marker).then(|| UnderlineStyle {
thickness: px(1.0),
color: Some(accent),
wavy: false,
}),
strikethrough: s.strike.then(|| StrikethroughStyle {
thickness: px(1.0),
color: Some(dimmed),
}),
}
})
.collect();
column = column.child(row_element(row, runs, size, base, &colors));
}
column.probe("Markdown")
}
}
struct RowColors {
text: gpui::Hsla,
dimmed: gpui::Hsla,
border: gpui::Hsla,
code_bg: gpui::Hsla,
}
fn row_element(
row: RowPlan,
runs: Vec<TextRun>,
size: f32,
base: f32,
colors: &RowColors,
) -> gpui::Div {
let m = metrics(&row.kind);
let text = StyledText::new(SharedString::from(row.visible)).with_runs(runs);
let mut line = div()
.flex()
.flex_row()
.items_start()
.w_full()
.text_size(px(size))
.pt(px(base * m.pad_top))
.pb(px(base * m.pad_bottom));
match &row.kind {
RowKind::Blank => return div().h(px(base * 0.6)),
RowKind::Rule => {
return div()
.py(px(base * 0.5))
.child(div().w_full().h(px(1.0)).bg(colors.border))
}
RowKind::Fence { .. } | RowKind::FrontMatter => return div(),
RowKind::Code { .. } => {
return div()
.w_full()
.px(px(base * 0.6))
.bg(colors.code_bg)
.text_size(px(size))
.child(text);
}
RowKind::Quote { depth } => {
for _ in 0..(*depth).max(1) {
line = line.child(
div()
.flex_none()
.w(px(2.0))
.h(px(base * 1.4))
.mr(px(base * 0.6))
.bg(colors.border),
);
}
}
RowKind::Bullet { cols } => {
line = line
.pl(px(*cols as f32 * base * 0.5))
.child(marker(base, colors.dimmed, "\u{2022}"));
}
RowKind::Ordered { cols, number } => {
line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
base,
colors.dimmed,
format!("{number}."),
));
}
RowKind::Task { cols, checked } => {
let glyph = if *checked { "\u{2611}" } else { "\u{2610}" };
line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
base,
if *checked { colors.dimmed } else { colors.text },
glyph,
));
}
RowKind::Heading(_) | RowKind::Paragraph | RowKind::Table => {}
}
line.child(div().flex_1().min_w(px(0.0)).child(text))
}
fn marker(base: f32, color: gpui::Hsla, glyph: impl Into<SharedString>) -> gpui::Div {
div()
.flex_none()
.min_w(px(base * 1.1))
.mr(px(base * 0.4))
.text_color(color)
.child(glyph.into())
}
fn mono_block(kind: &RowKind) -> bool {
matches!(kind, RowKind::Code { .. } | RowKind::Fence { .. })
}