diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst/src/layout/grid/mod.rs index ec0504f31..b57d67446 100644 --- a/crates/typst/src/layout/grid/mod.rs +++ b/crates/typst/src/layout/grid/mod.rs @@ -413,11 +413,17 @@ pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); cast! { TrackSizings, self => self.0.into_value(), - sizing: Sizing => Self(smallvec![sizing]), + sizing: Sizing => Self::from(sizing), count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), } +impl From for TrackSizings { + fn from(sizing: Sizing) -> Self { + Self(smallvec![sizing]) + } +} + /// Any child of a grid element. #[derive(Debug, PartialEq, Clone, Hash)] pub enum GridChild { diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 1d3c9c957..9ed59b02e 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -14,8 +14,9 @@ use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; use crate::layout::{ - AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, - PlaceElem, PlacementScope, VAlignment, VElem, + AlignElem, Alignment, BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, + GridFooter, GridHeader, GridItem, HAlignment, Length, OuterVAlignment, PlaceElem, + PlacementScope, Sizing, TrackSizings, VAlignment, VElem, }; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::text::{Lang, Region, TextElem}; @@ -330,10 +331,27 @@ impl Show for Packed { // Build the caption, if any. if let Some(caption) = self.caption(styles) { - let v = VElem::new(self.gap(styles).into()).with_weak(true).pack(); - realized = match caption.position(styles) { - OuterVAlignment::Top => caption.pack() + v + realized, - OuterVAlignment::Bottom => realized + v + caption.pack(), + let gap = self.gap(styles); + let v = || VElem::new(gap.into()).with_weak(true).pack(); + realized = match (caption.repeat(styles), caption.position(styles)) { + (true, OuterVAlignment::Top) => GridElem::new(vec![ + GridChild::Header(Packed::new(GridHeader::new(vec![ + GridItem::Cell(Packed::new(GridCell::new(caption.pack()))), + ]))), + GridChild::Item(GridItem::Cell(Packed::new(GridCell::new(realized)))), + ]) + .with_row_gutter(TrackSizings::from(Sizing::from(gap))) + .pack(), + (true, OuterVAlignment::Bottom) => GridElem::new(vec![ + GridChild::Item(GridItem::Cell(Packed::new(GridCell::new(realized)))), + GridChild::Footer(Packed::new(GridFooter::new(vec![ + GridItem::Cell(Packed::new(GridCell::new(caption.pack()))), + ]))), + ]) + .with_row_gutter(TrackSizings::from(Sizing::from(gap))) + .pack(), + (false, OuterVAlignment::Top) => caption.pack() + v() + realized, + (false, OuterVAlignment::Bottom) => realized + v() + caption.pack(), }; } @@ -497,6 +515,25 @@ pub struct FigureCaption { #[default(OuterVAlignment::Bottom)] pub position: OuterVAlignment, + /// Whether the figure caption should be repeated if the figure breaks. + /// + /// ```example + /// #show figure.where(kind: table): set block(breakable: true) + /// #set page(height: 7em) + /// #figure( + /// table( + /// columns: 3, + /// [A], [B], [C], + /// [D], [E], [F], + /// [G], [H], [I], + /// [J], [K], [L] + /// ), + /// caption: figure.caption(repeat: true)[A nice table.] + /// ) + /// ``` + #[default(false)] + pub repeat: bool, + /// The separator which will appear between the number and body. /// /// If set to `{auto}`, the separator will be adapted to the current diff --git a/tests/ref/figure-caption-repeat-bottom.png b/tests/ref/figure-caption-repeat-bottom.png new file mode 100644 index 000000000..9e72f07d6 Binary files /dev/null and b/tests/ref/figure-caption-repeat-bottom.png differ diff --git a/tests/ref/figure-caption-repeat-top.png b/tests/ref/figure-caption-repeat-top.png new file mode 100644 index 000000000..a9fe48c80 Binary files /dev/null and b/tests/ref/figure-caption-repeat-top.png differ diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index fbd0ab295..0a0731606 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -219,6 +219,34 @@ We can clearly see that @fig-cylinder and // Error: 31-38 expected `top` or `bottom`, found horizon #set figure.caption(position: horizon) +--- figure-caption-repeat-bottom --- +#show figure.where(kind: table): set block(breakable: true) +#set page(height: 7em) +#figure( + table( + columns: 3, + [A], [B], [C], + [D], [E], [F], + [G], [H], [I], + [J], [K], [L] + ), + caption: figure.caption(repeat: true)[A nice table.] +) + +--- figure-caption-repeat-top --- +#show figure.where(kind: table): set block(breakable: true) +#set page(height: 7em) +#figure( + table( + columns: 3, + [A], [B], [C], + [D], [E], [F], + [G], [H], [I], + [J], [K], [L] + ), + caption: figure.caption(position: top, repeat: true)[A nice table.] +) + --- figure-localization-fr --- // Test French #set text(lang: "fr")