Add figure.caption element (#1704)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
MALO 2023-09-12 14:47:36 +02:00 committed by GitHub
parent e39be71a54
commit 976abdfe7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 265 additions and 79 deletions

View File

@ -11,9 +11,9 @@ use crate::visualize::ImageElem;
/// A figure with an optional caption. /// A figure with an optional caption.
/// ///
/// Automatically detects its contents to select the correct counting track. /// Automatically detects its contents to select the correct counting track. For
/// For example, figures containing images will be numbered separately from /// example, figures containing images will be numbered separately from figures
/// figures containing tables. /// containing tables.
/// ///
/// # Examples /// # Examples
/// The example below shows a basic figure with an image: /// 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 /// This behaviour can be overridden by explicitly specifying the figure's
/// `kind`. All figures of the same kind share a common counter. /// `kind`. All figures of the same kind share a common counter.
/// ///
/// # Modifying the appearance { #modifying-appearance } /// # Figure behaviour
/// You can completely customize the look of your figures with a [show /// By default, figures are placed within the flow of content. To make them
/// rule]($styling/#show-rules). In the example below, we show the figure's /// float to the top or bottom of the page, you can use the
/// caption above its body and display its supplement and counter after the /// [`placement`]($figure.placement) argument.
/// caption. ///
/// 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 /// ```example
/// #show figure: it => align(center)[ /// #show figure.caption: emph
/// #it.caption |
/// #emph[
/// #it.supplement
/// #it.counter.display(it.numbering)
/// ]
/// #v(10pt, weak: true)
/// #it.body
/// ]
/// ///
/// #figure( /// #figure(
/// image("molecular.jpg", width: 80%), /// rect[Hello],
/// caption: [ /// caption: [I am emphasized!],
/// The molecular testing pipeline.
/// ],
/// ) /// )
/// ``` /// ```
/// ///
/// If your figure is too large and its contents are breakable across pages /// By using a [`where`]($function.where) selector, we can scope such rules to
/// (e.g. if it contains a large table), then you can make the figure breakable /// specific kinds of figures. For example, to position the caption above
/// across pages as well by using `[#show figure: set block(breakable: true)]` /// tables, but keep it below for all other kinds of figures, we could write the
/// (see the [block]($block) documentation for more information). /// following show-set rule:
#[elem(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] ///
/// ```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 { pub struct FigureElem {
/// The content of the figure. Often, an [image]($image). /// The content of the figure. Often, an [image]($image).
#[required] #[required]
@ -88,6 +103,10 @@ pub struct FigureElem {
/// - `{top}`: The figure floats to the top of the page. /// - `{top}`: The figure floats to the top of the page.
/// - `{bottom}`: The figure floats to the bottom 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 /// ```example
/// #set page(height: 200pt) /// #set page(height: 200pt)
/// ///
@ -102,33 +121,7 @@ pub struct FigureElem {
pub placement: Option<Smart<VAlign>>, pub placement: Option<Smart<VAlign>>,
/// The figure's caption. /// The figure's caption.
pub caption: Option<Content>, pub caption: Option<FigureCaption>,
/// 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,
/// The kind of figure this is. /// The kind of figure this is.
/// ///
@ -204,6 +197,12 @@ pub struct FigureElem {
pub counter: Option<Counter>, pub counter: Option<Counter>,
} }
#[scope]
impl FigureElem {
#[elem]
type FigureCaption;
}
impl Synthesize for FigureElem { impl Synthesize for FigureElem {
fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
let numbering = self.numbering(styles); let numbering = self.numbering(styles);
@ -238,9 +237,9 @@ impl Synthesize for FigureElem {
bail!(self.span(), "please specify the figure's supplement") 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)) => { Smart::Custom(Some(supplement)) => {
// Resolve the supplement with the first descendant of the kind or // Resolve the supplement with the first descendant of the kind or
// just the body, if none was found. // just the body, if none was found.
@ -252,7 +251,7 @@ impl Synthesize for FigureElem {
}; };
let target = descendant.unwrap_or_else(|| self.body()); 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_placement(self.placement(styles));
self.push_caption_pos(self.caption_pos(styles)); self.push_caption(caption);
self.push_caption(self.caption(styles));
self.push_kind(Smart::Custom(kind)); 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_numbering(numbering);
self.push_outlined(self.outlined(styles)); self.push_outlined(self.outlined(styles));
self.push_counter(Some(counter)); self.push_counter(Some(counter));
@ -279,18 +287,18 @@ impl Synthesize for FigureElem {
impl Show for FigureElem { impl Show for FigureElem {
#[tracing::instrument(name = "FigureElem::show", skip_all)] #[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(); let mut realized = self.body();
// Build the caption, if any. // 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(); let v = VElem::weak(self.gap(styles).into()).pack();
realized = if self.caption_pos(styles) == VAlign::Bottom { realized = if caption.position(styles) == VAlign::Bottom {
realized + v + caption realized + v + caption.pack()
} else { } else {
caption + v + realized caption.pack() + v + realized
} };
}; }
// Wrap the contents in a block. // Wrap the contents in a block.
realized = BlockElem::new() realized = BlockElem::new()
@ -351,14 +359,9 @@ impl Outlinable for FigureElem {
return Ok(None); return Ok(None);
} }
self.full_caption(vt) let Some(mut caption) =
} self.caption(StyleChain::default()).map(|caption| caption.body())
} else {
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 {
return Ok(None); return Ok(None);
}; };
@ -375,7 +378,7 @@ impl FigureElem {
let numbers = counter.at(vt, location)?.display(vt, &numbering)?; let numbers = counter.at(vt, location)?.display(vt, &numbering)?;
if !supplement.is_empty() { if !supplement.is_empty() {
supplement += TextElem::packed("\u{a0}"); supplement += TextElem::packed('\u{a0}');
} }
caption = supplement + numbers + TextElem::packed(": ") + caption; 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`]. /// The `kind` parameter of a [`FigureElem`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum FigureKind { pub enum FigureKind {

View File

@ -124,8 +124,8 @@ description: |
- Miscellaneous Improvements - Miscellaneous Improvements
- Added [`bookmarked`]($heading.bookmarked) argument to heading to control - Added [`bookmarked`]($heading.bookmarked) argument to heading to control
whether a heading becomes part of the PDF outline whether a heading becomes part of the PDF outline
- Added [`caption-pos`]($figure.caption-pos) argument to control the position - Added [`caption-pos`]($figure.caption.position) argument to control the
of a figure's caption position of a figure's caption
- Added [`metadata`]($metadata) function for exposing an arbitrary value to - Added [`metadata`]($metadata) function for exposing an arbitrary value to
the introspection system the introspection system
- Fixed that a [`state`]($state) was identified by the pair `(key, init)` - Fixed that a [`state`]($state) was identified by the pair `(key, init)`

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -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],
)

View File

@ -41,7 +41,7 @@ We can clearly see that @fig-cylinder and
#show figure.where(kind: "theorem"): it => { #show figure.where(kind: "theorem"): it => {
let name = none let name = none
if not it.caption == none { if not it.caption == none {
name = [ #emph(it.caption)] name = [ #emph(it.caption.body)]
} else { } else {
name = [] name = []
} }

View File

@ -17,7 +17,7 @@
Figure Figure
#numbering(it.numbering, #numbering(it.numbering,
..counter(figure).at(it.location())): ..counter(figure).at(it.location())):
#it.caption #it.caption.body
#box(width: 1fr, repeat[.]) #box(width: 1fr, repeat[.])
#counter(page).at(it.location()).first() \ #counter(page).at(it.location()).first() \
] ]