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 | |
| 11 | use crate::font::ShapedText; |
| 12 | use crate::ledger::{ |
| 13 | AnchorId, |
| 14 | Ref, |
| 15 | }; |
| 16 | |
| 17 | use oxedyne_fe2o3_core::prelude::*; |
| 18 | use oxedyne_fe2o3_jdat::prelude::*; |
| 19 | use oxedyne_fe2o3_graphics::{ |
| 20 | colour::Rgba, |
| 21 | path::Path, |
| 22 | }; |
| 23 | |
| 24 | use 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)] |
| 30 | pub struct Sp(pub i32); |
| 31 | |
| 32 | impl 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 | |
| 50 | impl 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 | |
| 55 | impl 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 | |
| 60 | impl std::ops::Neg for Sp { |
| 61 | type Output = Sp; |
| 62 | fn neg(self) -> Sp { Sp(self.0.saturating_neg()) } |
| 63 | } |
| 64 | |
| 65 | impl 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 | |
| 70 | impl std::ops::AddAssign for Sp { |
| 71 | fn add_assign(&mut self, other: Sp) { self.0 = self.0.saturating_add(other.0); } |
| 72 | } |
| 73 | |
| 74 | impl ToDat for Sp { |
| 75 | fn to_dat(&self) -> Outcome<Dat> { |
| 76 | Ok(dat!(self.0)) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | impl 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)] |
| 89 | pub struct Span { |
| 90 | pub start: u32, |
| 91 | pub end: u32, |
| 92 | } |
| 93 | |
| 94 | impl 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)] |
| 101 | pub struct Dims { |
| 102 | pub width: Sp, |
| 103 | pub height: Sp, |
| 104 | pub depth: Sp, |
| 105 | } |
| 106 | |
| 107 | impl 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 | |
| 117 | impl 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 | |
| 127 | impl 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)] |
| 149 | pub struct Glue { |
| 150 | pub natural: Sp, |
| 151 | pub stretch: Sp, |
| 152 | pub shrink: Sp, |
| 153 | } |
| 154 | |
| 155 | impl 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)] |
| 169 | pub 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 | |
| 176 | impl 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)] |
| 216 | pub 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)] |
| 227 | pub 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)] |
| 237 | pub 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)] |
| 245 | pub 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)] |
| 257 | pub 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)] |
| 267 | pub 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 | |
| 273 | impl 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)] |
| 303 | pub 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)] |
| 313 | pub 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 | |
| 320 | impl 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)] |
| 388 | pub struct BoxNode { |
| 389 | pub list: Vec<Node>, |
| 390 | pub dims: Dims, |
| 391 | } |
| 392 | |
| 393 | impl 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)] |
| 404 | pub 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)] |
| 413 | pub 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)] |
| 421 | pub struct Floating { |
| 422 | pub side: FloatPlacement, |
| 423 | pub scope: FloatScope, |
| 424 | } |
| 425 | |
| 426 | impl 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)] |
| 444 | pub 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 | |
| 452 | impl 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)] |
| 461 | pub struct PageColumns { |
| 462 | pub count: usize, |
| 463 | pub gutter: Sp, |
| 464 | } |
| 465 | |
| 466 | impl 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)] |
| 484 | pub struct ColumnsNode { |
| 485 | pub list: Vec<Node>, |
| 486 | pub count: usize, |
| 487 | pub gutter: Sp, |
| 488 | } |
| 489 | |
| 490 | impl 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)] |
| 498 | pub 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 | |
| 519 | impl 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. |
| 559 | pub 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)] |
| 575 | pub struct StubMetrics { |
| 576 | pub em: Sp, // advance and body height of one character |
| 577 | pub depth: Sp, // descent below the baseline |
| 578 | } |
| 579 | |
| 580 | impl StubMetrics { |
| 581 | pub fn new(em: Sp, depth: Sp) -> Self { |
| 582 | Self { em, depth } |
| 583 | } |
| 584 | } |
| 585 | |
| 586 | impl 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 | } |