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