iris: add replaceable glyph atlas buckets
This commit is contained in:
1 parent
1be6cc2248
commit
2b9d8c49a0
8 files changed
+342
-87
No files matched your search
+178
-18
@@ -5,7 +5,7 @@ use crate::{
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
fontique::{Blob, FontInfoOverride},
|
||||
fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth},
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -35,6 +35,7 @@ pub struct FontDiagnostics {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FontRegistrationError {
|
||||
AlreadyRegistered(String),
|
||||
NotRegistered(String),
|
||||
InvalidFont(String),
|
||||
}
|
||||
|
||||
@@ -44,6 +45,9 @@ impl fmt::Display for FontRegistrationError {
|
||||
Self::AlreadyRegistered(family) => {
|
||||
write!(f, "font data is already registered for {family:?}")
|
||||
}
|
||||
Self::NotRegistered(family) => {
|
||||
write!(f, "no font data is registered for {family:?}")
|
||||
}
|
||||
Self::InvalidFont(family) => {
|
||||
write!(
|
||||
f,
|
||||
@@ -56,6 +60,23 @@ impl fmt::Display for FontRegistrationError {
|
||||
|
||||
impl std::error::Error for FontRegistrationError {}
|
||||
|
||||
/// The bucket used by [`crate::Ui::register_font`] when none is specified.
|
||||
pub const DEFAULT_GLYPH_BUCKET: &str = "default";
|
||||
|
||||
struct RegisteredFace {
|
||||
family: FamilyId,
|
||||
width: FontWidth,
|
||||
style: FontStyle,
|
||||
weight: FontWeight,
|
||||
}
|
||||
|
||||
struct RegisteredFont {
|
||||
private_name: String,
|
||||
bucket: u64,
|
||||
font_id: u64,
|
||||
faces: Vec<RegisteredFace>,
|
||||
}
|
||||
|
||||
pub struct TextData {
|
||||
pub font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<PaintId>,
|
||||
@@ -71,8 +92,11 @@ pub struct TextData {
|
||||
/// truth would mean carrying a `Painter` (or output size) into every
|
||||
/// input handler for the sake of one field.
|
||||
pub density: f32,
|
||||
registered_families: HashMap<String, String>,
|
||||
registered_families: HashMap<String, RegisteredFont>,
|
||||
glyph_buckets: HashMap<String, u64>,
|
||||
font_buckets: HashMap<u64, u64>,
|
||||
next_registered_family: u64,
|
||||
next_glyph_bucket: u64,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
@@ -86,7 +110,10 @@ impl Default for TextData {
|
||||
atlas: GlyphAtlas::default(),
|
||||
density: 1.0,
|
||||
registered_families: HashMap::new(),
|
||||
glyph_buckets: HashMap::new(),
|
||||
font_buckets: HashMap::new(),
|
||||
next_registered_family: 0,
|
||||
next_glyph_bucket: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,29 +195,145 @@ impl TextData {
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
self.register_font_in(family, DEFAULT_GLYPH_BUCKET, data)
|
||||
}
|
||||
|
||||
pub(crate) fn register_font_in(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
bucket: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
let family = family.as_ref().to_owned();
|
||||
if self.registered_families.contains_key(&family) {
|
||||
return Err(FontRegistrationError::AlreadyRegistered(family));
|
||||
}
|
||||
|
||||
// Parley selects fonts by family rather than by a face handle. Give
|
||||
// application data a private family name so selecting it cannot find
|
||||
// a system font that happens to carry the same embedded metadata.
|
||||
let private_name = format!("__iris_registered_font_{}__", self.next_registered_family);
|
||||
let blob = Blob::new(Arc::new(data));
|
||||
Self::validate_font(&family, blob.clone())?;
|
||||
let bucket = self.glyph_bucket(bucket.as_ref());
|
||||
let registered = self.add_font(private_name, bucket, blob);
|
||||
self.next_registered_family += 1;
|
||||
self.font_buckets.insert(registered.font_id, bucket);
|
||||
self.registered_families.insert(family, registered);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn replace_font(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
let family = family.as_ref();
|
||||
let Some(bucket) = self.registered_families.get(family).map(|font| font.bucket) else {
|
||||
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
|
||||
};
|
||||
self.replace_font_with(family, bucket, data)
|
||||
}
|
||||
|
||||
pub(crate) fn replace_font_in(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
bucket: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
let family = family.as_ref();
|
||||
if !self.registered_families.contains_key(family) {
|
||||
return Err(FontRegistrationError::NotRegistered(family.to_owned()));
|
||||
}
|
||||
let bucket = self.glyph_bucket(bucket.as_ref());
|
||||
self.replace_font_with(family, bucket, data)
|
||||
}
|
||||
|
||||
fn replace_font_with(
|
||||
&mut self,
|
||||
family: &str,
|
||||
bucket: u64,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
let blob = Blob::new(Arc::new(data));
|
||||
Self::validate_font(family, blob.clone())?;
|
||||
let old = self.registered_families.remove(family).unwrap();
|
||||
for face in old.faces {
|
||||
self.font_cx.collection.unregister_font(
|
||||
face.family,
|
||||
face.width,
|
||||
face.style,
|
||||
face.weight,
|
||||
);
|
||||
}
|
||||
self.font_buckets.remove(&old.font_id);
|
||||
self.atlas.clear_bucket(old.bucket);
|
||||
|
||||
let registered = self.add_font(old.private_name, bucket, blob);
|
||||
self.font_buckets.insert(registered.font_id, bucket);
|
||||
self.registered_families
|
||||
.insert(family.to_owned(), registered);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_font(family: &str, blob: Blob<u8>) -> Result<(), FontRegistrationError> {
|
||||
let mut collection = Collection::new(CollectionOptions {
|
||||
shared: false,
|
||||
system_fonts: false,
|
||||
});
|
||||
let fonts = collection.register_fonts(blob, None);
|
||||
if fonts.is_empty() {
|
||||
Err(FontRegistrationError::InvalidFont(family.to_owned()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn add_font(&mut self, private_name: String, bucket: u64, blob: Blob<u8>) -> RegisteredFont {
|
||||
let font_id = blob.id();
|
||||
let fonts = self.font_cx.collection.register_fonts(
|
||||
Blob::new(Arc::new(data)),
|
||||
blob,
|
||||
Some(FontInfoOverride {
|
||||
family_name: Some(&private_name),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
if fonts.is_empty() {
|
||||
return Err(FontRegistrationError::InvalidFont(family));
|
||||
debug_assert!(!fonts.is_empty(), "validated font failed its second scan");
|
||||
let faces = fonts
|
||||
.into_iter()
|
||||
.flat_map(|(family, fonts)| {
|
||||
fonts.into_iter().map(move |font| RegisteredFace {
|
||||
family,
|
||||
width: font.width(),
|
||||
style: font.style(),
|
||||
weight: font.weight(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
RegisteredFont {
|
||||
private_name,
|
||||
bucket,
|
||||
font_id,
|
||||
faces,
|
||||
}
|
||||
self.next_registered_family += 1;
|
||||
self.registered_families.insert(family, private_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn glyph_bucket(&mut self, name: &str) -> u64 {
|
||||
if name == DEFAULT_GLYPH_BUCKET {
|
||||
return crate::render::DEFAULT_GLYPH_BUCKET_ID;
|
||||
}
|
||||
if let Some(bucket) = self.glyph_buckets.get(name) {
|
||||
return *bucket;
|
||||
}
|
||||
let bucket = self.next_glyph_bucket;
|
||||
self.next_glyph_bucket += 1;
|
||||
self.glyph_buckets.insert(name.to_owned(), bucket);
|
||||
bucket
|
||||
}
|
||||
|
||||
fn bucket_for_font(&self, font: u64) -> u64 {
|
||||
self.font_buckets
|
||||
.get(&font)
|
||||
.copied()
|
||||
.unwrap_or(crate::render::DEFAULT_GLYPH_BUCKET_ID)
|
||||
}
|
||||
|
||||
pub(crate) fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
||||
@@ -201,8 +344,8 @@ impl TextData {
|
||||
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
||||
/// run, paid only when the layout is rebuilt.
|
||||
pub fn resolve_family(&self, family: &str) -> String {
|
||||
if let Some(name) = self.registered_families.get(family) {
|
||||
return name.clone();
|
||||
if let Some(font) = self.registered_families.get(family) {
|
||||
return font.private_name.clone();
|
||||
}
|
||||
family.to_owned()
|
||||
}
|
||||
@@ -304,9 +447,10 @@ impl TextData {
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
let font_id = font.data.id();
|
||||
let bucket = self.bucket_for_font(font_id);
|
||||
|
||||
for glyph in run.positioned_glyphs() {
|
||||
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
|
||||
let (x, subpixel) = horizontal_phase(glyph.x);
|
||||
let key = GlyphKey {
|
||||
font: font_id,
|
||||
glyph: glyph.id,
|
||||
@@ -314,7 +458,7 @@ impl TextData {
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let entry = match self.atlas.get(&key) {
|
||||
let entry = match self.atlas.get(bucket, &key) {
|
||||
Some(entry) => entry,
|
||||
None => {
|
||||
let mut scaler = self
|
||||
@@ -333,9 +477,9 @@ impl TextData {
|
||||
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
|
||||
.render(&mut scaler, glyph.id as u16);
|
||||
match image {
|
||||
Some(image) => self.atlas.insert(key, &image, textures),
|
||||
Some(image) => self.atlas.insert(bucket, key, &image, textures),
|
||||
None => {
|
||||
self.atlas.insert_empty(key);
|
||||
self.atlas.insert_empty(bucket, key);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -345,7 +489,7 @@ impl TextData {
|
||||
placed.push(PlacedGlyph {
|
||||
entry,
|
||||
offset: Vec2::new(
|
||||
glyph.x.floor() + entry.left as f32,
|
||||
x + entry.left as f32,
|
||||
glyph.y.floor() - entry.top as f32,
|
||||
),
|
||||
paint: run_color.slot(),
|
||||
@@ -376,6 +520,11 @@ impl TextData {
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_phase(x: f32) -> (f32, u8) {
|
||||
let quarters = (x * 4.0).round() as i32;
|
||||
(quarters.div_euclid(4) as f32, quarters.rem_euclid(4) as u8)
|
||||
}
|
||||
|
||||
pub const SANS_SERIF: &str = "sans-serif";
|
||||
pub const SERIF: &str = "serif";
|
||||
pub const MONOSPACE: &str = "monospace";
|
||||
@@ -574,6 +723,10 @@ impl TextBuffer {
|
||||
}
|
||||
|
||||
fn invalidate(&mut self) {
|
||||
// A layout owns the font blobs it was shaped with. Drop it now so
|
||||
// replacing a registered font does not retain the old bytes until an
|
||||
// off-screen text resource happens to be shaped again.
|
||||
self.layout = Layout::new();
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
@@ -849,6 +1002,13 @@ impl TextHandle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_horizontal_phase_carries_across_pixel_boundaries() {
|
||||
assert_eq!(horizontal_phase(10.20), (10.0, 1));
|
||||
assert_eq!(horizontal_phase(10.90), (11.0, 0));
|
||||
assert_eq!(horizontal_phase(-0.20), (-1.0, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_font_data_is_reported() {
|
||||
let mut data = TextData::default();
|
||||
|
||||
Reference in new issue
Block a user