Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/src/driver.rs

62.6 KiB, 252 runs

created by r1870400018:35659, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The two-pass streaming driver, and its convergence loop.
2//!
3//! This is the heart of Phase 0. A pass composes the document: it runs the box-glue-penalty stream
4//! through a greedy vertical page breaker, places each line, records every anchor it meets, and
5//! resolves a forward reference against the width reserved for it. Pass A sees an empty ledger, so
6//! backward-looking anchors resolve but forward references show nothing yet. Pass B re-composes with
7//! Pass A's ledger loaded, so a forward reference now reads the value it points at.
8//!
9//! The loop terminates two ways, and only two, and it is honest about which:
10//!
11//! * *Converged.* From the second pass on, if the new ledger is stable against the last -- same page
12//! count, and no anchor changed page -- the document has stopped moving. The current pages are
13//! final. This is the normal outcome, and by construction it is two passes when every forward
14//! reference fits the width reserved for it.
15//! * *Did not converge.* If the pass cap is reached with the ledger still moving, the driver does
16//! not guess. It differences the last two ledgers and returns an error naming the anchor that
17//! moved, the pages it moved between, and any reference whose realised value overflowed its
18//! reservation -- which is the thing that broke the two-pass guarantee.
19
20use crate::{
21 ir::{
22 BoxNode,
23 ColumnsNode,
24 Dims,
25 FloatNode,
26 FloatPlacement,
27 FloatScope,
28 Footnote,
29 Leaf,
30 LeafKind,
31 Metrics,
32 Node,
33 PageColumns,
34 Sp,
35 },
36 ledger::{
37 Anchor,
38 Ledger,
39 Position,
40 },
41 page::{
42 Frame,
43 Page,
44 PageGeometry,
45 Placed,
46 PlacedKind,
47 Region,
48 },
49};
50
51use oxedyne_fe2o3_core::prelude::*;
52
53use std::collections::HashSet;
54
55/// The spacing that frames the footnotes at the foot of a page: the gap above the separator rule, the
56/// rule itself and how wide it runs, the gap below it before the first note, and the gap between one
57/// note and the next. Every length is scaled points, so the foot never leaves the integer domain the
58/// driver breaks on. The note lines themselves carry their own leading; this is only the furniture
59/// around them.
60#[derive(Clone, Copy, Debug)]
61pub struct FootStyle {
62 pub gap_above_rule: Sp,
63 pub rule_thick: Sp,
64 pub rule_width: Sp,
65 pub gap_below_rule: Sp,
66 pub gap_between: Sp,
67}
68
69impl Default for FootStyle {
70 fn default() -> Self {
71 Self {
72 gap_above_rule: Sp::from_pt(8.0),
73 rule_thick: Sp::from_pt(0.4),
74 rule_width: Sp::from_pt(120.0), // a short rule, about a third of a text block
75 gap_below_rule: Sp::from_pt(4.0),
76 gap_between: Sp::from_pt(3.0),
77 }
78 }
79}
80
81/// A trivial in-memory document: a vertical box-glue-penalty stream, the geometry every page takes, and
82/// the foot spacing its footnotes are framed with. Phase 0 has one geometry for the whole document;
83/// per-chapter geometry is later.
84#[derive(Clone, Debug)]
85pub struct Document {
86 pub nodes: Vec<Node>,
87 pub geom: PageGeometry,
88 pub foot: FootStyle,
89}
90
91impl Document {
92 pub fn new(nodes: Vec<Node>, geom: PageGeometry) -> Self {
93 Self { nodes, geom, foot: FootStyle::default() }
94 }
95}
96
97/// How hard the driver tries to converge. `max_passes` caps the loop; when it is reached with the
98/// ledger still moving, the driver reports a non-convergence rather than looping forever. Three is
99/// the architecture's stated worst case (two passes, plus one when a reservation is exceeded); a
100/// little headroom above that catches a genuine oscillation without hiding it.
101#[derive(Clone, Copy, Debug)]
102pub struct Config {
103 pub max_passes: u32,
104}
105
106impl Default for Config {
107 fn default() -> Self {
108 Self { max_passes: 4 }
109 }
110}
111
112/// The result of a converged compile: the final pages, the ledger that fixed them, and how many
113/// passes it took -- the last being the number the flat-memory claim is proved against.
114#[derive(Debug)]
115pub struct CompileOutput {
116 pub pages: Vec<Page>,
117 pub ledger: Ledger,
118 pub passes: u32,
119}
120
121/// Runs the document to a fixed point, or reports why it would not settle.
122pub fn run<M: Metrics>(
123 doc: &Document,
124 metrics: &M,
125 cfg: Config,
126)
127 -> Outcome<CompileOutput>
128{
129 if cfg.max_passes < 2 {
130 return Err(err!(
131 "The driver needs at least two passes to resolve a forward reference, but max_passes \
132 is {}.", cfg.max_passes; Input, Invalid, Configuration));
133 }
134 let mut prev = Ledger::new(); // Pass A sees no resolved forward references.
135 let mut pass = 0u32;
136 loop {
137 pass += 1;
138 let (pages, ledger) = res!(compose(doc, metrics, &prev));
139
140 // A ledger is only meaningfully stable once a second pass has had the first pass's ledger to
141 // read; comparing Pass A against the empty ledger it started from would converge falsely.
142 if pass >= 2 && ledger.is_stable_against(&prev) {
143 return Ok(CompileOutput { pages, ledger, passes: pass });
144 }
145
146 if pass >= cfg.max_passes {
147 return Err(non_convergence(pass, &ledger, &prev));
148 }
149 prev = ledger;
150 }
151}
152
153/// One composition pass over the whole document: material is stacked into the current column until the
154/// next atom would overflow it, then the flow hops to the next column, or -- from the last column, or on a
155/// page of one column -- breaks the page. The column layout in force is set by the stream's
156/// [`Node::PageColumns`] markers (Typst's `set page(columns:)`); a document carrying none flows one column
157/// to the page throughout, which is the ordinary page.
158///
159/// The break is atom-aware, not line-by-line. A run of boxes with no legal breakpoint between them -- a
160/// paragraph's first two lines welded by the orphan penalty, its last two by the widow penalty (see
161/// [`doc::guard_widows`](crate::doc)) -- is an atom, weighed and placed whole: if the atom will not fit,
162/// the column breaks before it rather than splitting a single line off, leaving the column a line short.
163/// This is the page-bottom slack Typst leaves for widow and orphan avoidance.
164///
165/// Footnotes couple to the break: a footnote's note is set at the foot of the column its mark lands on
166/// (the page's foot, on a page of one column), so the effective bottom shrinks by the note's height as
167/// marks accumulate, and a line is judged against a bottom already charged for its own note. This does
168/// not threaten convergence: a note's height is fixed at author time, so the reservation a column pays is
169/// a pure function of which marks fell in it. What footnotes can do is push a heading or an anchor to a
170/// later page than a footnote-free fill would -- exactly the movement the ledger already reconciles.
171///
172/// Floats come in two scopes (Typst's `place.scope`). A column-scoped float settles at the top or foot of
173/// the column it is met in, or of the next column with room; a parent-scoped float spans every column, at
174/// the top or foot of the page. On a page of one column the two coincide. A parent-scoped float met on a
175/// page whose columns already carry material is seated by relayout, as Typst seats it: the page is
176/// re-flowed from where it opened with the float already in its band, so the columns shorten beneath it
177/// rather than the float waiting for a later page.
178fn compose<M: Metrics>(
179 doc: &Document,
180 metrics: &M,
181 incoming: &Ledger,
182)
183 -> Outcome<(Vec<Page>, Ledger)>
184{
185 let mut flow = Flow::new(doc, metrics, incoming);
186 let nodes = &doc.nodes;
187 let mut idx = 0usize;
188 while idx < nodes.len() {
189 idx = res!(flow.step(nodes, idx));
190 }
191 flow.finish()
192}
193
194/// Where a page opened: everything a parent-scoped float's relayout rewinds to. It is taken when a page
195/// (or a column layout) opens, after the waiting page floats are flushed into its bands but before its
196/// first column opens, and refreshed each time a relayout seats another float, so a second relayout keeps
197/// the first float's band. Footnotes need no record: a page opens with none.
198#[derive(Clone)]
199struct Checkpoint {
200 idx: usize,
201 frame: Frame,
202 y: Sp,
203 col: usize,
204 col_top: Sp,
205 col_start: usize,
206 bands: FloatBands,
207 cbands: FloatBands,
208 pending: Vec<FloatNode>,
209 cpending: Vec<FloatNode>,
210 repeat: Option<BoxNode>,
211 strong_open: bool,
212 stamp: bool, // the page opened on an overflow, so its first column restamps a repeated header
213 marks: (u32, u32), // the ledger's first-heading and back-matter pages when the page opened
214}
215
216/// The state one composition pass threads through the stream: the page under construction and the column
217/// being filled, the float and footnote queues, and the checkpoint a relayout rewinds to. See [`compose`].
218struct Flow<'a, M: Metrics> {
219 doc: &'a Document,
220 metrics: &'a M,
221 incoming: &'a Ledger,
222 geom: PageGeometry,
223 top: Sp,
224 bottom: Sp,
225 ledger: Ledger,
226 pages: Vec<Page>,
227 frame: Frame,
228 page_no: u32,
229 cols: PageColumns, // the column layout in force
230 col: usize, // the column being filled, from 0
231 col_top: Sp, // the y every column of this page starts at, below the page's top floats
232 col_start: usize, // the frame index the current column's material starts at
233 y: Sp,
234 // Just after a break, leading glue and penalties are discarded.
235 at_top: bool,
236 // A break is permitted at the cursor: a legal breakpoint has just passed, or the column is fresh.
237 at_break: bool,
238 // The last non-marker node was a box, so glue after it is a legal breakpoint (the TeX rule that makes
239 // the glue a forbidden penalty leaves in front illegal too, welding the atom).
240 prev_box: bool,
241 // A strong `#pagebreak()` opened a page no flow content has yet filled, so it is emitted even if it stays
242 // empty (a trailing or consecutive strong break's blank page).
243 strong_open: bool,
244 // The footnotes whose marks landed in the current column; set at its foot when it closes.
245 notes: Vec<Footnote>,
246 // The page's parent-scoped float bands, and the current column's own.
247 bands: FloatBands,
248 cbands: FloatBands,
249 // The floats deferred out of the flow, in document order, awaiting room: parent-scoped ones a page,
250 // column-scoped ones a column. The heights are fixed at author time, so each queue is a pure function
251 // of the stream and the geometry and does not threaten convergence.
252 pending: Vec<FloatNode>,
253 cpending: Vec<FloatNode>,
254 // The header a breakable table asks repeated, armed by a `Node::RepeatHead(Some(..))` and cleared by
255 // `RepeatHead(None)`; stamped at the top of every column the rows spill onto.
256 repeat: Option<BoxNode>,
257 check: Checkpoint,
258 seated: HashSet<usize>, // the parent-scoped floats a relayout seated, by stream index, skipped on replay
259}
260
261impl<'a, M: Metrics> Flow<'a, M> {
262 fn new(doc: &'a Document, metrics: &'a M, incoming: &'a Ledger) -> Self {
263 let geom = doc.geom;
264 let top = geom.content_top();
265 let bottom = geom.content_top() + geom.content_height();
266 let ledger = Ledger::new();
267 let marks = (ledger.body_start_page, ledger.back_matter_start_page);
268 Self {
269 doc,
270 metrics,
271 incoming,
272 geom,
273 top,
274 bottom,
275 ledger,
276 pages: Vec::new(),
277 frame: Frame::new(),
278 page_no: 1,
279 cols: PageColumns::ONE,
280 col: 0,
281 col_top: top,
282 col_start: 0,
283 y: top,
284 at_top: true,
285 at_break: true,
286 prev_box: false,
287 strong_open: false,
288 notes: Vec::new(),
289 bands: FloatBands::empty(),
290 cbands: FloatBands::empty(),
291 pending: Vec::new(),
292 cpending: Vec::new(),
293 repeat: None,
294 check: Checkpoint {
295 idx: 0,
296 frame: Frame::new(),
297 y: top,
298 col: 0,
299 col_top: top,
300 col_start: 0,
301 bands: FloatBands::empty(),
302 cbands: FloatBands::empty(),
303 pending: Vec::new(),
304 cpending: Vec::new(),
305 repeat: None,
306 strong_open: false,
307 stamp: false,
308 marks,
309 },
310 seated: HashSet::new(),
311 }
312 }
313
314 /// Does the layout in force set more than one column?
315 fn multi(&self) -> bool {
316 self.cols.count > 1
317 }
318
319 /// The geometry of the column being filled: the page's own on a page of one column.
320 fn col_geom(&self) -> PageGeometry {
321 if self.multi() {
322 self.geom.column_slice(self.col, self.cols.count, self.cols.gutter)
323 } else {
324 self.geom
325 }
326 }
327
328 /// The lowest y the current column's material may reach: the page foot less the page's and the column's
329 /// foot floats. Footnotes are charged against this separately, as they accumulate.
330 fn col_bottom(&self) -> Sp {
331 self.bottom - self.bands.bot_reserve - self.cbands.bot_reserve
332 }
333
334 /// Records where the page opened, for a relayout to rewind to: `idx` is the stream index the page's
335 /// flow starts from. Only a layout of several columns ever rewinds, so a page of one column takes no
336 /// copy of its frame.
337 fn mark(&mut self, idx: usize, stamp: bool) {
338 if !self.multi() {
339 return;
340 }
341 self.check = Checkpoint {
342 idx,
343 frame: self.frame.clone(),
344 y: self.y,
345 col: self.col,
346 col_top: self.col_top,
347 col_start: self.col_start,
348 bands: self.bands,
349 cbands: self.cbands,
350 pending: self.pending.clone(),
351 cpending: self.cpending.clone(),
352 repeat: self.repeat.clone(),
353 strong_open: self.strong_open,
354 stamp,
355 marks: (self.ledger.body_start_page, self.ledger.back_matter_start_page),
356 };
357 }
358
359 /// Rewinds to where the page opened, its first column not yet open, and returns the stream index to
360 /// replay from. The anchors laid since are simply re-recorded as the replay lays them again -- the ledger
361 /// keeps the last placement of each identity -- so only its first-heading and back-matter marks, which
362 /// a rewound anchor may have fixed, need restoring.
363 fn rewind(&mut self) -> usize {
364 let c = self.check.clone();
365 self.frame = c.frame;
366 self.y = c.y;
367 self.col = c.col;
368 self.col_top = c.col_top;
369 self.col_start = c.col_start;
370 self.bands = c.bands;
371 self.cbands = c.cbands;
372 self.pending = c.pending;
373 self.cpending = c.cpending;
374 self.notes = Vec::new();
375 self.repeat = c.repeat;
376 self.strong_open = c.strong_open;
377 self.at_top = true;
378 self.at_break = true;
379 self.prev_box = false;
380 self.ledger.body_start_page = c.marks.0;
381 self.ledger.back_matter_start_page = c.marks.1;
382 c.idx
383 }
384
385 /// Sets the current column's footnotes at its foot, above its foot floats, and clears them.
386 fn lay_notes(&mut self) -> Outcome<()> {
387 let g = self.col_geom();
388 let foot = self.col_bottom();
389 res!(lay_footnotes(
390 &mut self.frame, &self.notes, self.page_no, g, &self.doc.foot, foot, self.metrics, self.incoming,
391 &mut self.ledger));
392 self.notes.clear();
393 Ok(())
394 }
395
396 /// Closes the page -- its last column's footnotes set, the page stored -- and opens the next: the page
397 /// floats that were waiting flushed into its bands, the page marked for a relayout, then its first column
398 /// opened below them. `idx` is the stream index the new page's flow starts from; `stamp` restamps a
399 /// repeated table header, for a page opened by rows overflowing the last.
400 fn next_page(&mut self, idx: usize, stamp: bool) -> Outcome<()> {
401 res!(self.lay_notes());
402 self.pages.push(Page::new(self.page_no, self.geom, std::mem::take(&mut self.frame)));
403 self.page_no += 1;
404 self.y = self.top;
405 self.bands = FloatBands::empty();
406 res!(flush_floats(
407 &mut self.pending, &mut self.frame, &mut self.y, &mut self.bands, Sp::ZERO, self.page_no, self.geom,
408 self.top, self.bottom, self.metrics, self.incoming, &mut self.ledger));
409 self.mark(idx, stamp);
410 self.open_columns(stamp)
411 }
412
413 /// Opens the page's first column below its top floats, with its leading collapsed, restamping a
414 /// repeated table header when the page opened on rows that overflowed the last.
415 fn open_columns(&mut self, stamp: bool) -> Outcome<()> {
416 res!(self.open_column(0));
417 self.at_top = true;
418 if stamp {
419 res!(self.stamp_repeat());
420 }
421 Ok(())
422 }
423
424 /// Opens column `col` of the current page: the first at the cursor (below the page's top floats), a
425 /// later one at the same top as the first. The column floats that were waiting are flushed into it.
426 fn open_column(&mut self, col: usize) -> Outcome<()> {
427 self.col = col;
428 if col == 0 {
429 self.col_top = self.y;
430 }
431 self.y = self.col_top;
432 self.col_start = self.frame.len();
433 self.cbands = FloatBands::empty();
434 if self.multi() {
435 res!(self.flush_col_floats());
436 }
437 Ok(())
438 }
439
440 /// Stamps the armed repeated table header at the cursor, in the current column. The rows that broke
441 /// onto this region are placed just below it; the header makes the region non-empty, so no leading is
442 /// discarded under it.
443 fn stamp_repeat(&mut self) -> Outcome<()> {
444 if let Some(head) = self.repeat.clone() {
445 let g = self.col_geom();
446 res!(place_vbox(&head, self.y, self.page_no, g, self.metrics, self.incoming, &mut self.frame, &mut self.ledger));
447 self.y += head.dims.vextent();
448 }
449 Ok(())
450 }
451
452 /// Moves the flow on: to the next column, or from the last column (or a page of one) to a fresh page
453 /// whose flow starts at stream index `idx`. An overflow break stamps a repeated table header at the top
454 /// of the region the rows spill onto, below any floats; a forced break does not, since no row broke
455 /// across it.
456 fn hop(&mut self, idx: usize, overflow: bool) -> Outcome<()> {
457 if self.multi() && self.col + 1 < self.cols.count {
458 res!(self.lay_notes());
459 res!(self.open_column(self.col + 1));
460 self.at_top = true;
461 if overflow {
462 res!(self.stamp_repeat());
463 }
464 Ok(())
465 } else {
466 self.next_page(idx, overflow)
467 }
468 }
469
470 /// Seats the waiting column floats that fit in the current column, in document order, stopping at the
471 /// first that will not. The front float of an empty column is seated even when it will not fit, so the
472 /// queue always drains.
473 fn flush_col_floats(&mut self) -> Outcome<()> {
474 let mut placed_any = false;
475 while !self.cpending.is_empty() {
476 let force = self.frame.len() == self.col_start && !placed_any;
477 let f = self.cpending[0].clone();
478 if res!(self.try_insert_col_float(&f, false, force)) {
479 self.cpending.remove(0);
480 placed_any = true;
481 } else {
482 break;
483 }
484 }
485 Ok(())
486 }
487
488 /// Inserts one column-scoped float into the current column's top or foot band by Typst's rule -- the
489 /// midpoint rule for `auto` -- shifting that column's material (and only that column's) to make room, as
490 /// [`try_insert_float`] does for a page band. `clearance_flag` is whether the clearance counts in the fit,
491 /// `force` seats the float even when it will not fit. Returns whether it was seated.
492 fn try_insert_col_float(&mut self, f: &FloatNode, clearance_flag: bool, force: bool) -> Outcome<bool> {
493 let g = self.col_geom();
494 let x0 = g.content_left();
495 let x1 = g.content_left() + g.content_width();
496 let foot_now = foot_reserve(&self.notes, &[], &self.doc.foot);
497 let base = (self.bottom - self.bands.bot_reserve) - self.col_top; // the column's own height
498 let band = f.height + f.clearance;
499 let need_fit = f.height + if clearance_flag { f.clearance } else { Sp::ZERO };
500 let remaining = (self.col_bottom() - foot_now) - self.y;
501 if need_fit > remaining && !force {
502 return Ok(false);
503 }
504 let used = base - remaining;
505 let side = match f.placement {
506 FloatPlacement::Top => FloatPlacement::Top,
507 FloatPlacement::Bottom => FloatPlacement::Bottom,
508 FloatPlacement::Auto => auto_side(used, need_fit, base),
509 };
510 let start = self.frame.len();
511 match side {
512 FloatPlacement::Bottom => {
513 // The column's existing foot floats rise by this float's band, and it takes the very foot of
514 // the column, just above the page's own foot band.
515 self.frame.shift_region_from(self.col_start, -band, Region::Foot);
516 self.ledger.shift_region_anchors_within(self.page_no, -band, Region::Foot, x0, x1);
517 let at = (self.bottom - self.bands.bot_reserve) - f.height;
518 res!(place_float(f, at, self.page_no, g, self.metrics, self.incoming, &mut self.frame, &mut self.ledger, Region::Foot));
519 self.frame.stamp_region(start, Region::Foot);
520 self.cbands.bot_reserve = self.cbands.bot_reserve + band;
521 },
522 _ => {
523 // Seated below the column's top floats already stacked, the column's body shifting down.
524 let at = self.col_top + self.cbands.top_used;
525 self.frame.shift_region_from(self.col_start, band, Region::Body);
526 self.ledger.shift_region_anchors_within(self.page_no, band, Region::Body, x0, x1);
527 res!(place_float(f, at, self.page_no, g, self.metrics, self.incoming, &mut self.frame, &mut self.ledger, Region::Top));
528 self.frame.stamp_region(start, Region::Top);
529 self.cbands.top_used = self.cbands.top_used + band;
530 self.y += band;
531 },
532 }
533 Ok(true)
534 }
535
536 /// Seats a parent-scoped float met at stream index `idx` on a page of several columns, spanning them all
537 /// in the page's top or foot band. Its side is chosen where it stands in the flow; then, if the page's
538 /// bands have room, the page is rewound to where it opened, the float seated in its band, and the page's
539 /// flow replayed beneath it -- Typst's relayout. The replay skips this float, already seated. A float the
540 /// page has no room for waits for the next page. Returns the stream index to continue from.
541 fn seat_parent_float(&mut self, f: &FloatNode, idx: usize) -> Outcome<usize> {
542 if !self.pending.is_empty() {
543 self.pending.push(f.clone());
544 return Ok(idx + 1);
545 }
546 let free = (self.bottom - self.bands.bot_reserve) - (self.top + self.bands.top_used);
547 let base = self.geom.content_height();
548 let fresh = self.check.frame.is_empty(); // the page opened with nothing on it
549 if f.height > free && !fresh {
550 self.pending.push(f.clone());
551 return Ok(idx + 1);
552 }
553 // The side `auto` takes is decided where the float stands, by the midpoint rule over the page region.
554 let used = self.y - self.top;
555 let side = match f.placement {
556 FloatPlacement::Top => FloatPlacement::Top,
557 FloatPlacement::Bottom => FloatPlacement::Bottom,
558 FloatPlacement::Auto => auto_side(used, f.height, base),
559 };
560 let mut seat = f.clone();
561 seat.placement = side;
562 let resume = self.rewind();
563 let stamp = self.check.stamp;
564 // The rewound page carries no flow content, so the clearance does not count in the fit (as a re-queued
565 // float's does not); the fit was checked above, so the float is seated even should it overflow.
566 res!(try_insert_float(
567 &seat, false, true, &mut self.y, &mut self.bands, Sp::ZERO, self.page_no, self.geom, self.top,
568 self.bottom, self.metrics, self.incoming, &mut self.frame, &mut self.ledger));
569 self.seated.insert(idx);
570 self.mark(resume, stamp);
571 res!(self.open_columns(stamp));
572 Ok(resume)
573 }
574
575 /// Processes the node at `idx` and returns the index to continue from: the next node, or -- after a
576 /// relayout -- the node the page's flow opened at.
577 fn step(&mut self, nodes: &[Node], idx: usize) -> Outcome<usize> {
578 let node = &nodes[idx];
579 match node {
580 Node::Glue(g) => {
581 // Glue at the very top of a column is discarded, as TeX discards it, so a column does not open
582 // with blank space left over from the break.
583 if !self.at_top {
584 self.y += g.natural;
585 }
586 // Glue after a box is a legal breakpoint, so the next box opens a fresh atom; glue after a
587 // penalty or glue is not, and leaves `at_break` as it stands.
588 if self.prev_box {
589 self.at_break = true;
590 }
591 self.prev_box = false;
592 },
593 Node::Penalty(p) => {
594 if p.is_forced() {
595 if p.is_column() && self.multi() {
596 // A column break (`#colbreak()`): the next column, or the next page from the last. A weak one
597 // is dropped in a column that holds nothing yet.
598 if p.is_strong() || self.frame.len() > self.col_start {
599 res!(self.hop(idx + 1, false));
600 self.strong_open = false;
601 }
602 } else {
603 // A strong eject (the default `#pagebreak()`, or a column break on a page of one column)
604 // ejects unconditionally; a weak one (a chapter end, section furniture,
605 // `#pagebreak(weak: true)`) only a page carrying content, and is dropped on an empty page.
606 // A strong break on an empty page materialises that blank page and opens another -- Typst's
607 // trailing/consecutive-strong-break behaviour.
608 let strong = p.is_strong();
609 if !self.frame.is_empty() || strong {
610 res!(self.next_page(idx + 1, false));
611 // The page just opened is owed by this break only if the break was strong.
612 self.strong_open = strong;
613 self.check.strong_open = strong;
614 }
615 }
616 self.at_break = true;
617 } else if p.is_forbidden() {
618 // A forbidden penalty (the widow/orphan weld) blocks a break here, and blocks the glue that
619 // follows it from being one, so the lines either side of it stay in one atom.
620 self.at_break = false;
621 } else {
622 self.at_break = true;
623 }
624 self.prev_box = false;
625 },
626 Node::Anchor(id) => {
627 // The body-start and back-matter markers are noticed by `Ledger::record` as the anchor is
628 // recorded. An anchor is transparent to breakability.
629 let x = self.col_geom().content_left();
630 self.ledger.record(Anchor::new(id.clone(), Position::new(self.page_no, x, self.y)));
631 },
632 Node::Float(f) if self.multi() => {
633 match f.scope {
634 FloatScope::Parent => {
635 if self.seated.contains(&idx) {
636 return Ok(idx + 1); // seated by the relayout that is replaying this page
637 }
638 return self.seat_parent_float(f, idx);
639 },
640 FloatScope::Column => {
641 // A column float settles in the current column's bands, or waits for the next column
642 // with room; an earlier waiting float holds it back, so document order is kept.
643 if !self.cpending.is_empty() {
644 self.cpending.push(f.clone());
645 } else {
646 let force = self.frame.len() == self.col_start;
647 if !res!(self.try_insert_col_float(f, !self.at_top, force)) {
648 self.cpending.push(f.clone());
649 }
650 }
651 },
652 }
653 },
654 Node::Float(f) => {
655 // On a page of one column the two scopes coincide: the float is inserted into the page's top or
656 // foot band and the body already on the page shifts to make room (Typst's relayout). An earlier
657 // queued float holds this one back so document order is kept; a float that will not fit defers.
658 if !self.pending.is_empty() {
659 self.pending.push(f.clone());
660 } else {
661 let was_empty = self.frame.is_empty();
662 let foot_now = foot_reserve(&self.notes, &[], &self.doc.foot);
663 // Clearance counts in the fit only when the page already carries FLOW content -- `at_top`
664 // tracks its absence. An empty page seats the float regardless and overflows rather than
665 // deferring forever.
666 let ok = res!(try_insert_float(
667 f, !self.at_top, was_empty, &mut self.y, &mut self.bands, foot_now, self.page_no, self.geom,
668 self.top, self.bottom, self.metrics, self.incoming, &mut self.frame, &mut self.ledger));
669 if !ok {
670 self.pending.push(f.clone());
671 }
672 }
673 },
674 Node::Columns(c) => {
675 if self.multi() {
676 return Err(err!(
677 "A columns block was met on a page already set in {} columns; nested column layouts are \
678 not lowered by the author, so this is a construction bug in the stream's builder.",
679 self.cols.count; Invalid, Bug));
680 }
681 // A columns block on a page of one column flows its own material through the multi-column
682 // pass, which starts at the cursor and resumes the flow below its tallest column.
683 res!(flow_columns(
684 c, &mut self.pages, &mut self.frame, &mut self.page_no, &mut self.y, self.top, self.bottom,
685 self.geom, &mut self.notes, &self.doc.foot, &mut self.bands, &mut self.pending, &mut self.at_top,
686 self.metrics, self.incoming, &mut self.ledger));
687 self.at_break = true;
688 self.prev_box = false;
689 self.strong_open = false;
690 },
691 Node::PageColumns(pc) => {
692 // A new column layout takes effect on a fresh page, as Typst's `set page` does: a page carrying
693 // content is closed first; an empty one simply takes the new layout.
694 if *pc != self.cols {
695 if !self.frame.is_empty() {
696 res!(self.lay_notes());
697 self.pages.push(Page::new(self.page_no, self.geom, std::mem::take(&mut self.frame)));
698 self.page_no += 1;
699 self.y = self.top;
700 self.bands = FloatBands::empty();
701 self.strong_open = false;
702 }
703 // Column floats still waiting join the page queue when the layout drops to one column.
704 if pc.count <= 1 && !self.cpending.is_empty() {
705 let mut queue = std::mem::take(&mut self.cpending);
706 queue.append(&mut self.pending);
707 self.pending = queue;
708 }
709 self.cols = *pc;
710 if self.frame.is_empty() {
711 res!(flush_floats(
712 &mut self.pending, &mut self.frame, &mut self.y, &mut self.bands, Sp::ZERO, self.page_no,
713 self.geom, self.top, self.bottom, self.metrics, self.incoming, &mut self.ledger));
714 }
715 self.mark(idx + 1, false);
716 res!(self.open_columns(false));
717 self.at_break = true;
718 self.prev_box = false;
719 }
720 },
721 Node::RepeatHead(head) => {
722 // Arm or disarm the repeated header. Transparent to breakability, like an anchor.
723 self.repeat = head.as_ref().map(|b| (**b).clone());
724 },
725 Node::HBox(_) | Node::VBox(_) | Node::Leaf(_) => {
726 // At an atom start, weigh the whole atom -- every box up to the next legal breakpoint, with the
727 // footnotes they introduce reserved from the foot -- and break before it if it will not fit. A box
728 // in mid-atom is placed unconditionally: the atom was found to fit when it opened.
729 if self.at_break {
730 let (atom_ext, atom_reserve) = atom_measure(nodes, idx, &self.notes, &self.doc.foot);
731 // A page of one column breaks whenever it carries anything; a column breaks unless it is the
732 // empty first column of an empty page, so an over-tall atom is seated (overflowing) only there
733 // and never spins from column to column.
734 let may_break = if self.multi() {
735 !(self.at_top && self.frame.is_empty())
736 } else {
737 !self.frame.is_empty()
738 };
739 if may_break && self.y + atom_ext > self.col_bottom() - atom_reserve {
740 res!(self.hop(idx, true));
741 }
742 }
743 let v = node.vextent();
744 let mut marks: Vec<Footnote> = Vec::new();
745 collect_marks(node, &mut marks);
746 let g = self.col_geom();
747 res!(place_node(node, self.y, self.page_no, g, self.metrics, self.incoming, &mut self.frame, &mut self.ledger));
748 self.notes.append(&mut marks); // its marks now belong to the column the node landed in
749 self.y += v;
750 self.at_top = false;
751 self.at_break = false;
752 self.prev_box = true;
753 self.strong_open = false;
754 },
755 }
756 Ok(idx + 1)
757 }
758
759 /// Drains the floats still waiting at the end of the stream -- column floats into the columns that
760 /// follow, then page floats onto the current page and fresh ones -- and closes the last page, returning
761 /// every page and the ledger that fixed them.
762 fn finish(mut self) -> Outcome<(Vec<Page>, Ledger)> {
763 let end = self.doc.nodes.len();
764 // A fresh column seats at least its front float, so each hop shrinks the queue and the loop ends.
765 while self.multi() && !self.cpending.is_empty() {
766 res!(self.flush_col_floats());
767 if self.cpending.is_empty() {
768 break;
769 }
770 res!(self.hop(end, false));
771 self.strong_open = false;
772 }
773 // The page floats: as many as fit on the current page (respecting the bands and footnotes it already
774 // carries), then a fresh page per remaining batch. A fresh, empty page seats at least its front float,
775 // so `pending` shrinks by at least one per fresh page and the loop terminates.
776 loop {
777 let foot_now = foot_reserve(&self.notes, &[], &self.doc.foot);
778 res!(flush_floats(
779 &mut self.pending, &mut self.frame, &mut self.y, &mut self.bands, foot_now, self.page_no, self.geom,
780 self.top, self.bottom, self.metrics, self.incoming, &mut self.ledger));
781 if self.pending.is_empty() {
782 break;
783 }
784 res!(self.next_page(end, false));
785 self.strong_open = false;
786 }
787
788 // The last page holds whatever is left, unless nothing is; its footnotes are set at its foot first.
789 if !self.frame.is_empty() {
790 res!(self.lay_notes());
791 self.pages.push(Page::new(self.page_no, self.geom, std::mem::take(&mut self.frame)));
792 } else if self.strong_open {
793 // A trailing strong `#pagebreak()` opened a fresh page that nothing filled: it is emitted rather than
794 // dropped (Typst 0.15.1 lays a trailing strong break as a blank page).
795 self.pages.push(Page::new(self.page_no, self.geom, Frame::new()));
796 } else if self.pages.is_empty() {
797 // A document with no material is still one blank page, so a page count is always at least one.
798 self.pages.push(Page::new(self.page_no, self.geom, Frame::new()));
799 }
800 self.ledger.total_pages = self.pages.len() as u32;
801 Ok((self.pages, self.ledger))
802 }
803}
804
805/// Flows a columns block's material into `count` equal side-by-side columns, filling each top to bottom
806/// before hopping to the next, and breaking to a fresh page's first column when the last column fills.
807/// This is the same greedy, atom-aware breaker the page body uses (see [`compose`]) run once per column:
808/// a page is a one-column flow, and this is its generalisation, so the two share the page-close and
809/// float-flush helpers ([`finish_page`], [`flush_floats`]) rather than re-implementing the page break.
810/// Sequential fill, never balancing, matches Typst's `columns(n)` -- the last column of the last page
811/// simply ends where the material runs out.
812///
813/// The block begins at the incoming cursor `*y` (below whatever body already sits on the page) and every
814/// column of that first page starts at the same top; a fresh page opened mid-block starts its columns at
815/// the region top below any top floats a flush seats. On return `*y` sits at the foot of the tallest
816/// column of the block's last page, so the ordinary flow resumes there. Two-pass resolution is untouched:
817/// an anchor -- an index entry's reserved folio slot, or a `Node::Anchor` -- records its x at the column
818/// left, and the reserved slot resolves against the incoming ledger exactly as it does in the body flow.
819#[allow(clippy::too_many_arguments)]
820fn flow_columns<M: Metrics>(
821 cols: &ColumnsNode,
822 pages: &mut Vec<Page>,
823 frame: &mut Frame,
824 page_no: &mut u32,
825 y: &mut Sp,
826 top: Sp,
827 bottom: Sp,
828 geom: PageGeometry,
829 notes: &mut Vec<Footnote>,
830 foot: &FootStyle,
831 bands: &mut FloatBands,
832 pending: &mut Vec<FloatNode>,
833 at_top: &mut bool,
834 metrics: &M,
835 incoming: &Ledger,
836 ledger: &mut Ledger,
837)
838 -> Outcome<()>
839{
840 let n = cols.count.max(1);
841 let list = &cols.list;
842
843 let mut col = 0usize; // the column being filled
844 let mut col_top = *y; // the y every column of the current page starts at
845 let mut yy = col_top; // the cursor within the current column
846 let mut deepest = col_top; // the deepest column foot reached on the current page, for the resume
847 let mut at_col_top = true; // just after a column or page break, leading glue is discarded
848 let mut at_break = true; // a break (a column or page hop) is permitted at the cursor
849 let mut prev_box = false; // the last non-marker node was a box, so glue after it may break
850
851 let mut idx = 0usize;
852 while idx < list.len() {
853 match &list[idx] {
854 Node::Glue(g) => {
855 if !at_col_top {
856 yy += g.natural;
857 }
858 if prev_box {
859 at_break = true;
860 }
861 prev_box = false;
862 },
863 Node::Penalty(p) => {
864 // A forced penalty breaks the column here (Typst's `colbreak`): hop to the next column, or to a
865 // fresh page's first column when the last column is full. A forbidden penalty welds across the
866 // break; any other penalty is an ordinary breakpoint.
867 if p.is_forced() {
868 res!(column_hop(
869 &mut col, n, &mut yy, &mut col_top, &mut deepest, &mut at_col_top, pages, frame, page_no,
870 y, top, bottom, geom, notes, foot, bands, pending, metrics, incoming, ledger));
871 at_break = true;
872 } else if p.is_forbidden() {
873 at_break = false;
874 } else {
875 at_break = true;
876 }
877 prev_box = false;
878 },
879 Node::Anchor(id) => {
880 let col_geom = geom.column_slice(col, n, cols.gutter);
881 ledger.record(Anchor::new(id.clone(), Position::new(*page_no, col_geom.content_left(), yy)));
882 },
883 node @ (Node::HBox(_) | Node::VBox(_) | Node::Leaf(_)) => {
884 if at_break {
885 let (atom_ext, atom_reserve) = atom_measure(list, idx, notes, foot);
886 let col_bottom = bottom - bands.bot_reserve - atom_reserve;
887 // Guard on `!(at_col_top && frame.is_empty())`, not `!at_col_top`: the body breaks whenever the
888 // frame is non-empty, and keying only on the column top diverged -- at a page foot the first atom
889 // of column 0 and then of column 1 both seated below the bottom before any page break, since each
890 // fresh column reads `at_col_top` true. This seats an over-tall atom only on a genuinely empty page
891 // (no spin), and otherwise hops or page-breaks so nothing lands past the foot.
892 if !(at_col_top && frame.is_empty()) && yy + atom_ext > col_bottom {
893 res!(column_hop(
894 &mut col, n, &mut yy, &mut col_top, &mut deepest, &mut at_col_top, pages, frame, page_no,
895 y, top, bottom, geom, notes, foot, bands, pending, metrics, incoming, ledger));
896 continue; // weigh the same atom against the fresh column, without advancing idx
897 }
898 }
899 let col_geom = geom.column_slice(col, n, cols.gutter);
900 let v = node.vextent();
901 let mut marks: Vec<Footnote> = Vec::new();
902 collect_marks(node, &mut marks);
903 res!(place_node(node, yy, *page_no, col_geom, metrics, incoming, frame, ledger));
904 notes.append(&mut marks); // a note introduced in a column belongs to the page the column is on
905 yy += v;
906 if yy > deepest {
907 deepest = yy;
908 }
909 at_col_top = false;
910 at_break = false;
911 prev_box = true;
912 },
913 // A repeated-header marker never carries vertical extent and never opens or closes an atom, so a
914 // columns block that happened to enclose one just passes it through untouched.
915 Node::RepeatHead(_) => (),
916 // A float or a nested columns block inside a columns block is a construction error the parser never
917 // builds. It is refused loudly rather than silently dropped, keeping the project's loud-refusal
918 // stance: reaching here means the lowering built an impossible shape, which is a bug to surface.
919 Node::Float(_) | Node::Columns(_) | Node::PageColumns(_) => return Err(err!(
920 "A float, a nested columns block or a page-column marker was found inside a columns block's \
921 list, which the lowering never builds; this is a construction bug in the caller that assembled \
922 the columns node."; Invalid, Bug)),
923 }
924 idx += 1;
925 }
926
927 // The flow resumes below the tallest column of the block's last page. A block that placed nothing leaves
928 // the cursor and the page's `at_top` where they were.
929 *y = deepest;
930 if deepest > col_top {
931 *at_top = false;
932 }
933 Ok(())
934}
935
936/// Advances a columns flow to the next column, or -- when the last column is full -- closes the page,
937/// flushes any pending floats onto the fresh page, and restarts at its first column. The flow's cursors
938/// (`col`, `yy`, `col_top`, `deepest`, `at_col_top`) are reset in place for the column or page just
939/// opened; on a page break the page-body state (`pages`, `frame`, `page_no`, `y`, `bands`, `notes`) is
940/// carried through the same [`finish_page`]/[`flush_floats`] helpers the body flow uses, and the new
941/// column top is taken from the shared cursor below any top band the flush stacked.
942#[allow(clippy::too_many_arguments)]
943fn column_hop<M: Metrics>(
944 col: &mut usize,
945 n: usize,
946 yy: &mut Sp,
947 col_top: &mut Sp,
948 deepest: &mut Sp,
949 at_col_top: &mut bool,
950 pages: &mut Vec<Page>,
951 frame: &mut Frame,
952 page_no: &mut u32,
953 y: &mut Sp,
954 top: Sp,
955 bottom: Sp,
956 geom: PageGeometry,
957 notes: &mut Vec<Footnote>,
958 foot: &FootStyle,
959 bands: &mut FloatBands,
960 pending: &mut Vec<FloatNode>,
961 metrics: &M,
962 incoming: &Ledger,
963 ledger: &mut Ledger,
964)
965 -> Outcome<()>
966{
967 if *col + 1 < n {
968 // Another column on this page: the same top, a fresh cursor.
969 *col += 1;
970 *yy = *col_top;
971 *at_col_top = true;
972 } else {
973 // The last column is full: close the page (its footnotes set at the foot), reset the float bands, and
974 // flush any deferred floats onto the fresh page, then restart at column 0 below whatever top band the
975 // flush stacked. The resume tracker restarts from the new page's column top.
976 res!(finish_page(
977 pages, frame, page_no, y, top, geom, notes, foot, bottom, bands.bot_reserve, metrics, incoming, ledger));
978 *bands = FloatBands::empty();
979 res!(flush_floats(
980 pending, frame, y, bands, Sp::ZERO, *page_no, geom, top, bottom, metrics, incoming, ledger));
981 *col = 0;
982 *col_top = *y; // finish_page reset y to the region top; flush_floats advanced it past any top floats
983 *yy = *col_top;
984 *deepest = *col_top;
985 *at_col_top = true;
986 }
987 Ok(())
988}
989
990/// Closes the current page: sets its footnotes at the foot, stores it, clears the note accumulator, and
991/// resets the frame and cursor before advancing the folio.
992#[allow(clippy::too_many_arguments)]
993fn finish_page<M: Metrics>(
994 pages: &mut Vec<Page>,
995 frame: &mut Frame,
996 page_no: &mut u32,
997 y: &mut Sp,
998 top: Sp,
999 geom: PageGeometry,
1000 notes: &mut Vec<Footnote>,
1001 foot: &FootStyle,
1002 bottom: Sp,
1003 bot_reserve: Sp, // height a foot float claimed on this page; footnotes sit above it
1004 metrics: &M,
1005 incoming: &Ledger,
1006 ledger: &mut Ledger,
1007)
1008 -> Outcome<()>
1009{
1010 res!(lay_footnotes(frame, notes, *page_no, geom, foot, bottom - bot_reserve, metrics, incoming, ledger));
1011 pages.push(Page::new(*page_no, geom, std::mem::take(frame)));
1012 notes.clear();
1013 *page_no += 1;
1014 *y = top;
1015 Ok(())
1016}
1017
1018/// The side an `auto` float settles on: Typst's midpoint rule. `used` is the region height already
1019/// consumed, `need` the float's height plus its clearance, `base` the full page-text height. The float
1020/// goes to the top when its own midpoint (`used + need/2`) would fall in the upper half of the page were
1021/// it set in the flow, and to the foot otherwise. Computed in i64 so a full page's scaled points cannot
1022/// overflow the doubling.
1023fn auto_side(used: Sp, need: Sp, base: Sp) -> FloatPlacement {
1024 let lhs = (used.raw() as i64) * 2 + (need.raw() as i64);
1025 if lhs <= base.raw() as i64 {
1026 FloatPlacement::Top
1027 } else {
1028 FloatPlacement::Bottom
1029 }
1030}
1031
1032/// Sets a float's material as a small vertical list from `y_top`: each child placed like a keep box's, and
1033/// the float's own [`Float`](crate::ledger::AnchorKind::Float) anchor recorded at the y it reaches, so a
1034/// cross-reference resolves the page and position it settled on. The material carries no framing glue --
1035/// the caller lays the clearance around it -- so this places the list as it stands.
1036fn place_float<M: Metrics>(
1037 f: &FloatNode,
1038 y_top: Sp,
1039 page_no: u32,
1040 geom: PageGeometry,
1041 metrics: &M,
1042 incoming: &Ledger,
1043 frame: &mut Frame,
1044 ledger: &mut Ledger,
1045 region: Region, // the band the float lands in; every anchor recorded while its material is laid inherits it
1046)
1047 -> Outcome<()>
1048{
1049 // Every anchor recorded from here on -- the float's own direct `Node::Anchor`, and any nested through
1050 // `place_line`, `place_vbox` or `place_leaf` (a margin note, claim label, ref or index entry inside the
1051 // float's body) -- claims this float's band, restored once its material is laid. Membership is decided
1052 // here, at the one place that knows a float's material is being laid, rather than at each helper that
1053 // happens to record an anchor.
1054 let prev_region = ledger.enter_region(region);
1055 let mut yy = y_top;
1056 for child in &f.list {
1057 match child {
1058 Node::Glue(g) => {
1059 yy += g.natural;
1060 },
1061 Node::HBox(b) => {
1062 res!(place_line(b, yy, page_no, geom, metrics, incoming, frame, ledger));
1063 yy += b.dims.vextent();
1064 },
1065 Node::VBox(b) => {
1066 res!(place_vbox(b, yy, page_no, geom, metrics, incoming, frame, ledger));
1067 yy += b.dims.vextent();
1068 },
1069 Node::Leaf(l) => {
1070 res!(place_leaf(l, geom.content_left(), yy, page_no, metrics, incoming, frame, ledger));
1071 yy += l.dims.vextent();
1072 },
1073 Node::Anchor(id) => {
1074 ledger.record(Anchor::new(id.clone(), Position::new(page_no, geom.content_left(), yy)));
1075 },
1076 Node::Penalty(_) => (),
1077 // A float never nests inside another float; a nested one would be a construction error, so it is
1078 // left unplaced rather than silently flattened.
1079 Node::Float(_) => (),
1080 // A columns block is only ever a top-level document node; one nested in a float's body would be a
1081 // construction error, so it draws nothing rather than being flattened here.
1082 Node::Columns(_) => (),
1083 // A repeated-header marker and a page-column marker are top-level control nodes; one in a float's
1084 // body is a construction error the lowering never builds, so it is passed over rather than flattened.
1085 Node::RepeatHead(_) | Node::PageColumns(_) => (),
1086 }
1087 }
1088 ledger.leave_region(prev_region);
1089 Ok(())
1090}
1091
1092/// A page's float bands: the top band already stacked (top floats plus their clearances) and the foot band
1093/// already reserved (foot floats plus their clearances). The body fills what is left between them.
1094#[derive(Clone, Copy, Debug)]
1095struct FloatBands {
1096 top_used: Sp,
1097 bot_reserve: Sp,
1098}
1099
1100impl FloatBands {
1101 fn empty() -> Self { Self { top_used: Sp::ZERO, bot_reserve: Sp::ZERO } }
1102}
1103
1104/// Inserts one float into the current page's top or foot region by Typst's rule, shifting the body (a top
1105/// float) or the existing foot band (a foot float) to make room -- Typst re-flows the region on a float
1106/// insertion; this shifts only what moved. `clearance_flag` is whether the clearance counts in the FIT and
1107/// midpoint budget: Typst sets it only when the page already carries flow content, and a re-queued float is
1108/// re-processed with it false. The clearance is ALWAYS laid in the band, per Typst's finalize. `force`
1109/// seats the float even when it will not fit (an empty page's front float, which overflows rather than
1110/// deferring forever). `foot_now` is the footnote furniture already reserved, so a float never lands over
1111/// the notes. Returns `true` when placed, `false` when it does not fit and `force` is unset.
1112#[allow(clippy::too_many_arguments)]
1113fn try_insert_float<M: Metrics>(
1114 f: &FloatNode,
1115 clearance_flag: bool,
1116 force: bool,
1117 y: &mut Sp,
1118 bands: &mut FloatBands,
1119 foot_now: Sp,
1120 page_no: u32,
1121 geom: PageGeometry,
1122 top: Sp,
1123 bottom: Sp,
1124 metrics: &M,
1125 incoming: &Ledger,
1126 frame: &mut Frame,
1127 ledger: &mut Ledger,
1128)
1129 -> Outcome<bool>
1130{
1131 let base = geom.content_height();
1132 let band = f.height + f.clearance; // finalize always lays the clearance
1133 let need_fit = f.height + if clearance_flag { f.clearance } else { Sp::ZERO };
1134 let remaining = (bottom - bands.bot_reserve - foot_now) - *y;
1135 if need_fit > remaining && !force {
1136 return Ok(false);
1137 }
1138 let used = base - remaining;
1139 let side = match f.placement {
1140 FloatPlacement::Top => FloatPlacement::Top,
1141 FloatPlacement::Bottom => FloatPlacement::Bottom,
1142 FloatPlacement::Auto => auto_side(used, need_fit, base),
1143 };
1144 match side {
1145 FloatPlacement::Bottom => {
1146 // Shift the existing foot band up by this float's band and seat the new float at the very foot,
1147 // so document order runs top-to-bottom down the foot region (the earliest foot float highest). The
1148 // body and top regions stay put; the float's own material is stamped into the foot band after it is
1149 // placed (`place_float` lays it as ordinary body material first).
1150 let start = frame.len();
1151 frame.shift_region(-band, Region::Foot);
1152 ledger.shift_region_anchors(page_no, -band, Region::Foot);
1153 res!(place_float(f, bottom - f.height, page_no, geom, metrics, incoming, frame, ledger, Region::Foot));
1154 frame.stamp_region(start, Region::Foot);
1155 bands.bot_reserve = bands.bot_reserve + band;
1156 },
1157 // Top (auto resolved to top or bottom above, so this arm is top).
1158 _ => {
1159 // Seat the float below the top band already stacked and shift the whole body region down, so
1160 // document order runs top-to-bottom down the top region (the earliest top float highest). Shifting
1161 // by region, not by a y window, moves a first body line that cap-height seating raised above the
1162 // band edge along with the rest of its body -- keying on the raised glyph y would strand it under
1163 // the float. The float's own material is stamped into the top band after it is placed.
1164 let at = top + bands.top_used;
1165 let start = frame.len();
1166 frame.shift_region(band, Region::Body);
1167 ledger.shift_region_anchors(page_no, band, Region::Body);
1168 res!(place_float(f, at, page_no, geom, metrics, incoming, frame, ledger, Region::Top));
1169 frame.stamp_region(start, Region::Top);
1170 bands.top_used = bands.top_used + band;
1171 *y += band;
1172 },
1173 }
1174 Ok(true)
1175}
1176
1177/// Sets the queued floats that fit on the current page, in document order, updating its [`FloatBands`].
1178/// A queued float is re-processed with `clearance_flag` false (Typst's relayout does the same); the front
1179/// float of an empty page is seated even when it will not fit, so the queue always drains, and flushing
1180/// stops at the first float that will not fit so a float never jumps ahead of an earlier one. `foot_now` is
1181/// the footnote furniture already on the page (zero on a freshly opened one), so a float never lands over
1182/// the notes -- the end-of-document drain calls this on a part-filled page and must respect them.
1183#[allow(clippy::too_many_arguments)]
1184fn flush_floats<M: Metrics>(
1185 pending: &mut Vec<FloatNode>,
1186 frame: &mut Frame,
1187 y: &mut Sp,
1188 bands: &mut FloatBands,
1189 foot_now: Sp,
1190 page_no: u32,
1191 geom: PageGeometry,
1192 top: Sp,
1193 bottom: Sp,
1194 metrics: &M,
1195 incoming: &Ledger,
1196 ledger: &mut Ledger,
1197)
1198 -> Outcome<()>
1199{
1200 let mut placed_any = false;
1201 while !pending.is_empty() {
1202 let force = frame.is_empty() && !placed_any;
1203 let f = pending[0].clone();
1204 let ok = res!(try_insert_float(
1205 &f, false, force, y, bands, foot_now, page_no, geom, top, bottom, metrics, incoming, frame, ledger));
1206 if ok {
1207 pending.remove(0);
1208 placed_any = true;
1209 } else {
1210 break;
1211 }
1212 }
1213 Ok(())
1214}
1215
1216/// Dispatches a node to the placement helper for its shape. The break decision is the caller's; this
1217/// only lays the node's ink at `y` on `page_no`.
1218fn place_node<M: Metrics>(
1219 node: &Node,
1220 y: Sp,
1221 page_no: u32,
1222 geom: PageGeometry,
1223 metrics: &M,
1224 incoming: &Ledger,
1225 frame: &mut Frame,
1226 ledger: &mut Ledger,
1227)
1228 -> Outcome<()>
1229{
1230 match node {
1231 // A keep box (a heading bound to the first line of its paragraph) is placed whole, so the greedy
1232 // breaker moves it entire rather than splitting it.
1233 Node::VBox(b) => place_vbox(b, y, page_no, geom, metrics, incoming, frame, ledger),
1234 Node::HBox(b) => place_line(b, y, page_no, geom, metrics, incoming, frame, ledger),
1235 Node::Leaf(l) => place_leaf(l, geom.content_left(), y, page_no, metrics, incoming, frame, ledger).map(|_| ()),
1236 _ => Ok(()),
1237 }
1238}
1239
1240/// Weighs the atom beginning at `start`: the boxes from there up to the next legal page breakpoint, their
1241/// stacked vertical extent, and the footnotes they introduce (as a foot reservation, `notes` being the
1242/// page's existing notes). A break is legal at glue that follows a box, and at a non-forbidden penalty; a
1243/// forbidden penalty (the widow/orphan weld set by [`doc::guard_widows`](crate::doc)) blocks the break and
1244/// welds the boxes either side of it into the one atom. Anchors and floats are transparent, taking no
1245/// extent and neither opening nor closing the atom. The caller checks this extent against the room left on
1246/// the page and breaks before the atom rather than splitting a line off it.
1247fn atom_measure(nodes: &[Node], start: usize, notes: &[Footnote], foot: &FootStyle) -> (Sp, Sp) {
1248 let mut ext = Sp::ZERO;
1249 let mut marks: Vec<Footnote> = Vec::new();
1250 let mut prev_box = false;
1251 let mut j = start;
1252 while j < nodes.len() {
1253 match &nodes[j] {
1254 Node::Glue(g) => {
1255 if prev_box {
1256 break; // glue after a box: the atom's first legal breakpoint
1257 }
1258 ext += g.natural; // interior glue (after a forbidden penalty) is part of the atom
1259 prev_box = false;
1260 },
1261 Node::Penalty(p) => {
1262 if p.is_forced() {
1263 break;
1264 }
1265 if p.is_forbidden() {
1266 prev_box = false; // the weld: the atom continues across it
1267 } else {
1268 break; // a non-forbidden penalty is a legal breakpoint
1269 }
1270 },
1271 Node::HBox(_) | Node::VBox(_) | Node::Leaf(_) => {
1272 ext += nodes[j].vextent();
1273 collect_marks(&nodes[j], &mut marks);
1274 prev_box = true;
1275 },
1276 // Transparent to the atom: none opens or closes it, and a repeated-header marker carries no extent.
1277 Node::Anchor(_) | Node::Float(_) | Node::Columns(_) | Node::RepeatHead(_) | Node::PageColumns(_) => (),
1278 }
1279 j += 1;
1280 }
1281 (ext, foot_reserve(notes, &marks, foot))
1282}
1283
1284/// Gathers the footnotes whose marks fall anywhere within `node`, in the document order they were set,
1285/// by walking its boxes. A mark is a [`LeafKind::Mark`] leaf; the note it carries is what the page
1286/// breaker reserves foot space for and what the closing page sets at its foot.
1287fn collect_marks(node: &Node, out: &mut Vec<Footnote>) {
1288 match node {
1289 Node::HBox(b) | Node::VBox(b) => for child in &b.list { collect_marks(child, out); },
1290 Node::Leaf(l) => if let LeafKind::Mark(f) = &l.kind { out.push(f.clone()); },
1291 _ => (),
1292 }
1293}
1294
1295/// The height a set of footnotes takes at the foot: the separator furniture, the notes' own stacked
1296/// heights, and the gaps between them. Zero when there are none, so a page with no footnote keeps the
1297/// whole body height. `existing` are the page's notes already; `extra` are a candidate line's, weighed
1298/// in so a line and its own note are judged against the same page together.
1299fn foot_reserve(existing: &[Footnote], extra: &[Footnote], foot: &FootStyle) -> Sp {
1300 let n = existing.len() + extra.len();
1301 if n == 0 {
1302 return Sp::ZERO;
1303 }
1304 let mut h = Sp::ZERO;
1305 for f in existing.iter().chain(extra.iter()) {
1306 h += f.height;
1307 }
1308 foot.gap_above_rule + foot.rule_thick + foot.gap_below_rule + h + foot.gap_between * (n as i32 - 1)
1309}
1310
1311/// Sets a page's accumulated footnotes at its foot: a short separator rule, then each note as the small
1312/// paragraph it was set into, seated so the whole block's foot meets the bottom of the text block, above
1313/// the folio. The block's height is exactly [`foot_reserve`]'s, so the body above it -- placed against a
1314/// bottom shrunk by that same amount -- never collides with it.
1315#[allow(clippy::too_many_arguments)]
1316fn lay_footnotes<M: Metrics>(
1317 frame: &mut Frame,
1318 notes: &[Footnote],
1319 page_no: u32,
1320 geom: PageGeometry,
1321 foot: &FootStyle,
1322 bottom: Sp,
1323 metrics: &M,
1324 incoming: &Ledger,
1325 ledger: &mut Ledger,
1326)
1327 -> Outcome<()>
1328{
1329 if notes.is_empty() {
1330 return Ok(());
1331 }
1332 let total = foot_reserve(notes, &[], foot);
1333 let mut yy = bottom - total;
1334
1335 yy += foot.gap_above_rule;
1336 frame.push(Placed::new(
1337 geom.content_left(), yy, Dims::new(foot.rule_width, foot.rule_thick, Sp::ZERO), PlacedKind::Rule));
1338 yy = yy + foot.rule_thick + foot.gap_below_rule;
1339
1340 for (i, f) in notes.iter().enumerate() {
1341 for child in &f.note {
1342 match child {
1343 Node::HBox(b) => {
1344 res!(place_line(b, yy, page_no, geom, metrics, incoming, frame, ledger));
1345 yy += b.dims.vextent();
1346 },
1347 Node::Glue(g) => {
1348 yy += g.natural;
1349 },
1350 _ => (),
1351 }
1352 }
1353 if i + 1 < notes.len() {
1354 yy += foot.gap_between;
1355 }
1356 }
1357 Ok(())
1358}
1359
1360/// Lays one horizontal box -- a line -- left to right, placing each child and recording any anchor
1361/// or forward reference it carries. Nested boxes are placed as their own rectangle in Phase 0;
1362/// shaping their contents is Phase 1.
1363fn place_line<M: Metrics>(
1364 line: &BoxNode,
1365 y: Sp,
1366 page_no: u32,
1367 geom: PageGeometry,
1368 metrics: &M,
1369 incoming: &Ledger,
1370 frame: &mut Frame,
1371 ledger: &mut Ledger,
1372)
1373 -> Outcome<()>
1374{
1375 let mut x = geom.content_left();
1376 for child in &line.list {
1377 match child {
1378 Node::Leaf(l) => {
1379 x = res!(place_leaf(l, x, y, page_no, metrics, incoming, frame, ledger));
1380 },
1381 Node::Glue(g) => {
1382 x += g.natural;
1383 },
1384 Node::Anchor(id) => {
1385 ledger.record(Anchor::new(id.clone(), Position::new(page_no, x, y)));
1386 },
1387 Node::Penalty(_) => {
1388 // A line arrives here already broken: `linebreak::break_paragraph` runs the Knuth-Plass
1389 // optimiser upstream and hands the driver finished HBox lines of words and justified
1390 // glue. A penalty inside such a line would be a later intra-line refinement (a kept
1391 // discretionary break), which Phase 1 does not yet place, so there is nothing to weigh.
1392 },
1393 Node::HBox(b) | Node::VBox(b) => {
1394 frame.push(Placed::new(x, y, b.dims, PlacedKind::Rule));
1395 x += b.dims.width;
1396 },
1397 // A float is a block-level node the driver handles before it ever reaches a line; one woven into a
1398 // line would be a construction error, so it draws nothing rather than being flattened here.
1399 Node::Float(_) => (),
1400 // A columns block is a top-level node; one woven into a line would be a construction error, so it
1401 // draws nothing rather than being flattened here.
1402 Node::Columns(_) => (),
1403 // A repeated-header or page-column marker is a top-level control node; nested here it is a
1404 // construction error the lowering never builds, so it is passed over rather than flattened.
1405 Node::RepeatHead(_) | Node::PageColumns(_) => (),
1406 }
1407 }
1408 Ok(())
1409}
1410
1411/// Sets a vertical keep box: its children stacked from the box top, each at the content left. Lines
1412/// are placed, glue advances the cursor, and an anchor is recorded at the y it reaches -- so a
1413/// heading's anchor takes the page and position the box settled on, never a provisional one from
1414/// before the box was moved to fit.
1415fn place_vbox<M: Metrics>(
1416 vbox: &BoxNode,
1417 y_top: Sp,
1418 page_no: u32,
1419 geom: PageGeometry,
1420 metrics: &M,
1421 incoming: &Ledger,
1422 frame: &mut Frame,
1423 ledger: &mut Ledger,
1424)
1425 -> Outcome<()>
1426{
1427 let mut yy = y_top;
1428 for child in &vbox.list {
1429 match child {
1430 Node::HBox(b) => {
1431 res!(place_line(b, yy, page_no, geom, metrics, incoming, frame, ledger));
1432 yy += b.dims.vextent();
1433 },
1434 Node::VBox(b) => {
1435 res!(place_vbox(b, yy, page_no, geom, metrics, incoming, frame, ledger));
1436 yy += b.dims.vextent();
1437 },
1438 Node::Leaf(l) => {
1439 res!(place_leaf(l, geom.content_left(), yy, page_no, metrics, incoming, frame, ledger));
1440 yy += l.dims.vextent();
1441 },
1442 Node::Glue(g) => {
1443 yy += g.natural;
1444 },
1445 Node::Anchor(id) => {
1446 ledger.record(Anchor::new(id.clone(), Position::new(page_no, geom.content_left(), yy)));
1447 },
1448 Node::Penalty(_) => (),
1449 // A float never nests inside a keep box; one that did would be a construction error, so it draws
1450 // nothing rather than being flattened into the box.
1451 Node::Float(_) => (),
1452 // A columns block never nests inside a keep box; one that did would be a construction error, so it
1453 // draws nothing rather than being flattened into the box.
1454 Node::Columns(_) => (),
1455 // A repeated-header or page-column marker is a top-level control node; nested here it is a
1456 // construction error the lowering never builds, so it is passed over rather than flattened.
1457 Node::RepeatHead(_) | Node::PageColumns(_) => (),
1458 }
1459 }
1460 Ok(())
1461}
1462
1463/// Places one leaf at `(x, y)` and returns the x the next child starts at. A rule is drawn as it
1464/// stands. A forward reference reserves a slot: the width it needs for the value resolved from the
1465/// previous pass, never less than the width the author declared. When the resolved value outgrows
1466/// the declared reservation the slot grows to fit it, which shifts everything after it -- the honest
1467/// cause of a further pass, recorded on the anchor as an overflow.
1468fn place_leaf<M: Metrics>(
1469 leaf: &Leaf,
1470 x: Sp,
1471 y: Sp,
1472 page_no: u32,
1473 metrics: &M,
1474 incoming: &Ledger,
1475 frame: &mut Frame,
1476 ledger: &mut Ledger,
1477)
1478 -> Outcome<Sp>
1479{
1480 // The leaf's own vertical shift moves its ink off the line's baseline without a nested box -- a
1481 // maths script raised, a fraction's numerator lifted and its bar seated on the axis. The x advance
1482 // is unaffected, so the horizontal cursor the caller tracks is untouched.
1483 let y = y + leaf.shift;
1484 match &leaf.kind {
1485 LeafKind::Rule => {
1486 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Rule));
1487 Ok(x + leaf.dims.width)
1488 },
1489 LeafKind::Text(shaped) => {
1490 // Already shaped and measured; place it and advance by its width. The writer reads the run
1491 // back out of the frame to draw the glyphs.
1492 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Text(shaped.clone())));
1493 Ok(x + leaf.dims.width)
1494 },
1495 LeafKind::Mark(footnote) => {
1496 // The superscript number is drawn like any run; its raised dims put the baseline above the
1497 // line's. The note it carries is set at the page foot by the breaker, not here.
1498 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Text(footnote.mark.clone())));
1499 Ok(x + leaf.dims.width)
1500 },
1501 LeafKind::Graphic(g) => {
1502 // A figure placed whole: its ops are translated to this position and drawn by the emitter.
1503 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Graphic(g.clone())));
1504 Ok(x + leaf.dims.width)
1505 },
1506 LeafKind::Reserved(id, refr, hold, bold) => {
1507 // A forward reference. What it resolves to is the reference's own business (a total count, a
1508 // cross-referenced page); the driver only asks the previous pass's ledger for the value and
1509 // holds the declared width open until it has one.
1510 let reserved = leaf.dims.width;
1511 let (realised, resolved) = match refr.resolve_text(incoming) {
1512 Some(text) => {
1513 // The previous pass fixed the value. Shape it as real text when a font backs the
1514 // metric, or keep the reservation box under the fontless stub; either way its realised
1515 // width is recorded so the overflow logic still governs a further pass. A main index
1516 // reference's folio (`bold`) shapes in the bold face, reproducing in-dexter's strong-set
1517 // main page number.
1518 let shaped = if *bold { res!(metrics.shape_bold(&text)) } else { res!(metrics.shape(&text)) };
1519 match shaped {
1520 Some(shaped) => {
1521 let w = shaped.dims().width;
1522 let dims = Dims::new(w, leaf.dims.height, leaf.dims.depth);
1523 frame.push(Placed::new(x, y, dims, PlacedKind::Text(shaped)));
1524 (w, true)
1525 },
1526 None => {
1527 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Reserved));
1528 (res!(metrics.measure(&text)).width, true)
1529 },
1530 }
1531 },
1532 None => {
1533 // Pass A: no value yet. Hold the reservation open and realise nothing, so no overflow
1534 // is charged before there is a value that could exceed the width.
1535 frame.push(Placed::new(x, y, leaf.dims, PlacedKind::Reserved));
1536 (Sp::ZERO, false)
1537 },
1538 };
1539
1540 // A value wider than its reservation always grows the slot -- the honest cause of a further
1541 // pass, charged as the anchor's overflow. Within the reservation, furniture (`hold`) keeps the
1542 // declared width so a right-aligned column stays put, while an inline reference shrinks to the
1543 // resolved value so it reads without a gap; a still-unresolved slot keeps its reservation.
1544 let slot = if realised > reserved {
1545 realised
1546 } else if *hold || !resolved {
1547 reserved
1548 } else {
1549 realised
1550 };
1551 let mut anchor = Anchor::new(id.clone(), Position::new(page_no, x, y));
1552 anchor.reserved = reserved;
1553 anchor.realised = realised;
1554 ledger.record(anchor);
1555 Ok(x + slot)
1556 },
1557 }
1558}
1559
1560/// Builds the non-convergence error: the ledger difference the architecture promises, naming the
1561/// anchor that moved and the pages it moved between, plus any reference that overflowed its
1562/// reservation.
1563fn non_convergence(
1564 pass: u32,
1565 ledger: &Ledger,
1566 prev: &Ledger,
1567)
1568 -> Error<ErrTag>
1569{
1570 let deltas = ledger.diff(prev);
1571 let overflows = ledger.overflowed();
1572
1573 let mut moved = String::new();
1574 for d in &deltas {
1575 moved.push_str(&fmt!(" [{:?} {} moved p{}->p{}]", d.id.kind, d.id.key, d.from, d.to));
1576 }
1577 let mut over = String::new();
1578 for id in &overflows {
1579 over.push_str(&fmt!(" [{:?} {} overflowed its reservation]", id.kind, id.key));
1580 }
1581 err!(
1582 "Composition did not converge after {} passes; the ledger is still moving. Moved anchors:{}. \
1583 Reservations exceeded:{}.", pass, moved, over;
1584 Data, Excessive, LimitReached)
1585}