use std::{cell::RefCell, ops::Range, rc::Rc};
use gpui::{
AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString, StrikethroughStyle,
StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window, canvas, div, font,
img, point, prelude::*, px, quad, size,
};
use theme::{TextStyle, Theme, Typeset};
use crate::{
block,
doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
layout::Layout,
preview,
select::{Cursor, Selection},
typography::Typography,
};
const BLOCK_GAP: f32 = 12.0;
const LIST_GAP: f32 = 4.0;
const INDENT_WIDTH: f32 = 22.0;
const MARKER_WIDTH: f32 = 18.0;
const MARKER_GAP: f32 = 8.0;
const CODE_PADDING_X: f32 = 12.0;
const CODE_PADDING_Y: f32 = 10.0;
pub const PLAIN_LANGUAGE: &str = "Plain";
const CARET_WIDTH: f32 = 1.5;
const INLINE_CODE_RADIUS: f32 = 4.5;
const INLINE_CODE_PAD_X: f32 = 2.0;
const INLINE_CODE_INSET_Y: f32 = 2.0;
const CHIP_PAD_X: f32 = 4.0;
const CHIP_INSET_Y: f32 = 1.0;
const CHIP_BLOCK_PAD_X: f32 = 8.0;
const CHIP_BLOCK_PAD_Y: f32 = 3.0;
const CHIP_ICON: f32 = 15.0;
const CARD_HEIGHT: f32 = 116.0;
const CARD_IMAGE_WIDTH: f32 = 180.0;
const CARD_COVER_HEIGHT: f32 = 200.0;
const CARD_PADDING: f32 = 14.0;
const CARD_BORDER: f32 = 1.0;
const CARD_ICON: f32 = 16.0;
const CARD_COVER: f32 = 44.0;
const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
const CAPTION_GAP: f32 = 4.0;
const IMAGE_EMPTY: &str = "Add an image";
const CAPTION_HINT: &str = "Write a caption";
const TABLE_CELL_PADDING: f32 = 12.0;
const TABLE_DIVIDER: f32 = 1.0;
const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Caption {
#[default]
Shown,
Hidden,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum CopyButton {
#[default]
Shown,
Hidden,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Annotation {
#[default]
Open,
Resolved,
Active,
}
impl Annotation {
fn wash(self, theme: &Theme) -> Hsla {
match self {
Self::Open => theme.warning.opacity(0.20),
Self::Resolved => theme.warning.opacity(0.08),
Self::Active => theme.warning.opacity(0.38),
}
}
}
pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
#[derive(Clone)]
pub enum Toggle {
Handled(OnToggle),
HitTested,
}
#[derive(Clone)]
pub struct Editing<'a> {
pub selection: Option<Selection>,
pub caret_on: bool,
pub layouts: Option<&'a BlockLayouts>,
pub annotations: &'a [(Selection, Annotation)],
pub placeholder: Option<SharedString>,
pub caption: Caption,
pub typography: Option<Typography>,
pub toggle: Option<Toggle>,
pub copy: CopyButton,
}
impl Default for Editing<'_> {
fn default() -> Self {
Self {
selection: None,
caret_on: true,
layouts: None,
annotations: &[],
placeholder: None,
caption: Caption::default(),
typography: None,
toggle: None,
copy: CopyButton::default(),
}
}
}
#[derive(Clone, Default)]
pub struct BlockLayouts(Rc<RefCell<Frames>>);
#[derive(Default)]
struct Frames {
texts: Vec<Painted>,
blocks: Vec<(usize, Bounds<Pixels>)>,
languages: Vec<(usize, Bounds<Pixels>)>,
pictures: Vec<(usize, Bounds<Pixels>)>,
checkboxes: Vec<(usize, Bounds<Pixels>)>,
}
struct Painted {
block: usize,
part: Part,
range: Range<usize>,
layout: TextLayout,
}
impl BlockLayouts {
pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
let entries = &self.0.borrow().texts;
let cursor = |painted: &Painted| {
let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
Cursor::new(
painted.block,
painted.part,
painted.range.start + offset.min(painted.range.len()),
)
};
if let Some(painted) = entries
.iter()
.find(|painted| painted.layout.bounds().contains(&point))
{
return Some(cursor(painted));
}
entries
.iter()
.min_by_key(|painted| {
let bounds = painted.layout.bounds();
let above = (bounds.origin.y - point.y).abs();
let below = (bounds.origin.y + bounds.size.height - point.y).abs();
f32::from(above.min(below)) as i64
})
.map(cursor)
}
pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
let entries = &self.0.borrow().texts;
let painted = entries.iter().find(|painted| {
painted.block == at.block
&& painted.part == at.part
&& painted.range.start <= at.offset
&& at.offset <= painted.range.end
})?;
let point = painted
.layout
.position_for_index(at.offset - painted.range.start)?;
Some((point, painted.layout.line_height()))
}
pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
let (start, end) = selection.ordered();
self.0
.borrow()
.texts
.iter()
.filter_map(|painted| {
let here = Cursor::new(painted.block, painted.part, 0);
let (from, to) = (
Cursor::new(start.block, start.part, 0),
Cursor::new(end.block, end.part, 0),
);
if here < from || here > to {
return None;
}
let len = painted.range.len();
let first = if here == from { start.offset } else { 0 };
let last = if here == to { end.offset } else { usize::MAX };
let range = first.saturating_sub(painted.range.start).min(len)
..last.saturating_sub(painted.range.start).min(len);
(range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
})
.flatten()
.collect()
}
pub fn step_row(
&self,
at: Cursor,
from: Point<Pixels>,
down: bool,
) -> Option<(Cursor, Pixels)> {
let entries = &self.0.borrow().texts;
let ix = entries.iter().position(|painted| {
painted.block == at.block
&& painted.part == at.part
&& painted.range.start <= at.offset
&& at.offset <= painted.range.end
})?;
let here = &entries[ix];
let line = here.layout.line_height();
let index_at = |painted: &Painted, y: Pixels| {
let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
(
Cursor::new(
painted.block,
painted.part,
painted.range.start + offset.min(painted.range.len()),
),
y,
)
};
let bounds = here.layout.bounds();
let target = if down { from.y + line } else { from.y - line };
if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
return Some(index_at(here, target));
}
let next = match down {
true => entries.get(ix + 1)?,
false => entries.get(ix.checked_sub(1)?)?,
};
let bounds = next.layout.bounds();
let row = match down {
true => bounds.origin.y,
false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
};
Some(index_at(next, row))
}
pub fn over_text(&self, point: Point<Pixels>) -> bool {
self.0
.borrow()
.texts
.iter()
.any(|painted| painted.layout.bounds().contains(&point))
}
pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
let blocks = &self.0.borrow().blocks;
blocks
.iter()
.find(|(_, bounds)| bounds.contains(&point))
.or_else(|| {
blocks.iter().min_by_key(|(_, bounds)| {
let above = (bounds.origin.y - point.y).abs();
let below = (bounds.origin.y + bounds.size.height - point.y).abs();
f32::from(above.min(below)) as i64
})
})
.map(|(ix, _)| *ix)
}
pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
let texts = &self.0.borrow().texts;
let painted = texts.iter().find(|painted| painted.block == ix)?;
Some((
painted.layout.bounds().origin.y,
painted.layout.line_height(),
))
}
pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0
.borrow()
.blocks
.iter()
.find(|(block, _)| *block == ix)
.map(|(_, bounds)| *bounds)
}
pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0
.borrow()
.languages
.iter()
.find(|(block, _)| *block == ix)
.map(|(_, bounds)| *bounds)
}
pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0
.borrow()
.pictures
.iter()
.find(|(block, _)| *block == ix)
.map(|(_, bounds)| *bounds)
}
pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0
.borrow()
.checkboxes
.iter()
.find(|(block, _)| *block == ix)
.map(|(_, bounds)| *bounds)
}
fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
self.0.borrow_mut().texts.push(Painted {
block,
part,
range,
layout,
});
}
fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
self.0.borrow_mut().blocks.push((ix, bounds));
}
fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
self.0.borrow_mut().languages.push((ix, bounds));
}
fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
self.0.borrow_mut().pictures.push((ix, bounds));
}
fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
self.0.borrow_mut().checkboxes.push((ix, bounds));
}
fn clear(&self) {
let mut frames = self.0.borrow_mut();
frames.texts.clear();
frames.blocks.clear();
frames.languages.clear();
frames.pictures.clear();
frames.checkboxes.clear();
}
}
#[derive(Clone, Copy)]
struct Overlay<'a> {
block: usize,
part: Part,
selection: Option<Selection>,
caret_on: bool,
layouts: Option<&'a BlockLayouts>,
annotations: &'a [(Selection, Annotation)],
placeholder: Option<&'a SharedString>,
caption: Caption,
toggle: Option<&'a Toggle>,
copy: CopyButton,
}
impl<'a> Overlay<'a> {
fn at(self, part: Part) -> Self {
Self { part, ..self }
}
fn here(&self) -> Cursor {
Cursor::new(self.block, self.part, 0)
}
fn caret_painted(&self) -> Option<usize> {
self.caret_on.then(|| self.caret()).flatten()
}
fn caret(&self) -> Option<usize> {
self.selection
.map(|selection| selection.head)
.filter(|head| head.block == self.block && head.part == self.part)
.map(|head| head.offset)
}
fn selected(&self, len: usize) -> Option<Range<usize>> {
self.clip(self.selection?, len)
}
fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
self.annotations
.iter()
.filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
.collect()
}
fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
if selection.is_collapsed() {
return None;
}
let (start, end) = selection.ordered();
let here = self.here();
let (first, last) = (
Cursor::new(start.block, start.part, 0),
Cursor::new(end.block, end.part, 0),
);
if here < first || here > last {
return None;
}
let from = if here == first { start.offset } else { 0 };
let to = if here == last { end.offset } else { len };
(from < to).then_some(from..to.min(len))
}
fn covers_block(&self) -> bool {
let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
return false;
};
let (start, end) = selection.ordered();
start.block < self.block && self.block < end.block
}
}
pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
let doc = crate::parse_with(source, &crate::Marks::of(cx));
render(&doc, Caption::default(), window, cx)
}
pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
render_with(
doc,
Editing {
caption,
..Editing::default()
},
window,
cx,
)
}
pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
let Editing {
selection,
caret_on,
layouts,
annotations,
placeholder,
caption,
typography,
toggle,
copy,
} = editing;
let reset = layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
.absolute()
.size(px(0.0))
});
let theme = Theme::of(cx).clone();
let typography = typography.unwrap_or_else(|| Typography::of(cx));
let mut column = div().flex().flex_col().children(reset);
for (ix, block) in doc.blocks.iter().enumerate() {
let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
None => 0.0,
Some(previous) if tight(previous, block) => LIST_GAP,
Some(_) => BLOCK_GAP,
};
let overlay = Overlay {
block: ix,
part: Part::Body,
selection,
caret_on,
layouts,
annotations,
placeholder: placeholder.as_ref(),
caption,
toggle: toggle.as_ref(),
copy,
};
let frame = layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_block(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
});
column = column.child(
div()
.mt(px(gap))
.pl(px(block.indent as f32 * INDENT_WIDTH))
.child(
div()
.w_full()
.relative()
.children(frame)
.when(overlay.covers_block() && block.opaque(), |el| {
el.rounded(px(4.0)).bg(theme.selection)
})
.child(block_element(
block,
overlay,
&typography,
&theme,
window,
cx,
)),
),
);
}
column.into_any_element()
}
fn tight(previous: &Block, next: &Block) -> bool {
let marker = |block: &Block| {
matches!(
block.kind,
BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
)
};
marker(previous) && (marker(next) || next.indent > previous.indent)
}
fn block_element(
block: &Block,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let body = overlay.at(Part::Body);
match &block.kind {
BlockKind::Paragraph(text) => text_element(
text,
typography.body.size(),
typography.body.line_height(),
FontWeight::NORMAL,
body,
theme,
cx,
),
BlockKind::Heading { level, text } => {
let heading = typography.heading(*level);
text_element(
text,
heading.size(),
heading.line_height(),
heading.weight,
body,
theme,
cx,
)
}
BlockKind::Bullet(text) => {
marker_row(disc(typography, theme), text, body, typography, theme, cx)
}
BlockKind::Ordered { number, text } => marker_row(
div()
.flex_none()
.w(px(MARKER_WIDTH))
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.text_color(theme.text_muted)
.child(SharedString::from(format!("{number}.")))
.into_any_element(),
text,
body,
typography,
theme,
cx,
),
BlockKind::Task { checked, text } => marker_row(
checkbox(*checked, overlay, typography, theme),
text,
body,
typography,
theme,
cx,
),
BlockKind::Quote { kind, text } => div()
.border_l_2()
.border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
.pl(px(12.0))
.pr(px(10.0))
.py(px(2.0))
.text_color(theme.text_muted)
.children(kind.map(|kind| {
div()
.pb(px(2.0))
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.font_weight(FontWeight::SEMIBOLD)
.text_color(alert_color(kind, theme))
.child(kind.label())
}))
.child(text_element(
text,
typography.body.size(),
typography.body.line_height(),
FontWeight::NORMAL,
body,
theme,
cx,
))
.into_any_element(),
BlockKind::Code { language, code } => {
let overlay = overlay.at(Part::Code);
let painted = overlay
.caret()
.is_none()
.then(|| block::render(language.as_deref(), &code.text, window, cx))
.flatten();
match painted {
Some(element) => div()
.when(overlay.covers_block(), |el| {
el.rounded(px(4.0)).bg(theme.selection)
})
.child(element)
.into_any_element(),
None => code_block(
language.as_deref(),
&code.text,
overlay,
typography,
theme,
window,
cx,
),
}
}
BlockKind::Image { url, alt, width } => {
image(url, alt, *width, overlay, typography, theme, cx)
}
BlockKind::Bookmark { url, form } => {
bookmark(overlay.block, url, *form, typography, theme, cx)
}
BlockKind::Table {
align,
header,
rows,
} => table(align, header, rows, overlay, typography, theme, window, cx),
BlockKind::Rule => div()
.h(px(1.0))
.w_full()
.bg(theme.border)
.into_any_element(),
}
}
fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
div()
.flex_none()
.w(px(MARKER_WIDTH))
.h(px(typography.body.line_height()))
.flex()
.items_center()
.child(
div()
.ml(px(1.0))
.w(px(5.0))
.h(px(5.0))
.rounded_full()
.bg(theme.text_faint),
)
.into_any_element()
}
fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
let ix = overlay.block;
let mut box_ = div()
.relative()
.w(px(13.0))
.h(px(13.0))
.rounded(px(3.5))
.border_1()
.flex()
.items_center()
.justify_center();
box_ = if checked {
box_.bg(theme.solid)
.border_color(theme.solid)
.text_style(TextStyle::Caption)
.text_color(theme.on_solid)
.child("✓")
} else {
box_.border_color(theme.border_strong)
};
box_ = box_.children(overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_checkbox(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
}));
if overlay.toggle.is_some() {
box_ = box_.cursor_pointer();
}
if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
cx.stop_propagation();
toggle(ix, window, cx);
});
}
div()
.flex_none()
.w(px(MARKER_WIDTH))
.h(px(typography.body.line_height()))
.flex()
.items_center()
.child(box_)
.into_any_element()
}
fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
match kind {
QuoteKind::Note => theme.accent,
QuoteKind::Tip => theme.success,
QuoteKind::Important => theme.busy,
QuoteKind::Warning => theme.warning,
QuoteKind::Caution => theme.danger,
}
}
fn marker_row(
marker: AnyElement,
text: &Text,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> AnyElement {
div()
.flex()
.flex_row()
.gap(px(MARKER_GAP))
.child(marker)
.child(div().flex_1().min_w_0().child(text_element(
text,
typography.body.size(),
typography.body.line_height(),
FontWeight::NORMAL,
overlay,
theme,
cx,
)))
.into_any_element()
}
pub struct Flat {
pub text: SharedString,
pub runs: Vec<TextRun>,
pub links: Vec<(Range<usize>, String)>,
pub code: Vec<Range<usize>>,
pub chips: Vec<Range<usize>>,
}
pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
flatten_with(text, base_weight, theme, |_| None)
}
pub fn flatten_with(
text: &Text,
base_weight: FontWeight,
theme: &Theme,
paint: impl Fn(&str) -> Option<crate::MarkPaint>,
) -> Flat {
let mut cuts: Vec<usize> = text
.marks
.iter()
.flat_map(|span| [span.range.start, span.range.end])
.chain([0, text.text.len()])
.filter(|cut| *cut <= text.text.len())
.collect();
cuts.sort_unstable();
cuts.dedup();
let mut runs = Vec::new();
let mut links: Vec<(Range<usize>, String)> = Vec::new();
let mut code: Vec<Range<usize>> = Vec::new();
let mut chips: Vec<Range<usize>> = Vec::new();
for pair in cuts.windows(2) {
let (start, end) = (pair[0], pair[1]);
let covering = text
.marks
.iter()
.filter(|span| span.range.start <= start && span.range.end >= end);
let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
let mut chip = false;
let mut link = None;
let mut custom = crate::MarkPaint::default();
for span in covering {
match &span.mark {
Mark::Bold => bold = true,
Mark::Italic => italic = true,
Mark::Strike => strike = true,
Mark::Code => mono = true,
Mark::Mention { url, .. } => {
chip = true;
link = Some(url.clone());
}
Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
Mark::Custom(name) => {
let Some(painted) = paint(name) else { continue };
custom.color = painted.color.or(custom.color);
custom.background = painted.background.or(custom.background);
custom.weight = painted.weight.or(custom.weight);
custom.italic |= painted.italic;
custom.underline |= painted.underline;
custom.strikethrough |= painted.strikethrough;
}
}
}
let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
if mono {
match code.last_mut() {
Some(range) if range.end == start => range.end = end,
_ => code.push(start..end),
}
}
if chip {
match chips.last_mut() {
Some(range) if range.end == start => range.end = end,
_ => chips.push(start..end),
}
}
if let Some(url) = &link {
match links.last_mut() {
Some((range, last)) if range.end == start && last == url => range.end = end,
_ => links.push((start..end, url.clone())),
}
}
let mut face = font(if mono {
theme.font_mono.clone()
} else {
theme.font_body.clone()
});
face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
FontWeight::SEMIBOLD
} else {
custom.weight.unwrap_or(base_weight)
};
face.style = if italic {
FontStyle::Italic
} else {
FontStyle::Normal
};
runs.push(TextRun {
len: end - start,
font: face,
color: match (mono, custom.color) {
(_, Some(color)) => color,
(true, None) => theme.code_text,
(false, None) => theme.text,
},
background_color: custom.background,
underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
color: Some(theme.text_muted),
thickness: px(1.0),
wavy: false,
}),
strikethrough: strike.then_some(StrikethroughStyle {
thickness: px(1.0),
color: Some(theme.text_muted),
}),
});
}
Flat {
text: text.text.clone().into(),
runs,
links,
code,
chips,
}
}
fn text_element(
text: &Text,
size: f32,
line_height: f32,
weight: FontWeight,
overlay: Overlay,
theme: &Theme,
cx: &App,
) -> AnyElement {
let flat = flatten_with(text, weight, theme, |name| {
crate::marks::paint_of(cx, name, theme)
});
painted_text(flat, text.text.len(), size, line_height, overlay, theme)
}
fn painted_text(
flat: Flat,
len: usize,
size: f32,
line_height: f32,
overlay: Overlay,
theme: &Theme,
) -> AnyElement {
let (ix, part) = (overlay.block, overlay.part);
let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
let span = 0..len;
let hint = overlay
.placeholder
.filter(|_| len == 0 && overlay.caret().is_some())
.map(|hint| {
div()
.absolute()
.text_color(theme.text_faint)
.child(hint.clone())
});
let styled = StyledText::new(flat.text).with_runs(flat.runs);
let layout = styled.layout().clone();
let painted: AnyElement = if flat.links.is_empty() {
styled.into_any_element()
} else {
let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
.on_click(ranges, move |clicked, _window, cx| {
if let Some(url) = urls.get(clicked) {
cx.open_url(url);
}
})
.into_any_element()
};
let wash = theme.code_wash;
let code_ranges = flat.code;
let chip_wash = theme.element_hover;
let chip_edge = theme.border;
let chip_ranges = flat.chips;
let caret_color = theme.caret;
let selection_color = theme.selection;
let annotated = overlay.annotated(len, theme);
let layouts = overlay.layouts.cloned();
let underlay = canvas(
|_, _, _| (),
move |_, _, window, _| {
if let Some(layouts) = &layouts {
layouts.record(ix, part, span.clone(), layout.clone());
}
for (range, wash) in &annotated {
for rect in range_rects(&layout, range, 0.0, 0.0) {
window.paint_quad(quad(
rect,
px(2.0),
*wash,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
if let Some(range) = &selected {
for rect in range_rects(&layout, range, 0.0, 0.0) {
window.paint_quad(quad(
rect,
px(2.0),
selection_color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
if let Some(offset) = caret
&& let Some(head) = layout.position_for_index(offset)
{
window.paint_quad(quad(
caret_quad(head, size, layout.line_height()),
px(0.0),
caret_color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
for range in &code_ranges {
for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
window.paint_quad(quad(
rect,
px(INLINE_CODE_RADIUS),
wash,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
for range in &chip_ranges {
for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
window.paint_quad(quad(
rect,
px(Theme::control_radius()),
chip_wash,
px(1.0),
chip_edge,
BorderStyle::Solid,
));
}
}
},
)
.absolute()
.size_full();
div()
.text_size(px(size))
.line_height(px(line_height))
.relative()
.child(underlay)
.children(hint)
.child(painted)
.into_any_element()
}
fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
let inset = (line_height - px(size)) / 2.0;
Bounds::new(
head + point(px(0.0), inset),
gpui::size(px(CARET_WIDTH), px(size)),
)
}
fn range_rects(
layout: &gpui::TextLayout,
range: &Range<usize>,
pad_x: f32,
inset_y: f32,
) -> Vec<Bounds<Pixels>> {
let mut rects = Vec::new();
let line_height = layout.line_height();
let mut origin = layout.bounds().origin;
let mut line_start = 0;
for line in layout.line_layouts() {
let shaped = &line.unwrapped_layout;
let row_ends = line
.wrap_boundaries()
.iter()
.map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
.chain([line.len()]);
let mut row_start = 0;
for (row, row_end) in row_ends.enumerate() {
let from = range
.start
.saturating_sub(line_start)
.clamp(row_start, row_end);
let to = range.end.saturating_sub(line_start).min(row_end);
let row_x = shaped.x_for_index(row_start);
let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
if from < to && right > left {
rects.push(Bounds::new(
origin
+ point(
left - row_x - px(pad_x),
line_height * row as f32 + px(inset_y),
),
size(
right - left + px(2.0 * pad_x),
line_height - px(2.0 * inset_y),
),
));
}
row_start = row_end;
}
origin.y += line.size(line_height).height;
line_start += line.len() + 1;
}
rects
}
pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
let Editing {
selection,
caret_on,
layouts,
annotations,
typography,
..
} = editing;
let reset = layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
.absolute()
.size(px(0.0))
});
let theme = Theme::of(cx).clone();
let typography = typography.unwrap_or_else(|| Typography::of(cx));
let overlay = Overlay {
block: 0,
part: Part::Code,
selection,
caret_on,
layouts,
annotations,
placeholder: None,
caption: Caption::default(),
toggle: None,
copy: CopyButton::Hidden,
};
let (underlay, lines) = code_lines(
Some(crate::source::LANGUAGES[0]),
code,
overlay,
&typography,
&theme,
cx,
);
let style = crate::SourceStyle::of(cx);
let digits = lines.len().to_string().len().max(style.gutter_min_digits);
let gap = style.gutter_gap.max(0.0) * typography.code.size();
let gutter_width = digits as f32 * typography.code.size() + gap;
let lines = lines
.into_iter()
.enumerate()
.map(|(index, line)| {
if !style.line_numbers {
return line;
}
div()
.flex()
.items_start()
.child(
div()
.w(px(gutter_width))
.flex_shrink_0()
.pr(px(gap))
.font_family(theme.font_mono.clone())
.text_color(style.gutter_color.unwrap_or(theme.text_faint))
.text_right()
.child((index + 1).to_string()),
)
.child(div().flex_1().min_w_0().child(line))
.into_any_element()
})
.collect();
div()
.flex()
.flex_col()
.children(reset)
.child(code_body(0, underlay, lines, &typography, true))
.into_any_element()
}
fn code_lines(
language: Option<&str>,
code: &str,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> (AnyElement, Vec<AnyElement>) {
let ix = overlay.block;
let spans = crate::highlight::spans(cx, language, code).or_else(|| {
language
.filter(|language| crate::source::is_markdown(language))
.map(|_| crate::source::spans(code))
});
let mono = font(theme.font_mono.clone());
let run = |len: usize, color: Hsla| TextRun {
len,
font: mono.clone(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
let mut offset = 0usize;
let lines: Vec<AnyElement> = code
.split('\n')
.map(|line| {
let start = offset;
offset += line.len() + 1;
let mut runs = Vec::new();
let mut pos = 0usize;
if let Some(spans) = &spans {
let end = start + line.len();
for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
let s = range.start.clamp(start, end) - start;
let e = range.end.min(end) - start;
if s > pos {
runs.push(run(s - pos, theme.text));
}
runs.push(run(e - s, theme.syntax.color(*kind)));
pos = e;
}
}
if pos < line.len() {
runs.push(run(line.len() - pos, theme.text));
}
if runs.is_empty() {
runs.push(run(0, theme.text));
}
let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
rows.push((start..start + line.len(), styled.layout().clone()));
styled.into_any_element()
})
.collect();
let caret = overlay.caret_painted();
let selected = overlay.selected(code.len());
let sink = overlay.layouts.cloned();
let code_size = typography.code.size();
let annotated = overlay.annotated(code.len(), theme);
let (caret_color, selection_color) = (theme.caret, theme.selection);
let underlay = canvas(
|_, _, _| (),
move |_, _, window, _| {
for (span, layout) in &rows {
if let Some(sink) = &sink {
sink.record(ix, Part::Code, span.clone(), layout.clone());
}
for (range, wash) in &annotated {
let (from, to) = (range.start.max(span.start), range.end.min(span.end));
if from < to {
for rect in
range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
{
window.paint_quad(quad(
rect,
px(2.0),
*wash,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
}
if let Some(range) = &selected {
let (from, to) = (range.start.max(span.start), range.end.min(span.end));
if from < to {
for rect in
range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
{
window.paint_quad(quad(
rect,
px(2.0),
selection_color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
}
if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
&& let Some(head) = layout.position_for_index(offset - span.start)
{
window.paint_quad(quad(
caret_quad(head, code_size, layout.line_height()),
px(0.0),
caret_color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
},
)
.absolute()
.size_full();
(underlay.into_any_element(), lines)
}
fn code_block(
language: Option<&str>,
code: &str,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let ix = overlay.block;
let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
div()
.rounded(px(Theme::panel_radius()))
.bg(theme.ink(0.035))
.border_1()
.border_color(theme.border)
.overflow_hidden()
.relative()
.child(
div()
.relative()
.flex()
.flex_row()
.items_center()
.px(px(CODE_PADDING_X))
.py(px(5.0))
.border_b_1()
.border_color(theme.border)
.bg(theme.ink(0.02))
.text_style(TextStyle::Subheadline)
.text_color(match language {
Some(_) => theme.text_muted,
None => theme.text_faint,
})
.child(
div()
.relative()
.children(overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_language(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
}))
.child(SharedString::from(
language.unwrap_or(PLAIN_LANGUAGE).to_string(),
)),
),
)
.child(body)
.children(
(overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
)
.into_any_element()
}
fn code_body(
ix: usize,
underlay: AnyElement,
lines: Vec<AnyElement>,
typography: &Typography,
wrap: bool,
) -> AnyElement {
let column = div()
.flex()
.flex_col()
.px(px(CODE_PADDING_X))
.children(lines);
let body = div()
.id(ElementId::named_usize("md-code", ix))
.relative()
.py(px(CODE_PADDING_Y))
.text_size(px(typography.code.size()))
.line_height(px(typography.code.line_height()))
.child(underlay);
if wrap {
body.child(column.w_full()).into_any_element()
} else {
ui::scroll::Viewport::new(
format!("md-code-scroll-{ix}"),
body.flex()
.flex_row()
.whitespace_nowrap()
.child(column.items_start()),
gpui::Axis::Horizontal,
)
.into_any_element()
}
}
fn copy_button(
code: &str,
ix: usize,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
let showing = *copied.read(cx);
let text: SharedString = code.to_string().into();
div()
.id(ElementId::named_usize("md-copy", ix))
.absolute()
.top(px(3.0))
.right(px(5.0))
.h(px(20.0))
.px(px(6.0))
.rounded(px(5.0))
.flex()
.items_center()
.cursor_pointer()
.text_style(TextStyle::Caption)
.text_color(theme.text_muted)
.hover(|el| el.bg(theme.element_hover))
.child(if showing { "Copied" } else { "Copy" })
.on_click({
let copied = copied.clone();
move |_, _, cx| {
cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
copied.update(cx, |state, cx| {
*state = true;
cx.notify();
});
}
})
.on_hover(move |hovering, _, cx| {
if !*hovering && *copied.read(cx) {
copied.update(cx, |state, cx| {
*state = false;
cx.notify();
});
}
})
.into_any_element()
}
fn image(
url: &str,
alt: &Text,
width: Option<u32>,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> AnyElement {
let hint = SharedString::new_static(CAPTION_HINT);
let overlay = Overlay {
placeholder: Some(&hint),
..overlay.at(Part::Caption)
};
let picture = if url.is_empty() {
div()
.h(px(IMAGE_EMPTY_HEIGHT))
.flex()
.items_center()
.px(px(CARD_PADDING))
.rounded(px(Theme::button_radius()))
.border_1()
.border_dashed()
.border_color(theme.border)
.text_size(px(typography.body.size()))
.text_color(theme.text_muted)
.child(IMAGE_EMPTY)
} else {
let picture = match url.contains("://") {
true => img(SharedString::from(url.to_string())),
false => img(std::path::PathBuf::from(url)),
};
let box_ = div()
.relative()
.rounded(px(Theme::button_radius()))
.overflow_hidden()
.border_1()
.border_color(theme.border)
.children(overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
let ix = overlay.block;
canvas(
move |bounds, _, _| layouts.record_picture(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
}));
match width {
Some(width) => box_
.self_start()
.max_w_full()
.w(px(width as f32))
.child(picture.w(px(width as f32)).max_w_full()),
None => box_.child(picture.max_w_full()),
}
};
div()
.flex()
.flex_col()
.gap(px(CAPTION_GAP))
.child(picture)
.when(
overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
|el| {
el.child(text_element(
alt,
typography.caption.size(),
typography.caption.line_height(),
FontWeight::NORMAL,
overlay,
theme,
cx,
))
},
)
.into_any_element()
}
fn bookmark(
ix: usize,
url: &str,
form: Form,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> AnyElement {
let preview = preview::of(cx, url).unwrap_or_default();
let host = SharedString::from(preview::host(url).to_string());
let label = preview.label.clone().unwrap_or_else(|| host.clone());
let title = preview
.title
.clone()
.unwrap_or_else(|| SharedString::from(url.to_string()));
let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
let site = host.clone();
let mark = move |size: f32| {
let host = site.clone();
match icon.clone() {
Some(icon) => img(icon)
.size(px(size))
.rounded(px(size / 4.0))
.with_fallback(move || initial(&host, size, muted, wash))
.into_any_element(),
None => initial(&host, size, muted, wash),
}
};
if form == Form::Chip {
let open = url.to_string();
let pill = div()
.id(ElementId::named_usize("md-chip", ix))
.flex()
.flex_row()
.items_center()
.gap(px(6.0))
.px(px(CHIP_BLOCK_PAD_X))
.py(px(CHIP_BLOCK_PAD_Y))
.rounded(px(Theme::control_radius()))
.border_1()
.border_color(theme.border)
.bg(theme.element_hover)
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.text_color(theme.text)
.cursor(CursorStyle::PointingHand)
.hover(|el| el.bg(theme.element_active))
.on_click(move |_, _, cx| cx.open_url(&open))
.child(mark(CHIP_ICON))
.child(
div()
.min_w_0()
.truncate()
.child(preview.title.unwrap_or(label)),
);
return div().flex().flex_row().child(pill).into_any_element();
}
let words = div()
.flex()
.flex_col()
.min_w_0()
.px(px(CARD_PADDING))
.py(px(CARD_PADDING - 2.0))
.child(
div()
.truncate()
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.text_color(theme.text)
.child(title),
)
.children(preview.description.map(|blurb| {
div()
.line_clamp(2)
.text_size(px(typography.card.size()))
.line_height(px(typography.card.line_height()))
.text_color(theme.text_muted)
.child(blurb)
}))
.child(
div()
.mt_auto()
.pt(px(6.0))
.flex()
.items_center()
.gap(px(6.0))
.text_size(px(typography.card.size()))
.text_color(theme.text_muted)
.child(mark(CARD_ICON))
.child(div().truncate().child(label)),
);
let picture = corners(div(), form)
.bg(theme.surface)
.flex()
.items_center()
.justify_center()
.overflow_hidden()
.child(match preview.image {
Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
.with_fallback(move || mark(CARD_COVER))
.into_any_element(),
None => mark(CARD_COVER),
});
let open = url.to_string();
let card = div()
.id(ElementId::named_usize("md-bookmark", ix))
.flex()
.w_full()
.overflow_hidden()
.rounded(px(Theme::button_radius()))
.border(px(CARD_BORDER))
.border_color(theme.border)
.bg(theme.surface_card)
.cursor(CursorStyle::PointingHand)
.hover(|el| el.bg(theme.element_hover))
.on_click(move |_, _, cx| cx.open_url(&open));
if form == Form::Embed {
card.flex_col()
.child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
.child(words.w_full())
} else {
card.h(px(CARD_HEIGHT))
.child(words.flex_1())
.child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
}
.into_any_element()
}
fn corners<T: Styled>(element: T, form: Form) -> T {
let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
match form {
Form::Embed => element.rounded_t(corner),
_ => element.rounded_r(corner),
}
}
fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
div()
.flex_none()
.size(px(size))
.rounded(px(size / 4.0))
.bg(wash)
.flex()
.items_center()
.justify_center()
.text_size(px(size * 0.55))
.text_color(color)
.child(SharedString::from(
host.chars()
.next()
.unwrap_or('?')
.to_uppercase()
.to_string(),
))
.into_any_element()
}
#[expect(
clippy::too_many_arguments,
reason = "a table, its overlay, and what paints them"
)]
fn table(
align: &[Align],
header: &[Text],
rows: &[Vec<Text>],
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &App,
) -> AnyElement {
let ix = overlay.block;
let all: Vec<&[Text]> = std::iter::once(header)
.filter(|row| !row.is_empty())
.chain(rows.iter().map(|row| row.as_slice()))
.collect();
let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
if columns == 0 {
return gpui::Empty.into_any_element();
}
let has_header = !header.is_empty();
let text_system = window.text_system();
let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
let mut content = vec![0.0f32; columns];
for (r, row) in all.iter().enumerate() {
let weight = if has_header && r == 0 {
FontWeight::BOLD
} else {
FontWeight::NORMAL
};
let mut out = Vec::with_capacity(columns);
for (c, natural) in content.iter_mut().enumerate() {
let Some(cell) = row.get(c) else {
out.push(None);
continue;
};
let flat = flatten_with(cell, weight, theme, |name| {
crate::marks::paint_of(cx, name, theme)
});
if !flat.text.is_empty() {
let width = f32::from(
text_system
.shape_line(
flat.text.clone(),
px(typography.body.size()),
&flat.runs,
None,
)
.width(),
);
*natural = natural.max(width);
}
out.push(Some(flat));
}
flats.push(out);
}
let naturals: Vec<f32> = content
.iter()
.map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
.collect();
let minimums: Vec<f32> = naturals
.iter()
.map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
.collect();
let hairline = theme.hairline(0.10);
let mut inner = div()
.flex()
.flex_col()
.w_full()
.min_w(px(minimums.iter().sum::<f32>()));
for (r, row) in flats.into_iter().enumerate() {
if r > 0 {
inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
}
let mut row_el = div().flex().flex_row();
for (c, cell) in row.into_iter().enumerate() {
let mut cell_el = div()
.flex_grow(naturals[c])
.flex_shrink(naturals[c])
.flex_basis(px(0.0))
.min_w(px(minimums[c]))
.p(px(TABLE_CELL_PADDING))
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()));
cell_el = match align.get(c).copied().unwrap_or_default() {
Align::Left => cell_el,
Align::Center => cell_el.text_center(),
Align::Right => cell_el.text_right(),
};
if let Some(flat) = cell {
let row = if has_header { r } else { r + 1 };
let len = flat.text.len();
cell_el = cell_el.child(painted_text(
flat,
len,
typography.body.size(),
typography.body.line_height(),
overlay.at(Part::Cell { row, column: c }),
theme,
));
}
row_el = row_el.child(cell_el);
}
inner = inner.child(row_el);
}
ui::scroll::Viewport::new(
format!("md-table-scroll-{ix}"),
div()
.id(ElementId::named_usize("md-table", ix))
.w_full()
.child(inner),
gpui::Axis::Horizontal,
)
.into_any_element()
}