iris: add replaceable glyph atlas buckets

This commit is contained in:
iris committed 2026-09-12 18:10:55 -04:00
1 parent c97df72015
commit 90dffce514
12 files changed
+401 -91

No files matched your search

+37
View File
@@ -550,6 +550,43 @@ mod apply_tests {
assert!(!rsc.widgets().has_updates());
}
#[test]
fn replacing_a_font_only_clears_its_glyph_bucket() {
let mut rsc = TestRsc {
ui: Ui::default(),
events: EventManager::default(),
};
rsc.ui.resize((800.0, 600.0));
rsc.ui
.register_font_in("first-icons", "first", ICON_FONT)
.unwrap();
rsc.ui
.register_font_in("second-icons", "second", ICON_FONT)
.unwrap();
let first = wtext(icon::OPEN)
.family("first-icons")
.add_strong(&mut rsc)
.any();
let second = wtext(icon::OPEN)
.family("second-icons")
.add_strong(&mut rsc)
.any();
rsc.draw(&first);
rsc.draw(&second);
let glyphs_before = rsc.ui.text.borrow().atlas.glyph_count();
let pages_before = rsc.ui.text.borrow().atlas.page_count();
assert_eq!((glyphs_before, pages_before), (2, 2));
rsc.ui.replace_font("first-icons", ICON_FONT).unwrap();
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 1);
assert_eq!(rsc.ui.text.borrow().atlas.page_count(), 1);
rsc.draw(&first);
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 2);
assert_eq!(rsc.ui.text.borrow().atlas.page_count(), 2);
}
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
let mut rsc = TestRsc {
ui: Ui::default(),
+7
View File
@@ -286,6 +286,13 @@ accepted duplication rather than threading a `Painter` into every input
handler for one field, the same tradeoff `AndroidRenderer::content_scale`
already makes for the Diagnostics page.
Glyph masks are cached at four horizontal quarter-pixel phases. Their final
quad edges snap to physical pixels after retained move offsets are applied;
the CPU mask geometry uses the same calculation as the shader. In particular,
a fractional scroll offset therefore moves text and other primitives in whole
physical-pixel steps instead of resampling the atlas vertically with the
nearest sampler.
**What did not change**: `rel`/`rest` are unaffected (already
resolution-independent, a fraction of the parent). `Span::gap` and
`Padding`'s four sides moved from bare `f32` to `Len` so `dp(...)` works
+10 -3
View File
@@ -823,9 +823,16 @@ a text widget's `TextHandle` wraps the UI-local `RscHandle` into it. The same
generic arena owns texture and managed-paint lifetimes. Its `StrongRscId` and
`WeakRscId` are sendable IDs rather than data pointers; clone and drop events
cross one arena-owned standard channel, and reference counts remain in the
arena instead of allocating one atomic counter per resource. Registering
another family invalidates the live text entries and dirties their active owner
widgets, so registration is also valid after the first shape. ai-app owns the
arena instead of allocating one atomic counter per resource. The ordinary
`register_font(family, data)` call needs no policy argument and uses the shared
default glyph-atlas bucket; `register_font_in` opts a reloadable or sparse font
into a named bucket. `replace_font` unregisters the old faces, releases only
that bucket's pages, and invalidates the live text entries so their active owner
widgets reshape automatically. Freed array layers are reused without moving
live pages. Glyph rasterisation retains four horizontal quarter-pixel phases,
while final primitive edges (including retained scroll moves) snap to physical
pixels; this favours low-DPI text stability and keeps vertical scrolling from
resampling atlas masks. ai-app owns the
`ai-app-icons` family, its Nerd Fonts subset, its codepoints, its license and
the script that rebuilds it; the CSS generic names `sans-serif` and `monospace`
continue to resolve through the platform.
+5 -1
View File
@@ -18,7 +18,11 @@ Glyph atlas pages are layers of one `texture_2d_array`. `GpuTextures` doubles
the array when it runs out of layers, copies the old layers on the GPU, and
rebuilds every bind group that referenced the old view. Page numbers are
assigned synchronously by `Textures::add_page` because glyph insertion needs
the layer before the renderer processes queued texture updates.
the layer before the renderer processes queued texture updates. Atlas maps and
pages are grouped into named buckets: ordinary font registration uses one
shared default bucket, while callers can isolate fonts they expect to replace.
Replacing a font drops its bucket. The resource slots and array layers from
those pages are reused without compacting or changing any live page number.
Standalone images each own a bind group and are not placed in the glyph
array. Each render layer keeps ordinary rect/glyph instances separately from
+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();
+28 -26
View File
@@ -46,8 +46,10 @@ pub struct Textures {
/// drawing it and its slot is never recycled underneath one.
shared: HashMap<SharedTextureKey, TextureHandle>,
/// 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,
free_page_layers: Vec<u32>,
updates: Vec<Update>,
}
@@ -87,6 +89,7 @@ impl Textures {
kinds: Vec::new(),
shared: HashMap::new(),
next_page_layer: 0,
free_page_layers: Vec::new(),
updates: Vec::new(),
}
}
@@ -95,16 +98,20 @@ impl Textures {
let image = image.into();
let size = image.dimensions().into();
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 {
self.free();
let image = image.into();
let size = image.dimensions().into();
let layer = self.next_page_layer;
self.next_page_layer += 1;
let layer = self.free_page_layers.pop().unwrap_or_else(|| {
let layer = self.next_page_layer;
self.next_page_layer += 1;
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 {
@@ -119,22 +126,10 @@ impl Textures {
Some(self.handle(id))
}
fn push(
&mut self,
kind: TextureKind,
size: Vec2,
image: DynamicImage,
recycle: bool,
) -> TextureHandle {
fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle {
self.free();
let old_capacity = self.resources.borrow().capacity();
let id = if recycle {
self.resources.borrow_mut().add(TextureRsc { kind, size })
} else {
self.resources
.borrow_mut()
.add_unrecycled(TextureRsc { kind, size })
};
let id = self.resources.borrow_mut().add(TextureRsc { kind, size });
let i = id.slot();
if (i as usize) < old_capacity {
self.images[i as usize] = Some(image);
@@ -201,10 +196,14 @@ impl Textures {
pub fn free(&mut self) {
let updates = &mut self.updates;
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();
images[idx as usize] = None;
updates.push(Update::Free(idx));
if let TextureKind::Page { layer } = resource.kind {
free_page_layers.push(layer);
}
});
}
@@ -321,16 +320,19 @@ mod tests {
}
#[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 page = textures.add_page(image(4));
let page_slot = page.rsc.id().slot();
drop(page);
let first = textures.add_page(image(4));
let second = textures.add_page(image(4));
let first_layer = first.layer();
let second_layer = second.layer();
drop(first);
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]
+54 -32
View File
@@ -6,6 +6,7 @@ use image::RgbaImage;
use swash::scale::image::{Content, Image};
pub(crate) const PAGE: u32 = 1024;
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
const PAD: u32 = 1;
@@ -42,40 +43,47 @@ struct Page {
}
#[derive(Default)]
pub struct GlyphAtlas {
struct Bucket {
pages: Vec<Page>,
generation: u64,
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
}
#[derive(Default)]
pub struct GlyphAtlas {
buckets: HashMap<u64, Bucket>,
generation: u64,
}
impl GlyphAtlas {
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.entries.get(key).copied()
pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.buckets.get(&bucket)?.entries.get(key).copied()
}
/// 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.
pub fn insert(
&mut self,
bucket: u64,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
let bucket = self.buckets.entry(bucket).or_default();
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
self.entries.insert(key, None);
bucket.entries.insert(key, None);
return None;
}
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
// A single glyph larger than a page. Refusing is better than
// silently drawing a cropped one; the caller draws nothing.
self.entries.insert(key, None);
bucket.entries.insert(key, None);
return None;
}
let (page_idx, x, y) = self.allocate(w, h, textures);
let page = &self.pages[page_idx];
let (page_idx, x, y) = bucket.allocate(w, h, textures);
let page = &bucket.pages[page_idx];
let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
@@ -89,7 +97,7 @@ impl GlyphAtlas {
};
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 entry = GlyphEntry {
uv_min: [x as f32 * scale, y as f32 * scale],
@@ -101,10 +109,46 @@ impl GlyphAtlas {
is_color: matches!(image.content, Content::Color),
layer: page.handle.layer(),
};
self.entries.insert(key, Some(entry));
bucket.entries.insert(key, 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) {
let need_w = w + PAD;
let need_h = h + PAD;
@@ -130,28 +174,6 @@ impl GlyphAtlas {
});
(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 {
+2 -2
View File
@@ -137,8 +137,8 @@ fn corners_of(inst: PrimitiveInstance) -> Corners {
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
let move_delta = resolve_move(inst.move_idx);
return Corners(
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(top_left_rel * window.dim) + floor(top_left_abs + move_delta),
floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta),
);
}
+9 -7
View File
@@ -12,8 +12,8 @@ const MIN_ARRAY_LAYERS: u32 = 2;
enum Slot {
Empty,
Image(ImageGpu),
/// The array layer a page occupies. Pages are never freed (see
/// `Textures::free`), so this is the only variant that outlives a `Free`.
/// The array layer a live page occupies. Dropping the page empties this
/// resource slot; its array layer may then be assigned to a new page.
Page(u32),
}
@@ -41,7 +41,9 @@ pub struct GpuTextures {
array_texture: Texture,
array_view: TextureView,
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,
/// Bound in the image slot of the main draw's bind group, which has
@@ -125,7 +127,7 @@ impl GpuTextures {
rebuilt = true;
}
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)
}
}
@@ -205,7 +207,7 @@ impl GpuTextures {
self.pages_grown += 1;
let new_capacity = self.array_capacity * 2;
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
.device
.create_command_encoder(&CommandEncoderDescriptor {
@@ -227,7 +229,7 @@ impl GpuTextures {
Extent3d {
width: 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()));
@@ -368,7 +370,7 @@ impl GpuTextures {
array_texture,
array_view,
array_capacity,
page_count: 0,
layer_high_water: 0,
sampler,
null_view,
bind_group_creates: 0,
+43 -1
View File
@@ -75,10 +75,52 @@ impl Ui {
&mut self,
family: impl AsRef<str>,
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> {
let owners = {
let mut text = self.data.text.borrow_mut();
text.register_font(family, data)?;
update(&mut text)?;
text.invalidate_all()
};
let active: Vec<WidgetId> = {
+1 -1
View File
@@ -1006,7 +1006,7 @@ impl UiRenderState {
let inst = self.primitives.instance(slot);
let delta = self.resolve_move_chain(inst.move_idx, rsc);
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 {
top_left: corner(inst.region.top_left()),
bot_right: corner(inst.region.bot_right()),
+27
View File
@@ -362,6 +362,33 @@ fn scrolling_moves_in_o1_without_a_redraw() {
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]
fn hit_testing_follows_a_scrolled_widget() {
let mut rsc = TestRsc { ui: Ui::default() };