Documents: inline formulas typeset in text; \( and \[ read as math
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 35s
CI / crate (push) Failing after 35s
CI / platforms (windows-latest) (push) Successful in 5m41s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 22:42:07 -04:00
1 parent 7d704d97f4
commit dceca9f3c6
12 files changed
+569 -61

No files matched your search

+2 -2
View File
@@ -5,7 +5,7 @@ use std::ops::Range;
use markdown::mdast::Node;
use super::{Change, EditRefusal, after, block, cut, holder, marks, room};
use crate::documents::{markdown::span, tree::inside};
use crate::documents::markdown::{math_inside, span};
/// The backslashes of escapes in a text node's span.
fn escapes(text: &str, whole: Range<usize>) -> Vec<Range<usize>> {
@@ -33,7 +33,7 @@ fn meta(node: &Node, text: &str, autolinks: &[Range<usize>]) -> Vec<Range<usize>
Node::Text(_) if holder(autolinks, &whole).is_some() => Vec::new(),
Node::Text(_) => escapes(text, whole),
Node::InlineMath(_) => {
let inner = inside(text, whole.clone(), '$');
let inner = math_inside(text, whole.clone());
vec![whole.start..inner.start, inner.end..whole.end]
}
Node::FootnoteReference(_) => vec![whole.start..whole.start + 2, whole.end - 1..whole.end],
+78 -5
View File
@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{borrow::Cow, ops::Range};
use gpui::App;
use markdown::{Constructs, ParseOptions, mdast};
@@ -20,19 +20,92 @@ pub(crate) fn options() -> ParseOptions {
/// `text` as a syntax tree.
pub(crate) fn tree(text: &str) -> mdast::Node {
markdown::to_mdast(text, &options()).expect("markdown without MDX always parses")
parsed(text, &options())
}
fn parsed(text: &str, options: &ParseOptions) -> mdast::Node {
let text = tex_math(text, options);
markdown::to_mdast(&text, options).expect("markdown without MDX always parses")
}
/// `\(` and `\[` delimiters as `$`, byte for byte.
fn tex_math<'a>(text: &'a str, options: &ParseOptions) -> Cow<'a, str> {
if !text.contains("\\(") && !text.contains("\\[") {
return Cow::Borrowed(text);
}
let root = markdown::to_mdast(text, options).expect("markdown without MDX always parses");
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let mut kept = vec![false; text.len()];
for node in nodes {
use mdast::Node::{Code, Html, InlineCode, InlineMath, Math, Toml, Yaml};
if matches!(
node,
Code(_) | InlineCode(_) | Math(_) | InlineMath(_) | Html(_) | Yaml(_) | Toml(_)
) {
kept[span(node)].fill(true);
}
}
let bytes = text.as_bytes();
let mut out = bytes.to_vec();
let mut open: Option<(usize, u8)> = None;
let mut at = 0;
while at + 1 < bytes.len() {
match bytes[at] {
// A blank line ends a paragraph and its formula.
b'\n' if blank_after(&bytes[at + 1..]) => open = None,
b'\\' if !kept[at] => {
match (bytes[at + 1], open) {
(b'(', None) => open = Some((at, b')')),
(b'[', None) => open = Some((at, b']')),
(close, Some((start, wanted)))
if close == wanted && at > start + 2 && !text[start..at].contains('$') =>
{
out[start..start + 2].copy_from_slice(b"$ ");
out[at..at + 2].copy_from_slice(b" $");
open = None;
}
_ => {}
}
// An escaped backslash escapes nothing after it.
at += 2;
continue;
}
_ => {}
}
at += 1;
}
match out == bytes {
true => Cow::Borrowed(text),
false => Cow::Owned(String::from_utf8(out).expect("ASCII swaps keep UTF-8")),
}
}
/// Whether the line `rest` starts holds only white space.
fn blank_after(rest: &[u8]) -> bool {
rest.iter()
.take_while(|byte| **byte != b'\n')
.all(u8::is_ascii_whitespace)
}
/// An inline formula's TeX: inside `$` runs or `\(` pairs.
pub(crate) fn math_inside(source: &str, whole: Range<usize>) -> Range<usize> {
match source[whole.clone()].starts_with('$') {
true => crate::documents::tree::inside(source, whole, '$'),
false => whole.start + 2..whole.end - 2,
}
}
/// How a whole file parses: front matter too.
pub fn file_options() -> ParseOptions {
pub(crate) fn file_options() -> ParseOptions {
let mut options = options();
options.constructs.frontmatter = true;
options
}
/// A whole file as a syntax tree.
pub(crate) fn file_tree(text: &str) -> mdast::Node {
markdown::to_mdast(text, &file_options()).expect("markdown without MDX always parses")
pub fn file_tree(text: &str) -> mdast::Node {
parsed(text, &file_options())
}
/// The bytes of the source a parsed node spans.
+1 -1
View File
@@ -46,7 +46,7 @@ pub(crate) use history::Change;
pub use history::{PageHistoryDiff, Version, VersionHistory};
pub use knowledge::{Backlink, Backlinks, Favorite, Favorites, TrashBin, Trashed};
pub use link::LinkEditor;
pub use markdown::file_options as markdown_options;
pub use markdown::file_tree as markdown_tree;
pub use markdown::markdown_highlights;
pub use modes::{MarkdownEditor, MarkdownMode};
pub use pages::{PageCover, PageIcon};
+48 -25
View File
@@ -3,7 +3,7 @@ use std::{path::Path, rc::Rc, sync::Arc};
use gpui::{
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StrikethroughStyle, Styled,
StyledText, UnderlineStyle, Window, div, prelude::*, relative,
StyledText, TextLayout, UnderlineStyle, Window, div, prelude::*, relative,
};
use super::{
@@ -21,6 +21,7 @@ use crate::{
};
mod blocks;
mod math_text;
mod point;
mod table;
@@ -170,6 +171,38 @@ fn highlight(style: &Style, colors: &Palette, rule: Pixels) -> HighlightStyle {
}
}
/// `inline` as text; `hidden` ranges lose their highlight.
fn drawn_text(
inline: &Inline,
hidden: &[std::ops::Range<usize>],
links: Option<ElementId>,
on_link: &Option<OnLink>,
cx: &App,
) -> (AnyElement, TextLayout) {
let theme = cx.theme();
let colors = &theme.colors;
let rule = theme.underline_thickness();
let styles: Vec<_> = (inline.styles.iter())
.filter(|(range, _)| !hidden.contains(range))
.map(|(range, style)| (range.clone(), highlight(style, colors, rule)))
.collect();
let styled = StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles));
let layout = styled.layout().clone();
let drawn = match links {
None => styled.into_any_element(),
Some(id) => {
let (ranges, urls): (Vec<_>, Vec<_>) = inline.links.iter().cloned().unzip();
let on_link = on_link.clone();
InteractiveText::new(id, styled)
.on_click(ranges, move |ix, window, cx| {
open_link(&on_link, &urls[ix], window, cx)
})
.into_any_element()
}
};
(drawn, layout)
}
/// Draws nodes under one owner's ids and hooks.
struct Draw<'a> {
id: &'a ElementId,
@@ -198,30 +231,20 @@ impl Draw<'_> {
}
fn inline(&self, inline: &Inline, cx: &App) -> AnyElement {
let theme = cx.theme();
let colors = &theme.colors;
let rule = theme.underline_thickness();
let styles: Vec<_> = inline
.styles
.iter()
.map(|(range, style)| (range.clone(), highlight(style, colors, rule)))
.collect();
let styled =
StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles));
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)
let links = (!inline.links.is_empty()).then(|| self.key("links"));
if !inline.styles.iter().any(|(_, style)| *style == Style::Math) {
let (drawn, layout) = drawn_text(inline, &[], links, self.hooks.on_link, cx);
let at = move |position| {
layout
.index_for_position(position)
.unwrap_or_else(|near| near)
};
return self.point_text(drawn, at, inline);
}
let on_link = self.hooks.on_link.clone();
let (drawn, at) =
math_text::MathText::new(self.key("math"), inline.clone(), links, on_link);
self.point_text(drawn.into_any_element(), at, inline)
}
fn nodes(&self, nodes: &[Node], cx: &App) -> Vec<AnyElement> {
+355
View File
@@ -0,0 +1,355 @@
//! Inline formulas typeset among a paragraph's words.
use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
use gpui::{
AnyElement, App, Bounds, Corners, Element, ElementId, GlobalElementId, InspectorElementId,
IntoElement, LayoutId, Pixels, Point, RenderImage, SharedString, Size, TextLayout,
TextStyleRefinement, Window, point,
};
use super::{OnLink, drawn_text};
use crate::{
documents::tree::{Inline, Style},
typography::typeset,
};
/// Glue that reserves a formula's width and never breaks.
const GLUE: char = '\u{00A0}';
/// An original span and the drawn span standing for it.
type Swap = (Range<usize>, Range<usize>);
/// The drawn text and how its bytes map back.
struct Shown {
layout: TextLayout,
swaps: Vec<Swap>,
}
/// Paragraph text whose formulas draw typeset over reserved glue.
pub(super) struct MathText {
id: ElementId,
inline: Inline,
links: Option<ElementId>,
on_link: Option<OnLink>,
shown: Rc<RefCell<Option<Shown>>>,
text: Option<AnyElement>,
/// Each picture, its size, and the drawn span it covers.
pictures: Vec<(Arc<RenderImage>, Size<Pixels>, Range<usize>)>,
}
impl MathText {
/// The element and how a point finds its original byte.
pub(super) fn new(
id: ElementId,
inline: Inline,
links: Option<ElementId>,
on_link: Option<OnLink>,
) -> (Self, impl Fn(Point<Pixels>) -> usize + 'static) {
let shown = Rc::new(RefCell::new(None));
let read = shown.clone();
let at = move |position| match &*read.borrow() {
Some(Shown { layout, swaps }) => back(
swaps,
layout
.index_for_position(position)
.unwrap_or_else(|near| near),
),
None => 0,
};
let text = Self {
id,
inline,
links,
on_link,
shown,
text: None,
pictures: Vec::new(),
};
(text, at)
}
}
/// Where original byte `at` is drawn.
fn forward(swaps: &[Swap], at: usize) -> usize {
let mut shift = 0isize;
for (written, drawn) in swaps {
if at < written.start {
break;
}
if at < written.end {
return drawn.start;
}
shift = drawn.end as isize - written.end as isize;
}
(at as isize + shift) as usize
}
/// The original byte drawn byte `at` stands for.
fn back(swaps: &[Swap], at: usize) -> usize {
let mut shift = 0isize;
for (written, drawn) in swaps {
if at < drawn.start {
break;
}
if at < drawn.end {
return written.start;
}
shift = written.end as isize - drawn.end as isize;
}
(at as isize + shift) as usize
}
/// `inline` with each swapped span drawn as `glue[ix]` glue.
fn shown(inline: &Inline, swaps: &mut [Swap], glue: &[usize]) -> Inline {
let mut text = String::new();
let mut from = 0;
for ((written, drawn), count) in swaps.iter_mut().zip(glue) {
text.push_str(&inline.text[from..written.start]);
let start = text.len();
text.extend(std::iter::repeat_n(GLUE, *count));
*drawn = start..text.len();
from = written.end;
}
text.push_str(&inline.text[from..]);
let span = |range: &Range<usize>| forward(swaps, range.start)..forward(swaps, range.end);
Inline {
text,
styles: (inline.styles.iter())
.map(|(range, style)| (span(range), *style))
.collect(),
links: (inline.links.iter())
.map(|(range, url)| (span(range), url.clone()))
.collect(),
sources: Vec::new(),
}
}
impl IntoElement for MathText {
type Element = Self;
fn into_element(self) -> Self {
self
}
}
impl Element for MathText {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let style = window.text_style();
let rem = window.rem_size();
let font = style.font_size.to_pixels(rem);
let glue = String::from(GLUE);
let run = style.to_run(glue.len());
let width = window
.text_system()
.shape_line(glue.into(), font, &[run], None)
.width;
let mut maths: Vec<Range<usize>> = (self.inline.styles.iter())
.filter(|(_, style)| *style == Style::Math)
.map(|(range, _)| range.clone())
.collect();
maths.sort_by_key(|range| range.start);
let mut swaps = Vec::new();
let mut counts = Vec::new();
let mut pictures = Vec::new();
for (ix, range) in maths.into_iter().enumerate() {
let source = SharedString::from(self.inline.text[range.clone()].to_string());
match typeset(
(self.id.clone(), SharedString::from(format!("formula-{ix}"))),
(&source, false),
font,
window,
cx,
) {
Some(Ok((image, size))) => {
counts.push((size.width / width).ceil().max(1.0) as usize);
pictures.push((image, size));
swaps.push((range, 0..0));
}
// The formula is the user's text: not logged.
Some(Err(_)) => log::warn!("markdown: an inline formula shows as TeX, unset"),
None => {}
}
}
let inline = shown(&self.inline, &mut swaps, &counts);
let hidden: Vec<Range<usize>> = swaps.iter().map(|(_, drawn)| drawn.clone()).collect();
let (mut text, layout) =
drawn_text(&inline, &hidden, self.links.clone(), &self.on_link, cx);
let tallest = pictures
.iter()
.map(|(_, size)| size.height)
.fold(Pixels::ZERO, Pixels::max);
// A tall formula opens the paragraph's lines.
let taller = (tallest > style.line_height_in_pixels(rem)).then(|| TextStyleRefinement {
line_height: Some(tallest.into()),
..TextStyleRefinement::default()
});
let id = window.with_text_style(taller, |window| text.request_layout(window, cx));
self.pictures = (pictures.into_iter().zip(&hidden))
.map(|((image, size), drawn)| (image, size, drawn.clone()))
.collect();
*self.shown.borrow_mut() = Some(Shown { layout, swaps });
self.text = Some(text);
(id, ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
window: &mut Window,
cx: &mut App,
) {
if let Some(text) = &mut self.text {
text.prepaint(window, cx);
}
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut (),
_: &mut (),
window: &mut Window,
cx: &mut App,
) {
let Some(text) = &mut self.text else {
return;
};
text.paint(window, cx);
let shown = self.shown.borrow();
let Some(Shown { layout, .. }) = &*shown else {
return;
};
let line = layout.line_height();
for (image, size, drawn) in &self.pictures {
let (Some(start), Some(end)) = (
layout.position_for_index(drawn.start),
layout.position_for_index(drawn.end),
) else {
continue;
};
// Glue at a line's end ends there.
let room = if end.y == start.y {
end.x - start.x
} else {
size.width
};
let origin = start + point((room - size.width) / 2.0, (line - size.height) / 2.0);
let bounds = Bounds {
origin,
size: *size,
};
if let Err(error) =
window.paint_image(bounds, bounds, Corners::default(), image.clone(), 0, false)
{
log::error!("markdown: an inline formula did not paint: {error}");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bytes_map_both_ways_around_a_formula() {
let inline = Inline {
text: "a x^2 b".into(),
styles: vec![(2..5, Style::Math), (6..7, Style::Strong)],
..Inline::default()
};
let mut swaps = vec![(2..5, 0..0)];
let drawn = shown(&inline, &mut swaps, &[4]);
assert_eq!(drawn.text, format!("a {} b", GLUE.to_string().repeat(4)));
let glue = GLUE.len_utf8() * 4;
assert_eq!(swaps[0].1, 2..2 + glue);
assert_eq!(
drawn.styles[1].0,
3 + glue..4 + glue,
"the bold b moves past the glue"
);
assert_eq!(back(&swaps, 3), 2, "glue reads back to its formula");
assert_eq!(back(&swaps, 3 + glue), 6);
assert_eq!(forward(&swaps, 6), 3 + glue);
}
type Found = Rc<
RefCell<
Option<(
Rc<RefCell<Option<Shown>>>,
Box<dyn Fn(Point<Pixels>) -> usize>,
)>,
>,
>;
struct Probe(Found);
impl gpui::Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
use gpui::{ParentElement as _, Styled as _};
let inline = Inline {
text: "a x^2 b".into(),
styles: vec![(2..5, Style::Math)],
..Inline::default()
};
let (text, at) = MathText::new("probe".into(), inline, None, None);
*self.0.borrow_mut() = Some((text.shown.clone(), Box::new(at)));
gpui::div().text_size(gpui::px(16.0)).child(text)
}
}
#[gpui::test]
fn a_formula_draws_over_glue_and_reads_back(cx: &mut gpui::TestAppContext) {
cx.update(crate::theme::Theme::init);
let found = Found::default();
let held = found.clone();
let (_, cx) = cx.add_window_view(move |_, _| Probe(held));
for _ in 0..4 {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
}
let found = found.borrow();
let (shown, at) = found.as_ref().expect("drawn");
let shown = shown.borrow();
let Shown { layout, swaps } = shown.as_ref().expect("laid out");
assert_eq!(swaps.len(), 1, "the formula typeset in place");
let drawn = swaps[0].1.clone();
let (start, end) = (
layout.position_for_index(drawn.start),
layout.position_for_index(drawn.end),
);
let (start, end) = (start.expect("its start"), end.expect("its end"));
assert!(
end.x - start.x > gpui::px(10.0),
"glue holds the picture's width"
);
let middle = point(
(start.x + end.x) / 2.0,
start.y + layout.line_height() / 2.0,
);
assert_eq!(at(middle), 2, "a press on the picture is the formula");
}
}
+3 -5
View File
@@ -140,11 +140,11 @@ impl Draw<'_> {
})
}
/// Drawn `inline`; a press finds its byte through `layout`.
/// Drawn `inline`; a press finds its byte through `at`.
pub(super) fn point_text(
&self,
element: AnyElement,
layout: TextLayout,
at: impl Fn(gpui::Point<gpui::Pixels>) -> usize + 'static,
inline: &Inline,
) -> AnyElement {
if self.hooks.on_point.is_none() {
@@ -152,9 +152,7 @@ impl Draw<'_> {
}
let inline = Rc::new(inline.clone());
self.pointable(element, move |event| {
let at = layout
.index_for_position(event.position)
.unwrap_or_else(|near| near);
let at = at(event.position);
let link = inline.links.iter().find(|(range, _)| range.contains(&at));
Pointed {
link: link.map(|(_, url)| url.clone()),
+5 -3
View File
@@ -318,12 +318,14 @@ impl<'a> Reader<'a> {
}
}
/// Text split at its images; lone `$$` math is block.
/// Text split at its images; lone `$$` or `\[` math is block.
fn paragraph(&self, node: &mdast::Node, children: &[mdast::Node], out: &mut Vec<Node>) {
if let [mdast::Node::InlineMath(math)] = children
&& self.source[span(node)].starts_with("$$")
&& ["$$", "\\["]
.iter()
.any(|open| self.source[span(node)].starts_with(open))
{
return out.push(Node::Math(math.value.clone()));
return out.push(Node::Math(math.value.trim().to_string()));
}
let mut run = Run {
table: matches!(node, mdast::Node::TableCell(_)),
+4 -1
View File
@@ -32,7 +32,10 @@ pub(super) fn written(node: &Node, source: &str) -> Option<(Range<usize>, Escape
Some(match node {
Node::Text(_) => (whole, Escapes::Markdown),
Node::InlineCode(_) => (inside(source, whole, '`'), Escapes::Plain),
Node::InlineMath(_) => (inside(source, whole, '$'), Escapes::Plain),
Node::InlineMath(_) => (
crate::documents::markdown::math_inside(source, whole),
Escapes::Plain,
),
_ => (whole, Escapes::Plain),
})
}
+38
View File
@@ -416,3 +416,41 @@ fn the_first_shortcode_decides_its_text_node() {
assert_eq!(line(source).text, shown, "{source:?}");
}
}
#[test]
fn latex_delimiters_read_as_math() {
assert_eq!(parse(r"\[c = 1\]"), [Node::Math("c = 1".into())]);
assert_eq!(parse("\\[\nc = 1\n\\]"), [Node::Math("c = 1".into())]);
let Node::Paragraph(inline) = &parse(r"so \(c\) holds")[0] else {
panic!("inline math stays in its paragraph")
};
assert_eq!(inline.text, "so c holds");
assert_eq!(inline.styles, [(3..4, Style::Math)]);
let Node::Paragraph(alone) = &parse(r"\(c\)")[0] else {
panic!("a lone formula stays inline")
};
assert_eq!(alone.styles, [(0..1, Style::Math)]);
}
#[test]
fn latex_delimiters_leave_code_escapes_and_open_pairs() {
let text = |source: &str| match &parse(source)[0] {
Node::Paragraph(inline) => (inline.text.clone(), inline.styles.clone()),
other => panic!("a paragraph, not {other:?}"),
};
assert_eq!(text(r"`\(c\)`").1, [(0..5, Style::Code)], "code stays code");
assert_eq!(
text(r"\\(c\\)"),
(r"\(c\)".into(), Vec::new()),
"escaped backslashes"
);
assert_eq!(text(r"a \(c").0, "a (c", "no closing, no formula");
let split = parse("a \\(c\n\nd\\) e");
assert!(
split.iter().all(|node| match node {
Node::Paragraph(inline) => inline.styles.is_empty(),
_ => false,
}),
"a blank line ends the formula: {split:?}"
);
}
+29 -17
View File
@@ -96,7 +96,7 @@ struct Formula {
}
/// A typeset formula: its picture and logical size.
type Typeset = Result<(Arc<RenderImage>, Size<Pixels>), MathError>;
pub(crate) type Typeset = Result<(Arc<RenderImage>, Size<Pixels>), MathError>;
enum Typesetter {}
@@ -201,27 +201,39 @@ fn framed(svg: &str, width: f32, height: f32) -> Result<(String, Size<Pixels>),
Ok((reframed, size(px(w), px(h))))
}
impl Latex {
/// The asset key this formula draws from in `window`.
fn formula(&self, window: &Window, cx: &App) -> (Formula, Pixels) {
let theme = cx.theme();
let font = theme.text_size(self.size).to_pixels(window.rem_size());
let formula = Formula {
source: self.source.clone(),
display: self.display,
rgba: u32::from(theme.colors.fg.to_rgb()),
font: (font.as_f32() * 100.0).round() as u32,
scale: (window.scale_factor() * 100.0).round() as u32,
};
(formula, font)
/// The asset key for `source` at `font` in `window`.
fn formula(
(source, display): (&SharedString, bool),
font: Pixels,
window: &Window,
cx: &App,
) -> Formula {
Formula {
source: source.clone(),
display,
rgba: u32::from(cx.theme().colors.fg.to_rgb()),
font: (font.as_f32() * 100.0).round() as u32,
scale: (window.scale_factor() * 100.0).round() as u32,
}
}
/// `source` typeset at `font` in `window`; None while it loads.
pub(crate) fn typeset(
id: impl Into<ElementId>,
(source, display): (&SharedString, bool),
font: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<Typeset> {
let formula = formula((source, display), font, window, cx);
let picture = |typeset: &Typeset| typeset.as_ref().ok().map(|(image, _)| image.clone());
hold::<Typesetter>(id, &formula, picture, window, cx)
}
impl RenderOnce for Latex {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (formula, font) = self.formula(window, cx);
let picture = |typeset: &Typeset| typeset.as_ref().ok().map(|(image, _)| image.clone());
match hold::<Typesetter>(self.id, &formula, picture, window, cx) {
let font = cx.theme().text_size(self.size).to_pixels(window.rem_size());
match typeset(self.id, (&self.source, self.display), font, window, cx) {
Some(Ok((picture, shape))) => img(picture)
.w(shape.width)
.h(shape.height)
+5 -2
View File
@@ -114,8 +114,11 @@ fn flush(cx: &mut gpui::VisualTestContext) {
/// Whether `source`'s picture sits in the asset cache.
fn cached(source: &str, cx: &mut gpui::VisualTestContext) -> bool {
cx.update(|window, cx| {
let latex = Latex::parse("probe", source).expect("parses");
let (formula, _) = latex.formula(window, cx);
let font = cx
.theme()
.text_size(TextSize::Lg)
.to_pixels(window.rem_size());
let formula = super::formula((&source.to_string().into(), false), font, window, cx);
cx.has_asset::<Typesetter>(&formula)
})
}
+1
View File
@@ -26,6 +26,7 @@ pub use formatted::{
pub use inline::{Blockquote, Code, Highlight, Kbd, KbdCombo};
pub use link::{ExternalLink, Link};
pub(crate) use math::failed;
pub(crate) use math::typeset;
pub use math::{Latex, MathError, formula_svg};
pub use rolling::AnimatedNumber;
pub use select::SelectableText;