Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/src/ir.rs

24.4 KiB, 151 runs

created by r1870400018:35665, 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 box, glue and penalty intermediate representation.
2//!
3//! This is TeX's model: vertical and horizontal material is a stream of boxes (rigid rectangles),
4//! glue (stretchable, shrinkable space) and penalties (the cost of breaking at a point). Austenite
5//! adds an anchor node, a zero-size marker that records where an identity landed so the ledger can
6//! resolve references to it.
7//!
8//! Lengths are [`Sp`], scaled points, per the architecture's reproducibility rule. Floating point
9//! appears only at the output boundary, where a coordinate becomes a device length.
10
11use crate::font::ShapedText;
12use crate::ledger::{
13 AnchorId,
14 Ref,
15};
16
17use oxedyne_fe2o3_core::prelude::*;
18use oxedyne_fe2o3_jdat::prelude::*;
19use oxedyne_fe2o3_graphics::{
20 colour::Rgba,
21 path::Path,
22};
23
24use std::sync::Arc;
25
26/// A length in scaled points: one sixty-five-thousand-five-hundred-and-thirty-sixth of a point, as
27/// in TeX. Integer arithmetic makes every break decision exact and every build byte-identical, which
28/// is the whole reason the type is not an `f64`.
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct Sp(pub i32);
31
32impl Sp {
33 pub const ZERO: Sp = Sp(0);
34 pub const UNIT: i32 = 65_536; // scaled points to the point
35
36 /// Converts a length in points to scaled points, rounding to the nearest unit. This is a
37 /// boundary conversion; once inside the engine a length never leaves the integer domain.
38 pub fn from_pt(pt: f64) -> Self {
39 Sp((pt * Sp::UNIT as f64).round() as i32)
40 }
41
42 /// The length in points, for the output boundary only.
43 pub fn to_pt(self) -> f64 {
44 self.0 as f64 / Sp::UNIT as f64
45 }
46
47 pub fn raw(self) -> i32 { self.0 }
48}
49
50impl std::ops::Add for Sp {
51 type Output = Sp;
52 fn add(self, other: Sp) -> Sp { Sp(self.0.saturating_add(other.0)) }
53}
54
55impl std::ops::Sub for Sp {
56 type Output = Sp;
57 fn sub(self, other: Sp) -> Sp { Sp(self.0.saturating_sub(other.0)) }
58}
59
60impl std::ops::Neg for Sp {
61 type Output = Sp;
62 fn neg(self) -> Sp { Sp(self.0.saturating_neg()) }
63}
64
65impl std::ops::Mul<i32> for Sp {
66 type Output = Sp;
67 fn mul(self, k: i32) -> Sp { Sp(self.0.saturating_mul(k)) }
68}
69
70impl std::ops::AddAssign for Sp {
71 fn add_assign(&mut self, other: Sp) { self.0 = self.0.saturating_add(other.0); }
72}
73
74impl ToDat for Sp {
75 fn to_dat(&self) -> Outcome<Dat> {
76 Ok(dat!(self.0))
77 }
78}
79
80impl FromDat for Sp {
81 fn from_dat(dat: Dat) -> Outcome<Self> {
82 Ok(Sp(try_extract_dat!(dat, I32)))
83 }
84}
85
86/// A span of the source a box came from, in byte offsets, so a diagnostic can quote it. The
87/// architecture makes this universal; Phase 0 carries it but does not yet render carets.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct Span {
90 pub start: u32,
91 pub end: u32,
92}
93
94impl Span {
95 pub fn new(start: u32, end: u32) -> Self { Self { start, end } }
96}
97
98/// The three measurements a box occupies, split at the baseline as TeX splits them: `height` reaches
99/// up from the baseline, `depth` hangs below it, and the two sum to the visual extent.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct Dims {
102 pub width: Sp,
103 pub height: Sp,
104 pub depth: Sp,
105}
106
107impl Dims {
108 pub fn new(width: Sp, height: Sp, depth: Sp) -> Self {
109 Self { width, height, depth }
110 }
111
112 /// The vertical extent, height above the baseline plus depth below it, which is what a vertical
113 /// list advances by when it stacks this box.
114 pub fn vextent(&self) -> Sp { self.height + self.depth }
115}
116
117impl ToDat for Dims {
118 fn to_dat(&self) -> Outcome<Dat> {
119 Ok(listdat![
120 res!(self.width.to_dat()),
121 res!(self.height.to_dat()),
122 res!(self.depth.to_dat()),
123 ])
124 }
125}
126
127impl FromDat for Dims {
128 fn from_dat(dat: Dat) -> Outcome<Self> {
129 let v = try_extract_dat!(dat, List);
130 if v.len() != 3 {
131 return Err(err!(
132 "Dims expects a list of three scaled lengths, found {}.", v.len();
133 Input, Invalid, Mismatch));
134 }
135 Ok(Self {
136 width: res!(Sp::from_dat(v[0].clone())),
137 height: res!(Sp::from_dat(v[1].clone())),
138 depth: res!(Sp::from_dat(v[2].clone())),
139 })
140 }
141}
142
143/// Space that can grow and shrink. `natural` is the length at rest, `stretch` the amount it will
144/// grow when a line or page is loose, `shrink` the amount it will give up when tight.
145///
146/// Phase 0 is finite glue only. TeX's infinite orders (fil, fill, filll), which centre and fill,
147/// are a Phase 1 addition and belong here as a `stretch_order` beside the amount.
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149pub struct Glue {
150 pub natural: Sp,
151 pub stretch: Sp,
152 pub shrink: Sp,
153}
154
155impl Glue {
156 pub fn fixed(natural: Sp) -> Self {
157 Self { natural, stretch: Sp::ZERO, shrink: Sp::ZERO }
158 }
159
160 pub fn new(natural: Sp, stretch: Sp, shrink: Sp) -> Self {
161 Self { natural, stretch, shrink }
162 }
163}
164
165/// The cost of breaking a line or page at a point, and whether the break is flagged (two flagged
166/// breaks in a row are themselves penalised, which is how TeX avoids two hyphens ending consecutive
167/// lines). A cost of [`Penalty::INFINITY`] forbids a break; [`Penalty::EJECT`] forces one.
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169pub struct Penalty {
170 pub cost: i32,
171 pub flagged: bool,
172 pub strong: bool, // a strong eject: ejects even on an empty page (a trailing/consecutive `#pagebreak()`), never set on any other break
173 pub column: bool, // a forced column break (`#colbreak()`): the next column, or the next page from the last
174}
175
176impl Penalty {
177 pub const INFINITY: i32 = 10_000; // a break here is forbidden
178 pub const EJECT: i32 = -10_000; // a break here is forced
179
180 pub fn new(cost: i32, flagged: bool) -> Self {
181 Self { cost, flagged, strong: false, column: false }
182 }
183
184 /// A forced break, which the page breaker must take -- a chapter end, section furniture, or a weak
185 /// `#pagebreak(weak: true)`. The driver drops it on an already-empty page, so it opens no blank page.
186 pub fn eject() -> Self { Self { cost: Self::EJECT, flagged: false, strong: false, column: false } }
187
188 /// A strong forced break -- the default `#pagebreak()`. Ejects unconditionally, opening a blank page when
189 /// it lands on an already-empty page or trails the document, matching Typst 0.15.1's strong pagebreak.
190 pub fn strong_eject() -> Self { Self { cost: Self::EJECT, flagged: false, strong: true, column: false } }
191
192 /// A forced column break -- Typst's `#colbreak()`, strong unless `weak`. In a layout of several columns
193 /// it moves the flow to the next column, or to the next page from the last; on a page of one column it
194 /// is a page break, as Typst makes it. A weak one is dropped in a column that holds nothing yet.
195 pub fn column_eject(weak: bool) -> Self { Self { cost: Self::EJECT, flagged: false, strong: !weak, column: true } }
196
197 /// Is a break at this penalty forbidden?
198 pub fn is_forbidden(&self) -> bool { self.cost >= Self::INFINITY }
199
200 /// Is a break at this penalty forced?
201 pub fn is_forced(&self) -> bool { self.cost <= Self::EJECT }
202
203 /// Is this a strong forced eject, one that opens a page even where the current one is empty?
204 pub fn is_strong(&self) -> bool { self.strong }
205
206 /// Is this a column break rather than a page break?
207 pub fn is_column(&self) -> bool { self.column }
208}
209
210/// A footnote: the superscript mark set in the running text, and the note set at the foot of the page
211/// the mark lands on. The number is assigned at author time as a document-order fold, so it is content
212/// order and never a layout query. `note` is the note already set as a small paragraph (the lines and
213/// their leading), prefixed by its own superscript number; `height` is what that stack occupies, which
214/// the page breaker reserves from the body of the page the mark falls on.
215#[derive(Clone, Debug)]
216pub struct Footnote {
217 pub number: u32,
218 pub mark: ShapedText, // the superscript number drawn where the mark falls
219 pub note: Vec<Node>, // the note as HBox lines and interline glue, set at the foot measure
220 pub height: Sp, // the note's stacked vertical extent, reserved from the body
221}
222
223/// A decoded raster image: straight, eight-bit RGBA samples, row-major with the top row first, ready to
224/// hand to the PDF and SVG writers. Held behind an [`Arc`] in a [`DrawOp::Image`] so a figure's pixels
225/// are shared, not copied, as the graphic rides the stream.
226#[derive(Clone, Debug)]
227pub struct RasterImage {
228 pub width: usize, // samples across
229 pub height: usize, // samples down
230 pub rgba: Vec<u8>, // width * height * 4 straight-RGBA bytes, top row first
231}
232
233/// A hint from an `image(...)` or `padded-image(...)` call for how large to draw a figure: a fraction of
234/// the measure (`50%`) or an absolute length in points (`4cm`). An axis with no hint is taken from the
235/// other axis and the image's own aspect, and a figure with no hint at all fills the measure.
236#[derive(Clone, Copy, Debug, PartialEq)]
237pub enum Length {
238 Rel(f64), // a fraction of the container measure
239 Abs(f64), // an absolute length in points
240}
241
242/// One drawing operation within a [`Graphic`]: a filled or stroked path, or a placed raster, in the
243/// graphic's own frame, which is y down and in points, so placing the graphic needs only a translation.
244#[derive(Clone, Debug)]
245pub enum DrawOp {
246 Fill { path: Path, colour: Rgba },
247 Stroke { path: Path, colour: Rgba, width: f32 }, // stroke width in points
248 // A raster drawn to fill the rectangle at top-left (x, y), w wide and h tall, in the graphic's frame.
249 Image { image: Arc<RasterImage>, x: f32, y: f32, w: f32, h: f32 },
250}
251
252/// Where a link points: out of the document to a URI, or into it to a named anchor. An internal target
253/// carries the anchor's identity, not a page or a block address, so it stays stable as the document
254/// repaginates -- the reader resolves it against the shipped ledger, which turns the identity into the
255/// page it landed on and then into the content-addressed block of that page.
256#[derive(Clone, Debug, PartialEq, Eq)]
257pub enum LinkTarget {
258 Uri(String), // an external address, followed as-is
259 Anchor(AnchorId), // an internal cross-reference, resolved through the ledger to a block and page
260}
261
262/// A self-contained piece of drawn ink -- a diagram, a figure, a baked label run -- as a bag of paths
263/// with a bounding box. It rides the stream as a [`LeafKind::Graphic`] leaf and is placed like any box;
264/// the emitter translates its ops to where it landed and draws them. Every path is already flattened
265/// here, a text label's glyph outlines included, so a built graphic never reaches back into shaping.
266#[derive(Clone, Debug)]
267pub struct Graphic {
268 pub ops: Vec<DrawOp>,
269 pub dims: Dims,
270 pub link: Option<LinkTarget>, // where the whole graphic links, drawn over its placement box
271}
272
273impl Graphic {
274 pub fn new(ops: Vec<DrawOp>, dims: Dims) -> Self {
275 Self { ops, dims, link: None }
276 }
277
278 /// Makes the whole graphic a clickable link to the external `url` -- the PDF writer draws a link
279 /// annotation over its placement box, and Pearl carries it as a `link` leaf. The meta page's "Made
280 /// with AI" chip carries the scheme URL this way; the SVG writer, which sets the mark as a plain
281 /// image, leaves it unlinked.
282 pub fn with_link(mut self, url: String) -> Self {
283 self.link = Some(LinkTarget::Uri(url));
284 self
285 }
286
287 /// Makes the whole graphic a cross-reference to `anchor` within the document, resolved through the
288 /// ledger at read time to the content-addressed block the anchor landed in.
289 pub fn with_link_anchor(mut self, anchor: AnchorId) -> Self {
290 self.link = Some(LinkTarget::Anchor(anchor));
291 self
292 }
293}
294
295/// What an atomic box draws. `Reserved` holds open the width a forward reference will need once the
296/// ledger resolves it, which is what lets two passes suffice by construction. Its first `bool` is whether
297/// the slot holds that reserved width even when the resolved value is narrower: true for right-aligned
298/// furniture (a table-of-contents folio), whose column must stay put, and false for a reference set in
299/// running prose, which shrinks to the value so it reads without a gap. Its second `bool` is whether the
300/// resolved value sets bold rather than in the body face -- a main index reference's folio, reproducing
301/// in-dexter's `index-main = index.with(fmt: strong)`.
302#[derive(Clone, Debug)]
303pub enum LeafKind {
304 Rule,
305 Reserved(AnchorId, Ref, bool, bool), // a forward reference: its identity, what it resolves to, whether it holds width, whether it sets bold
306 Text(ShapedText), // a shaped run of real text, drawn as glyph outlines
307 Mark(Footnote), // a footnote reference mark; its note is set at the page foot
308 Graphic(Arc<Graphic>), // a self-contained figure, its ops drawn at the leaf's placement
309}
310
311/// An atomic box: intrinsic dimensions, what it draws, and where in the source it came from.
312#[derive(Clone, Debug)]
313pub struct Leaf {
314 pub kind: LeafKind,
315 pub dims: Dims,
316 pub shift: Sp, // downward offset applied at placement; positive lowers, negative raises
317 pub span: Option<Span>,
318}
319
320impl Leaf {
321 pub fn rule(dims: Dims) -> Self {
322 Self { kind: LeafKind::Rule, dims, shift: Sp::ZERO, span: None }
323 }
324
325 /// A forward reference whose slot holds its reserved width even when the value comes out narrower --
326 /// for a right-aligned folio in a table of contents, where the column must not move.
327 pub fn reserved(id: AnchorId, refr: Ref, dims: Dims) -> Self {
328 Self { kind: LeafKind::Reserved(id, refr, true, false), dims, shift: Sp::ZERO, span: None }
329 }
330
331 /// A forward reference set in running prose: its slot shrinks to the resolved value, so a page number
332 /// reads tightly in the sentence rather than trailing a gap the reservation held open.
333 pub fn reserved_inline(id: AnchorId, refr: Ref, dims: Dims) -> Self {
334 Self { kind: LeafKind::Reserved(id, refr, false, false), dims, shift: Sp::ZERO, span: None }
335 }
336
337 /// A forward reference set in running prose whose resolved value sets bold -- a main index reference's
338 /// folio, reproducing in-dexter's `index-main = index.with(fmt: strong)`. It shrinks to the value like
339 /// any inline reference.
340 pub fn reserved_inline_bold(id: AnchorId, refr: Ref, dims: Dims) -> Self {
341 Self { kind: LeafKind::Reserved(id, refr, false, true), dims, shift: Sp::ZERO, span: None }
342 }
343
344 /// A leaf of real shaped text, taking its dimensions from the run.
345 pub fn text(shaped: ShapedText) -> Self {
346 let dims = shaped.dims();
347 Self { kind: LeafKind::Text(shaped), dims, shift: Sp::ZERO, span: None }
348 }
349
350 /// A leaf of shaped text with dimensions the caller sets rather than the run's own, so a run can be
351 /// raised or seated within a taller line -- a footnote's superscript number, say.
352 pub fn text_dims(shaped: ShapedText, dims: Dims) -> Self {
353 Self { kind: LeafKind::Text(shaped), dims, shift: Sp::ZERO, span: None }
354 }
355
356 /// A footnote mark. `dims` is the superscript's box, its height reduced so the baseline the emitter
357 /// draws at (`y + height`) sits raised above the surrounding line's baseline, and its width small
358 /// enough that line breaking flows around it as around any narrow box.
359 pub fn mark(footnote: Footnote, dims: Dims) -> Self {
360 Self { kind: LeafKind::Mark(footnote), dims, shift: Sp::ZERO, span: None }
361 }
362
363 /// A graphic leaf: a figure set as one box, its ink the graphic's own paths. The dims are the
364 /// graphic's bounding box, so the vertical list stacks it and a line flows around it as any box.
365 pub fn graphic(graphic: Graphic) -> Self {
366 let dims = graphic.dims;
367 Self { kind: LeafKind::Graphic(Arc::new(graphic)), dims, shift: Sp::ZERO, span: None }
368 }
369
370 pub fn with_span(mut self, span: Span) -> Self {
371 self.span = Some(span);
372 self
373 }
374
375 /// Sets the vertical shift applied when the leaf is placed, so a glyph run or a rule can sit above
376 /// or below its line's baseline without a nested box. Maths uses this to stack a fraction's parts
377 /// and to raise a script; a positive shift lowers the leaf, a negative one raises it.
378 pub fn with_shift(mut self, shift: Sp) -> Self {
379 self.shift = shift;
380 self
381 }
382}
383
384/// A box holding a list set either horizontally or vertically, with its own resolved dimensions. The
385/// orientation lives in the enclosing [`Node`] variant, not here, because it is the parent's list
386/// direction that decides how these children stack.
387#[derive(Clone, Debug)]
388pub struct BoxNode {
389 pub list: Vec<Node>,
390 pub dims: Dims,
391}
392
393impl BoxNode {
394 pub fn new(list: Vec<Node>, dims: Dims) -> Self {
395 Self { list, dims }
396 }
397}
398
399/// Where a float asks to sit on the page it settles on. `Auto` (Typst's `placement: auto`) lets the
400/// engine choose between top and foot by the midpoint rule -- the side the float's centre would fall on
401/// were it set in the flow. `Top` and `Bottom` pin the side (Typst's `placement: top`/`bottom`). `Auto`
402/// is a distinct variant, not a synonym for `Top`, because the choice it defers is the whole point.
403#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404pub enum FloatPlacement {
405 Auto,
406 Top,
407 Bottom,
408}
409
410/// How wide a float runs: within the column it is met in, or across every column of the page (Typst's
411/// `scope: "column"` and `scope: "parent"`). On a page of one column the two coincide.
412#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
413pub enum FloatScope {
414 #[default]
415 Column,
416 Parent,
417}
418
419/// Where a float settles and how wide it runs: Typst's `placement` and `scope` taken together.
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421pub struct Floating {
422 pub side: FloatPlacement,
423 pub scope: FloatScope,
424}
425
426impl Floating {
427 /// A float within its column, Typst's default scope.
428 pub fn column(side: FloatPlacement) -> Self {
429 Self { side, scope: FloatScope::Column }
430 }
431}
432
433/// A block-level float: self-contained vertical material -- a figure and its caption, or an aside box --
434/// set at the top or foot of a page rather than in the flow, its parts kept together (Typst's
435/// `figure(placement: auto | top | bottom)`).
436///
437/// `list` is the material as a small vertical list (no block spacing around it -- Typst frames a float
438/// with `clearance`, not paragraph glue). `height` is the material's own extent, what the break weighs.
439/// `clearance` is the gap between the float and the body (Typst's `place.clearance`, default 1.5em of the
440/// float's font size), applied above a foot float and below a top float, and only when the page carries
441/// other content. `scope` decides whether it settles within its column or spans the page's columns. A float
442/// is only ever a top-level node of the document stream, never nested in a line or keep box.
443#[derive(Clone, Debug)]
444pub struct FloatNode {
445 pub list: Vec<Node>,
446 pub height: Sp,
447 pub clearance: Sp,
448 pub placement: FloatPlacement,
449 pub scope: FloatScope,
450}
451
452impl FloatNode {
453 pub fn new(list: Vec<Node>, height: Sp, clearance: Sp, floating: Floating) -> Self {
454 Self { list, height, clearance, placement: floating.side, scope: floating.scope }
455 }
456}
457
458/// The column layout a run of pages flows in: `count` equal columns parted by `gutter` (Typst's
459/// `page.columns` and `columns.gutter`). One column is the ordinary page.
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub struct PageColumns {
462 pub count: usize,
463 pub gutter: Sp,
464}
465
466impl PageColumns {
467 pub const ONE: Self = Self { count: 1, gutter: Sp::ZERO };
468
469 pub fn new(count: usize, gutter: Sp) -> Self {
470 Self { count: count.max(1), gutter }
471 }
472}
473
474/// A block of vertical material set in equal side-by-side columns, filled sequentially: the first column
475/// fills top to bottom, then the flow hops to the next column at the same top, and when the last column
476/// fills the driver breaks to a fresh page's first column. This is Typst's `columns(n)` -- no balancing,
477/// the last column of the last page simply ends where the material runs out.
478///
479/// `list` is the material as an ordinary vertical box-glue-penalty stream, exactly what the page body
480/// flows, so the same greedy atom-aware breaker sets it; `count` is the number of columns and `gutter`
481/// the space between two adjacent columns. A columns block is only ever a top-level node of the document
482/// stream, never nested in a line, keep box or float.
483#[derive(Clone, Debug)]
484pub struct ColumnsNode {
485 pub list: Vec<Node>,
486 pub count: usize,
487 pub gutter: Sp,
488}
489
490impl ColumnsNode {
491 pub fn new(list: Vec<Node>, count: usize, gutter: Sp) -> Self {
492 Self { list, count, gutter }
493 }
494}
495
496/// One item of a box-glue-penalty list: the closed vocabulary the whole engine is built on.
497#[derive(Clone, Debug)]
498pub enum Node {
499 HBox(BoxNode), // a list set left to right
500 VBox(BoxNode), // a list set top to bottom
501 Leaf(Leaf),
502 Glue(Glue),
503 Penalty(Penalty),
504 Anchor(AnchorId), // a zero-size marker recording where an identity landed
505 Float(FloatNode), // a block-level float, deferred by the driver to the top or foot of a later page
506 Columns(ColumnsNode), // a block flowed into equal side-by-side columns, filled left to right
507 // A zero-size marker setting the column layout the following pages flow in (Typst's `set page(columns:)`):
508 // a page already carrying material is closed first, so the new layout starts on a fresh page. Like
509 // `RepeatHead`, a driver-time control node, never ToDat-serialised.
510 PageColumns(PageColumns),
511 // A zero-size marker arming (Some) or disarming (None) a repeated header: while armed, the driver
512 // stamps the boxed header at the top of every fresh page the following material spills onto, so a
513 // breakable table's column heads repeat down a multi-page run (Typst's `table.header` repeat). It is
514 // never ToDat-serialised -- it is a driver-time control node the lowerer weaves, not shipped IR -- so
515 // adding it changes no on-disc format. Transparent to breakability, like `Anchor`.
516 RepeatHead(Option<Box<BoxNode>>),
517}
518
519impl Node {
520 /// The amount a vertical list advances when it stacks this node. A box or leaf contributes its
521 /// height plus depth, glue its natural length, and a penalty or anchor nothing -- they mark a
522 /// position without occupying one.
523 pub fn vextent(&self) -> Sp {
524 match self {
525 Node::HBox(b) => b.dims.vextent(),
526 Node::VBox(b) => b.dims.vextent(),
527 Node::Leaf(l) => l.dims.vextent(),
528 Node::Glue(g) => g.natural,
529 Node::Penalty(_) => Sp::ZERO,
530 Node::Anchor(_) => Sp::ZERO,
531 // A float takes no space where it stands: it leaves the flow, and the driver charges its height
532 // against the page it settles on, not this position.
533 Node::Float(_) => Sp::ZERO,
534 // A columns block is flowed by the driver's own multi-column pass, which advances the cursor
535 // itself; it contributes no simple vertical extent to weigh where it stands.
536 Node::Columns(_) => Sp::ZERO,
537 // A repeated-header marker is a zero-size control node: it arms or disarms the driver's header
538 // repeat and occupies no vertical space where it stands.
539 Node::RepeatHead(_) => Sp::ZERO,
540 // A column-layout marker occupies nothing; the driver acts on it between pages.
541 Node::PageColumns(_) => Sp::ZERO,
542 }
543 }
544
545 /// Is this a legal place to break a page? A page may break at glue that follows a non-discardable
546 /// item, and at a penalty that is not forbidden. Phase 0 keeps the rule to those two, which is
547 /// enough to paginate; widow and orphan penalties join it in Phase 2.
548 pub fn is_breakpoint(&self) -> bool {
549 match self {
550 Node::Glue(_) => true,
551 Node::Penalty(p) => !p.is_forbidden(),
552 _ => false,
553 }
554 }
555}
556
557/// A source of glyph and box metrics: the seam for real typesetting. Implemented over `fe2o3_font`
558/// by [`FontMetrics`](crate::font::FontMetrics), and by [`StubMetrics`] for running without a font.
559pub trait Metrics {
560 fn measure(&self, text: &str) -> Outcome<Dims>;
561
562 /// Shapes text into a placeable run when a font backs this metric, or `None` for the fontless
563 /// stub. A forward reference resolved against a font metric is shaped and drawn here as real
564 /// glyphs; against the stub it stays a reservation, measured but not drawn.
565 fn shape(&self, text: &str) -> Outcome<Option<ShapedText>>;
566
567 /// Shapes text in the bold face rather than the metric's own -- a main index reference's folio, which
568 /// in-dexter sets `strong`. The fontless stub ignores the face and behaves as [`shape`](Metrics::shape).
569 fn shape_bold(&self, text: &str) -> Outcome<Option<ShapedText>>;
570}
571
572/// A placeholder metric: every character one fixed em wide, one em tall, a fixed depth. It runs the
573/// driver without a font, beside the real [`FontMetrics`](crate::font::FontMetrics).
574#[derive(Clone, Copy, Debug)]
575pub struct StubMetrics {
576 pub em: Sp, // advance and body height of one character
577 pub depth: Sp, // descent below the baseline
578}
579
580impl StubMetrics {
581 pub fn new(em: Sp, depth: Sp) -> Self {
582 Self { em, depth }
583 }
584}
585
586impl Metrics for StubMetrics {
587 fn measure(&self, text: &str) -> Outcome<Dims> {
588 let n = text.chars().count() as i32;
589 Ok(Dims::new(self.em * n, self.em, self.depth))
590 }
591
592 fn shape(&self, _text: &str) -> Outcome<Option<ShapedText>> {
593 Ok(None) // no font behind the stub, so nothing to shape into glyphs
594 }
595
596 fn shape_bold(&self, _text: &str) -> Outcome<Option<ShapedText>> {
597 Ok(None) // no font behind the stub, so the face makes no difference
598 }
599}