Each drawn character finds its written one, skipping only white space, escapes and quote marks, so a press lands on the character itself. Code, front matter and link definitions read back line for line. An HTML block keeps each text run's source byte, and an alert keeps its text past the marker; alert() now takes the source. A secondary press refuses a target its write-back changed, and its focus no longer opens an empty document. Run's element stack moves to tree/stack.rs and the HTML-block reader to tree/html_block.rs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
198 lines
7.1 KiB
Rust
198 lines
7.1 KiB
Rust
use gpui::{
|
|
AnyElement, App, FontWeight, IntoElement, MouseButton, ParentElement, Styled, StyledText, div,
|
|
prelude::*,
|
|
};
|
|
|
|
use super::{Draw, point::point_lines};
|
|
use crate::{
|
|
documents::tree::{AlertKind, DocumentHeading, Node},
|
|
primitives::{FocusRing as _, Icon, IconName},
|
|
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
|
};
|
|
|
|
impl Draw<'_> {
|
|
/// A list; bullets turn with depth, as browsers draw them.
|
|
pub(super) fn list(
|
|
&self,
|
|
start: &Option<u64>,
|
|
items: &[(Option<bool>, Vec<Node>)],
|
|
cx: &App,
|
|
) -> AnyElement {
|
|
let colors = cx.theme().colors.clone();
|
|
let depth = self.lists.get();
|
|
self.lists.set(depth + 1);
|
|
let bullet = match depth {
|
|
0 => "•",
|
|
1 => "◦",
|
|
_ => "▪",
|
|
};
|
|
let list = div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.children(items.iter().enumerate().map(|(ix, (task, body))| {
|
|
let marker: AnyElement = match (task, start) {
|
|
(Some(done), _) => Icon::new(if *done {
|
|
IconName::SquareCheck
|
|
} else {
|
|
IconName::Square
|
|
})
|
|
.size(IconSize::Sm)
|
|
.color(if *done {
|
|
colors.focus
|
|
} else {
|
|
colors.fg_subtle
|
|
})
|
|
.into_any_element(),
|
|
(None, Some(first)) => div()
|
|
.text_color(colors.fg_muted)
|
|
.child(format!("{}.", first + ix as u64))
|
|
.into_any_element(),
|
|
(None, None) => div()
|
|
.debug_selector(move || format!("markdown-bullet-{depth}"))
|
|
.text_color(colors.fg_muted)
|
|
.child(bullet)
|
|
.into_any_element(),
|
|
};
|
|
div()
|
|
.flex()
|
|
.gap_2()
|
|
.child(
|
|
div()
|
|
.flex_none()
|
|
.min_w_5()
|
|
.flex()
|
|
.justify_end()
|
|
.child(marker),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.when(*task == Some(true), |body| {
|
|
body.text_color(colors.fg_subtle)
|
|
})
|
|
.children(self.nodes(body, cx)),
|
|
)
|
|
}))
|
|
.into_any_element();
|
|
self.lists.set(depth);
|
|
list
|
|
}
|
|
|
|
/// A GitHub alert: a colored rule, icon, title and body.
|
|
pub(super) fn alert(&self, kind: AlertKind, body: &[Node], cx: &App) -> AnyElement {
|
|
let colors = &cx.theme().colors;
|
|
let (tone, icon) = match kind {
|
|
AlertKind::Note => (colors.info, IconName::Info),
|
|
AlertKind::Tip => (colors.success, IconName::Lightbulb),
|
|
// The palette's violet, read at 4.5:1.
|
|
AlertKind::Important => (colors.syntax.keyword, IconName::MessageSquareWarning),
|
|
AlertKind::Warning => (colors.warning, IconName::TriangleAlert),
|
|
AlertKind::Caution => (colors.danger, IconName::OctagonAlert),
|
|
};
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.py_1()
|
|
.pl_4()
|
|
.border_l_2()
|
|
.border_color(tone)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.text_color(tone)
|
|
.child(Icon::new(icon).size(IconSize::Sm).color(tone))
|
|
.child(kind.title()),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.text_color(colors.fg)
|
|
.children(self.nodes(body, cx)),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
/// `[toc]`: each heading by depth; a press reveals it.
|
|
pub(super) fn toc(&self, headings: &[DocumentHeading], cx: &App) -> AnyElement {
|
|
let colors = cx.theme().colors.clone();
|
|
let top = headings.iter().map(|heading| heading.depth).min();
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.children(headings.iter().enumerate().map(|(ix, heading)| {
|
|
let entry = div()
|
|
.id(self.key("toc"))
|
|
.debug_selector(move || format!("markdown-toc-{ix}"))
|
|
.child(heading.title.clone());
|
|
let entry = match heading.depth - top.expect("a heading is listed") {
|
|
0 => entry,
|
|
1 => entry.pl_4(),
|
|
2 => entry.pl_8(),
|
|
3 => entry.pl_12(),
|
|
4 => entry.pl_16(),
|
|
_ => entry.pl_20(),
|
|
};
|
|
let Some(reveal) = self.hooks.on_heading.clone() else {
|
|
return entry.text_color(colors.fg_muted).into_any_element();
|
|
};
|
|
let (at, pressed) = (heading.at, reveal.clone());
|
|
entry
|
|
.text_color(colors.link)
|
|
.tab_index(0)
|
|
.focus_ring(cx)
|
|
.cursor_pointer()
|
|
// On press: the layout may move before release.
|
|
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
|
if event.modifiers.secondary() {
|
|
window.prevent_default();
|
|
cx.stop_propagation();
|
|
pressed(at, window, cx);
|
|
}
|
|
})
|
|
.on_click(move |event, window, cx| {
|
|
if !event.modifiers().secondary() {
|
|
reveal(at, window, cx);
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
|
|
/// Front matter as written, inside its box.
|
|
pub(super) fn front_matter(&self, text: &str, from: usize, cx: &App) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let drawn = StyledText::new(text.to_string());
|
|
let layout = drawn.layout().clone();
|
|
let block = div()
|
|
.p_3()
|
|
.rounded(theme.radius(Radius::Md))
|
|
.bg(theme.colors.sunken)
|
|
.font_family(theme.mono_family.clone())
|
|
.text_size(theme.text_size(TextSize::Sm) * self.scale)
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(drawn)
|
|
.into_any_element();
|
|
point_lines(
|
|
block,
|
|
layout,
|
|
text,
|
|
from,
|
|
self.hooks.on_point,
|
|
(false, false),
|
|
)
|
|
}
|
|
}
|