iris: add replaceable glyph atlas buckets

This commit is contained in:
iris committed 2026-09-12 18:10:55 -04:00
1 parent 1be6cc2248
commit 2b9d8c49a0
8 files changed
+342 -87

No files matched your search

+178 -18
View File
@@ -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();