T21a: Documents, editing

- 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
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 03:32:24 -04:00
1 parent 0ffa001a61
commit 6a9ce8a2c3
46 files changed
+5635 -193

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
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(),
}
}
}