Skip to main content

iced_markdown_editor/
page.rs

1//! Paper setup: page size, orientation and margins.
2//!
3//! A page size is either an ISO 216 / 269 format (`A4`, `B5`, `C5`, ...),
4//! a North American format (`Letter`, `Legal`, `Tabloid`), a custom size
5//! given as [`Measure`]s, or [`PageSize::Fill`]: no paper, the editor fills
6//! the parent container.
7//!
8//! ```
9//! use iced_markdown_editor::{Margins, Measure, Paper, PageSize};
10//!
11//! let a4 = Paper::default();                                  // A4 portrait, 2.5 cm margins
12//! let a5 = Paper::new(PageSize::A5).landscape();
13//! let custom = Paper::new(PageSize::custom(Measure::mm(180.0), Measure::mm(240.0)))
14//!     .with_margins(Margins::all(Measure::cm(1.5)));
15//! let full = Paper::fill();
16//! let parsed: PageSize = "A4".parse().unwrap();
17//! let parsed: PageSize = "21cm x 29.7cm".parse().unwrap();
18//! ```
19
20use std::fmt;
21use std::str::FromStr;
22
23/// Logical pixels per inch (the CSS / Windows reference resolution).
24pub const DPI: f32 = 96.0;
25
26/// Unit of a [`Measure`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Unit {
29    Millimeter,
30    Centimeter,
31    Inch,
32    /// Typographic point (1/72 inch).
33    Point,
34    /// Logical pixel (1/96 inch).
35    Pixel,
36}
37
38impl Unit {
39    pub fn symbol(self) -> &'static str {
40        match self {
41            Unit::Millimeter => "mm",
42            Unit::Centimeter => "cm",
43            Unit::Inch => "in",
44            Unit::Point => "pt",
45            Unit::Pixel => "px",
46        }
47    }
48}
49
50/// A physical length.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Measure {
53    pub value: f32,
54    pub unit: Unit,
55}
56
57impl Measure {
58    pub const fn new(value: f32, unit: Unit) -> Self {
59        Self { value, unit }
60    }
61
62    pub const fn mm(value: f32) -> Self {
63        Self::new(value, Unit::Millimeter)
64    }
65
66    pub const fn cm(value: f32) -> Self {
67        Self::new(value, Unit::Centimeter)
68    }
69
70    pub const fn inches(value: f32) -> Self {
71        Self::new(value, Unit::Inch)
72    }
73
74    pub const fn pt(value: f32) -> Self {
75        Self::new(value, Unit::Point)
76    }
77
78    pub const fn px(value: f32) -> Self {
79        Self::new(value, Unit::Pixel)
80    }
81
82    /// The length in logical pixels at 100 % zoom.
83    pub fn to_px(self) -> f32 {
84        match self.unit {
85            Unit::Millimeter => self.value / 25.4 * DPI,
86            Unit::Centimeter => self.value / 2.54 * DPI,
87            Unit::Inch => self.value * DPI,
88            Unit::Point => self.value / 72.0 * DPI,
89            Unit::Pixel => self.value,
90        }
91    }
92
93    /// The length in millimetres.
94    pub fn to_mm(self) -> f32 {
95        self.to_px() / DPI * 25.4
96    }
97}
98
99impl fmt::Display for Measure {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "{}{}", self.value, self.unit.symbol())
102    }
103}
104
105/// Error returned when parsing a [`Measure`] or [`PageSize`] fails.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ParseError(pub String);
108
109impl fmt::Display for ParseError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(&self.0)
112    }
113}
114
115impl std::error::Error for ParseError {}
116
117fn split_number(text: &str) -> (&str, &str) {
118    let end = text
119        .char_indices()
120        .find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == ','))
121        .map(|(i, _)| i)
122        .unwrap_or(text.len());
123    (&text[..end], text[end..].trim())
124}
125
126fn parse_unit(text: &str) -> Option<Unit> {
127    match text.to_ascii_lowercase().as_str() {
128        "mm" => Some(Unit::Millimeter),
129        "cm" => Some(Unit::Centimeter),
130        "in" | "inch" | "inches" | "\"" => Some(Unit::Inch),
131        "pt" => Some(Unit::Point),
132        "px" => Some(Unit::Pixel),
133        _ => None,
134    }
135}
136
137impl FromStr for Measure {
138    type Err = ParseError;
139
140    /// Parses `"25mm"`, `"2.5 cm"`, `"1in"`, `"72pt"`, `"96px"`.
141    fn from_str(text: &str) -> Result<Self, Self::Err> {
142        let text = text.trim();
143        let (number, unit) = split_number(text);
144        let value: f32 = number
145            .replace(',', ".")
146            .parse()
147            .map_err(|_| ParseError(format!("invalid number in `{text}`")))?;
148        let unit = parse_unit(unit)
149            .ok_or_else(|| ParseError(format!("missing or unknown unit in `{text}`")))?;
150        Ok(Self::new(value, unit))
151    }
152}
153
154/// ISO 216 / 269 series.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156pub enum IsoSeries {
157    A,
158    B,
159    C,
160}
161
162const ISO_A: [(f32, f32); 11] = [
163    (841.0, 1189.0),
164    (594.0, 841.0),
165    (420.0, 594.0),
166    (297.0, 420.0),
167    (210.0, 297.0),
168    (148.0, 210.0),
169    (105.0, 148.0),
170    (74.0, 105.0),
171    (52.0, 74.0),
172    (37.0, 52.0),
173    (26.0, 37.0),
174];
175
176const ISO_B: [(f32, f32); 11] = [
177    (1000.0, 1414.0),
178    (707.0, 1000.0),
179    (500.0, 707.0),
180    (353.0, 500.0),
181    (250.0, 353.0),
182    (176.0, 250.0),
183    (125.0, 176.0),
184    (88.0, 125.0),
185    (62.0, 88.0),
186    (44.0, 62.0),
187    (31.0, 44.0),
188];
189
190const ISO_C: [(f32, f32); 11] = [
191    (917.0, 1297.0),
192    (648.0, 917.0),
193    (458.0, 648.0),
194    (324.0, 458.0),
195    (229.0, 324.0),
196    (162.0, 229.0),
197    (114.0, 162.0),
198    (81.0, 114.0),
199    (57.0, 81.0),
200    (40.0, 57.0),
201    (28.0, 40.0),
202];
203
204/// Page size.
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub enum PageSize {
207    /// ISO format, e.g. `Iso(IsoSeries::A, 4)` for A4. Numbers 0-10.
208    Iso(IsoSeries, u8),
209    /// US Letter, 8.5 × 11 in.
210    Letter,
211    /// US Legal, 8.5 × 14 in.
212    Legal,
213    /// Tabloid, 11 × 17 in.
214    Tabloid,
215    /// Any size (use [`Orientation`] to decide which side is longer).
216    Custom { width: Measure, height: Measure },
217    /// No paper: the editor fills 100 % of the parent container's width and
218    /// height. Paper margins are ignored; no sheet, no page breaks.
219    Fill,
220}
221
222impl PageSize {
223    pub const A3: PageSize = PageSize::Iso(IsoSeries::A, 3);
224    pub const A4: PageSize = PageSize::Iso(IsoSeries::A, 4);
225    pub const A5: PageSize = PageSize::Iso(IsoSeries::A, 5);
226    pub const A6: PageSize = PageSize::Iso(IsoSeries::A, 6);
227    pub const B4: PageSize = PageSize::Iso(IsoSeries::B, 4);
228    pub const B5: PageSize = PageSize::Iso(IsoSeries::B, 5);
229    pub const C5: PageSize = PageSize::Iso(IsoSeries::C, 5);
230
231    /// ISO format; the number is clamped to 0-10.
232    pub fn iso(series: IsoSeries, number: u8) -> Self {
233        PageSize::Iso(series, number.min(10))
234    }
235
236    pub fn custom(width: Measure, height: Measure) -> Self {
237        PageSize::Custom { width, height }
238    }
239
240    /// Dimensions as given, or `None` for [`PageSize::Fill`].
241    pub fn dimensions(&self) -> Option<(Measure, Measure)> {
242        match *self {
243            PageSize::Iso(series, number) => {
244                let table = match series {
245                    IsoSeries::A => &ISO_A,
246                    IsoSeries::B => &ISO_B,
247                    IsoSeries::C => &ISO_C,
248                };
249                let (w, h) = table[number.min(10) as usize];
250                Some((Measure::mm(w), Measure::mm(h)))
251            }
252            PageSize::Letter => Some((Measure::inches(8.5), Measure::inches(11.0))),
253            PageSize::Legal => Some((Measure::inches(8.5), Measure::inches(14.0))),
254            PageSize::Tabloid => Some((Measure::inches(11.0), Measure::inches(17.0))),
255            PageSize::Custom { width, height } => Some((width, height)),
256            PageSize::Fill => None,
257        }
258    }
259}
260
261impl fmt::Display for PageSize {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        match self {
264            PageSize::Iso(series, number) => write!(f, "{series:?}{number}"),
265            PageSize::Letter => f.write_str("Letter"),
266            PageSize::Legal => f.write_str("Legal"),
267            PageSize::Tabloid => f.write_str("Tabloid"),
268            PageSize::Custom { width, height } => write!(f, "{width} x {height}"),
269            PageSize::Fill => f.write_str("Fill"),
270        }
271    }
272}
273
274impl FromStr for PageSize {
275    type Err = ParseError;
276
277    /// Parses `"A4"`, `"b5"`, `"Letter"`, `"legal"`, `"tabloid"`,
278    /// `"fill"` / `"full"`, or a custom size like `"210mm x 297mm"`,
279    /// `"21 x 29.7 cm"`, `"8.5in x 11in"`.
280    fn from_str(text: &str) -> Result<Self, Self::Err> {
281        let lower = text.trim().to_ascii_lowercase();
282
283        match lower.as_str() {
284            "letter" => return Ok(PageSize::Letter),
285            "legal" => return Ok(PageSize::Legal),
286            "tabloid" | "ledger" => return Ok(PageSize::Tabloid),
287            "fill" | "full" | "fullscreen" | "full screen" | "full width" => {
288                return Ok(PageSize::Fill);
289            }
290            _ => {}
291        }
292
293        let mut chars = lower.chars();
294        if let Some(first) = chars.next() {
295            let series = match first {
296                'a' => Some(IsoSeries::A),
297                'b' => Some(IsoSeries::B),
298                'c' => Some(IsoSeries::C),
299                _ => None,
300            };
301            if let Some(series) = series {
302                if let Ok(number) = chars.as_str().parse::<u8>() {
303                    if number <= 10 {
304                        return Ok(PageSize::Iso(series, number));
305                    }
306                }
307            }
308        }
309
310        let parts: Vec<&str> = lower.split(['x', '×', '*']).map(str::trim).collect();
311        if parts.len() == 2 {
312            let height: Measure = parts[1].parse()?;
313            let width = match parts[0].parse::<Measure>() {
314                Ok(width) => width,
315                Err(_) => {
316                    let value: f32 = parts[0]
317                        .replace(',', ".")
318                        .parse()
319                        .map_err(|_| ParseError(format!("invalid width in `{text}`")))?;
320                    Measure::new(value, height.unit)
321                }
322            };
323            return Ok(PageSize::Custom { width, height });
324        }
325
326        Err(ParseError(format!("unknown page size `{text}`")))
327    }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
331pub enum Orientation {
332    #[default]
333    Portrait,
334    Landscape,
335}
336
337/// Page margins.
338#[derive(Debug, Clone, Copy, PartialEq)]
339pub struct Margins {
340    pub top: Measure,
341    pub right: Measure,
342    pub bottom: Measure,
343    pub left: Measure,
344}
345
346impl Margins {
347    pub fn new(top: Measure, right: Measure, bottom: Measure, left: Measure) -> Self {
348        Self {
349            top,
350            right,
351            bottom,
352            left,
353        }
354    }
355
356    pub fn all(margin: Measure) -> Self {
357        Self::new(margin, margin, margin, margin)
358    }
359
360    pub fn symmetric(vertical: Measure, horizontal: Measure) -> Self {
361        Self::new(vertical, horizontal, vertical, horizontal)
362    }
363}
364
365impl Default for Margins {
366    /// 2.5 cm on every side.
367    fn default() -> Self {
368        Self::all(Measure::cm(2.5))
369    }
370}
371
372/// Complete page setup.
373#[derive(Debug, Clone, Copy, PartialEq)]
374pub struct Paper {
375    pub size: PageSize,
376    pub orientation: Orientation,
377    pub margins: Margins,
378}
379
380impl Default for Paper {
381    /// A4 portrait with 2.5 cm margins.
382    fn default() -> Self {
383        Self::new(PageSize::A4)
384    }
385}
386
387impl Paper {
388    pub fn new(size: PageSize) -> Self {
389        Self {
390            size,
391            orientation: Orientation::Portrait,
392            margins: Margins::default(),
393        }
394    }
395
396    pub fn a4() -> Self {
397        Self::new(PageSize::A4)
398    }
399
400    pub fn letter() -> Self {
401        Self::new(PageSize::Letter).with_margins(Margins::all(Measure::inches(1.0)))
402    }
403
404    /// No paper: 100 % width and height of the parent container.
405    pub fn fill() -> Self {
406        Self::new(PageSize::Fill)
407    }
408
409    pub fn with_size(mut self, size: PageSize) -> Self {
410        self.size = size;
411        self
412    }
413
414    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
415        self.orientation = orientation;
416        self
417    }
418
419    pub fn landscape(self) -> Self {
420        self.with_orientation(Orientation::Landscape)
421    }
422
423    pub fn portrait(self) -> Self {
424        self.with_orientation(Orientation::Portrait)
425    }
426
427    pub fn with_margins(mut self, margins: Margins) -> Self {
428        self.margins = margins;
429        self
430    }
431
432    pub fn is_fill(&self) -> bool {
433        self.size == PageSize::Fill
434    }
435
436    /// Page size in logical pixels at 100 % zoom, orientation applied.
437    /// `None` for [`PageSize::Fill`].
438    pub fn size_px(&self) -> Option<(f32, f32)> {
439        let (w, h) = self.size.dimensions()?;
440        let (w, h) = (w.to_px(), h.to_px());
441        let (short, long) = (w.min(h), w.max(h));
442        Some(match self.orientation {
443            Orientation::Portrait => (short, long),
444            Orientation::Landscape => (long, short),
445        })
446    }
447}