Oregami
Repositories/oxedyne/fe2o3

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
8use crate::font::ShapedText;
9use crate::ir::{
10 Dims,
11 Graphic,
12 Sp,
13};
14
15use std::sync::Arc;
16
17use oxedyne_fe2o3_core::prelude::*;
18use 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)]
26pub 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
35impl 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)]
103pub 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)]
116pub 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)]
126pub 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
134impl 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)]
144pub struct Frame {
145 pub placed: Vec<Placed>,
146}
147
148impl 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)]
202pub 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
213impl 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}