Skip to main content

fleuron/style/properties/
page.rs

1//! The page box: its trim, its margins, the columns it divides
2//! into, and the margin boxes around it.
3
4use serde::Serialize;
5
6use super::edges::{BorderStyle, Edges, MEDIUM};
7
8/// The rule painted down a page's gutters, from `column-rule-width`
9/// and `column-rule-style`. It takes no room: the gutter is the room
10/// it has.
11#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
12pub struct ColumnRule {
13    /// How the rule is drawn.
14    pub style: BorderStyle,
15    /// Thickness in points, whether or not it is drawn.
16    pub width: f32,
17}
18
19impl ColumnRule {
20    /// The initial rule: `medium` wide, and not drawn.
21    pub const NONE: ColumnRule = ColumnRule {
22        style: BorderStyle::None,
23        width: MEDIUM,
24    };
25
26    /// The thickness the rule paints at: nothing unless it is drawn.
27    pub fn used(self) -> f32 {
28        match self.style {
29            BorderStyle::None => 0.0,
30            BorderStyle::Solid => self.width.max(0.0),
31        }
32    }
33}
34
35/// How a page's content box divides, from `column-count`,
36/// `column-width` and `column-gap`.
37///
38/// Both `column-count` and `column-width` are what the author asked
39/// for rather than what the page does: a content box only so wide
40/// takes only so many columns of a given width, and
41/// [`PageGeometry::column_count`] is where the two meet.
42#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
43pub struct Columns {
44    /// `column-count`, or `None` for `auto`.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub count: Option<u32>,
47    /// `column-width`: the width a column is asked to have, or
48    /// `None` for `auto`.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub width: Option<f32>,
51    /// `column-gap`: the gutter between two columns.
52    pub gap: f32,
53    /// What is painted down each gutter.
54    pub rule: ColumnRule,
55}
56
57impl Columns {
58    /// One column, the whole content box: what a page that declares
59    /// no column property divides into.
60    pub const fn undivided(gap: f32) -> Columns {
61        Columns {
62            count: None,
63            width: None,
64            gap,
65            rule: ColumnRule::NONE,
66        }
67    }
68
69    /// Whether the page asked for no division at all. A page box
70    /// that divides into nothing serializes without a `columns`
71    /// field.
72    pub fn single(&self) -> bool {
73        self.count.is_none() && self.width.is_none()
74    }
75}
76
77/// Where a page's content sits down its content box, from
78/// `align-content` on `@page`.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum AlignContent {
82    /// `start`: against the top of the content box.
83    Start,
84    /// `center`: the same distance from the top and the bottom.
85    Center,
86    /// `end`: against the bottom of the content box.
87    End,
88}
89
90impl AlignContent {
91    /// Whether the content sits at the top, which is where it sits
92    /// when a page names nothing.
93    pub fn is_start(&self) -> bool {
94        *self == AlignContent::Start
95    }
96}
97
98/// Page trim, margins and columns, in points: the resolved `@page`
99/// box.
100#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
101pub struct PageGeometry {
102    /// Trimmed page width.
103    pub width: f32,
104    /// Trimmed page height.
105    pub height: f32,
106    /// Margins, resolved from the page's own `margin`. Mirroring
107    /// across the spread is `@page :left` and `@page :right` saying
108    /// different things, not a property of its own.
109    pub margin: Edges,
110    /// How the content box divides.
111    #[serde(skip_serializing_if = "Columns::single")]
112    pub columns: Columns,
113    /// Where the content of a page that ends short of the foot of its
114    /// content box sits.
115    #[serde(skip_serializing_if = "AlignContent::is_start")]
116    pub align_content: AlignContent,
117}
118
119impl PageGeometry {
120    /// Origin (top-left) of the content box, in page coordinates.
121    pub fn content_origin(self) -> (f32, f32) {
122        (self.margin.left, self.margin.top)
123    }
124
125    /// Size of the content box.
126    pub fn content_size(self) -> (f32, f32) {
127        (
128            self.width - self.margin.left - self.margin.right,
129            self.height - self.margin.top - self.margin.bottom,
130        )
131    }
132
133    /// How many columns the content box divides into.
134    ///
135    /// A declared `column-width` is a preference: as many columns of
136    /// that width as fit, and where `column-count` was declared too
137    /// it is the ceiling on that. A box too narrow for one column of
138    /// that width still divides into one.
139    pub fn column_count(self) -> u32 {
140        let available = self.content_size().0;
141        let fitting = self.columns.width.map(|width| {
142            let pitch = width.max(0.0) + self.columns.gap;
143            if pitch <= 0.0 {
144                return 1;
145            }
146            (((available + self.columns.gap) / pitch).floor() as i32).max(1) as u32
147        });
148        match (self.columns.count, fitting) {
149            (Some(count), Some(fitting)) => count.max(1).min(fitting),
150            (Some(count), None) => count.max(1),
151            (None, Some(fitting)) => fitting,
152            (None, None) => 1,
153        }
154    }
155
156    /// The measure line layout breaks to: one column's width, which
157    /// on an undivided page is the content box's own.
158    pub fn measure(self) -> f32 {
159        let count = self.column_count() as f32;
160        let gutters = self.columns.gap * (count - 1.0);
161        ((self.content_size().0 - gutters) / count).max(0.0)
162    }
163
164    /// Origin (top-left) of one column, in page coordinates.
165    pub fn column_origin(self, index: u32) -> (f32, f32) {
166        let (x, y) = self.content_origin();
167        (x + index as f32 * (self.measure() + self.columns.gap), y)
168    }
169}
170
171/// A page margin box, named as CSS names them.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
173#[serde(rename_all = "kebab-case")]
174pub enum MarginBox {
175    /// `@top-left-corner`
176    TopLeftCorner,
177    /// `@top-left`
178    TopLeft,
179    /// `@top-center`
180    TopCenter,
181    /// `@top-right`
182    TopRight,
183    /// `@top-right-corner`
184    TopRightCorner,
185    /// `@left-top`
186    LeftTop,
187    /// `@left-middle`
188    LeftMiddle,
189    /// `@left-bottom`
190    LeftBottom,
191    /// `@right-top`
192    RightTop,
193    /// `@right-middle`
194    RightMiddle,
195    /// `@right-bottom`
196    RightBottom,
197    /// `@bottom-left-corner`
198    BottomLeftCorner,
199    /// `@bottom-left`
200    BottomLeft,
201    /// `@bottom-center`
202    BottomCenter,
203    /// `@bottom-right`
204    BottomRight,
205    /// `@bottom-right-corner`
206    BottomRightCorner,
207}
208
209impl MarginBox {
210    /// The at-rule name, without the `@`.
211    pub fn keyword(self) -> &'static str {
212        match self {
213            MarginBox::TopLeftCorner => "top-left-corner",
214            MarginBox::TopLeft => "top-left",
215            MarginBox::TopCenter => "top-center",
216            MarginBox::TopRight => "top-right",
217            MarginBox::TopRightCorner => "top-right-corner",
218            MarginBox::LeftTop => "left-top",
219            MarginBox::LeftMiddle => "left-middle",
220            MarginBox::LeftBottom => "left-bottom",
221            MarginBox::RightTop => "right-top",
222            MarginBox::RightMiddle => "right-middle",
223            MarginBox::RightBottom => "right-bottom",
224            MarginBox::BottomLeftCorner => "bottom-left-corner",
225            MarginBox::BottomLeft => "bottom-left",
226            MarginBox::BottomCenter => "bottom-center",
227            MarginBox::BottomRight => "bottom-right",
228            MarginBox::BottomRightCorner => "bottom-right-corner",
229        }
230    }
231
232    /// Parses an at-rule name inside `@page`.
233    pub fn parse(keyword: &str) -> Option<MarginBox> {
234        Self::ALL
235            .into_iter()
236            .find(|candidate| candidate.keyword().eq_ignore_ascii_case(keyword))
237    }
238
239    /// Every margin box CSS defines.
240    pub const ALL: [MarginBox; 16] = [
241        MarginBox::TopLeftCorner,
242        MarginBox::TopLeft,
243        MarginBox::TopCenter,
244        MarginBox::TopRight,
245        MarginBox::TopRightCorner,
246        MarginBox::LeftTop,
247        MarginBox::LeftMiddle,
248        MarginBox::LeftBottom,
249        MarginBox::RightTop,
250        MarginBox::RightMiddle,
251        MarginBox::RightBottom,
252        MarginBox::BottomLeftCorner,
253        MarginBox::BottomLeft,
254        MarginBox::BottomCenter,
255        MarginBox::BottomRight,
256        MarginBox::BottomRightCorner,
257    ];
258
259    /// The margin the box sits in, and where in it: `None` for the
260    /// boxes the engine parses but does not paint.
261    pub fn band(self) -> Option<(Band, Align)> {
262        match self {
263            MarginBox::TopLeft => Some((Band::Top, Align::Start)),
264            MarginBox::TopCenter => Some((Band::Top, Align::Center)),
265            MarginBox::TopRight => Some((Band::Top, Align::End)),
266            MarginBox::BottomLeft => Some((Band::Bottom, Align::Start)),
267            MarginBox::BottomCenter => Some((Band::Bottom, Align::Center)),
268            MarginBox::BottomRight => Some((Band::Bottom, Align::End)),
269            _ => None,
270        }
271    }
272}
273
274/// Which margin a painted margin box lives in.
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum Band {
277    /// The top margin: running heads.
278    Top,
279    /// The bottom margin: folios and running feet.
280    Bottom,
281}
282
283/// Where in its band a margin box's content sits. `Center` centres on
284/// the trim rather than on the content box: a folio belongs on the
285/// page's axis, and mirrored margins put the content box off it.
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum Align {
288    /// The content box's leading edge.
289    Start,
290    /// The trim's axis.
291    Center,
292    /// The content box's trailing edge.
293    End,
294}