use crate::{
backend::{Backend, BackendBuilder, RasterFont},
core::{
AtlasIndex, IGlyphOffset, IGlyphRegion, Sequence, Token, UGlyphRegion, UGlyphSize, Unique,
},
meta::{CustomGlyph, FontLayout, FontTrack, GlyphOverride, PackingMode},
};
pub mod prelude {
pub use super::{FontAtlasBuilder, GlyphSheet, errors::FontBuilderError};
}
#[cfg(feature = "bevy")]
use bevy_platform::collections::HashMap;
#[cfg(not(feature = "bevy"))]
use std::collections::HashMap;
use std::{error::Error, marker::PhantomData};
#[derive(Clone, Debug)]
pub struct UTokenProps {
pub region: UGlyphRegion,
pub offset: IGlyphOffset,
}
#[derive(Clone, Debug)]
pub struct ITokenProps {
pub region: IGlyphRegion,
pub offset: IGlyphOffset,
}
impl FontTrack {
pub const fn as_token_props(&self) -> ITokenProps {
ITokenProps {
region: self.glyph_region,
offset: self.offset,
}
}
pub const fn props<'f>(&'f self, overrides: Option<&'f GlyphOverride>) -> ITokenProps {
if let Some(GlyphOverride { offset, region }) = overrides {
let offset = match offset.as_ref() {
Some(&offset) => offset,
None => self.offset,
};
let region = match region.as_ref() {
Some(®ion) => region,
None => self.glyph_region,
};
ITokenProps { offset, region }
} else {
self.as_token_props()
}
}
}
pub struct Unpopulated;
pub struct Populated;
mod _marker {
use super::*;
pub trait Marker {}
impl Marker for Unpopulated {}
impl Marker for Populated {}
pub trait ImageMarker {}
impl ImageMarker for PhantomData<Unpopulated> {}
impl<T> ImageMarker for GlyphSheet<T> {}
}
use _marker::{ImageMarker, Marker};
#[derive(Clone, Debug)]
pub struct FontAtlasBuilder<
LayoutPopulated: Marker = Unpopulated,
CustomPopulated: Marker = Unpopulated,
Sheet: ImageMarker = PhantomData<Unpopulated>,
Named: Marker = Unpopulated,
> {
name: Option<String>,
image: Sheet,
scratch: Scratch,
_state: PhantomData<(LayoutPopulated, CustomPopulated, Named)>,
}
#[derive(Clone, Default, Debug)]
pub(crate) struct Scratch {
glyphs: Vec<UTokenProps>,
sequence_map: HashMap<Sequence, AtlasIndex>,
}
#[derive(Clone, Debug)]
pub struct GlyphSheet<Image> {
pub image: Image,
pub size: UGlyphSize,
}
impl Default for FontAtlasBuilder {
fn default() -> Self {
Self {
name: None,
image: PhantomData,
scratch: Scratch::default(),
_state: PhantomData,
}
}
}
impl<A: Marker, B: Marker, C: ImageMarker> FontAtlasBuilder<A, B, C, Unpopulated> {
#[inline]
pub fn with_name(self, name: Option<String>) -> FontAtlasBuilder<A, B, C, Populated> {
FontAtlasBuilder {
name,
image: self.image,
scratch: self.scratch,
_state: PhantomData,
}
}
}
impl<A: Marker, B: Marker, C: ImageMarker, Named: Marker> FontAtlasBuilder<A, B, C, Named> {
#[doc(hidden)]
#[inline]
fn rebrand<X: Marker, Y: Marker>(self) -> FontAtlasBuilder<X, Y, C, Named> {
FontAtlasBuilder {
name: self.name,
image: self.image,
scratch: self.scratch,
_state: PhantomData,
}
}
pub fn add_token(&mut self, token: &Token, props: UTokenProps) {
let index = AtlasIndex(self.scratch.glyphs.len());
self.scratch.glyphs.push(props);
self.scratch
.sequence_map
.extend(token.iter().cloned().zip(std::iter::repeat(index)));
}
pub fn get_glyph_by_sequence(&self, token: &Sequence) -> Option<&UTokenProps> {
let index = self.scratch.sequence_map.get(token)?;
self.scratch.glyphs.get(index.0)
}
}
impl FontAtlasBuilder {
#[inline]
pub fn with_capacity(num_unique_glyphs: usize, num_unique_sequences: usize) -> Self {
Self {
name: None,
scratch: Scratch {
glyphs: Vec::with_capacity(num_unique_glyphs),
sequence_map: HashMap::with_capacity(num_unique_sequences),
},
_state: PhantomData,
image: PhantomData,
}
}
}
impl<Named: Marker> FontAtlasBuilder<Unpopulated, Unpopulated, PhantomData<Unpopulated>, Named> {
#[inline]
pub fn with_image<Image>(
self,
image: GlyphSheet<Image>,
) -> FontAtlasBuilder<Unpopulated, Unpopulated, GlyphSheet<Image>, Named>
where
GlyphSheet<Image>: ImageMarker,
{
FontAtlasBuilder {
name: self.name,
image,
scratch: self.scratch,
_state: PhantomData,
}
}
}
impl From<Unique<'_>> for FontAtlasBuilder {
#[inline]
fn from(unique: Unique) -> Self {
Self::with_capacity(unique.num_regions, unique.sequences.len())
}
}
#[inline]
#[must_use]
fn extract_region(tile_start: IGlyphOffset, region: IGlyphRegion) -> UGlyphRegion {
let region = IGlyphRegion {
min: tile_start + region.min,
max: tile_start + region.max,
};
region.as_urect()
}
impl<Image, Named: Marker> FontAtlasBuilder<Unpopulated, Unpopulated, GlyphSheet<Image>, Named>
where
GlyphSheet<Image>: ImageMarker,
{
pub fn populate_layout(
self,
layout: &FontLayout,
) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
match layout.packing_mode() {
PackingMode::Uniform { track } => self.uniform_layout(layout, track),
PackingMode::Dynamic { tracks } => self.dynamic_layout(layout, tracks),
}
}
pub fn uniform_layout(
mut self,
layout: &FontLayout,
track: &FontTrack,
) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
for (tile_index, token) in layout.ord_layout().iter().enumerate() {
let ITokenProps { offset, region } =
track.props(token.first().and_then(|t| layout.get_override(t)));
let props = UTokenProps {
offset,
region: {
let tile_start_x = (tile_index as u32) * track.grid_tile_size.x;
let tile_start = UGlyphSize::new(
(tile_start_x) % self.image.size.x,
(tile_start_x) / self.image.size.x * track.grid_tile_size.y,
)
.as_ivec2();
extract_region(tile_start, region)
},
};
self.add_token(token, props);
}
self.rebrand()
}
pub fn dynamic_layout(
mut self,
layout: &FontLayout,
tracks: &HashMap<Token, FontTrack>,
) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
struct DynamicPackingCursor {
pos: IGlyphOffset,
tallest_in_row: i32,
max_x: i32,
}
impl DynamicPackingCursor {
const fn new(image_size: UGlyphSize) -> Self {
Self {
pos: IGlyphOffset::ZERO,
tallest_in_row: 0,
max_x: image_size.x as i32,
}
}
fn advance(&mut self, track: &FontTrack) {
let tile = track.grid_tile_size.as_ivec2();
if self.pos.x + tile.x > self.max_x {
self.pos.x = 0;
self.pos.y += self.tallest_in_row;
self.tallest_in_row = 0;
}
self.tallest_in_row = self.tallest_in_row.max(tile.y);
self.pos.x += tile.x;
}
}
let mut current_track: Option<&FontTrack> = None;
let mut cursor = DynamicPackingCursor::new(self.image.size);
for token in layout.ord_layout().iter() {
if let Some(track) = tracks.get(token) {
current_track = Some(track);
}
let track = match current_track {
Some(track) => track,
None => continue,
};
let ITokenProps { offset, region } =
track.props(token.first().and_then(|t| layout.get_override(t)));
let props = UTokenProps {
offset,
region: {
let tile_start = cursor.pos;
cursor.advance(track);
extract_region(tile_start, region)
},
};
self.add_token(token, props);
}
self.rebrand()
}
}
impl<Sheet: ImageMarker, Named: Marker> FontAtlasBuilder<Populated, Unpopulated, Sheet, Named> {
pub fn custom_glyphs<'a>(
mut self,
iter: impl IntoIterator<Item = (&'a Token, &'a CustomGlyph)>,
) -> Result<FontAtlasBuilder<Populated, Populated, Sheet, Named>, UnknownSequence> {
for (token, sub_glyph) in iter {
#[allow(clippy::match_ref_pats)]
let props = match sub_glyph {
&CustomGlyph::Absolute { offset, region } => UTokenProps { offset, region },
&CustomGlyph::Relative {
offset,
ref reference,
region,
} => {
let Some(reference_props) = self.get_glyph_by_sequence(reference) else {
return Err(UnknownSequence(reference.clone()));
};
UTokenProps {
offset,
region: extract_region(reference_props.region.min.as_ivec2(), region),
}
}
};
self.add_token(token, props);
}
Ok(self.rebrand())
}
}
pub struct RawFont<Sheet> {
pub sheet: GlyphSheet<Sheet>,
pub glyphs: Vec<UTokenProps>,
#[allow(rustdoc::private_intra_doc_links)]
pub computed_height: u32,
}
impl<C: Marker, Image, Named: Marker> FontAtlasBuilder<Populated, C, GlyphSheet<Image>, Named>
where
GlyphSheet<Image>: ImageMarker,
{
pub fn build<B: Backend, Ctx: BackendBuilder<Backend = B, Error: Error, Sheet = Image>>(
self,
resource_builder: Ctx,
) -> Result<RasterFont<B>, FontBuilderError<Ctx::Error>> {
let computed_height = self
.scratch
.max_glyph_height()
.ok_or(EmptyFont(PhantomData))?;
let resources = resource_builder
.build_resources(RawFont {
glyphs: self.scratch.glyphs,
sheet: self.image,
computed_height,
})
.map_err(FontBuilderError::BackendBuilderError)?;
RasterFont::new(
self.name,
computed_height,
resources,
self.scratch.sequence_map,
)
.map_err(FontBuilderError::LigatureBindingError)
}
}
use errors::{EmptyFont, FontBuilderError, UnknownSequence};
pub mod errors {
use crate::core::Sequence;
use std::{
error::Error,
fmt::{Display, Formatter, Result},
marker::PhantomData,
};
#[derive(Debug)]
pub enum FontBuilderError<E: Error> {
EmptyFont,
UnknownSequence(UnknownSequence),
LigatureBindingError(crate::tree::BuildError),
BackendBuilderError(E),
}
impl<E: Error> From<EmptyFont> for FontBuilderError<E> {
#[inline]
fn from(_: EmptyFont) -> Self {
FontBuilderError::EmptyFont
}
}
impl<E: Error> From<UnknownSequence> for FontBuilderError<E> {
#[inline]
fn from(e: UnknownSequence) -> Self {
FontBuilderError::UnknownSequence(e)
}
}
impl<E: Error> Error for FontBuilderError<E> {}
impl<E: Error> Display for FontBuilderError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Self::EmptyFont => EmptyFont(PhantomData).fmt(f),
FontBuilderError::UnknownSequence(a) => a.fmt(f),
FontBuilderError::LigatureBindingError(a) => a.fmt(f),
FontBuilderError::BackendBuilderError(e) => {
write!(
f,
"Failed to build backend-specific resources for a raster font: {e}"
)
}
}
}
}
#[derive(Debug)]
pub struct EmptyFont(pub(super) PhantomData<()>);
impl Error for EmptyFont {}
impl Display for EmptyFont {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "Will not build empty font (0 glyphs).")
}
}
#[derive(Debug)]
pub struct UnknownSequence(pub(super) Sequence);
impl Error for UnknownSequence {}
impl Display for UnknownSequence {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"A custom glyph references a sequence that does not exist in the layout: {}",
self.0
)
}
}
}
impl Scratch {
#[allow(dead_code)]
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&Sequence, &UTokenProps)> {
self.sequence_map
.iter()
.filter_map(|(seq, index)| self.glyphs.get(index.0).map(|props| (seq, props)))
}
#[inline]
pub fn iter_props(&self) -> impl Iterator<Item = &UTokenProps> {
self.sequence_map
.values()
.filter_map(|index| self.glyphs.get(index.0))
}
#[inline]
pub fn max_glyph_height(&self) -> Option<u32> {
self.iter_props()
.map(|g| g.region.height().saturating_add_signed(g.offset.y))
.max()
}
}