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

View file

@ -23,6 +23,20 @@ pub enum TextStyle {
Monospace, 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)] #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum FontFamily { pub enum FontFamily {
@ -108,7 +122,9 @@ impl Fonts {
return; 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: // Make the top left pixel fully white:
@ -119,37 +135,29 @@ impl Fonts {
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));
self.definitions = definitions.clone(); self.definitions = definitions;
let FontDefinitions {
pixels_per_point,
fonts,
ttf_data,
emoji_ttf_data,
} = definitions;
self.fonts = fonts let mut font_impl_cache = FontImplCache::new(atlas.clone(), &self.definitions);
.into_iter()
.map(|(text_style, (family, size))| {
let typeface_data = ttf_data
.get(&family)
.unwrap_or_else(|| panic!("Missing TTF data for {:?}", family));
let font_impl = Arc::new(FontImpl::new( self.fonts = self
atlas.clone(), .definitions
typeface_data, .fonts
size, .iter()
pixels_per_point, .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 { if family == FontFamily::Monospace {
let emoji_font_impl = Arc::new(FontImpl::new( // monospace should have ubuntu as fallback (for √ etc):
atlas.clone(), fonts.push(
emoji_ttf_data, font_impl_cache
size, .font_impl(FontSource::Family(FontFamily::VariableWidth), size),
pixels_per_point, );
)); }
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); fonts.push(emoji_font_impl);
} }
@ -189,3 +197,79 @@ impl std::ops::Index<TextStyle> for Fonts {
&self.fonts[&text_style] &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
}
}