use crate::AppExt as _;
use std::{
cell::RefCell,
collections::HashMap,
hash::{DefaultHasher, Hash, Hasher},
ops::Range,
path::Path,
rc::Rc,
sync::Arc,
};
use ui::AppExt as _;
use ui::widgets::Layout as _;
use gpui::{
AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
ImageSource, 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, Run, Text, quote_runs},
preview,
select::{Affinity, Cursor, Selection},
typography::Typography,
};
mod code;
mod column;
mod layouts;
mod media;
pub use media::hover_bookmark;
mod table;
mod text;
pub use code::*;
use column::*;
pub use layouts::*;
use media::*;
use table::*;
pub use text::*;
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 MENTION_CARD_WIDTH: f32 = 320.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;
pub const TABLE_CONTROL_SIZE: f32 = 16.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)]
#[non_exhaustive]
pub enum Annotation {
#[default]
Open,
Resolved,
Active,
Highlight(crate::HighlightColor),
Match,
Current,
}
impl Annotation {
fn wash(
self,
theme: &Theme,
highlight: crate::HighlightPaint,
find: crate::FindPaint,
comment: crate::CommentPaint,
) -> Hsla {
match self {
Self::Open => comment(theme).open,
Self::Resolved => comment(theme).resolved,
Self::Active => comment(theme).active,
Self::Highlight(color) => highlight(color, theme),
Self::Match => find(theme).0,
Self::Current => find(theme).1,
}
}
}
pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
pub type OnImageMenu = Rc<dyn Fn(usize, Point<Pixels>, &mut Window, &mut App)>;
pub type OnJump = Rc<dyn Fn(usize, &mut Window, &mut App)>;
pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
#[derive(Clone)]
pub struct FenceHost {
pub rewrite: OnRewrite,
pub leave: OnLeave,
pub resize: Option<OnResize>,
}
pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
gpui::actions!(
markdown,
[
LeaveBlock
]
);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ImageOverlayCorner {
TopLeft,
TopRight,
BottomLeft,
#[default]
BottomRight,
}
#[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 image: Option<OnImage>,
pub image_menu: Option<OnImageMenu>,
pub image_overlay: Option<ImageOverlay>,
pub image_overlay_corner: ImageOverlayCorner,
pub fence: Option<FenceHost>,
pub sizing: Option<(usize, u32)>,
pub table_controls: bool,
pub copy: CopyButton,
pub base: Option<&'a Path>,
pub keep: &'a [usize],
pub scroll: Option<&'a gpui::ScrollHandle>,
pub jump: Option<OnJump>,
}
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,
image: None,
image_menu: None,
image_overlay: None,
image_overlay_corner: ImageOverlayCorner::BottomRight,
fence: None,
sizing: None,
table_controls: false,
copy: CopyButton::default(),
base: None,
keep: &[],
scroll: None,
jump: None,
}
}
}
#[derive(Clone, Copy)]
struct Overlay<'a> {
block: usize,
part: Part,
selection: Option<Selection>,
caret_on: bool,
caret_shape: ui::input::CaretShape,
caret_height: ui::input::CaretHeight,
caret_inactive: ui::input::InactiveCaret,
window_active: bool,
layouts: Option<&'a BlockLayouts>,
annotations: &'a [(Selection, Annotation)],
placeholder: Option<&'a SharedString>,
caption: Caption,
toggle: Option<&'a Toggle>,
image: Option<&'a OnImage>,
image_menu: Option<&'a OnImageMenu>,
image_overlay: Option<&'a ImageOverlay>,
image_overlay_corner: ImageOverlayCorner,
fence: Option<&'a FenceHost>,
sizing: Option<(usize, u32)>,
table_controls: bool,
copy: CopyButton,
base: Option<&'a Path>,
highlight: crate::HighlightPaint,
find: crate::FindPaint,
comment: crate::CommentPaint,
jump: Option<&'a crate::link::Jump>,
}
fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
let ix = overlay.block;
let mut parts = Vec::new();
if let Some(layouts) = overlay.layouts {
let layouts = layouts.clone();
parts.push(
canvas(
move |bounds, _, _| layouts.record_painted(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
.into_any_element(),
);
}
if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
let dragging = overlay.held_height().is_some();
parts.push(
div()
.absolute()
.left_0()
.right_0()
.bottom_0()
.flex()
.justify_center()
.child(
theme
.grip_handle()
.id(ElementId::named_usize("painted-grip", ix))
.debug_selector(|| "painted-grip".into())
.w(px(PAINTED_GRIP_WIDTH))
.when(!dragging, |el| {
el.invisible()
.group_hover(painted_group(ix), |el| el.visible())
})
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
cx.stop_propagation();
resize(ix, event.position.y, window, cx);
}),
)
.into_any_element(),
);
}
parts
}
const PAINTED_GRIP_WIDTH: f32 = 120.0;
fn painted_group(ix: usize) -> SharedString {
SharedString::from(format!("painted-{ix}"))
}
impl<'a> Overlay<'a> {
fn at(self, part: Part) -> Self {
Self { part, ..self }
}
fn held_height(&self) -> Option<u32> {
self.sizing
.filter(|(ix, _)| *ix == self.block)
.map(|(_, height)| height)
}
fn here(&self) -> Cursor {
Cursor::new(self.block, self.part, 0)
}
fn caret_painted(&self) -> Option<usize> {
let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
let shown = self
.caret_shape
.shown(self.window_active, self.caret_inactive);
(self.caret_on && collapsed && shown)
.then(|| self.caret())
.flatten()
}
fn affinity(&self) -> Affinity {
self.selection
.map_or_else(Affinity::default, |selection| selection.affinity)
}
fn covered(&self, text: &str, offset: usize) -> Option<Range<usize>> {
(self.affinity() == Affinity::Downstream)
.then(|| glyph_at(text, offset))
.flatten()
}
fn caret_hollow(&self) -> bool {
!self.window_active
}
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, self.highlight, self.find, self.comment),
))
})
.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 image_source(url: &str, base: Option<&Path>) -> ImageSource {
if url.contains("://") {
return SharedString::from(url.to_string()).into();
}
match base {
Some(base) => base.join(url).into(),
None => std::path::PathBuf::from(url).into(),
}
}
pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
let doc = crate::parse_with(source, &cx.marks());
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,
image,
image_menu,
image_overlay,
image_overlay_corner,
fence,
sizing,
table_controls,
copy,
base,
keep,
scroll,
jump,
} = editing;
let theme = Theme::of(cx).clone();
let typography = typography.unwrap_or_else(|| cx.typography());
let highlight = crate::marks::highlight_paint_of(cx);
let find = crate::find::find_paint_of(cx);
let comment = crate::comment::comment_paint_of(cx);
let jump = jump.map(|to| crate::link::Jump::new(doc, to));
let gaps: Vec<Pixels> = doc
.blocks
.iter()
.enumerate()
.map(|(ix, block)| {
px(match doc.blocks.get(ix.wrapping_sub(1)) {
None => 0.0,
Some(previous) if tight(previous, block) => LIST_GAP,
Some(_) => BLOCK_GAP,
})
})
.collect();
let runs = quote_runs(doc.blocks.iter().map(|block| (block.quote, block.indent)));
let Some(layouts) = layouts else {
let mut column = div().flex().flex_col();
for (ix, block) in doc.blocks.iter().enumerate() {
let overlay = Overlay {
block: ix,
part: Part::Body,
selection,
caret_on,
caret_shape: cx.caret_shape(),
caret_height: cx.caret_height(),
caret_inactive: cx.inactive_caret(),
window_active: window.is_window_active(),
layouts: None,
annotations,
placeholder: placeholder.as_ref(),
caption,
toggle: toggle.as_ref(),
image: image.as_ref(),
image_menu: image_menu.as_ref(),
image_overlay: image_overlay.as_ref(),
image_overlay_corner,
fence: fence.as_ref(),
sizing,
table_controls,
copy,
base,
highlight,
find,
comment,
jump: jump.as_ref(),
};
let quote = Quote {
run: runs[ix],
gap: gaps[ix],
};
column = column.child(
block_box(block, quote, overlay, &typography, &theme, window, cx).mt(gaps[ix]),
);
}
return column.into_any_element();
};
let keys: Vec<u64> = doc
.blocks
.iter()
.zip(&runs)
.map(|(block, run)| block_key(block, *run, &typography, table_controls))
.collect();
layouts.prune(&keys);
let guesses: Vec<Guess> = doc
.blocks
.iter()
.zip(&runs)
.map(|(block, run)| {
let mut guess = guess(block, *run, &typography);
if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
}
guess
})
.collect();
let mut kept = keep.to_vec();
kept.extend(selection.map(|selection| selection.head.block));
kept.extend(selection.map(|selection| selection.anchor.block));
let owned = Owned {
blocks: doc.blocks.clone(),
runs,
gaps: gaps.clone(),
selection,
caret_on,
layouts: layouts.clone(),
annotations: annotations.to_vec(),
placeholder,
caption,
toggle,
image,
image_menu,
image_overlay,
image_overlay_corner,
fence,
sizing,
table_controls,
copy,
base: base.map(Path::to_path_buf),
highlight,
find,
comment,
jump,
typography,
theme,
};
Column {
layouts: layouts.clone(),
keys: keys.into(),
gaps: gaps.into(),
guesses: guesses.into(),
keep: kept,
scroll: scroll.cloned(),
item_of: Rc::new(|at| at.block),
build: Box::new(move |ix, window, cx| {
let overlay = Overlay {
block: ix,
part: Part::Body,
selection: owned.selection,
caret_on: owned.caret_on,
caret_shape: cx.caret_shape(),
caret_height: cx.caret_height(),
caret_inactive: cx.inactive_caret(),
window_active: window.is_window_active(),
layouts: Some(&owned.layouts),
annotations: &owned.annotations,
placeholder: owned.placeholder.as_ref(),
caption: owned.caption,
toggle: owned.toggle.as_ref(),
image: owned.image.as_ref(),
image_menu: owned.image_menu.as_ref(),
image_overlay: owned.image_overlay.as_ref(),
image_overlay_corner: owned.image_overlay_corner,
fence: owned.fence.as_ref(),
sizing: owned.sizing,
table_controls: owned.table_controls,
copy: owned.copy,
base: owned.base.as_deref(),
highlight: owned.highlight,
find: owned.find,
comment: owned.comment,
jump: owned.jump.as_ref(),
};
let quote = Quote {
run: owned.runs[ix],
gap: owned.gaps[ix],
};
block_box(
&owned.blocks[ix],
quote,
overlay,
&owned.typography,
&owned.theme,
window,
cx,
)
.into_any_element()
}),
}
.into_any_element()
}
struct Owned {
blocks: Vec<Block>,
runs: Vec<Option<Run>>,
gaps: Vec<Pixels>,
selection: Option<Selection>,
caret_on: bool,
layouts: BlockLayouts,
annotations: Vec<(Selection, Annotation)>,
placeholder: Option<SharedString>,
caption: Caption,
toggle: Option<Toggle>,
image: Option<OnImage>,
image_menu: Option<OnImageMenu>,
image_overlay: Option<ImageOverlay>,
image_overlay_corner: ImageOverlayCorner,
fence: Option<FenceHost>,
sizing: Option<(usize, u32)>,
table_controls: bool,
copy: CopyButton,
base: Option<std::path::PathBuf>,
highlight: crate::HighlightPaint,
find: crate::FindPaint,
comment: crate::CommentPaint,
jump: Option<crate::link::Jump>,
typography: Typography,
theme: Theme,
}
#[derive(Clone, Copy)]
struct Quote {
run: Option<Run>,
gap: Pixels,
}
const QUOTE_INSET: f32 = 12.0;
const QUOTE_BAR: f32 = 2.0;
fn block_box(
block: &Block,
quote: Quote,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> gpui::Div {
let ix = overlay.block;
let frame = overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_block(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
});
div()
.w_full()
.font_family(theme.font_body.clone())
.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(quoted(
block,
quote,
block_element(block, overlay, typography, theme, window, cx),
typography,
theme,
)),
)
}
fn quoted(
block: &Block,
quote: Quote,
element: AnyElement,
typography: &Typography,
theme: &Theme,
) -> AnyElement {
let (Some(run), Some(quoted)) = (quote.run, block.quote) else {
return element;
};
let color = quoted
.alert
.map_or(theme.border_strong, |alert| alert_color(alert, theme));
let label = quoted.alert.filter(|_| run.first).map(|alert| {
div()
.pb(px(2.0))
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.font_weight(FontWeight::SEMIBOLD)
.text_color(color)
.child(alert.label())
});
let bar = div()
.absolute()
.left(px(
-((block.indent - run.level.min(block.indent)) as f32) * INDENT_WIDTH
))
.top(if run.first { px(0.0) } else { -quote.gap })
.bottom_0()
.w(px(QUOTE_BAR))
.bg(color);
div()
.child(bar)
.pl(px(QUOTE_BAR + QUOTE_INSET))
.pr(px(10.0))
.py(px(2.0))
.text_color(theme.text_muted)
.children(label)
.child(element)
.into_any_element()
}
fn block_key(
block: &Block,
run: Option<Run>,
typography: &Typography,
table_controls: bool,
) -> u64 {
let mut hasher = DefaultHasher::new();
table_controls.hash(&mut hasher);
block.hash(&mut hasher);
run.map(|run| run.first).hash(&mut hasher);
typography.body.size().to_bits().hash(&mut hasher);
typography.body.line_height().to_bits().hash(&mut hasher);
hasher.finish()
}
fn guess(block: &Block, run: Option<Run>, typography: &Typography) -> Guess {
let body = px(typography.body.line_height());
let prose = |chars: usize, line: Pixels| Guess {
chars,
line,
rows: 0,
extra: px(0.0),
indent: px(0.0),
};
let guess = match &block.kind {
BlockKind::Paragraph(text)
| BlockKind::Bullet(text)
| BlockKind::Ordered { text, .. }
| BlockKind::Task { text, .. } => prose(text.text.len(), body),
BlockKind::Heading { level, text } => prose(
text.text.len(),
px(typography.heading(*level).line_height()),
),
BlockKind::Code { code, .. } => Guess {
rows: code.text.lines().count().max(1),
line: px(typography.code.line_height()),
extra: px(2.0 * CODE_PADDING_Y) + body,
..prose(0, body)
},
BlockKind::Table { rows, .. } => Guess {
rows: rows.len() + 1,
extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
..prose(0, body)
},
BlockKind::Image { alt, .. } => Guess {
extra: px(240.0),
..prose(alt.text.len(), px(typography.caption.line_height()))
},
BlockKind::Bookmark { .. } => Guess {
extra: body * 3.0,
..prose(0, body)
},
BlockKind::Rule => Guess {
extra: body,
..prose(0, px(0.0))
},
};
let quoted = run.is_some();
let label = run.is_some_and(|run| run.first) && block.quote.is_some_and(|q| q.alert.is_some());
Guess {
indent: px(block.indent as f32 * INDENT_WIDTH)
+ if quoted {
px(QUOTE_BAR + QUOTE_INSET + 10.0)
} else {
px(0.0)
},
extra: guess.extra
+ if quoted { px(4.0) } else { px(0.0) }
+ if label { body + px(2.0) } else { px(0.0) },
..guess
}
}
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::Code {
language,
code,
height,
} => {
let overlay = overlay.at(Part::Code);
let height = overlay.held_height().or(*height);
let painted = overlay
.caret()
.is_none()
.then(|| {
let fence = block::Fence {
language: language.as_deref()?,
code: &code.text,
height,
rewrite: overlay.fence.map(|host| {
let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
rewrite(ix, code, window, cx)
}) as block::Rewrite
}),
};
block::render(&fence, window, cx)
})
.flatten();
match painted {
Some(element) => div()
.key_context(PAINTED_CONTEXT)
.relative()
.when_some(overlay.fence, |el, host| {
let (leave, ix) = (host.leave.clone(), overlay.block);
el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
})
.when(overlay.covers_block(), |el| {
el.rounded(px(4.0)).bg(theme.selection)
})
.group(painted_group(overlay.block))
.child(element)
.children(resizable(&overlay, theme))
.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, window, cx)
}
BlockKind::Bookmark { url, form } => match crate::link::card(
crate::Link::new(url, overlay.base),
match (*form, overlay.held_height()) {
(Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
(form, _) => form,
},
window,
cx,
) {
Some(element) => div()
.key_context(PAINTED_CONTEXT)
.relative()
.when_some(overlay.fence, |el, host| {
let (leave, ix) = (host.leave.clone(), overlay.block);
el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
})
.when(overlay.covers_block(), |el| {
el.rounded(px(4.0)).bg(theme.selection)
})
.group(painted_group(overlay.block))
.child(element)
.children(match form {
Form::Embed(_) => resizable(&overlay, theme),
_ => Vec::new(),
})
.into_any_element(),
None => bookmark(
overlay.block,
url,
overlay.base,
*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_auto().min_w_0().child(text_element(
text,
typography.body.size(),
typography.body.line_height(),
FontWeight::NORMAL,
overlay,
theme,
cx,
)))
.into_any_element()
}