Skip to main content

alpha2_layout/
lib.rs

1//! Where alpha2 text and layout boxes lie, without a GPU.
2//!
3//! A text style or a layout generator's box tree becomes the requests the
4//! text host (`alpha2-text`) draws and the rectangles they fill: text by its
5//! style's position and anchor, a layout by CSS flexbox and grid. The engine
6//! draws from these same requests and rectangles, so a measurement and a
7//! frame place text alike, and a host without a GPU (a wasm page, the Python
8//! package) measures what a render would draw. [`json`] writes the
9//! measurements in the one JSON form every binding returns.
10
11pub mod json;
12pub mod layout;
13mod text;
14
15use std::fmt;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use alpha2_text::{TextFont, TextRasterError};
20
21pub use layout::{
22    LAYOUT_WRAP_SLACK, LayoutBounds, NodeBounds, NodeRects, ResolvedLayout, layout_texts,
23    measure_layout, resolve, shape_texts,
24};
25pub use text::{
26    MAX_FONT_PIXELS, MAX_OUTLINE_PIXELS, MAX_PADDING_PIXELS, TextBounds, layout_text_key,
27    measure_text, rgba8, shaped_key, text_key, text_request,
28};
29
30/// The size, in pixels, of a picture text and layouts are placed on: never
31/// empty, and within the largest frame the engine draws
32/// ([`alpha2_media::MAX_FRAME_DIMENSION`], [`alpha2_media::MAX_FRAME_PIXELS`]).
33#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
34pub struct PictureSize {
35    width: u32,
36    height: u32,
37}
38
39/// Why a size is not a [`PictureSize`].
40#[derive(Copy, Clone, Debug, Eq, PartialEq)]
41#[non_exhaustive]
42pub enum PictureSizeError {
43    /// A side is zero.
44    Empty,
45    /// A side, or the area, is larger than any frame the engine draws.
46    TooLarge,
47}
48
49impl fmt::Display for PictureSizeError {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter.write_str(match self {
52            Self::Empty => "picture dimensions must be non-zero",
53            Self::TooLarge => "picture dimensions exceed the largest frame",
54        })
55    }
56}
57
58impl std::error::Error for PictureSizeError {}
59
60impl PictureSize {
61    /// A picture of `width` by `height` pixels.
62    ///
63    /// # Errors
64    /// Returns [`PictureSizeError`] for an empty side or a size larger than
65    /// any frame.
66    pub const fn new(width: u32, height: u32) -> Result<Self, PictureSizeError> {
67        if width == 0 || height == 0 {
68            Err(PictureSizeError::Empty)
69        } else if width > alpha2_media::MAX_FRAME_DIMENSION
70            || height > alpha2_media::MAX_FRAME_DIMENSION
71            || (width as u64) * (height as u64) > alpha2_media::MAX_FRAME_PIXELS
72        {
73            Err(PictureSizeError::TooLarge)
74        } else {
75            Ok(Self { width, height })
76        }
77    }
78
79    /// Width in pixels.
80    #[must_use]
81    pub const fn width(self) -> u32 {
82        self.width
83    }
84
85    /// Height in pixels.
86    #[must_use]
87    pub const fn height(self) -> u32 {
88        self.height
89    }
90
91    /// The shorter side in pixels: what lengths, a font size among them, are
92    /// fractions of.
93    #[must_use]
94    #[allow(clippy::cast_precision_loss)]
95    pub fn unit(self) -> f32 {
96        self.width.min(self.height) as f32
97    }
98}
99
100/// A rectangle as fractions of the canvas, y down.
101#[derive(Copy, Clone, Debug, PartialEq)]
102pub struct CanvasRect {
103    /// Left edge, a fraction of the canvas width.
104    pub left: f32,
105    /// Top edge, a fraction of the canvas height.
106    pub top: f32,
107    /// Right edge, a fraction of the canvas width.
108    pub right: f32,
109    /// Bottom edge, a fraction of the canvas height.
110    pub bottom: f32,
111}
112
113/// Where a text's first and last lines' alphabetic baselines lie, y as a
114/// fraction of the canvas (or picture) height, as a [`CanvasRect`]'s top is:
115/// a text's baseline lines up with another's, or with a point, by
116/// arithmetic.
117#[derive(Copy, Clone, Debug, PartialEq)]
118pub struct Baselines {
119    /// The first line's baseline.
120    pub first: f32,
121    /// The last line's baseline: the first's for one line.
122    pub last: f32,
123}
124
125/// Why a font file could not be registered ([`fonts_from_files`]).
126#[derive(Debug)]
127#[non_exhaustive]
128pub enum FontFileError {
129    /// The file could not be read.
130    Read {
131        /// The file.
132        path: PathBuf,
133        /// Why.
134        error: std::io::Error,
135    },
136    /// The file holds no face the text host draws at `face_index`.
137    Invalid {
138        /// The file.
139        path: PathBuf,
140        /// The face, counted from 0 within a collection.
141        face_index: u32,
142        /// The text host's reason.
143        error: TextRasterError,
144    },
145}
146
147impl fmt::Display for FontFileError {
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Read { path, error } => write!(formatter, "font {}: {error}", path.display()),
151            Self::Invalid {
152                path,
153                face_index,
154                error,
155            } => write!(
156                formatter,
157                "font {} face {face_index}: {error}",
158                path.display()
159            ),
160        }
161    }
162}
163
164impl std::error::Error for FontFileError {
165    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166        match self {
167            Self::Read { error, .. } => Some(error),
168            Self::Invalid { error, .. } => Some(error),
169        }
170    }
171}
172
173/// The faces a host registers from a list of font files, and each file's
174/// bytes ([`fonts_from_files`]).
175#[derive(Clone, Debug)]
176pub struct FontFiles {
177    /// Every face, in fallback order, sharing its file's bytes.
178    pub faces: Vec<TextFont>,
179    /// Each file's bytes, read once, in the order given.
180    pub bytes: Vec<Arc<[u8]>>,
181}
182
183/// The fonts of the files at `paths`, in fallback order, as a host registers
184/// them, and each file's bytes, read once: each file's family is its file
185/// stem, and every face of a collection is registered, in its order, sharing
186/// the file's bytes, so its weights can be matched. A measurement and a
187/// render given the same paths draw with the same faces.
188///
189/// # Errors
190/// Returns [`FontFileError`] naming the first file that cannot be read or
191/// holds no face the text host draws.
192pub fn fonts_from_files(paths: &[impl AsRef<Path>]) -> Result<FontFiles, FontFileError> {
193    let mut fonts = Vec::with_capacity(paths.len());
194    let mut files = Vec::with_capacity(paths.len());
195    for path in paths {
196        let path = path.as_ref();
197        let bytes: Arc<[u8]> = std::fs::read(path)
198            .map_err(|error| FontFileError::Read {
199                path: path.to_owned(),
200                error,
201            })?
202            .into();
203        let family = path
204            .file_stem()
205            .map_or_else(String::new, |stem| stem.to_string_lossy().into_owned());
206        for face_index in 0..alpha2_text::face_count(&bytes) {
207            alpha2_text::check_font(&bytes, face_index).map_err(|error| {
208                FontFileError::Invalid {
209                    path: path.to_owned(),
210                    face_index,
211                    error,
212                }
213            })?;
214            fonts.push(TextFont {
215                family: family.clone(),
216                bytes: Arc::clone(&bytes),
217                face_index,
218            });
219        }
220        files.push(bytes);
221    }
222    Ok(FontFiles {
223        faces: fonts,
224        bytes: files,
225    })
226}
227
228#[cfg(test)]
229mod tests;