summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMALO <57839069+MDLC01@users.noreply.github.com>2023-09-12 14:47:36 +0200
committerGitHub <noreply@github.com>2023-09-12 14:47:36 +0200
commit976abdfe7dc08ae42ee87e5c2d4ff46ebe172dd1 (patch)
tree836d1b46f9e2e25205eb0f85c1c65a216bc9d655
parente39be71a54a4264032115ab3b31bdd86845a966b (diff)
Add `figure.caption` element (#1704)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
-rw-r--r--crates/typst-library/src/meta/figure.rs280
-rw-r--r--docs/changelog.md4
-rw-r--r--tests/ref/meta/figure-caption.pngbin0 -> 9192 bytes
-rw-r--r--tests/typ/meta/figure-caption.typ56
-rw-r--r--tests/typ/meta/figure.typ2
-rw-r--r--tests/typ/meta/query-figure.typ2
6 files changed, 265 insertions, 79 deletions
diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs
index 6e95dce7..4aebdda6 100644
--- a/crates/typst-library/src/meta/figure.rs
+++ b/crates/typst-library/src/meta/figure.rs
@@ -11,9 +11,9 @@ use crate::visualize::ImageElem;
/// A figure with an optional caption.
///
-/// Automatically detects its contents to select the correct counting track.
-/// For example, figures containing images will be numbered separately from
-/// figures containing tables.
+/// Automatically detects its contents to select the correct counting track. For
+/// example, figures containing images will be numbered separately from figures
+/// containing tables.
///
/// # Examples
/// The example below shows a basic figure with an image:
@@ -44,36 +44,51 @@ use crate::visualize::ImageElem;
/// This behaviour can be overridden by explicitly specifying the figure's
/// `kind`. All figures of the same kind share a common counter.
///
-/// # Modifying the appearance { #modifying-appearance }
-/// You can completely customize the look of your figures with a [show
-/// rule]($styling/#show-rules). In the example below, we show the figure's
-/// caption above its body and display its supplement and counter after the
-/// caption.
+/// # Figure behaviour
+/// By default, figures are placed within the flow of content. To make them
+/// float to the top or bottom of the page, you can use the
+/// [`placement`]($figure.placement) argument.
+///
+/// If your figure is too large and its contents are breakable across pages
+/// (e.g. if it contains a large table), then you can make the figure itself
+/// breakable across pages as well with this show rule:
+/// ```typ
+/// #show figure: set block(breakable: true)
+/// ```
+///
+/// See the [block]($block.breakable) documentation for more information about
+/// breakable and non-breakable blocks.
+///
+/// # Caption customization
+/// You can modify the apperance of the figure's caption with its associated
+/// [`caption`]($figure.caption) function. In the example below, we emphasize
+/// all captions:
///
/// ```example
-/// #show figure: it => align(center)[
-/// #it.caption |
-/// #emph[
-/// #it.supplement
-/// #it.counter.display(it.numbering)
-/// ]
-/// #v(10pt, weak: true)
-/// #it.body
-/// ]
+/// #show figure.caption: emph
///
/// #figure(
-/// image("molecular.jpg", width: 80%),
-/// caption: [
-/// The molecular testing pipeline.
-/// ],
+/// rect[Hello],
+/// caption: [I am emphasized!],
/// )
/// ```
///
-/// If your figure is too large and its contents are breakable across pages
-/// (e.g. if it contains a large table), then you can make the figure breakable
-/// across pages as well by using `[#show figure: set block(breakable: true)]`
-/// (see the [block]($block) documentation for more information).
-#[elem(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
+/// By using a [`where`]($function.where) selector, we can scope such rules to
+/// specific kinds of figures. For example, to position the caption above
+/// tables, but keep it below for all other kinds of figures, we could write the
+/// following show-set rule:
+///
+/// ```example
+/// #show figure.where(
+/// kind: table
+/// ): set figure.caption(position: top)
+///
+/// #figure(
+/// table(columns: 2)[A][B][C][D],
+/// caption: [I'm up here],
+/// )
+/// ```
+#[elem(scope, Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
pub struct FigureElem {
/// The content of the figure. Often, an [image]($image).
#[required]
@@ -88,6 +103,10 @@ pub struct FigureElem {
/// - `{top}`: The figure floats to the top of the page.
/// - `{bottom}`: The figure floats to the bottom of the page.
///
+ /// The gap between the main flow content and the floating figure is
+ /// controlled by the [`clearance`]($place.clearance) argument on the
+ /// `place` function.
+ ///
/// ```example
/// #set page(height: 200pt)
///
@@ -102,33 +121,7 @@ pub struct FigureElem {
pub placement: Option<Smart<VAlign>>,
/// The figure's caption.
- pub caption: Option<Content>,
-
- /// The caption's position. Either `{top}` or `{bottom}`.
- ///
- /// ```example
- /// #figure(
- /// table(columns: 2)[A][B],
- /// caption: [I'm up here],
- /// caption-pos: top,
- /// )
- ///
- /// #figure(
- /// table(columns: 2)[A][B],
- /// caption: [I'm down here],
- /// )
- /// ```
- #[default(VAlign::Bottom)]
- #[parse({
- let option: Option<Spanned<VAlign>> = args.named("caption-pos")?;
- if let Some(Spanned { v: align, span }) = option {
- if align == VAlign::Horizon {
- bail!(span, "expected `top` or `bottom`");
- }
- }
- option.map(|spanned| spanned.v)
- })]
- pub caption_pos: VAlign,
+ pub caption: Option<FigureCaption>,
/// The kind of figure this is.
///
@@ -204,6 +197,12 @@ pub struct FigureElem {
pub counter: Option<Counter>,
}
+#[scope]
+impl FigureElem {
+ #[elem]
+ type FigureCaption;
+}
+
impl Synthesize for FigureElem {
fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
let numbering = self.numbering(styles);
@@ -238,9 +237,9 @@ impl Synthesize for FigureElem {
bail!(self.span(), "please specify the figure's supplement")
}
- name.unwrap_or_default()
+ Some(name.unwrap_or_default())
}
- Smart::Custom(None) => Content::empty(),
+ Smart::Custom(None) => None,
Smart::Custom(Some(supplement)) => {
// Resolve the supplement with the first descendant of the kind or
// just the body, if none was found.
@@ -252,7 +251,7 @@ impl Synthesize for FigureElem {
};
let target = descendant.unwrap_or_else(|| self.body());
- supplement.resolve(vt, [target])?
+ Some(supplement.resolve(vt, [target])?)
}
};
@@ -264,11 +263,20 @@ impl Synthesize for FigureElem {
}),
)));
+ // Fill the figure's caption.
+ let mut caption = self.caption(styles);
+ if let Some(caption) = &mut caption {
+ caption.push_kind(kind.clone());
+ caption.push_supplement(supplement.clone());
+ caption.push_numbering(numbering.clone());
+ caption.push_counter(Some(counter.clone()));
+ caption.push_location(self.0.location());
+ }
+
self.push_placement(self.placement(styles));
- self.push_caption_pos(self.caption_pos(styles));
- self.push_caption(self.caption(styles));
+ self.push_caption(caption);
self.push_kind(Smart::Custom(kind));
- self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
+ self.push_supplement(Smart::Custom(supplement.map(Supplement::Content)));
self.push_numbering(numbering);
self.push_outlined(self.outlined(styles));
self.push_counter(Some(counter));
@@ -279,18 +287,18 @@ impl Synthesize for FigureElem {
impl Show for FigureElem {
#[tracing::instrument(name = "FigureElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body();
// Build the caption, if any.
- if let Some(caption) = self.full_caption(vt)? {
+ if let Some(caption) = self.caption(styles) {
let v = VElem::weak(self.gap(styles).into()).pack();
- realized = if self.caption_pos(styles) == VAlign::Bottom {
- realized + v + caption
+ realized = if caption.position(styles) == VAlign::Bottom {
+ realized + v + caption.pack()
} else {
- caption + v + realized
- }
- };
+ caption.pack() + v + realized
+ };
+ }
// Wrap the contents in a block.
realized = BlockElem::new()
@@ -351,14 +359,9 @@ impl Outlinable for FigureElem {
return Ok(None);
}
- self.full_caption(vt)
- }
-}
-
-impl FigureElem {
- /// Builds the full caption for the figure (with supplement and numbering).
- pub fn full_caption(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
- let Some(mut caption) = self.caption(StyleChain::default()) else {
+ let Some(mut caption) =
+ self.caption(StyleChain::default()).map(|caption| caption.body())
+ else {
return Ok(None);
};
@@ -375,7 +378,7 @@ impl FigureElem {
let numbers = counter.at(vt, location)?.display(vt, &numbering)?;
if !supplement.is_empty() {
- supplement += TextElem::packed("\u{a0}");
+ supplement += TextElem::packed('\u{a0}');
}
caption = supplement + numbers + TextElem::packed(": ") + caption;
@@ -385,6 +388,133 @@ impl FigureElem {
}
}
+/// The caption of a figure. This element can be used in set and show rules to
+/// customize the appearance of captions for all figures or figures of a
+/// specific kind.
+///
+/// In addition to its `pos` and `body`, the `caption` also provides the
+/// figure's `kind`, `supplement`, `counter`, `numbering`, and `location` as
+/// fields. These parts can be used in [`where`]($function.where) selectors and
+/// show rules to build a completely custom caption.
+///
+/// ```example
+/// #show figure.caption: emph
+///
+/// #figure(
+/// rect[Hello],
+/// caption: [A rectangle],
+/// )
+/// ```
+#[elem(name = "caption", Synthesize, Show)]
+pub struct FigureCaption {
+ /// The caption's position in the figure. Either `{top}` or `{bottom}`.
+ ///
+ /// ```example
+ /// #show figure.where(
+ /// kind: table
+ /// ): set figure.caption(position: top)
+ ///
+ /// #figure(
+ /// table(columns: 2)[A][B],
+ /// caption: [I'm up here],
+ /// )
+ ///
+ /// #figure(
+ /// rect[Hi],
+ /// caption: [I'm down here],
+ /// )
+ ///
+ /// #figure(
+ /// table(columns: 2)[A][B],
+ /// caption: figure.caption(
+ /// position: bottom,
+ /// [I'm down here too!]
+ /// )
+ /// )
+ /// ```
+ #[default(VAlign::Bottom)]
+ #[parse({
+ let option: Option<Spanned<VAlign>> = args.named("position")?;
+ if let Some(Spanned { v: align, span }) = option {
+ if align == VAlign::Horizon {
+ bail!(span, "expected `top` or `bottom`");
+ }
+ }
+ option.map(|spanned| spanned.v)
+ })]
+ pub position: VAlign,
+
+ /// The caption's body.
+ ///
+ /// Can be used alongside `kind`, `supplement`, `counter`, `numbering`, and
+ /// `location` to completely customize the caption.
+ ///
+ /// ```example
+ /// #show figure.caption: it => [
+ /// #underline(it.body) |
+ /// #it.supplement #it.counter.display(it.numbering)
+ /// ]
+ ///
+ /// #figure(
+ /// rect[Hello],
+ /// caption: [A rectangle],
+ /// )
+ /// ```
+ #[required]
+ pub body: Content,
+
+ /// The figure's supplement.
+ #[synthesized]
+ pub kind: FigureKind,
+
+ /// The figure's supplement.
+ #[synthesized]
+ pub supplement: Option<Content>,
+
+ /// How to number the figure.
+ #[synthesized]
+ pub numbering: Option<Numbering>,
+
+ /// The counter for the figure.
+ #[synthesized]
+ pub counter: Option<Counter>,
+
+ /// The figure's location.
+ #[synthesized]
+ pub location: Option<Location>,
+}
+
+impl Synthesize for FigureCaption {
+ fn synthesize(&mut self, _: &mut Vt, styles: StyleChain) -> SourceResult<()> {
+ self.push_position(self.position(styles));
+ Ok(())
+ }
+}
+
+impl Show for FigureCaption {
+ #[tracing::instrument(name = "FigureCaption::show", skip_all)]
+ fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
+ let mut realized = self.body();
+
+ if let (Some(mut supplement), Some(numbering), Some(counter), Some(location)) =
+ (self.supplement(), self.numbering(), self.counter(), self.location())
+ {
+ let numbers = counter.at(vt, location)?.display(vt, &numbering)?;
+ if !supplement.is_empty() {
+ supplement += TextElem::packed('\u{a0}');
+ }
+ realized = supplement + numbers + TextElem::packed(": ") + realized;
+ }
+
+ Ok(realized)
+ }
+}
+
+cast! {
+ FigureCaption,
+ v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
+}
+
/// The `kind` parameter of a [`FigureElem`].
#[derive(Debug, Clone)]
pub enum FigureKind {
diff --git a/docs/changelog.md b/docs/changelog.md
index 591d2df4..fb3dd263 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -124,8 +124,8 @@ description: |
- Miscellaneous Improvements
- Added [`bookmarked`]($heading.bookmarked) argument to heading to control
whether a heading becomes part of the PDF outline
- - Added [`caption-pos`]($figure.caption-pos) argument to control the position
- of a figure's caption
+ - Added [`caption-pos`]($figure.caption.position) argument to control the
+ position of a figure's caption
- Added [`metadata`]($metadata) function for exposing an arbitrary value to
the introspection system
- Fixed that a [`state`]($state) was identified by the pair `(key, init)`
diff --git a/tests/ref/meta/figure-caption.png b/tests/ref/meta/figure-caption.png
new file mode 100644
index 00000000..8a1d4a59
--- /dev/null
+++ b/tests/ref/meta/figure-caption.png
Binary files differ
diff --git a/tests/typ/meta/figure-caption.typ b/tests/typ/meta/figure-caption.typ
new file mode 100644
index 00000000..2a12cc22
--- /dev/null
+++ b/tests/typ/meta/figure-caption.typ
@@ -0,0 +1,56 @@
+// Test figure captions.
+
+---
+// Test figure.caption element
+#show figure.caption: emph
+
+#figure(
+ [Not italicized],
+ caption: [Italicized],
+)
+
+---
+// Test figure.caption element for specific figure kinds
+#show figure.caption.where(kind: table): underline
+
+#figure(
+ [Not a table],
+ caption: [Not underlined],
+)
+
+#figure(
+ table[A table],
+ caption: [Underlined],
+)
+
+---
+// Test creating custom figure and custom caption
+
+#let gap = 0.7em
+#show figure.where(kind: "custom"): it => rect(inset: gap, {
+ align(center, it.body)
+ v(gap, weak: true)
+ line(length: 100%)
+ v(gap, weak: true)
+ align(center, it.caption)
+})
+
+#figure(
+ [A figure],
+ kind: "custom",
+ caption: [Hi],
+ supplement: [A],
+)
+
+#show figure.caption: it => emph[
+ #it.body
+ (#it.supplement
+ #it.counter.display(it.numbering))
+]
+
+#figure(
+ [Another figure],
+ kind: "custom",
+ caption: [Hi],
+ supplement: [B],
+)
diff --git a/tests/typ/meta/figure.typ b/tests/typ/meta/figure.typ
index 48dea0e8..62d163a9 100644
--- a/tests/typ/meta/figure.typ
+++ b/tests/typ/meta/figure.typ
@@ -41,7 +41,7 @@ We can clearly see that @fig-cylinder and
#show figure.where(kind: "theorem"): it => {
let name = none
if not it.caption == none {
- name = [ #emph(it.caption)]
+ name = [ #emph(it.caption.body)]
} else {
name = []
}
diff --git a/tests/typ/meta/query-figure.typ b/tests/typ/meta/query-figure.typ
index b1e59abe..0540d65a 100644
--- a/tests/typ/meta/query-figure.typ
+++ b/tests/typ/meta/query-figure.typ
@@ -17,7 +17,7 @@
Figure
#numbering(it.numbering,
..counter(figure).at(it.location())):
- #it.caption
+ #it.caption.body
#box(width: 1fr, repeat[.])
#counter(page).at(it.location()).first() \
]