Add variable width font as fallback to monospace

This commit is contained in:
Emil Ernerfeldt 2020-12-12 19:30:01 +01:00
parent 891c5d84d7
commit 8b9d58d753
2 changed files with 166 additions and 80 deletions

View file

@ -16,9 +16,9 @@ use super::texture_atlas::TextureAtlas;
// ----------------------------------------------------------------------------
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
// const REPLACEMENT_CHAR: char = '\u{FFFD}'; // <20> REPLACEMENT CHARACTER
const REPLACEMENT_CHAR: char = '?';
// const REPLACEMENT_CHAR: char = '?';
const REPLACEMENT_CHAR: char = '◻'; // white medium square
#[derive(Clone, Copy, Debug)]
pub struct UvRect {
@ -44,31 +44,43 @@ pub struct GlyphInfo {
pub uv_rect: Option<UvRect>,
}
impl Default for GlyphInfo {
fn default() -> Self {
Self {
id: rusttype::GlyphId(0),
advance_width: 0.0,
uv_rect: None,
}
}
}
// ----------------------------------------------------------------------------
/// A specific font with a size.
/// The interface uses points as the unit for everything.
pub struct FontImpl {
font: rusttype::Font<'static>,
rusttype_font: Arc<rusttype::Font<'static>>,
/// Maximum character height
scale_in_pixels: f32,
pixels_per_point: f32,
glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>,
glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>, // TODO: standard Mutex
atlas: Arc<Mutex<TextureAtlas>>,
}
impl FontImpl {
pub fn new(
atlas: Arc<Mutex<TextureAtlas>>,
font_data: &'static [u8],
scale_in_points: f32,
pixels_per_point: f32,
rusttype_font: Arc<rusttype::Font<'static>>,
scale_in_points: f32,
) -> FontImpl {
assert!(scale_in_points > 0.0);
assert!(pixels_per_point > 0.0);
let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font");
let scale_in_pixels = pixels_per_point * scale_in_points;
let font = Self {
font,
rusttype_font,
scale_in_pixels,
pixels_per_point,
glyph_info_cache: Default::default(),
@ -95,16 +107,19 @@ impl FontImpl {
}
// Add new character:
let glyph_info = allocate_glyph(
&mut self.atlas.lock(),
c,
&self.font,
self.scale_in_pixels,
self.pixels_per_point,
);
let glyph_info = glyph_info?;
self.glyph_info_cache.write().insert(c, glyph_info);
Some(glyph_info)
let glyph = self.rusttype_font.glyph(c);
if glyph.id().0 == 0 {
None
} else {
let glyph_info = allocate_glyph(
&mut self.atlas.lock(),
glyph,
self.scale_in_pixels,
self.pixels_per_point,
);
self.glyph_info_cache.write().insert(c, glyph_info);
Some(glyph_info)
}
}
pub fn pair_kerning(
@ -113,7 +128,7 @@ impl FontImpl {
glyph_id: rusttype::GlyphId,
) -> f32 {
let scale_in_pixels = Scale::uniform(self.scale_in_pixels);
self.font
self.rusttype_font
.pair_kerning(scale_in_pixels, last_glyph_id, glyph_id)
/ self.pixels_per_point
}
@ -134,7 +149,7 @@ type FontIndex = usize;
/// Wrapper over multiple `FontImpl` (commonly two: primary + emoji fallback)
pub struct Font {
fonts: Vec<Arc<FontImpl>>,
replacement_font_index_glyph_info: (FontIndex, GlyphInfo),
replacement_glyph: (FontIndex, GlyphInfo),
pixels_per_point: f32,
row_height: f32,
glyph_info_cache: RwLock<AHashMap<char, (FontIndex, GlyphInfo)>>,
@ -143,33 +158,25 @@ pub struct Font {
impl Font {
pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
assert!(!fonts.is_empty());
let replacement_glyph_font_index = 0;
let pixels_per_point = fonts[0].pixels_per_point();
let row_height = fonts[0].row_height();
let replacement_glyph_info = fonts[replacement_glyph_font_index]
.glyph_info(REPLACEMENT_CHAR)
let mut slf = Self {
fonts,
replacement_glyph: Default::default(),
pixels_per_point,
row_height,
glyph_info_cache: Default::default(),
};
let replacement_glyph = slf
.glyph_info_no_cache(REPLACEMENT_CHAR)
.unwrap_or_else(|| {
panic!(
"Failed to find replacement character {:?}",
REPLACEMENT_CHAR
)
});
let replacement_font_index_glyph_info =
(replacement_glyph_font_index, replacement_glyph_info);
let pixels_per_point = fonts[0].pixels_per_point();
let row_height = fonts[0].row_height();
let slf = Self {
fonts,
replacement_font_index_glyph_info,
pixels_per_point,
row_height,
glyph_info_cache: Default::default(),
};
slf.glyph_info_cache
.write()
.insert(REPLACEMENT_CHAR, replacement_font_index_glyph_info);
slf.replacement_glyph = replacement_glyph;
slf
}
@ -193,7 +200,7 @@ impl Font {
self.glyph_info(c).1.advance_width
}
/// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`)
/// `\n` will (intentionally) show up as `REPLACEMENT_CHAR`
fn glyph_info(&self, c: char) -> (FontIndex, GlyphInfo) {
{
if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
@ -202,8 +209,7 @@ impl Font {
}
let font_index_glyph_info = self.glyph_info_no_cache(c);
let font_index_glyph_info =
font_index_glyph_info.unwrap_or(self.replacement_font_index_glyph_info);
let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph);
self.glyph_info_cache
.write()
.insert(c, font_index_glyph_info);
@ -439,15 +445,11 @@ impl Font {
fn allocate_glyph(
atlas: &mut TextureAtlas,
c: char,
font: &rusttype::Font<'static>,
glyph: rusttype::Glyph<'static>,
scale_in_pixels: f32,
pixels_per_point: f32,
) -> Option<GlyphInfo> {
let glyph = font.glyph(c);
if glyph.id().0 == 0 {
return None; // Failed to find a glyph for the character
}
) -> GlyphInfo {
assert!(glyph.id().0 != 0);
let glyph = glyph.scaled(Scale::uniform(scale_in_pixels));
let glyph = glyph.positioned(point(0.0, 0.0));
@ -492,9 +494,9 @@ fn allocate_glyph(
let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point;
Some(GlyphInfo {
GlyphInfo {
id: glyph.id(),
advance_width: advance_width_in_points,
uv_rect,
})
}
}

View file

@ -23,6 +23,20 @@ pub enum TextStyle {
Monospace,
}
impl TextStyle {
pub fn all() -> impl Iterator<Item = TextStyle> {
[
TextStyle::Small,
TextStyle::Body,
TextStyle::Button,
TextStyle::Heading,
TextStyle::Monospace,
]
.iter()
.copied()
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum FontFamily {
@ -108,7 +122,9 @@ impl Fonts {
return;
}
let mut atlas = TextureAtlas::new(1024, 16); // TODO: better default?
// We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`,
// so we can show the Emoji picker demo window.
let mut atlas = TextureAtlas::new(2048, 64);
{
// Make the top left pixel fully white:
@ -119,37 +135,29 @@ impl Fonts {
let atlas = Arc::new(Mutex::new(atlas));
self.definitions = definitions.clone();
let FontDefinitions {
pixels_per_point,
fonts,
ttf_data,
emoji_ttf_data,
} = definitions;
self.definitions = definitions;
self.fonts = fonts
.into_iter()
.map(|(text_style, (family, size))| {
let typeface_data = ttf_data
.get(&family)
.unwrap_or_else(|| panic!("Missing TTF data for {:?}", family));
let mut font_impl_cache = FontImplCache::new(atlas.clone(), &self.definitions);
let font_impl = Arc::new(FontImpl::new(
atlas.clone(),
typeface_data,
size,
pixels_per_point,
));
self.fonts = self
.definitions
.fonts
.iter()
.map(|(&text_style, &(family, size))| {
let mut fonts = vec![];
let mut fonts = vec![font_impl];
fonts.push(font_impl_cache.font_impl(FontSource::Family(family), size));
for &emoji_ttf_data in &emoji_ttf_data {
let emoji_font_impl = Arc::new(FontImpl::new(
atlas.clone(),
emoji_ttf_data,
size,
pixels_per_point,
));
if family == FontFamily::Monospace {
// monospace should have ubuntu as fallback (for √ etc):
fonts.push(
font_impl_cache
.font_impl(FontSource::Family(FontFamily::VariableWidth), size),
);
}
for index in 0..self.definitions.emoji_ttf_data.len() {
let emoji_font_impl = font_impl_cache.font_impl(FontSource::Emoji(index), size);
fonts.push(emoji_font_impl);
}
@ -189,3 +197,79 @@ impl std::ops::Index<TextStyle> for Fonts {
&self.fonts[&text_style]
}
}
// ----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FontSource {
Family(FontFamily),
/// Emoji fonts are numbered from hight priority (0) and onwards
Emoji(usize),
}
pub struct FontImplCache {
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
font_families: std::collections::BTreeMap<FontFamily, Arc<rusttype::Font<'static>>>,
emoji_fonts: Vec<Arc<rusttype::Font<'static>>>,
/// can't have f32 in a HashMap or BTreeMap,
/// so let's do a linear search
cache: Vec<(FontSource, f32, Arc<FontImpl>)>,
}
impl FontImplCache {
pub fn new(atlas: Arc<Mutex<TextureAtlas>>, definitions: &super::FontDefinitions) -> Self {
let font_families = definitions
.ttf_data
.iter()
.map(|(family, ttf_data)| {
(
*family,
Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF")),
)
})
.collect();
let emoji_fonts = definitions
.emoji_ttf_data
.iter()
.map(|ttf_data| {
Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF"))
})
.collect();
Self {
atlas,
pixels_per_point: definitions.pixels_per_point,
font_families,
emoji_fonts,
cache: Default::default(),
}
}
pub fn rusttype_font(&self, source: FontSource) -> Arc<rusttype::Font<'static>> {
match source {
FontSource::Family(family) => self.font_families.get(&family).unwrap().clone(),
FontSource::Emoji(index) => self.emoji_fonts[index].clone(),
}
}
pub fn font_impl(&mut self, source: FontSource, scale_in_points: f32) -> Arc<FontImpl> {
for entry in &self.cache {
if (entry.0, entry.1) == (source, scale_in_points) {
return entry.2.clone();
}
}
let font_impl = Arc::new(FontImpl::new(
self.atlas.clone(),
self.pixels_per_point,
self.rusttype_font(source),
scale_in_points,
));
self.cache
.push((source, scale_in_points, font_impl.clone()));
font_impl
}
}