T21b and T21c: Documents, reading and knowledge

- DocumentViewer: pages the app drew on a desk, the page under the view's middle
  counted, zoom steps and fit, thumbnails, notes pinned where a press lands, and
  the owner's FindWidget over the pages, hits lit and the current one revealed
- PageThumbnailList, EpubReader (chapters, type size, progress), PrintPreview
- MarkdownRenderer feeds TableOfContents through sections and outline
- Footnote, TooltipTerm, Glossary, ReadingProgress, ZenMode
- Backlinks, Favorites, TrashBin, PageCover, PageIcon, TemplatePicker,
  VersionHistory with PageHistoryDiff
- editor::FindWidget shows the replace fold and option toggles only with handlers
- block editor tests move to documents/blocks
- icons: a-arrow-down, a-arrow-up
- gallery: reading and knowledge sections; scripted find, changes and zen
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 04:34:32 -04:00
1 parent 14a8990793
commit 9854cfa460
32 files changed
+4269 -411

No files matched your search

+92 -6
View File
@@ -1,7 +1,7 @@
use std::rc::Rc;
use gpui::{
AnyElement, App, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText,
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
IntoElement, ParentElement, RenderOnce, SharedString, StrikethroughStyle, Styled, StyledText,
UnderlineStyle, Window, div, prelude::*,
};
@@ -10,6 +10,7 @@ use pulldown_cmark::Alignment;
use super::tree::{Inline, Node, Style, parse};
use crate::{
editor::code_colors,
navigation::{Anchor, Sections},
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize, Radius, TextSize},
typography::Latex,
@@ -23,6 +24,8 @@ pub struct MarkdownRenderer {
id: ElementId,
source: SharedString,
on_link: Option<OnLink>,
size: TextSize,
sections: Option<Entity<Sections>>,
}
impl MarkdownRenderer {
@@ -31,9 +34,23 @@ impl MarkdownRenderer {
id: id.into(),
source: source.into(),
on_link: None,
size: TextSize::Base,
sections: None,
}
}
/// The body's size; headings keep theirs.
pub fn text_size(mut self, size: TextSize) -> Self {
self.size = size;
self
}
/// Marks each heading for a table of contents over `sections`; `outline` gives its entries.
pub fn sections(mut self, sections: &Entity<Sections>) -> Self {
self.sections = Some(sections.clone());
self
}
/// Takes link presses instead of the browser.
pub fn on_link(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
self.on_link = Some(Rc::new(handler));
@@ -46,6 +63,8 @@ struct Draw<'a> {
id: &'a ElementId,
on_link: &'a Option<OnLink>,
next: std::cell::Cell<usize>,
sections: &'a Option<Entity<Sections>>,
headings: std::cell::Cell<usize>,
}
impl Draw<'_> {
@@ -143,7 +162,7 @@ impl Draw<'_> {
3 => TextSize::Lg,
_ => TextSize::Md,
};
div()
let heading = div()
.pt_2()
.text_size(theme.text_size(size))
.font_weight(if *level <= 2 {
@@ -152,8 +171,15 @@ impl Draw<'_> {
FontWeight::MEDIUM
})
.text_color(colors.fg)
.child(self.inline(inline, cx))
.into_any_element()
.child(self.inline(inline, cx));
let ordinal = self.headings.get();
self.headings.set(ordinal + 1);
match self.sections {
Some(sections) => Anchor::new(sections, anchor(ordinal))
.child(heading)
.into_any_element(),
None => heading.into_any_element(),
}
}
Node::Quote(inner) => div()
.flex()
@@ -337,6 +363,39 @@ impl Draw<'_> {
}
}
/// The anchor of the document's `ordinal`th heading.
fn anchor(ordinal: usize) -> SharedString {
format!("heading-{ordinal}").into()
}
/// The headings of `markdown` in order, as a table of contents takes them: anchor, text and depth from 0.
pub fn outline(markdown: &str) -> Vec<(SharedString, SharedString, usize)> {
fn walk(nodes: &[Node], out: &mut Vec<(SharedString, SharedString, usize)>) {
for node in nodes {
match node {
Node::Heading(level, inline) => {
let ordinal = out.len();
out.push((
anchor(ordinal),
inline.text.clone().into(),
*level as usize - 1,
));
}
Node::Quote(inner) | Node::Footnote { body: inner, .. } => walk(inner, out),
Node::List { items, .. } => {
for (_, body) in items {
walk(body, out);
}
}
_ => {}
}
}
}
let mut out = Vec::new();
walk(&parse(markdown), &mut out);
out
}
impl RenderOnce for MarkdownRenderer {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
@@ -345,13 +404,40 @@ impl RenderOnce for MarkdownRenderer {
id: &self.id,
on_link: &self.on_link,
next: std::cell::Cell::new(0),
sections: &self.sections,
headings: std::cell::Cell::new(0),
};
div()
.flex()
.flex_col()
.gap_3()
.text_size(theme.text_size(TextSize::Base))
.line_height(theme.text_size(TextSize::Base) * 1.6)
.text_size(theme.text_size(self.size))
.line_height(theme.text_size(self.size) * 1.6)
.children(draw.nodes(&nodes, cx))
}
}
#[cfg(test)]
mod tests {
use super::outline;
#[test]
fn the_outline_numbers_headings_in_order_with_their_depth() {
let found =
outline("# Lift\n\ntext\n\n## In light\n\n> ### Quoted\n\n- #### Listed\n\n## In dark");
let shape: Vec<(&str, &str, usize)> = found
.iter()
.map(|(anchor, text, depth)| (anchor.as_ref(), text.as_ref(), *depth))
.collect();
assert_eq!(
shape,
[
("heading-0", "Lift", 0),
("heading-1", "In light", 1),
("heading-2", "Quoted", 2),
("heading-3", "Listed", 3),
("heading-4", "In dark", 1),
]
);
}
}