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::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||||
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
fontique::{Blob, FontInfoOverride},
|
fontique::{Blob, Collection, CollectionOptions, FamilyId, FontInfoOverride, FontWidth},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell, RefMut},
|
cell::{Ref, RefCell, RefMut},
|
||||||
@@ -35,6 +35,7 @@ pub struct FontDiagnostics {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum FontRegistrationError {
|
pub enum FontRegistrationError {
|
||||||
AlreadyRegistered(String),
|
AlreadyRegistered(String),
|
||||||
|
NotRegistered(String),
|
||||||
InvalidFont(String),
|
InvalidFont(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@ impl fmt::Display for FontRegistrationError {
|
|||||||
Self::AlreadyRegistered(family) => {
|
Self::AlreadyRegistered(family) => {
|
||||||
write!(f, "font data is already registered for {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) => {
|
Self::InvalidFont(family) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@@ -56,6 +60,23 @@ impl fmt::Display for FontRegistrationError {
|
|||||||
|
|
||||||
impl std::error::Error 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 struct TextData {
|
||||||
pub font_cx: FontContext,
|
pub font_cx: FontContext,
|
||||||
pub layout_cx: LayoutContext<PaintId>,
|
pub layout_cx: LayoutContext<PaintId>,
|
||||||
@@ -71,8 +92,11 @@ pub struct TextData {
|
|||||||
/// truth would mean carrying a `Painter` (or output size) into every
|
/// truth would mean carrying a `Painter` (or output size) into every
|
||||||
/// input handler for the sake of one field.
|
/// input handler for the sake of one field.
|
||||||
pub density: f32,
|
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_registered_family: u64,
|
||||||
|
next_glyph_bucket: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
@@ -86,7 +110,10 @@ impl Default for TextData {
|
|||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
density: 1.0,
|
density: 1.0,
|
||||||
registered_families: HashMap::new(),
|
registered_families: HashMap::new(),
|
||||||
|
glyph_buckets: HashMap::new(),
|
||||||
|
font_buckets: HashMap::new(),
|
||||||
next_registered_family: 0,
|
next_registered_family: 0,
|
||||||
|
next_glyph_bucket: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,29 +195,145 @@ impl TextData {
|
|||||||
&mut self,
|
&mut self,
|
||||||
family: impl AsRef<str>,
|
family: impl AsRef<str>,
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
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> {
|
) -> Result<(), FontRegistrationError> {
|
||||||
let family = family.as_ref().to_owned();
|
let family = family.as_ref().to_owned();
|
||||||
if self.registered_families.contains_key(&family) {
|
if self.registered_families.contains_key(&family) {
|
||||||
return Err(FontRegistrationError::AlreadyRegistered(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 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(
|
let fonts = self.font_cx.collection.register_fonts(
|
||||||
Blob::new(Arc::new(data)),
|
blob,
|
||||||
Some(FontInfoOverride {
|
Some(FontInfoOverride {
|
||||||
family_name: Some(&private_name),
|
family_name: Some(&private_name),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if fonts.is_empty() {
|
debug_assert!(!fonts.is_empty(), "validated font failed its second scan");
|
||||||
return Err(FontRegistrationError::InvalidFont(family));
|
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 {
|
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
|
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
||||||
/// run, paid only when the layout is rebuilt.
|
/// run, paid only when the layout is rebuilt.
|
||||||
pub fn resolve_family(&self, family: &str) -> String {
|
pub fn resolve_family(&self, family: &str) -> String {
|
||||||
if let Some(name) = self.registered_families.get(family) {
|
if let Some(font) = self.registered_families.get(family) {
|
||||||
return name.clone();
|
return font.private_name.clone();
|
||||||
}
|
}
|
||||||
family.to_owned()
|
family.to_owned()
|
||||||
}
|
}
|
||||||
@@ -304,9 +447,10 @@ impl TextData {
|
|||||||
};
|
};
|
||||||
let coords_hash = hash_coords(coords);
|
let coords_hash = hash_coords(coords);
|
||||||
let font_id = font.data.id();
|
let font_id = font.data.id();
|
||||||
|
let bucket = self.bucket_for_font(font_id);
|
||||||
|
|
||||||
for glyph in run.positioned_glyphs() {
|
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 {
|
let key = GlyphKey {
|
||||||
font: font_id,
|
font: font_id,
|
||||||
glyph: glyph.id,
|
glyph: glyph.id,
|
||||||
@@ -314,7 +458,7 @@ impl TextData {
|
|||||||
subpixel,
|
subpixel,
|
||||||
coords: coords_hash,
|
coords: coords_hash,
|
||||||
};
|
};
|
||||||
let entry = match self.atlas.get(&key) {
|
let entry = match self.atlas.get(bucket, &key) {
|
||||||
Some(entry) => entry,
|
Some(entry) => entry,
|
||||||
None => {
|
None => {
|
||||||
let mut scaler = self
|
let mut scaler = self
|
||||||
@@ -333,9 +477,9 @@ impl TextData {
|
|||||||
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
|
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
|
||||||
.render(&mut scaler, glyph.id as u16);
|
.render(&mut scaler, glyph.id as u16);
|
||||||
match image {
|
match image {
|
||||||
Some(image) => self.atlas.insert(key, &image, textures),
|
Some(image) => self.atlas.insert(bucket, key, &image, textures),
|
||||||
None => {
|
None => {
|
||||||
self.atlas.insert_empty(key);
|
self.atlas.insert_empty(bucket, key);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +489,7 @@ impl TextData {
|
|||||||
placed.push(PlacedGlyph {
|
placed.push(PlacedGlyph {
|
||||||
entry,
|
entry,
|
||||||
offset: Vec2::new(
|
offset: Vec2::new(
|
||||||
glyph.x.floor() + entry.left as f32,
|
x + entry.left as f32,
|
||||||
glyph.y.floor() - entry.top as f32,
|
glyph.y.floor() - entry.top as f32,
|
||||||
),
|
),
|
||||||
paint: run_color.slot(),
|
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 SANS_SERIF: &str = "sans-serif";
|
||||||
pub const SERIF: &str = "serif";
|
pub const SERIF: &str = "serif";
|
||||||
pub const MONOSPACE: &str = "monospace";
|
pub const MONOSPACE: &str = "monospace";
|
||||||
@@ -574,6 +723,10 @@ impl TextBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn invalidate(&mut self) {
|
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;
|
self.shaped = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -849,6 +1002,13 @@ impl TextHandle {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn invalid_font_data_is_reported() {
|
fn invalid_font_data_is_reported() {
|
||||||
let mut data = TextData::default();
|
let mut data = TextData::default();
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ pub struct Textures {
|
|||||||
/// drawing it and its slot is never recycled underneath one.
|
/// drawing it and its slot is never recycled underneath one.
|
||||||
shared: HashMap<SharedTextureKey, TextureHandle>,
|
shared: HashMap<SharedTextureKey, TextureHandle>,
|
||||||
/// Next layer to hand out to an atlas page. Page layers and resource slots
|
/// Next layer to hand out to an atlas page. Page layers and resource slots
|
||||||
/// are never reused, even if an explicit atlas clear drops their handles.
|
/// are separate identities: released page layers are reused without moving
|
||||||
|
/// any still-live page.
|
||||||
next_page_layer: u32,
|
next_page_layer: u32,
|
||||||
|
free_page_layers: Vec<u32>,
|
||||||
updates: Vec<Update>,
|
updates: Vec<Update>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ impl Textures {
|
|||||||
kinds: Vec::new(),
|
kinds: Vec::new(),
|
||||||
shared: HashMap::new(),
|
shared: HashMap::new(),
|
||||||
next_page_layer: 0,
|
next_page_layer: 0,
|
||||||
|
free_page_layers: Vec::new(),
|
||||||
updates: Vec::new(),
|
updates: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,16 +98,20 @@ impl Textures {
|
|||||||
let image = image.into();
|
let image = image.into();
|
||||||
let size = image.dimensions().into();
|
let size = image.dimensions().into();
|
||||||
let kind = TextureKind::Image;
|
let kind = TextureKind::Image;
|
||||||
self.push(kind, size, image, true)
|
self.push(kind, size, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||||
|
self.free();
|
||||||
let image = image.into();
|
let image = image.into();
|
||||||
let size = image.dimensions().into();
|
let size = image.dimensions().into();
|
||||||
let layer = self.next_page_layer;
|
let layer = self.free_page_layers.pop().unwrap_or_else(|| {
|
||||||
self.next_page_layer += 1;
|
let layer = self.next_page_layer;
|
||||||
|
self.next_page_layer += 1;
|
||||||
|
layer
|
||||||
|
});
|
||||||
let kind = TextureKind::Page { layer };
|
let kind = TextureKind::Page { layer };
|
||||||
self.push(kind, size, image, false)
|
self.push(kind, size, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle {
|
pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle {
|
||||||
@@ -119,22 +126,10 @@ impl Textures {
|
|||||||
Some(self.handle(id))
|
Some(self.handle(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(
|
fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle {
|
||||||
&mut self,
|
|
||||||
kind: TextureKind,
|
|
||||||
size: Vec2,
|
|
||||||
image: DynamicImage,
|
|
||||||
recycle: bool,
|
|
||||||
) -> TextureHandle {
|
|
||||||
self.free();
|
self.free();
|
||||||
let old_capacity = self.resources.borrow().capacity();
|
let old_capacity = self.resources.borrow().capacity();
|
||||||
let id = if recycle {
|
let id = self.resources.borrow_mut().add(TextureRsc { kind, size });
|
||||||
self.resources.borrow_mut().add(TextureRsc { kind, size })
|
|
||||||
} else {
|
|
||||||
self.resources
|
|
||||||
.borrow_mut()
|
|
||||||
.add_unrecycled(TextureRsc { kind, size })
|
|
||||||
};
|
|
||||||
let i = id.slot();
|
let i = id.slot();
|
||||||
if (i as usize) < old_capacity {
|
if (i as usize) < old_capacity {
|
||||||
self.images[i as usize] = Some(image);
|
self.images[i as usize] = Some(image);
|
||||||
@@ -201,10 +196,14 @@ impl Textures {
|
|||||||
pub fn free(&mut self) {
|
pub fn free(&mut self) {
|
||||||
let updates = &mut self.updates;
|
let updates = &mut self.updates;
|
||||||
let images = &mut self.images;
|
let images = &mut self.images;
|
||||||
self.resources.borrow_mut().apply(|id, _| {
|
let free_page_layers = &mut self.free_page_layers;
|
||||||
|
self.resources.borrow_mut().apply(|id, resource| {
|
||||||
let idx = id.slot();
|
let idx = id.slot();
|
||||||
images[idx as usize] = None;
|
images[idx as usize] = None;
|
||||||
updates.push(Update::Free(idx));
|
updates.push(Update::Free(idx));
|
||||||
|
if let TextureKind::Page { layer } = resource.kind {
|
||||||
|
free_page_layers.push(layer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,16 +320,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_released_atlas_page_slot_is_not_reused_by_an_image() {
|
fn a_released_atlas_page_layer_is_reused_without_moving_live_pages() {
|
||||||
let mut textures = Textures::new();
|
let mut textures = Textures::new();
|
||||||
let page = textures.add_page(image(4));
|
let first = textures.add_page(image(4));
|
||||||
let page_slot = page.rsc.id().slot();
|
let second = textures.add_page(image(4));
|
||||||
drop(page);
|
let first_layer = first.layer();
|
||||||
|
let second_layer = second.layer();
|
||||||
|
drop(first);
|
||||||
textures.free();
|
textures.free();
|
||||||
|
|
||||||
let plain = textures.add(image(4));
|
let replacement = textures.add_page(image(4));
|
||||||
|
|
||||||
assert_ne!(plain.image_index(), page_slot);
|
assert_eq!(replacement.layer(), first_layer);
|
||||||
|
assert_eq!(second.layer(), second_layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+54
-32
@@ -6,6 +6,7 @@ use image::RgbaImage;
|
|||||||
use swash::scale::image::{Content, Image};
|
use swash::scale::image::{Content, Image};
|
||||||
|
|
||||||
pub(crate) const PAGE: u32 = 1024;
|
pub(crate) const PAGE: u32 = 1024;
|
||||||
|
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
|
||||||
|
|
||||||
const PAD: u32 = 1;
|
const PAD: u32 = 1;
|
||||||
|
|
||||||
@@ -42,40 +43,47 @@ struct Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GlyphAtlas {
|
struct Bucket {
|
||||||
pages: Vec<Page>,
|
pages: Vec<Page>,
|
||||||
generation: u64,
|
|
||||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GlyphAtlas {
|
||||||
|
buckets: HashMap<u64, Bucket>,
|
||||||
|
generation: u64,
|
||||||
|
}
|
||||||
|
|
||||||
impl GlyphAtlas {
|
impl GlyphAtlas {
|
||||||
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||||
self.entries.get(key).copied()
|
self.buckets.get(&bucket)?.entries.get(key).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rasterised pixels in, a place in the atlas out. `None` means the glyph
|
/// Rasterised pixels in, a place in the atlas out. `None` means the glyph
|
||||||
/// has no pixels, which is a normal answer rather than a failure.
|
/// has no pixels, which is a normal answer rather than a failure.
|
||||||
pub fn insert(
|
pub fn insert(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
bucket: u64,
|
||||||
key: GlyphKey,
|
key: GlyphKey,
|
||||||
image: &Image,
|
image: &Image,
|
||||||
textures: &mut Textures,
|
textures: &mut Textures,
|
||||||
) -> Option<GlyphEntry> {
|
) -> Option<GlyphEntry> {
|
||||||
|
let bucket = self.buckets.entry(bucket).or_default();
|
||||||
let w = image.placement.width;
|
let w = image.placement.width;
|
||||||
let h = image.placement.height;
|
let h = image.placement.height;
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
self.entries.insert(key, None);
|
bucket.entries.insert(key, None);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
||||||
// A single glyph larger than a page. Refusing is better than
|
// A single glyph larger than a page. Refusing is better than
|
||||||
// silently drawing a cropped one; the caller draws nothing.
|
// silently drawing a cropped one; the caller draws nothing.
|
||||||
self.entries.insert(key, None);
|
bucket.entries.insert(key, None);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
let (page_idx, x, y) = bucket.allocate(w, h, textures);
|
||||||
let page = &self.pages[page_idx];
|
let page = &bucket.pages[page_idx];
|
||||||
|
|
||||||
let img = textures.image_mut(&page.handle);
|
let img = textures.image_mut(&page.handle);
|
||||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||||
@@ -89,7 +97,7 @@ impl GlyphAtlas {
|
|||||||
};
|
};
|
||||||
textures.patch(&page.handle, rect);
|
textures.patch(&page.handle, rect);
|
||||||
|
|
||||||
let page = &self.pages[page_idx];
|
let page = &bucket.pages[page_idx];
|
||||||
let scale = 1.0 / PAGE as f32;
|
let scale = 1.0 / PAGE as f32;
|
||||||
let entry = GlyphEntry {
|
let entry = GlyphEntry {
|
||||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
uv_min: [x as f32 * scale, y as f32 * scale],
|
||||||
@@ -101,10 +109,46 @@ impl GlyphAtlas {
|
|||||||
is_color: matches!(image.content, Content::Color),
|
is_color: matches!(image.content, Content::Color),
|
||||||
layer: page.handle.layer(),
|
layer: page.handle.layer(),
|
||||||
};
|
};
|
||||||
self.entries.insert(key, Some(entry));
|
bucket.entries.insert(key, Some(entry));
|
||||||
Some(entry)
|
Some(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert_empty(&mut self, bucket: u64, key: GlyphKey) {
|
||||||
|
self.buckets
|
||||||
|
.entry(bucket)
|
||||||
|
.or_default()
|
||||||
|
.entries
|
||||||
|
.insert(key, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_bucket(&mut self, bucket: u64) {
|
||||||
|
if self.buckets.remove(&bucket).is_some() {
|
||||||
|
self.generation += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generation(&self) -> u64 {
|
||||||
|
self.generation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_count(&self) -> usize {
|
||||||
|
self.buckets.values().map(|bucket| bucket.pages.len()).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn glyph_count(&self) -> usize {
|
||||||
|
self.buckets
|
||||||
|
.values()
|
||||||
|
.map(|bucket| bucket.entries.len())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.buckets.clear();
|
||||||
|
self.generation += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bucket {
|
||||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
||||||
let need_w = w + PAD;
|
let need_w = w + PAD;
|
||||||
let need_h = h + PAD;
|
let need_h = h + PAD;
|
||||||
@@ -130,28 +174,6 @@ impl GlyphAtlas {
|
|||||||
});
|
});
|
||||||
(self.pages.len() - 1, PAD, PAD)
|
(self.pages.len() - 1, PAD, PAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
|
||||||
self.entries.insert(key, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generation(&self) -> u64 {
|
|
||||||
self.generation
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn page_count(&self) -> usize {
|
|
||||||
self.pages.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.pages.clear();
|
|
||||||
self.entries.clear();
|
|
||||||
self.generation += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ fn corners_of(inst: PrimitiveInstance) -> Corners {
|
|||||||
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
|
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
|
||||||
let move_delta = resolve_move(inst.move_idx);
|
let move_delta = resolve_move(inst.move_idx);
|
||||||
return Corners(
|
return Corners(
|
||||||
floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta,
|
floor(top_left_rel * window.dim) + floor(top_left_abs + move_delta),
|
||||||
floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta,
|
floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const MIN_ARRAY_LAYERS: u32 = 2;
|
|||||||
enum Slot {
|
enum Slot {
|
||||||
Empty,
|
Empty,
|
||||||
Image(ImageGpu),
|
Image(ImageGpu),
|
||||||
/// The array layer a page occupies. Pages are never freed (see
|
/// The array layer a live page occupies. Dropping the page empties this
|
||||||
/// `Textures::free`), so this is the only variant that outlives a `Free`.
|
/// resource slot; its array layer may then be assigned to a new page.
|
||||||
Page(u32),
|
Page(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,9 @@ pub struct GpuTextures {
|
|||||||
array_texture: Texture,
|
array_texture: Texture,
|
||||||
array_view: TextureView,
|
array_view: TextureView,
|
||||||
array_capacity: u32,
|
array_capacity: u32,
|
||||||
page_count: u32,
|
/// One past the highest layer ever populated. Holes below this remain in
|
||||||
|
/// place when the array grows, while `Textures` can reuse their numbers.
|
||||||
|
layer_high_water: u32,
|
||||||
|
|
||||||
sampler: Sampler,
|
sampler: Sampler,
|
||||||
/// Bound in the image slot of the main draw's bind group, which has
|
/// Bound in the image slot of the main draw's bind group, which has
|
||||||
@@ -125,7 +127,7 @@ impl GpuTextures {
|
|||||||
rebuilt = true;
|
rebuilt = true;
|
||||||
}
|
}
|
||||||
self.write_full_layer(layer, image);
|
self.write_full_layer(layer, image);
|
||||||
self.page_count = self.page_count.max(layer + 1);
|
self.layer_high_water = self.layer_high_water.max(layer + 1);
|
||||||
(Slot::Page(layer), rebuilt)
|
(Slot::Page(layer), rebuilt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +207,7 @@ impl GpuTextures {
|
|||||||
self.pages_grown += 1;
|
self.pages_grown += 1;
|
||||||
let new_capacity = self.array_capacity * 2;
|
let new_capacity = self.array_capacity * 2;
|
||||||
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
||||||
if self.page_count > 0 {
|
if self.layer_high_water > 0 {
|
||||||
let mut encoder = self
|
let mut encoder = self
|
||||||
.device
|
.device
|
||||||
.create_command_encoder(&CommandEncoderDescriptor {
|
.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
@@ -227,7 +229,7 @@ impl GpuTextures {
|
|||||||
Extent3d {
|
Extent3d {
|
||||||
width: PAGE,
|
width: PAGE,
|
||||||
height: PAGE,
|
height: PAGE,
|
||||||
depth_or_array_layers: self.page_count,
|
depth_or_array_layers: self.layer_high_water,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
@@ -368,7 +370,7 @@ impl GpuTextures {
|
|||||||
array_texture,
|
array_texture,
|
||||||
array_view,
|
array_view,
|
||||||
array_capacity,
|
array_capacity,
|
||||||
page_count: 0,
|
layer_high_water: 0,
|
||||||
sampler,
|
sampler,
|
||||||
null_view,
|
null_view,
|
||||||
bind_group_creates: 0,
|
bind_group_creates: 0,
|
||||||
|
|||||||
+43
-1
@@ -75,10 +75,52 @@ impl Ui {
|
|||||||
&mut self,
|
&mut self,
|
||||||
family: impl AsRef<str>,
|
family: impl AsRef<str>,
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
|
self.update_fonts(|text| text.register_font(family, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register application-owned font data in a named glyph-atlas bucket.
|
||||||
|
/// Fonts registered without this method share the default bucket.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn register_font_in(
|
||||||
|
&mut self,
|
||||||
|
family: impl AsRef<str>,
|
||||||
|
bucket: impl AsRef<str>,
|
||||||
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
|
self.update_fonts(|text| text.register_font_in(family, bucket, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace an application's registered font while retaining its atlas
|
||||||
|
/// bucket. Text is reshaped and the bucket's old glyph pages are released.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn replace_font(
|
||||||
|
&mut self,
|
||||||
|
family: impl AsRef<str>,
|
||||||
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
|
self.update_fonts(|text| text.replace_font(family, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace an application's registered font and assign the replacement to
|
||||||
|
/// a named glyph-atlas bucket.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn replace_font_in(
|
||||||
|
&mut self,
|
||||||
|
family: impl AsRef<str>,
|
||||||
|
bucket: impl AsRef<str>,
|
||||||
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
|
self.update_fonts(|text| text.replace_font_in(family, bucket, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_fonts(
|
||||||
|
&mut self,
|
||||||
|
update: impl FnOnce(&mut TextResources) -> Result<(), crate::FontRegistrationError>,
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
let owners = {
|
let owners = {
|
||||||
let mut text = self.data.text.borrow_mut();
|
let mut text = self.data.text.borrow_mut();
|
||||||
text.register_font(family, data)?;
|
update(&mut text)?;
|
||||||
text.invalidate_all()
|
text.invalidate_all()
|
||||||
};
|
};
|
||||||
let active: Vec<WidgetId> = {
|
let active: Vec<WidgetId> = {
|
||||||
|
|||||||
@@ -1006,7 +1006,7 @@ impl UiRenderState {
|
|||||||
let inst = self.primitives.instance(slot);
|
let inst = self.primitives.instance(slot);
|
||||||
let delta = self.resolve_move_chain(inst.move_idx, rsc);
|
let delta = self.resolve_move_chain(inst.move_idx, rsc);
|
||||||
let size = self.output_size;
|
let size = self.output_size;
|
||||||
let corner = |c: UiVec2| (c.get_rel() * size).floor() + c.get_abs().floor() + delta;
|
let corner = |c: UiVec2| (c.get_rel() * size).floor() + (c.get_abs() + delta).floor();
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: corner(inst.region.top_left()),
|
top_left: corner(inst.region.top_left()),
|
||||||
bot_right: corner(inst.region.bot_right()),
|
bot_right: corner(inst.region.bot_right()),
|
||||||
|
|||||||
@@ -362,6 +362,33 @@ fn scrolling_moves_in_o1_without_a_redraw() {
|
|||||||
assert_eq!(moves, 1, "the scrolled subtree should move in one write");
|
assert_eq!(moves, 1, "the scrolled subtree should move in one write");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractional_scrolling_keeps_primitive_edges_on_physical_pixels() {
|
||||||
|
let mut rsc = TestRsc { ui: Ui::default() };
|
||||||
|
let (scroll, root, rects) = scrolled_rects(&mut rsc, 20);
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((800.0, 600.0));
|
||||||
|
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-37.25);
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
|
let slot = render
|
||||||
|
.first_primitive(rects[2].id())
|
||||||
|
.expect("the scrolled rect has a primitive");
|
||||||
|
let corners = render.primitive_corners(slot, &rsc);
|
||||||
|
for edge in [
|
||||||
|
corners.top_left.x,
|
||||||
|
corners.top_left.y,
|
||||||
|
corners.bot_right.x,
|
||||||
|
corners.bot_right.y,
|
||||||
|
] {
|
||||||
|
assert_eq!(edge.fract(), 0.0, "fractional rendered edge: {corners:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hit_testing_follows_a_scrolled_widget() {
|
fn hit_testing_follows_a_scrolled_widget() {
|
||||||
let mut rsc = TestRsc { ui: Ui::default() };
|
let mut rsc = TestRsc { ui: Ui::default() };
|
||||||
|
|||||||
Reference in new issue
Block a user