oxedyne/fe2o3/fe2o3_austenite/src/page.rs
9.4 KiB, 74 runs
created by r1870400018:35671, 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 page and frame model. |
| 2 | //! |
| 3 | //! A page is a view onto a frame: the frame holds boxes placed at absolute positions, and the page |
| 4 | //! adds its geometry and its folio. The flat-memory property lives here. Pass A builds one frame, |
| 5 | //! hands the page to a writer, and drops it -- so the engine holds one window of frames plus the |
| 6 | //! ledger, never the document. |
| 7 | |
| 8 | use crate::font::ShapedText; |
| 9 | use crate::ir::{ |
| 10 | Dims, |
| 11 | Graphic, |
| 12 | Sp, |
| 13 | }; |
| 14 | |
| 15 | use std::sync::Arc; |
| 16 | |
| 17 | use oxedyne_fe2o3_core::prelude::*; |
| 18 | use oxedyne_fe2o3_geom::rect::AbsSize; |
| 19 | |
| 20 | /// A page's physical geometry: its trim size and four margins. A book binds along one edge, so the |
| 21 | /// inside (binding) and outside (fore-edge) margins differ, and the two alternate between recto and |
| 22 | /// verso -- a mirror. The driver lays every page at the recto split (`content_left` = inside); a verso |
| 23 | /// page is the same frame shifted by [`mirror_shift`](Self::mirror_shift), which is why the geometry |
| 24 | /// keeps both margins rather than one left edge. |
| 25 | #[derive(Clone, Copy, Debug)] |
| 26 | pub struct PageGeometry { |
| 27 | pub width: Sp, |
| 28 | pub height: Sp, |
| 29 | pub inside: Sp, // the binding-edge margin: the left on a recto, the right on a verso |
| 30 | pub outside: Sp, // the fore-edge margin, opposite the binding |
| 31 | pub top: Sp, |
| 32 | pub bottom: Sp, |
| 33 | } |
| 34 | |
| 35 | impl PageGeometry { |
| 36 | /// A uniform margin on all four sides -- the demos' geometry, and single-file `ingot`. |
| 37 | pub fn new(width: Sp, height: Sp, margin: Sp) -> Self { |
| 38 | Self { width, height, inside: margin, outside: margin, top: margin, bottom: margin } |
| 39 | } |
| 40 | |
| 41 | /// A book geometry with mirror margins: `inside` binds, `outside` is the fore-edge. |
| 42 | pub fn with_margins(width: Sp, height: Sp, inside: Sp, outside: Sp, top: Sp, bottom: Sp) -> Self { |
| 43 | Self { width, height, inside, outside, top, bottom } |
| 44 | } |
| 45 | |
| 46 | /// A4 portrait, 595.276 by 841.890 points, with a two-centimetre margin (56.9 points). |
| 47 | pub fn a4() -> Self { |
| 48 | Self::new(Sp::from_pt(595.276), Sp::from_pt(841.890), Sp::from_pt(56.9)) |
| 49 | } |
| 50 | |
| 51 | pub fn content_left(&self) -> Sp { self.inside } |
| 52 | |
| 53 | pub fn content_top(&self) -> Sp { self.top } |
| 54 | |
| 55 | /// The width available to a line of text: the trim less both side margins. |
| 56 | pub fn content_width(&self) -> Sp { self.width - self.inside - self.outside } |
| 57 | |
| 58 | /// The height available to a column of vertical material before the page is full. |
| 59 | pub fn content_height(&self) -> Sp { self.height - self.top - self.bottom } |
| 60 | |
| 61 | /// The geometry of the `i`-th of `n` equal columns within this page's content block, adjacent columns |
| 62 | /// parted by `gutter`. Its [`content_left`](Self::content_left) and [`content_width`](Self::content_width) |
| 63 | /// are that column's; every other measurement -- the trim, the vertical margins, and so the mirror shift |
| 64 | /// -- is the page's own unchanged, so a caller sets a column's material with the ordinary placement |
| 65 | /// helpers at a recto x, and the single verso mirror still applies once, to the whole frame, afterwards. |
| 66 | /// The column width is the content width less the `n - 1` gutters, divided `n` ways in the integer |
| 67 | /// domain; any one-scaled-point remainder from that division falls to the fore-edge margin, so every |
| 68 | /// column is the same width and the split stays byte-identical run to run. |
| 69 | pub fn column_slice(&self, i: usize, n: usize, gutter: Sp) -> PageGeometry { |
| 70 | let n = n.max(1); |
| 71 | let i = i.min(n - 1); |
| 72 | let inner = self.content_width() - gutter * (n as i32 - 1); // width left for the columns themselves |
| 73 | let col_w = Sp(inner.raw() / n as i32); |
| 74 | let col_left = self.content_left() + (col_w + gutter) * i as i32; |
| 75 | Self { |
| 76 | width: self.width, |
| 77 | height: self.height, |
| 78 | inside: col_left, |
| 79 | outside: self.width - col_left - col_w, |
| 80 | top: self.top, |
| 81 | bottom: self.bottom, |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /// The horizontal shift that turns the recto frame the driver laid into a verso one: the content |
| 86 | /// block moves from `inside` to `outside` on the left, so the binding margin stays at the spine. |
| 87 | /// Zero when the margins are uniform, so a non-book page never moves. |
| 88 | pub fn mirror_shift(&self) -> Sp { self.outside - self.inside } |
| 89 | |
| 90 | /// The page extent in whole device points, for an SVG viewport. A viewport extent is non-negative |
| 91 | /// device-space, which is what `fe2o3_geom`'s unsigned `Dim` models; rounding to whole points is |
| 92 | /// harmless here. |
| 93 | pub fn media_box(&self) -> AbsSize { |
| 94 | let w = self.width.to_pt().round() as usize; |
| 95 | let h = self.height.to_pt().round() as usize; |
| 96 | AbsSize::from((w, h)) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// What a placed box draws. A `Reserved` is a forward reference's held-open space, outlined faintly so |
| 101 | /// a proof shows where a value will land. |
| 102 | #[derive(Clone, Debug)] |
| 103 | pub enum PlacedKind { |
| 104 | Rule, |
| 105 | Reserved, |
| 106 | Text(ShapedText), |
| 107 | Graphic(Arc<Graphic>), // a figure's baked paths, drawn at this box's position |
| 108 | } |
| 109 | |
| 110 | /// Which of a page's three vertical regions a placed item belongs to. A float insertion reflows one region |
| 111 | /// by moving the items that belong to it, so membership -- not a y-coordinate window -- decides what moves. |
| 112 | /// A body line whose glyphs were raised above the body band's top edge by cap-height seating (see |
| 113 | /// `linebreak::raise_leaves`) still belongs to the body, and moves down with it when a top float is inserted |
| 114 | /// above; keying the shift on the raised glyph y instead would leave that line drawn under the float. |
| 115 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 116 | pub enum Region { |
| 117 | Top, // a top float's band, stacked from the page top down |
| 118 | #[default] |
| 119 | Body, // the flowing column between the two float bands |
| 120 | Foot, // a foot float's band (footnotes are laid here too, once the page closes and nothing more shifts) |
| 121 | } |
| 122 | |
| 123 | /// A box set at an absolute position on a page. The position is the top-left of the box; the |
| 124 | /// baseline sits `dims.height` below it. |
| 125 | #[derive(Clone, Debug)] |
| 126 | pub struct Placed { |
| 127 | pub x: Sp, |
| 128 | pub y: Sp, |
| 129 | pub dims: Dims, |
| 130 | pub kind: PlacedKind, |
| 131 | pub region: Region, // which page region it flows in; decides float-relayout membership |
| 132 | } |
| 133 | |
| 134 | impl Placed { |
| 135 | /// A placed item defaults to the body region. A float's own material is placed through the same |
| 136 | /// helpers and then reclaimed for its band with [`Frame::stamp_region`]. |
| 137 | pub fn new(x: Sp, y: Sp, dims: Dims, kind: PlacedKind) -> Self { |
| 138 | Self { x, y, dims, kind, region: Region::Body } |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | /// The placed material of one page. Built in Pass A, written, then dropped. |
| 143 | #[derive(Clone, Debug, Default)] |
| 144 | pub struct Frame { |
| 145 | pub placed: Vec<Placed>, |
| 146 | } |
| 147 | |
| 148 | impl Frame { |
| 149 | pub fn new() -> Self { |
| 150 | Self { placed: Vec::new() } |
| 151 | } |
| 152 | |
| 153 | pub fn push(&mut self, item: Placed) { |
| 154 | self.placed.push(item); |
| 155 | } |
| 156 | |
| 157 | pub fn is_empty(&self) -> bool { |
| 158 | self.placed.is_empty() |
| 159 | } |
| 160 | |
| 161 | pub fn len(&self) -> usize { |
| 162 | self.placed.len() |
| 163 | } |
| 164 | |
| 165 | /// Translates every placed item belonging to `region` by `by` (down for a positive `by`, up for a |
| 166 | /// negative one). This is how a float inserted into a part-filled page makes room without disturbing the |
| 167 | /// other regions: a top float shifts the body region down, a foot float shifts the existing foot region |
| 168 | /// up, and the bands not being reflowed stay put. It mirrors Typst's relayout, which re-flows the whole |
| 169 | /// region when a float is inserted. Membership, not a y window, is the test, so a body line raised above |
| 170 | /// the band edge by cap-height seating still moves with its body (see [`Region`]). |
| 171 | pub fn shift_region(&mut self, by: Sp, region: Region) { |
| 172 | for item in &mut self.placed { |
| 173 | if item.region == region { |
| 174 | item.y = item.y + by; |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | /// As [`Frame::shift_region`], for the items from index `from` on only: a column float shifts the material |
| 180 | /// of its own column -- everything placed since the column opened -- and not the columns beside it. |
| 181 | pub fn shift_region_from(&mut self, from: usize, by: Sp, region: Region) { |
| 182 | for item in self.placed.iter_mut().skip(from) { |
| 183 | if item.region == region { |
| 184 | item.y = item.y + by; |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /// Reclaims every item from index `from` to the end for `region`. A float's material is placed through |
| 190 | /// the ordinary helpers, which stamp it `Body`; the caller records the frame length before placing the |
| 191 | /// float and calls this after, so exactly the float's own items join its band and the body items already |
| 192 | /// on the page keep their membership. |
| 193 | pub fn stamp_region(&mut self, from: usize, region: Region) { |
| 194 | for item in &mut self.placed[from..] { |
| 195 | item.region = region; |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | /// A page: its one-based folio, its geometry, and its frame. |
| 201 | #[derive(Clone, Debug)] |
| 202 | pub struct Page { |
| 203 | pub number: u32, |
| 204 | pub geom: PageGeometry, |
| 205 | pub frame: Frame, |
| 206 | // The count of placed items that are body, recorded before `doc::decorate` appends the running head |
| 207 | // and folio. The page-emit memo keys on `placed[..body_len]` and draws the furniture beyond it fresh, |
| 208 | // so an unedited page reuses its body SVG while its folio still renders per page. `usize::MAX` means |
| 209 | // the whole frame is body (nothing decorated it), which is what every non-memo path leaves it at. |
| 210 | body_len: usize, |
| 211 | } |
| 212 | |
| 213 | impl Page { |
| 214 | pub fn new(number: u32, geom: PageGeometry, frame: Frame) -> Self { |
| 215 | Self { number, geom, frame, body_len: usize::MAX } |
| 216 | } |
| 217 | |
| 218 | /// Records the body/furniture split point: the placed count at the moment before decoration. Called |
| 219 | /// once, by the memo-threading compile path, just before `doc::decorate` stamps the furniture on. |
| 220 | pub fn set_body_len(&mut self, n: usize) { self.body_len = n; } |
| 221 | |
| 222 | /// The split point clamped to the current frame, so `placed[..body_len()]` is always in bounds even |
| 223 | /// after the verso mirror shift has moved items around (it never adds or removes any). |
| 224 | pub fn body_len(&self) -> usize { self.body_len.min(self.frame.placed.len()) } |
| 225 | } |