documents: a press opens its block on the pressed character; a secondary press points

Each drawn text piece keeps its written span, so a press maps back to a
source byte. A secondary press opens nothing: it moves the caret there and
emits DocumentEvent::Pointed with the block's kind, link, picture and
whether it lies in a table or code. Block nodes count from their own start.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 09:38:08 -04:00
1 parent 12512217bd
commit 5af11fe489
17 files changed
+813 -82

No files matched your search

+77 -40
View File
@@ -22,6 +22,9 @@ use crate::{
};
mod blocks;
mod point;
pub(crate) use point::{OnPoint, Place, Pointed};
pub(crate) type OnLink = Rc<dyn Fn(&str, &mut Window, &mut App)>;
/// Reveals the heading whose source starts at a byte.
@@ -87,20 +90,28 @@ impl MarkdownRenderer {
}
}
/// Draws nodes under one owner's ids and link handler.
/// What an owner hands its drawing: pictures' folder and presses.
pub(crate) struct Hooks<'a> {
pub base: &'a Option<Arc<Path>>,
pub on_link: &'a Option<OnLink>,
pub on_heading: &'a Option<OnHeading>,
pub on_point: &'a Option<OnPoint>,
}
/// Draws nodes under one owner's ids and hooks.
struct Draw<'a> {
id: &'a ElementId,
on_link: &'a Option<OnLink>,
hooks: Hooks<'a>,
next: std::cell::Cell<usize>,
sections: &'a Option<Entity<Sections>>,
headings: std::cell::Cell<usize>,
code: &'a Option<DrawCode>,
base: &'a Option<Arc<Path>>,
/// Text size against the theme's.
scale: f32,
/// Lists open around the node drawn.
lists: std::cell::Cell<usize>,
on_heading: &'a Option<OnHeading>,
/// Drawing a table's cells.
table: std::cell::Cell<bool>,
}
impl Draw<'_> {
@@ -174,16 +185,20 @@ impl Draw<'_> {
.collect();
let styled =
StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles));
if inline.links.is_empty() {
return styled.into_any_element();
}
let (ranges, urls): (Vec<_>, Vec<_>) = inline.links.iter().cloned().unzip();
let on_link = self.on_link.clone();
InteractiveText::new(self.key("links"), styled)
.on_click(ranges, move |ix, window, cx| {
open_link(&on_link, &urls[ix], window, cx)
})
.into_any_element()
let layout = styled.layout().clone();
let drawn = match inline.links.is_empty() {
true => styled.into_any_element(),
false => {
let (ranges, urls): (Vec<_>, Vec<_>) = inline.links.iter().cloned().unzip();
let on_link = self.hooks.on_link.clone();
InteractiveText::new(self.key("links"), styled)
.on_click(ranges, move |ix, window, cx| {
open_link(&on_link, &urls[ix], window, cx)
})
.into_any_element()
}
};
self.point_text(drawn, layout, inline)
}
fn nodes(&self, nodes: &[Node], cx: &App) -> Vec<AnyElement> {
@@ -191,6 +206,14 @@ impl Draw<'_> {
}
fn node(&self, node: &Node, cx: &App) -> AnyElement {
let table = self.table.get();
self.table.set(table || matches!(node, Node::Table { .. }));
let drawn = self.drawn(node, cx);
self.table.set(table);
drawn
}
fn drawn(&self, node: &Node, cx: &App) -> AnyElement {
let theme = cx.theme();
let colors = theme.colors.clone();
match node {
@@ -234,9 +257,14 @@ impl Draw<'_> {
.text_color(colors.fg_muted)
.children(self.nodes(inner, cx))
.into_any_element(),
Node::Code { language, text } => {
Node::Code {
language,
text,
from,
} => {
if language.as_deref() == Some("mermaid") {
return Diagram::new(self.key("diagram"), text.clone()).into_any_element();
let diagram = Diagram::new(self.key("diagram"), text.clone());
return self.point_code(diagram.into_any_element(), None, None);
}
if let Some(draw) = self.code {
return draw(
@@ -251,7 +279,9 @@ impl Draw<'_> {
.map_or_else(Vec::new, |syntax| code_colors_with(text, syntax, cx)),
None => code_colors(text, cx),
};
div()
let code = StyledText::new(text.clone()).with_highlights(styles);
let layout = code.layout().clone();
let block = div()
.flex()
.flex_col()
.gap_1()
@@ -267,8 +297,9 @@ impl Draw<'_> {
.text_color(colors.fg_subtle)
.child(name)
}))
.child(StyledText::new(text.clone()).with_highlights(styles))
.into_any_element()
.child(code)
.into_any_element();
self.point_code(block, Some(layout), *from)
}
Node::List { start, items } => self.list(start, items, cx),
Node::Table { aligns, head, rows } => {
@@ -340,23 +371,28 @@ impl Draw<'_> {
Err(error) => failed(&error, tex, cx),
})
.into_any_element(),
Node::Image { alt, url, link } => {
Node::Image { alt, url, link, at } => {
let picture = div().id(self.key("image")).child(Picture {
id: self.key("picture"),
alt: alt.clone(),
url: url.clone(),
base: self.base.clone(),
base: self.hooks.base.clone(),
});
let Some(link) = link.clone() else {
return picture.into_any_element();
let drawn = match link.clone() {
None => picture.into_any_element(),
Some(address) => {
let on_link = self.hooks.on_link.clone();
picture
.tab_index(0)
.focus_ring(cx)
.cursor_pointer()
.on_click(move |_, window, cx| {
open_link(&on_link, &address, window, cx)
})
.into_any_element()
}
};
let on_link = self.on_link.clone();
picture
.tab_index(0)
.focus_ring(cx)
.cursor_pointer()
.on_click(move |_, window, cx| open_link(&on_link, &link, window, cx))
.into_any_element()
self.point_image(drawn, *at, url, link)
}
Node::Footnote { label, body } => div()
.flex()
@@ -389,27 +425,24 @@ fn open_link(on_link: &Option<OnLink>, url: &str, window: &mut Window, cx: &mut
}
}
/// Parsed `nodes` drawn under `id`; pictures read from `base`.
/// Parsed `nodes` drawn under `id`, with the owner's hooks.
pub(crate) fn draw_nodes(
id: &ElementId,
nodes: &[Node],
base: &Option<Arc<Path>>,
on_link: &Option<OnLink>,
on_heading: &Option<OnHeading>,
hooks: Hooks,
scale: f32,
cx: &App,
) -> Vec<AnyElement> {
let draw = Draw {
id,
on_link,
hooks,
next: std::cell::Cell::new(0),
sections: &None,
headings: std::cell::Cell::new(0),
code: &None,
base,
scale,
lists: std::cell::Cell::new(0),
on_heading,
table: std::cell::Cell::new(false),
};
draw.nodes(nodes, cx)
}
@@ -420,15 +453,19 @@ impl RenderOnce for MarkdownRenderer {
let nodes = parse(&self.source);
let draw = Draw {
id: &self.id,
on_link: &self.on_link,
hooks: Hooks {
base: &self.base,
on_link: &self.on_link,
on_heading: &None,
on_point: &None,
},
next: std::cell::Cell::new(0),
sections: &self.sections,
headings: std::cell::Cell::new(0),
code: &self.code,
base: &self.base,
scale: 1.0,
lists: std::cell::Cell::new(0),
on_heading: &None,
table: std::cell::Cell::new(false),
};
div()
.flex()