Skip to main content

fleuron/layout/
furniture.rs

1//! The page margin boxes: a folio, a running head, and the band
2//! each of them sits in.
3
4use std::collections::BTreeMap;
5
6use crate::lines::{Line, ParagraphStyle};
7use crate::pages::{DrawItem, Page, PageBox, Side};
8use crate::style::{Align, Band, Content, MarginBox, MarginBoxStyle, PageQuery, PageStyle};
9
10use super::Paginator;
11use super::flow::PageInfo;
12
13/// The running strings in force, by name.
14pub(super) type Strings = BTreeMap<String, String>;
15
16/// What one page's furniture resolves its content against: the folio
17/// the page counted to, and the running strings it opened with.
18struct Furniture<'a> {
19    folio: u32,
20    strings: &'a Strings,
21}
22
23/// The folio each page prints: one past the page before it, or the
24/// number a page restarts the count at.
25pub(crate) fn folios(infos: &[PageInfo]) -> Vec<u32> {
26    let mut folio = 0;
27    infos
28        .iter()
29        .map(|info| {
30            folio = info.reset.unwrap_or(folio + 1);
31            folio
32        })
33        .collect()
34}
35
36impl Paginator<'_> {
37    /// Settles numbering and side once the whole flow is assembled,
38    /// then paints each page's margin boxes: a folio's digits are not
39    /// known until the pages before it are.
40    ///
41    /// Idempotent. What an earlier paint left is discarded first, so
42    /// a session that only changed its furniture repaints in place.
43    ///
44    /// The folio counts pages, and `counter-reset: page` restarts it
45    /// where a section asked; the side counts leaves, and nothing
46    /// restarts that — recto and verso are where a page falls in the
47    /// sheet, not what is printed on it.
48    ///
49    /// A page inserted to square the sheet paints no furniture. A
50    /// blank leaf is blank: a page whose only content would be a
51    /// running head does not get one.
52    pub(crate) fn paint(&self, pages: &mut [Page], infos: &[PageInfo]) {
53        let numbers = folios(infos);
54        for (((index, page), info), folio) in pages.iter_mut().enumerate().zip(infos).zip(numbers) {
55            // Furniture is appended after the page's own content, so
56            // dropping the tail is all a repaint has to undo.
57            page.items.truncate(info.content_items);
58            page.number = folio;
59            page.side = Side::of_number(index as u32 + 1);
60            if info.slot.blank {
61                continue;
62            }
63            let master = self.styles.page(info.slot.query(page.side));
64            for which in MarginBox::ALL {
65                let Some(box_style) = master.margin_box(which) else {
66                    continue;
67                };
68                let Some((band, align)) = which.band() else {
69                    continue;
70                };
71                let furniture = Furniture {
72                    folio,
73                    strings: &info.strings,
74                };
75                self.paint_margin_box(page, master, box_style, band, align, furniture);
76            }
77        }
78    }
79
80    /// Paints one page margin box. Its content is a line like any
81    /// other — shaped, measured, placed on the band's baseline — so
82    /// furniture and prose paint through the same path.
83    fn paint_margin_box(
84        &self,
85        page: &mut Page,
86        master: &PageStyle,
87        box_style: &MarginBoxStyle,
88        band: Band,
89        align: Align,
90        furniture: Furniture<'_>,
91    ) {
92        let Some((line, area)) = self.margin_line(master, box_style, band, align, furniture) else {
93            return;
94        };
95        let baseline = area.y + line.box_.baseline;
96        page.items
97            .append(&mut self.text_items(&line, area.x, baseline, DrawItem::PAGE_FURNITURE));
98    }
99
100    /// One margin box's line, and the box it takes on the page: as
101    /// wide as the line, and as tall as the band. Nothing where the box
102    /// shows no text.
103    fn margin_line(
104        &self,
105        master: &PageStyle,
106        box_style: &MarginBoxStyle,
107        band: Band,
108        align: Align,
109        furniture: Furniture<'_>,
110    ) -> Option<(Line, PageBox)> {
111        let text = match &box_style.content {
112            Content::None | Content::Pieces(_) => return None,
113            Content::Counter(counter) => counter.format(furniture.folio),
114            Content::String(name) => furniture.strings.get(name).cloned().unwrap_or_default(),
115            Content::Text(text) => text.clone(),
116        };
117        if text.is_empty() {
118            return None;
119        }
120        let style = box_style.style.paragraph();
121        let line = self.line_of(&text, &style)?;
122        let (band_top, band_height) = margin_band(master, band, &style);
123        let text_width = self.line_width(&line);
124        let x = match align {
125            // Centred on the trim, not on the content box: a folio
126            // belongs on the page's axis, and mirrored margins put
127            // the content box off it.
128            Align::Center => (master.geometry.width - text_width) / 2.0,
129            Align::Start => master.geometry.margin.left,
130            Align::End => master.geometry.width - master.geometry.margin.right - text_width,
131        };
132        let area = PageBox {
133            page: 0,
134            x,
135            y: band_top,
136            width: text_width,
137            height: band_height,
138        };
139        Some((line, area))
140    }
141
142    /// The page a margin box on the page at `index` answers for, and
143    /// the box it takes there. Nothing for a page the book does not
144    /// have, or for a blank page, which shows no margin boxes. The box
145    /// is `None` where the margin box shows no text on that page.
146    pub(crate) fn margin_box<'i>(
147        &self,
148        infos: &'i [PageInfo],
149        index: usize,
150        which: MarginBox,
151    ) -> Option<(PageQuery<'i>, Option<PageBox>)> {
152        let info = infos.get(index)?;
153        if info.slot.blank {
154            return None;
155        }
156        let query = info.slot.query(Side::of_number(index as u32 + 1));
157        let master = self.styles.page(query);
158        let area = master.margin_box(which).and_then(|box_style| {
159            let (band, align) = which.band()?;
160            let furniture = Furniture {
161                folio: folios(infos)[index],
162                strings: &info.strings,
163            };
164            let (_, area) = self.margin_line(master, box_style, band, align, furniture)?;
165            Some(PageBox {
166                page: index as u32,
167                ..area
168            })
169        });
170        Some((query, area))
171    }
172}
173
174/// The band one margin box's line sits in: `(top, height)` in page
175/// coordinates, one line tall, centred in the margin it lives in.
176pub fn margin_band(master: &PageStyle, band: Band, style: &ParagraphStyle) -> (f32, f32) {
177    let (start, margin) = match band {
178        Band::Top => (0.0, master.geometry.margin.top),
179        Band::Bottom => (
180            master.geometry.height - master.geometry.margin.bottom,
181            master.geometry.margin.bottom,
182        ),
183    };
184    let height = style.size * style.line_height;
185    (start + margin / 2.0 - height / 2.0, height)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::layout::testing::{
192        chapter, folio, master, opens_a_chapter, paginate, run_width_pt, ua,
193    };
194    use crate::style::{MarginBox, Situation};
195
196    /// Folios are correct and sequential — every page that shows a
197    /// folio shows its own number, and the folios read in page order
198    /// with no repeats or gaps among body pages.
199    #[test]
200    fn folios_are_correct_and_sequential() {
201        let pages = paginate(vec![chapter("Chapter One", 24), chapter("Chapter Two", 24)]);
202        assert!(
203            pages.len() > 4,
204            "expected a multi-page book, got {}",
205            pages.len()
206        );
207        let mut numbered = Vec::new();
208        for page in &pages {
209            if let Some((_, digits)) = folio(page) {
210                assert_eq!(
211                    digits,
212                    page.number.to_string(),
213                    "page {} shows folio {digits}",
214                    page.number
215                );
216                numbered.push(page.number);
217            }
218        }
219        assert!(numbered.len() >= 2, "expected folios on the body pages");
220        assert!(
221            numbered.windows(2).all(|w| w[1] > w[0]),
222            "folios out of order: {numbered:?}"
223        );
224    }
225
226    /// The folio is suppressed on chapter opens, because
227    /// `@page chapter:first` says so. A chapter's first page counts —
228    /// the next folio is one past it — but shows nothing; inserted
229    /// blank versos are equally blind, by `@page :blank`.
230    #[test]
231    fn folios_are_suppressed_on_chapter_opens() {
232        let pages = paginate(vec![chapter("Chapter One", 14), chapter("Chapter Two", 14)]);
233        let opens: Vec<u32> = pages
234            .iter()
235            .filter(|p| opens_a_chapter(p))
236            .map(|p| p.number)
237            .collect();
238        assert_eq!(opens.len(), 2, "two chapters, two opening pages");
239        for page in &pages {
240            let blind = opens_a_chapter(page) || page.items.is_empty();
241            assert_eq!(
242                folio(page).is_some(),
243                !blind,
244                "page {}: folio presence wrong (opens chapter: {}, blank: {})",
245                page.number,
246                opens_a_chapter(page),
247                page.items.is_empty()
248            );
249        }
250        // Counted, not shown: the page after an open shows its own
251        // number, one past the blind one.
252        for open in opens {
253            if let Some(next) = pages.get(open as usize) {
254                assert_eq!(folio(next).map(|(_, d)| d), Some((open + 1).to_string()));
255            }
256        }
257    }
258
259    /// The folio baseline sits in the bottom margin box — strictly
260    /// below the content area, inside the margin band — and is
261    /// centred on the trim, not on the content box (whose mirrored
262    /// margins are off-centre).
263    #[test]
264    fn folio_baseline_sits_in_the_margin_box() {
265        let pages = paginate(vec![chapter("Chapter One", 20)]);
266        let mut checked = 0;
267        for page in &pages {
268            let master = master(Situation::Body(page.side));
269            let geometry = master.geometry;
270            let folio_style = master
271                .margin_box(MarginBox::BottomCenter)
272                .expect("body pages have a folio")
273                .style
274                .paragraph();
275            let (band_top, band_height) = margin_band(master, Band::Bottom, &folio_style);
276            let (_, content_top) = geometry.content_origin();
277            let content_bottom = content_top + geometry.content_size().1;
278            let Some((
279                DrawItem::Text {
280                    x, y, glyphs, size, ..
281                },
282                _,
283            )) = folio(page)
284            else {
285                continue;
286            };
287            assert!(
288                *y > content_bottom,
289                "page {}: folio baseline {y} is inside the content area (bottom {content_bottom})",
290                page.number
291            );
292            assert!(
293                *y >= band_top && *y <= band_top + band_height,
294                "page {}: folio baseline {y} outside the margin box [{band_top}, {}]",
295                page.number,
296                band_top + band_height
297            );
298            assert!(
299                *y + geometry.margin.bottom / 4.0 < geometry.height,
300                "page {}: folio baseline {y} runs off the trim",
301                page.number
302            );
303            let width = run_width_pt(glyphs, *size);
304            assert!(
305                (x + width / 2.0 - geometry.width / 2.0).abs() < 1e-3,
306                "page {}: folio centered at {}, trim center {}",
307                page.number,
308                x + width / 2.0,
309                geometry.width / 2.0
310            );
311            checked += 1;
312        }
313        assert!(checked >= 2, "expected folios to check");
314    }
315
316    /// The running-head slot is reserved geometry in the top margin
317    /// and stays empty: the built-in sheet generates no top margin
318    /// box, so nothing paints above the content box.
319    #[test]
320    fn running_head_slot_is_reserved_and_empty() {
321        let master = master(Situation::Body(Side::Recto));
322        let head = margin_band(master, Band::Top, &ua().root().paragraph());
323        let (_, content_top) = master.geometry.content_origin();
324        assert!(head.0 > 0.0);
325        assert!(
326            head.0 + head.1 <= content_top,
327            "running head overlaps the content box"
328        );
329        assert!(master.margin_box(MarginBox::TopCenter).is_none());
330        for page in paginate(vec![chapter("Chapter One", 14)]) {
331            for item in &page.items {
332                if let DrawItem::Text { y, .. } = item {
333                    assert!(
334                        *y >= content_top,
335                        "page {}: something painted in the running-head slot at {y}",
336                        page.number
337                    );
338                }
339            }
340        }
341    }
342}