1use 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
13pub(super) type Strings = BTreeMap<String, String>;
15
16struct Furniture<'a> {
19 folio: u32,
20 strings: &'a Strings,
21}
22
23pub(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 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 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 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 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 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 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
174pub 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 #[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 #[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 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 #[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 #[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}