fleuron/style/properties/
page.rs1use serde::Serialize;
5
6use super::edges::{BorderStyle, Edges, MEDIUM};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
12pub struct ColumnRule {
13 pub style: BorderStyle,
15 pub width: f32,
17}
18
19impl ColumnRule {
20 pub const NONE: ColumnRule = ColumnRule {
22 style: BorderStyle::None,
23 width: MEDIUM,
24 };
25
26 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#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
43pub struct Columns {
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub count: Option<u32>,
47 #[serde(skip_serializing_if = "Option::is_none")]
50 pub width: Option<f32>,
51 pub gap: f32,
53 pub rule: ColumnRule,
55}
56
57impl Columns {
58 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 pub fn single(&self) -> bool {
73 self.count.is_none() && self.width.is_none()
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum AlignContent {
82 Start,
84 Center,
86 End,
88}
89
90impl AlignContent {
91 pub fn is_start(&self) -> bool {
94 *self == AlignContent::Start
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
101pub struct PageGeometry {
102 pub width: f32,
104 pub height: f32,
106 pub margin: Edges,
110 #[serde(skip_serializing_if = "Columns::single")]
112 pub columns: Columns,
113 #[serde(skip_serializing_if = "AlignContent::is_start")]
116 pub align_content: AlignContent,
117}
118
119impl PageGeometry {
120 pub fn content_origin(self) -> (f32, f32) {
122 (self.margin.left, self.margin.top)
123 }
124
125 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
173#[serde(rename_all = "kebab-case")]
174pub enum MarginBox {
175 TopLeftCorner,
177 TopLeft,
179 TopCenter,
181 TopRight,
183 TopRightCorner,
185 LeftTop,
187 LeftMiddle,
189 LeftBottom,
191 RightTop,
193 RightMiddle,
195 RightBottom,
197 BottomLeftCorner,
199 BottomLeft,
201 BottomCenter,
203 BottomRight,
205 BottomRightCorner,
207}
208
209impl MarginBox {
210 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum Band {
277 Top,
279 Bottom,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum Align {
288 Start,
290 Center,
292 End,
294}