- markdown read with pulldown-cmark: marks for live styling, blocks, links, words - forms::Highlight gains weight, slant and strike; measure and paint share runs - Inter Italic embedded, so emphasis reads slanted - forms::mention's popup and keys become Suggestions, shared with the editors - RichTextEditor with FixedFormatToolbar, the floating toolbar, LinkEditor, @ and : suggestions, WordCount and ReadingTime - MarkdownEditor: visual (a pressed block opens as source), source, split - BlockEditor: prose, lists, to-dos, quotes, callouts, toggles, code, math, diagrams the app draws, dividers, tables that grow and merge, pictures to resize and align, cards for video and pages, columns, synced text; slash menu, markdown shortcuts, handles that drag and open a menu, one undo for all - TodoItem and Checklist - icons: heading-1..3, workflow, merge - gallery: the Documents page with scripted states
236 lines
9.0 KiB
Rust
236 lines
9.0 KiB
Rust
use gpui::{
|
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
|
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
|
};
|
|
|
|
use super::{Block, BlockEditor, BlockKind, media, table, view::Look};
|
|
use crate::{
|
|
buttons::CopyButton,
|
|
forms::Checkbox,
|
|
primitives::{Disclosure, Icon, IconName},
|
|
theme::IconSize,
|
|
typography::Latex,
|
|
};
|
|
|
|
impl BlockEditor {
|
|
/// What a block shows, by kind.
|
|
pub(crate) fn content(
|
|
&self,
|
|
block: &Block,
|
|
number: usize,
|
|
look: &Look,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let colors = look.colors.clone();
|
|
let (key, id) = (block.key, Self::id(cx));
|
|
let marker = |text: SharedString| {
|
|
div()
|
|
.flex_none()
|
|
.min_w_5()
|
|
.text_color(colors.fg_muted)
|
|
.child(text)
|
|
};
|
|
let row = |marker: AnyElement, field: AnyElement| {
|
|
div()
|
|
.flex()
|
|
.gap_1p5()
|
|
.child(marker)
|
|
.child(div().flex_1().min_w_0().child(field))
|
|
};
|
|
match &block.kind {
|
|
BlockKind::Paragraph => self.field_view(block, 0, window, cx),
|
|
BlockKind::Heading(level) => div()
|
|
.pt_2()
|
|
.text_size(look.headings[(*level as usize).clamp(1, 3) - 1])
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child(self.field_view(block, 0, window, cx))
|
|
.into_any_element(),
|
|
BlockKind::Bullet => row(
|
|
marker("•".into()).into_any_element(),
|
|
self.field_view(block, 0, window, cx),
|
|
)
|
|
.into_any_element(),
|
|
BlockKind::Numbered => row(
|
|
marker(format!("{number}.").into()).into_any_element(),
|
|
self.field_view(block, 0, window, cx),
|
|
)
|
|
.into_any_element(),
|
|
BlockKind::Todo(done) => {
|
|
let (entity, done) = (cx.entity(), *done);
|
|
let check = Checkbox::new((id.clone(), format!("todo-{key}")), done).on_change(
|
|
move |on, _, cx| {
|
|
entity.update(cx, |editor, cx| {
|
|
editor.set_kind(key, BlockKind::Todo(on), cx)
|
|
})
|
|
},
|
|
);
|
|
row(
|
|
check.into_any_element(),
|
|
self.field_view(block, 0, window, cx),
|
|
)
|
|
.when(done, |row| row.text_color(colors.fg_subtle))
|
|
.into_any_element()
|
|
}
|
|
BlockKind::Quote => div()
|
|
.pl_3()
|
|
.border_l_2()
|
|
.border_color(colors.border_strong)
|
|
.text_color(colors.fg_muted)
|
|
.child(self.field_view(block, 0, window, cx))
|
|
.into_any_element(),
|
|
BlockKind::Callout => div()
|
|
.flex()
|
|
.gap_2()
|
|
.p_3()
|
|
.rounded(look.radius)
|
|
.bg(colors.sunken)
|
|
.child(
|
|
Icon::new(IconName::Lightbulb)
|
|
.size(IconSize::Sm)
|
|
.color(colors.warning),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.child(self.field_view(block, 0, window, cx)),
|
|
)
|
|
.into_any_element(),
|
|
BlockKind::Toggle(open) => {
|
|
let (entity, open) = (cx.entity(), *open);
|
|
let chevron = div()
|
|
.id((id.clone(), format!("toggle-{key}")))
|
|
.flex_none()
|
|
.cursor_pointer()
|
|
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
|
.on_click(move |_, _, cx| {
|
|
entity.update(cx, |editor, cx| {
|
|
editor.set_kind(key, BlockKind::Toggle(!open), cx)
|
|
})
|
|
})
|
|
.child(Disclosure::new(
|
|
(id.clone(), format!("disclosure-{key}")),
|
|
open,
|
|
));
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.child(row(
|
|
chevron.into_any_element(),
|
|
self.field_view(block, 0, window, cx),
|
|
))
|
|
.when(open, |toggle| {
|
|
toggle.child(div().pl_6().child(self.field_view(block, 1, window, cx)))
|
|
})
|
|
.into_any_element()
|
|
}
|
|
BlockKind::Code => {
|
|
let text = block.fields[0].read(cx).text().to_string();
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.p_3()
|
|
.rounded(look.radius)
|
|
.bg(colors.sunken)
|
|
.font_family(look.mono.clone())
|
|
.text_size(look.small)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.justify_between()
|
|
.items_center()
|
|
.text_size(look.tiny)
|
|
.text_color(colors.fg_subtle)
|
|
.child("Code")
|
|
.child(CopyButton::new((id.clone(), format!("copy-{key}")), text)),
|
|
)
|
|
.child(self.field_view(block, 0, window, cx))
|
|
.into_any_element()
|
|
}
|
|
BlockKind::Math => {
|
|
let tex = block.fields[0].read(cx).text().to_string();
|
|
let shown = match Latex::parse(&tex) {
|
|
_ if tex.trim().is_empty() => div().into_any_element(),
|
|
Ok(math) => div()
|
|
.flex()
|
|
.justify_center()
|
|
.py_2()
|
|
.child(math)
|
|
.into_any_element(),
|
|
Err(error) => div()
|
|
.text_color(colors.danger)
|
|
.child(error.to_string())
|
|
.into_any_element(),
|
|
};
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.p_3()
|
|
.rounded(look.radius)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.child(
|
|
div()
|
|
.font_family(look.mono.clone())
|
|
.text_size(look.small)
|
|
.text_color(colors.fg_muted)
|
|
.child(self.field_view(block, 0, window, cx)),
|
|
)
|
|
.child(shown)
|
|
.into_any_element()
|
|
}
|
|
BlockKind::Diagram => media::diagram(self, block, look, window, cx),
|
|
BlockKind::Divider => div()
|
|
.py_2()
|
|
.child(div().h_0().border_t_1().border_color(colors.border))
|
|
.into_any_element(),
|
|
BlockKind::Table { .. } => table::table(self, block, look, window, cx),
|
|
BlockKind::Image(_) | BlockKind::Video(_) | BlockKind::Embed(_) => {
|
|
media::media(self, block, look, window, cx)
|
|
}
|
|
BlockKind::Columns(_) => div()
|
|
.flex()
|
|
.gap_4()
|
|
.children((0..block.fields.len()).map(|ix| {
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.when(ix > 0, |column| {
|
|
column.pl_4().border_l_1().border_color(colors.border)
|
|
})
|
|
.child(self.field_view(block, ix, window, cx))
|
|
}))
|
|
.into_any_element(),
|
|
BlockKind::Synced(name) => div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.px_3()
|
|
.py_2()
|
|
.rounded(look.radius)
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_1()
|
|
.text_size(look.tiny)
|
|
.text_color(colors.fg_subtle)
|
|
.child(
|
|
Icon::new(IconName::RefreshCw)
|
|
.size(IconSize::Xs)
|
|
.color(colors.fg_subtle),
|
|
)
|
|
.child(format!("Synced · {name}")),
|
|
)
|
|
.child(self.field_view(block, 0, window, cx))
|
|
.into_any_element(),
|
|
}
|
|
}
|
|
}
|