diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index f632d9332..a97e5a0a8 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -168,6 +168,15 @@ fn is_in_rect(pos: Point, size: Size, click: Point) -> bool { #[cfg(test)] mod tests { + //! This can be used in a normal test to determine positions: + //! ``` + //! #set page(background: place( + //! dx: 10pt, + //! dy: 10pt, + //! square(size: 2pt, fill: red), + //! )) + //! ``` + use std::num::NonZeroUsize; use typst::layout::{Abs, Point, Position}; @@ -200,7 +209,16 @@ mod tests { fn test_click(text: &str, click: Point, expected: Option) { let world = TestWorld::new(text); let doc = typst::compile(&world).output.unwrap(); - assert_eq!(jump_from_click(&world, &doc, &doc.pages[0].frame, click), expected); + let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click); + if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) = + (&jump, &expected) + { + assert_eq!(pos.page, expected.page); + assert_approx_eq!(pos.point.x, expected.point.x); + assert_approx_eq!(pos.point.y, expected.point.y); + } else { + assert_eq!(jump, expected); + } } #[track_caller] @@ -240,4 +258,10 @@ mod tests { test_cursor(s, 12, None); test_cursor(s, 14, pos(1, 37.55, 16.58)); } + + #[test] + fn test_backlink() { + let s = "#footnote[Hi]"; + test_click(s, point(10.0, 10.0), pos(1, 18.5, 37.1).map(Jump::Position)); + } } diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 8a3252fbc..5a22734b5 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -137,20 +137,26 @@ impl Content { self.inner.label } - /// Set the label of the content. + /// Attach a label to the content. pub fn labelled(mut self, label: Label) -> Self { - self.make_mut().label = Some(label); + self.set_label(label); self } - /// Check whether a show rule recipe is disabled. - pub fn is_guarded(&self, index: RecipeIndex) -> bool { - self.inner.lifecycle.contains(index.0) + /// Set the label of the content. + pub fn set_label(&mut self, label: Label) { + self.make_mut().label = Some(label); } - /// Whether this content has already been prepared. - pub fn is_prepared(&self) -> bool { - self.inner.lifecycle.contains(0) + /// Assigns a location to the content. + /// + /// This identifies the content and e.g. makes it linkable by + /// `.linked(Destination::Location(loc))`. + /// + /// Useful in combination with [`Location::variant`]. + pub fn located(mut self, loc: Location) -> Self { + self.set_location(loc); + self } /// Set the location of the content. @@ -158,12 +164,22 @@ impl Content { self.make_mut().location = Some(location); } + /// Check whether a show rule recipe is disabled. + pub fn is_guarded(&self, index: RecipeIndex) -> bool { + self.inner.lifecycle.contains(index.0) + } + /// Disable a show rule recipe. pub fn guarded(mut self, index: RecipeIndex) -> Self { self.make_mut().lifecycle.insert(index.0); self } + /// Whether this content has already been prepared. + pub fn is_prepared(&self) -> bool { + self.inner.lifecycle.contains(0) + } + /// Mark this content as prepared. pub fn mark_prepared(&mut self) { self.make_mut().lifecycle.insert(0); @@ -488,15 +504,6 @@ impl Content { self.styled(LinkElem::set_dests(smallvec![dest])) } - /// Make the content linkable by `.linked(Destination::Location(loc))`. - /// - /// Should be used in combination with [`Location::variant`]. - pub fn backlinked(self, loc: Location) -> Self { - let mut backlink = Content::empty().spanned(self.span()); - backlink.set_location(loc); - self - } - /// Set alignments for this content. pub fn aligned(self, align: Alignment) -> Self { self.styled(AlignElem::set_alignment(align)) diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index 9070442e6..ed593d45c 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -833,13 +833,16 @@ impl<'a> Generator<'a> { let dest = Destination::Location(*location); content = content.linked(dest); } - content.backlinked(backlink) + content }); // Render the main reference content. - let reference = renderer - .display_elem_children(&item.content, &mut prefix) - .backlinked(backlink); + let mut reference = + renderer.display_elem_children(&item.content, &mut prefix); + + // Attach a backlink to either the prefix or the reference so that + // we can link to the bibliography entry. + prefix.as_mut().unwrap_or(&mut reference).set_location(backlink); output.push((prefix, reference)); } diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs index 67c2260d8..f8f36eb23 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -286,7 +286,8 @@ impl Show for Packed { .pack() .spanned(span) .linked(Destination::Location(loc)) - .backlinked(loc.variant(1)); + .located(loc.variant(1)); + Ok(Content::sequence([ HElem::new(self.indent(styles).into()).pack(), sup, diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index 824ad3b1a..a67660c1f 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -119,6 +119,8 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { .store(EquationElem::new(content.clone()).pack().spanned(content.span())); } + // Styled elements and sequences can (at least currently) also have + // labels, so this needs to happen before they are handled. if let Some(realized) = process(self.engine, &mut self.locator, content, styles)? { self.engine.route.increase(); diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs index 150f07ab7..913ee8f0e 100644 --- a/crates/typst/src/realize/process.rs +++ b/crates/typst/src/realize/process.rs @@ -168,6 +168,7 @@ fn verdict<'a>( && map.is_empty() && (prepared || { target.label().is_none() + && target.location().is_none() && !target.can::() && !target.can::() && !target.can::()