From 84b66d43d601ab0e5d8a890a501002f77b441075 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:19:17 -0300 Subject: [PATCH] repeatable figure captions --- crates/typst/src/layout/grid/mod.rs | 8 +++- crates/typst/src/model/figure.rs | 49 ++++++++++++++++++--- tests/ref/figure-caption-repeat-bottom.png | Bin 0 -> 1750 bytes tests/ref/figure-caption-repeat-top.png | Bin 0 -> 1290 bytes tests/suite/model/figure.typ | 28 ++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 tests/ref/figure-caption-repeat-bottom.png create mode 100644 tests/ref/figure-caption-repeat-top.png 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 0000000000000000000000000000000000000000..9e72f07d62fe176cb8fd8ce8bbc54ee6763a83e9 GIT binary patch literal 1750 zcmV;{1}XW8P)3*f1vumt_7QN6g?6S_W;D*O)U8%;ONTH%&gm;RD;JNXCmuBvEx5m;x zO6$0JUV(qygET+yLYl2nSIq4)_4h2r-IbHfdSj zDuTP#0I}YOty!m!OW-XR>(>e*cuy%HcpuIzC|xRrkL9eW6TuOeeRIZfEiIv+OJFTE zD@Ab1L7%{?b8sd}3`bN_`K}0l5O53Id2hqLyRsoYu1WM%?|i?oToAzwoXjYgln00m zdjNlL-h64k-n0LD;&B_6=Q3qVt`17(w1AV$Z4$_!xk z1BU=0q`q?#;NEA3(y>WR59YFV?3Y^slj4)!@v3@Ep>-Xg0Npjg6S(?9@ez~-Wf4>G z;5#F~83hP}v?74=W=$->{}!A+cw{16ZCP)A%Ng7Ao(d+G-?GO~DhEt2Hm^~m;FF@t z9iG56WW0s4kC$vQx}2E&WXm)>lgQMxMRq_4q$!GBR|*fo@X~^#THw$9Oq9N%Hie>% zL_u)D$(&ik3*c0qVbv5Y1QXqmpU89q!R7?oPeDRC;0)x}W|A%!Zi118z(e>8!UvNt zO~GeVS;A|d!))QA@1}C}z3jzz|N2qVwFiDl7RCNt8&LLi0U5<`e^~~^2DHQ3XF3ip zZVrsLh9+h@cC0;l2iU&N@LJy5*xr|O)hGke@$B@@CWEjuk$`e_e z^Ih=phUM*i^eeRpwnqK7#ryD$cR@&U!IUI`#SaloRW#m**Vh7w;L;sdtML^Htd8{o z{EGo7%DgXvSLEeo`yNL5DU3Xa2u^}g-^0`_>BafH2>$n0FfNq9r5k(z({||8u}&8} zoSfKBR;=WyPDyBy2Z#*I>lpGGf0JP6#R|O`zNxy5ro!xkjTIF}U%*^_-YSO8KWrsY zBe-Dawrx(Iz@{L?FfZ`29$YZ#>3lu#_EH`XToyd~cwo+k3-0g@AHWa7C9r-@H8JZ3 zT=2--5t_I(@56#7RRZ5@ZD&+JEP@~IXIQ)rPpmF=$|F-=$B@@CWcZs3o3GZ?*J=iV zt_=fVjA{ei94=Yvxq#DFz7X1vWM^lg?-!rQ~tM)=DRkTc3b2oR zv?HK9_O}w0M|XcP1>52)SK9%CAiE4er!LwDaL#{UM84(KYR8U?uRGI_B_uVk9DHK&43c_+z<2`}@o6`Q~LpYEaUHU%&@s7aYhp@g55MJeW

~r zCm8*Jhp>^|n!T5%;7fZ^;BSQ4!ks^;Co%tYP;Xdu&WVYT{O(2C3pU{XyjHT#!+1;; z9DCCW&CiXLygsrj-QaietYi5J?HyokR?jO<;RlU=9g#g9&ze8j)Noy&IXk#cdkyQA zG3|K!jaE$B&oERC0yJtH7_&jYwjMNE1Kig3HEQo^wG34cGX7~RKyNK~>P?N4&FW!p sYdfjE?zFbalS_tWScYX-hG(Du1CqYIWRlmc6aWAK07*qoM6N<$f_dOu@c;k- literal 0 HcmV?d00001 diff --git a/tests/ref/figure-caption-repeat-top.png b/tests/ref/figure-caption-repeat-top.png new file mode 100644 index 0000000000000000000000000000000000000000..a9fe48c804463d335ca1d0a7f43cd2ceaa2d93a1 GIT binary patch literal 1290 zcmV+l1@-!gP)EUaInb7Aj(9{V4y?ZjH((S$TReX9>Hdbh+yDFy*OyxQBq&d&En0O$^(-3?*YDbc{t zA;qBjFSsJzzCw8djb1D88@)plj|^|q;CzGZQ=A@BIOAM>!-u*tEdac-h$Q_6R(=-6e!Y7i_a-%0NCY7Mn zUpji8cfUJ>O)dsMRKpQ0YLxxRQZ~(*z8WGnPh80%MS0R-?_R)!-an`sU2QJ*O@Lo_7lBxgJi!dsBbx&Oj|=MR zgbD&)6bR*Pa&1od`GP7s+j5N#PW#ez-ZoC)T~;5^*=}ic@amg%ZMld2d|@zy8O&e? z{}*_p!rqv{3}!G}$6)IiY#oEGW3b7^V789I)-f2&CES>j>hY@qR(XHcg9M8z0-g;x z)yX~F>Iy7=Wq?t%QmFP*H@@uw?qT;j02%@=2?f+|Q*f1^3E+K7puA|9fRFR}ymf$2 z3V`9AOY48*%0n~2OEAC%0f6!~3clrH0vO_p>G3TFSQYBk1O6H6tG?jg$p#36**XS$ z