Add variable width font as fallback to monospace
This commit is contained in:
parent
891c5d84d7
commit
8b9d58d753
2 changed files with 166 additions and 80 deletions
|
@ -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,17 +107,20 @@ impl FontImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new character:
|
// Add new character:
|
||||||
|
let glyph = self.rusttype_font.glyph(c);
|
||||||
|
if glyph.id().0 == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
let glyph_info = allocate_glyph(
|
let glyph_info = allocate_glyph(
|
||||||
&mut self.atlas.lock(),
|
&mut self.atlas.lock(),
|
||||||
c,
|
glyph,
|
||||||
&self.font,
|
|
||||||
self.scale_in_pixels,
|
self.scale_in_pixels,
|
||||||
self.pixels_per_point,
|
self.pixels_per_point,
|
||||||
);
|
);
|
||||||
let glyph_info = glyph_info?;
|
|
||||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||||
Some(glyph_info)
|
Some(glyph_info)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pair_kerning(
|
pub fn pair_kerning(
|
||||||
&self,
|
&self,
|
||||||
|
@ -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,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue