//! Glyph texture atlas: shelf-packing allocator for GPU text rendering. //! //! Packs rasterized glyph bitmaps into one or more R8 (grayscale) texture //! pages using a shelf-packing algorithm. Returns UV coordinates for each //! glyph so the Metal renderer can emit textured quads. use std::collections::HashMap; use we_css::values::Color; use we_layout::TextLine; use we_text::font::{Font, GlyphBitmap, GlyphCache}; // --------------------------------------------------------------------------- // Atlas region // --------------------------------------------------------------------------- /// Location of a glyph bitmap within the atlas. #[derive(Debug, Clone, Copy)] pub struct AtlasRegion { /// X offset in the atlas page (pixels). pub x: u32, /// Y offset in the atlas page (pixels). pub y: u32, /// Width of the glyph bitmap (pixels). pub width: u32, /// Height of the glyph bitmap (pixels). pub height: u32, /// Horizontal bearing from the glyph origin to the left edge of the bitmap. pub bearing_x: i32, /// Vertical bearing from the glyph origin (baseline) to the top edge. pub bearing_y: i32, /// Which atlas page this glyph lives in. pub page: usize, } // --------------------------------------------------------------------------- // Textured quad // --------------------------------------------------------------------------- /// A positioned, textured quad for GPU rendering of a single glyph. #[derive(Debug, Clone, Copy)] pub struct TexturedQuad { /// Screen X position (pixels). pub x: f32, /// Screen Y position (pixels). pub y: f32, /// Quad width (pixels). pub width: f32, /// Quad height (pixels). pub height: f32, /// Left UV coordinate (0.0–1.0). pub u0: f32, /// Top UV coordinate (0.0–1.0). pub v0: f32, /// Right UV coordinate (0.0–1.0). pub u1: f32, /// Bottom UV coordinate (0.0–1.0). pub v1: f32, /// RGBA text color (0.0–1.0). pub color: [f32; 4], /// Atlas page index. pub page: usize, } // --------------------------------------------------------------------------- // Shelf // --------------------------------------------------------------------------- /// A horizontal shelf (row) in the atlas page. struct Shelf { /// Y offset of this shelf in the page. y: u32, /// Height of this shelf (determined by the tallest glyph placed on it). height: u32, /// Next free X position on this shelf. cursor_x: u32, } // --------------------------------------------------------------------------- // Atlas page // --------------------------------------------------------------------------- /// A single atlas texture page. struct AtlasPage { width: u32, height: u32, /// R8 grayscale pixel data. pixels: Vec, shelves: Vec, /// True when pixel data has changed since last GPU upload. dirty: bool, } impl AtlasPage { fn new(width: u32, height: u32) -> Self { AtlasPage { width, height, pixels: vec![0; (width * height) as usize], shelves: Vec::new(), dirty: true, } } /// Try to allocate a rectangle of `w × h` pixels (including padding). /// Returns `(x, y)` on success. fn allocate(&mut self, w: u32, h: u32, padding: u32) -> Option<(u32, u32)> { let padded_w = w + padding; let padded_h = h + padding; // Try to fit on an existing shelf. for shelf in &mut self.shelves { if shelf.height >= padded_h && shelf.cursor_x + padded_w <= self.width { let x = shelf.cursor_x; let y = shelf.y; shelf.cursor_x += padded_w; return Some((x, y)); } } // Open a new shelf. let shelf_y = self.shelves.last().map(|s| s.y + s.height).unwrap_or(0); if shelf_y + padded_h > self.height || padded_w > self.width { return None; // Page is full. } self.shelves.push(Shelf { y: shelf_y, height: padded_h, cursor_x: padded_w, }); Some((0, shelf_y)) } /// Copy a glyph bitmap into the page at `(x, y)`. fn blit(&mut self, x: u32, y: u32, bitmap: &GlyphBitmap) { for row in 0..bitmap.height { let src_start = (row * bitmap.width) as usize; let src_end = src_start + bitmap.width as usize; let dst_start = ((y + row) * self.width + x) as usize; let dst_end = dst_start + bitmap.width as usize; if src_end <= bitmap.data.len() && dst_end <= self.pixels.len() { self.pixels[dst_start..dst_end].copy_from_slice(&bitmap.data[src_start..src_end]); } } self.dirty = true; } } // --------------------------------------------------------------------------- // Glyph atlas // --------------------------------------------------------------------------- /// Cache key: (font_id, glyph_id, quantized_size_px). /// /// The font id distinguishes glyphs from different fonts (e.g. a primary font /// and a per-character CJK fallback) so their overlapping glyph ids never /// collide in the atlas. type GlyphKey = (u32, u16, u16); /// Default atlas page dimensions. const DEFAULT_PAGE_SIZE: u32 = 1024; /// Padding between glyphs to prevent texture bleeding. const GLYPH_PADDING: u32 = 1; /// GPU glyph texture atlas using shelf-packing. /// /// Rasterized glyph bitmaps are packed into R8 (single-channel grayscale) /// texture pages. When a page fills up, a new one is created. The atlas /// tracks which glyphs are placed where so the Metal renderer can emit /// textured quads with correct UV coordinates. pub struct GlyphAtlas { pages: Vec, entries: HashMap, page_size: u32, } impl GlyphAtlas { /// Create a new empty glyph atlas with the default page size (1024×1024). pub fn new() -> Self { GlyphAtlas { pages: Vec::new(), entries: HashMap::new(), page_size: DEFAULT_PAGE_SIZE, } } /// Create a new glyph atlas with a custom page size. pub fn with_page_size(page_size: u32) -> Self { GlyphAtlas { pages: Vec::new(), entries: HashMap::new(), page_size, } } /// Look up a glyph in the atlas. Returns `None` if not yet inserted. pub fn lookup(&self, font_id: u32, glyph_id: u16, size_key: u16) -> Option<&AtlasRegion> { self.entries.get(&(font_id, glyph_id, size_key)) } /// Insert a glyph bitmap into the atlas. Returns the region on success. /// /// If the glyph is already in the atlas, returns the existing region. /// `font_id` identifies which font (the primary or a per-character /// fallback) the glyph belongs to; the glyph is rasterized from that font /// via [`Font::glyph_bitmap_for`] using `font` as the entry point. pub fn get_or_insert( &mut self, font_id: u32, glyph_id: u16, size_px: f32, font: &Font, ) -> Option { let size_key = GlyphCache::quantize_size(size_px); // Already in atlas? if let Some(region) = self.entries.get(&(font_id, glyph_id, size_key)) { return Some(*region); } // Rasterize the glyph from whichever font (primary/fallback) owns it. let bitmap = font.glyph_bitmap_for(font_id, glyph_id, size_px)?; self.insert_bitmap(font_id, glyph_id, size_key, &bitmap) } /// Insert a pre-rasterized glyph bitmap into the atlas. pub fn insert_bitmap( &mut self, font_id: u32, glyph_id: u16, size_key: u16, bitmap: &GlyphBitmap, ) -> Option { if let Some(region) = self.entries.get(&(font_id, glyph_id, size_key)) { return Some(*region); } if bitmap.width == 0 || bitmap.height == 0 { return None; } // Try to allocate in an existing page. for (page_idx, page) in self.pages.iter_mut().enumerate() { if let Some((x, y)) = page.allocate(bitmap.width, bitmap.height, GLYPH_PADDING) { page.blit(x, y, bitmap); let region = AtlasRegion { x, y, width: bitmap.width, height: bitmap.height, bearing_x: bitmap.bearing_x, bearing_y: bitmap.bearing_y, page: page_idx, }; self.entries.insert((font_id, glyph_id, size_key), region); return Some(region); } } // All pages full — create a new one. let page_w = self.page_size.max(bitmap.width + GLYPH_PADDING); let page_h = self.page_size.max(bitmap.height + GLYPH_PADDING); let mut page = AtlasPage::new(page_w, page_h); let (x, y) = page.allocate(bitmap.width, bitmap.height, GLYPH_PADDING)?; page.blit(x, y, bitmap); let page_idx = self.pages.len(); self.pages.push(page); let region = AtlasRegion { x, y, width: bitmap.width, height: bitmap.height, bearing_x: bitmap.bearing_x, bearing_y: bitmap.bearing_y, page: page_idx, }; self.entries.insert((font_id, glyph_id, size_key), region); Some(region) } /// Number of atlas pages. pub fn page_count(&self) -> usize { self.pages.len() } /// Dimensions of a given page. pub fn page_size(&self, page: usize) -> Option<(u32, u32)> { self.pages.get(page).map(|p| (p.width, p.height)) } /// R8 pixel data for a given page. pub fn page_pixels(&self, page: usize) -> Option<&[u8]> { self.pages.get(page).map(|p| p.pixels.as_slice()) } /// Whether a page has been modified since the last `clear_dirty` call. pub fn is_page_dirty(&self, page: usize) -> bool { self.pages.get(page).map(|p| p.dirty).unwrap_or(false) } /// Mark a page as clean (after GPU upload). pub fn clear_dirty(&mut self, page: usize) { if let Some(p) = self.pages.get_mut(page) { p.dirty = false; } } /// Number of cached glyph entries. pub fn glyph_count(&self) -> usize { self.entries.len() } /// Reset the atlas: remove all pages and cached entries. pub fn clear(&mut self) { self.pages.clear(); self.entries.clear(); } /// Estimated heap bytes used by the atlas (pixel data + metadata). pub fn memory_usage(&self) -> usize { let page_pixels: usize = self.pages.iter().map(|p| p.pixels.capacity()).sum(); let page_shelves: usize = self .pages .iter() .map(|p| p.shelves.capacity() * std::mem::size_of::()) .sum(); let page_structs = self.pages.capacity() * std::mem::size_of::(); let entry_map = self.entries.capacity() * (std::mem::size_of::() + std::mem::size_of::() + 8); page_pixels + page_shelves + page_structs + entry_map } /// Utilization percentage: fraction of allocated atlas pixels actually used by glyphs. pub fn utilization(&self) -> f32 { if self.pages.is_empty() { return 0.0; } let total_pixels: u64 = self .pages .iter() .map(|p| p.width as u64 * p.height as u64) .sum(); if total_pixels == 0 { return 0.0; } let used_pixels: u64 = self .entries .values() .map(|r| r.width as u64 * r.height as u64) .sum(); (used_pixels as f32 / total_pixels as f32) * 100.0 } /// Build textured quads for a `TextLine`, inserting any missing glyphs. /// /// Each glyph in the text becomes a `TexturedQuad` with UV coordinates /// pointing into the atlas. Glyphs with no outline (e.g. space) are /// skipped. pub fn build_text_quads( &mut self, line: &TextLine, font: &Font, scale: f32, ) -> Vec { let size_px = line.font_size; // Rasterize glyphs at device resolution (size × backing scale) so they // stay crisp, but keep quad geometry in logical coordinates by dividing // the device-pixel bitmap metrics back down by `scale`. Shaping uses // the logical size for advances/offsets. let render_px = size_px * scale; let raster_px = GlyphCache::quantize_size(render_px) as f32; let bitmap_to_logical = if raster_px > 0.0 { (render_px / raster_px) / scale } else { 1.0 / scale }; let shaped = font.shape_text(&line.text, size_px); let color = color_to_f32(&line.color); let synthesize_bold = should_synthesize_bold(line, font); let mut quads = Vec::with_capacity(shaped.len()); for (glyph_index, sg) in shaped.iter().enumerate() { let region = match self.get_or_insert(sg.font_id, sg.glyph_id, render_px, font) { Some(r) => r, None => continue, // no outline (space, etc.) }; let (page_w, page_h) = match self.page_size(region.page) { Some(s) => s, None => continue, }; // Screen position: TextLine origin + glyph offset + bearing. // // `TextLine.y` is the top of the fragment's inline box. The // baseline offset comes from layout's resolved font metrics. // `bearing_y` is the distance from the baseline up to the glyph's // top edge, so the bitmap top sits at `baseline - bearing_y`. // Bitmap metrics are device px → divide by `scale` for logical // quad geometry. let baseline = line.y + line.baseline_offset; let gx = line.x + sg.x_offset + line.letter_spacing * glyph_index as f32 + region.bearing_x as f32 * bitmap_to_logical; let gy = baseline - region.bearing_y as f32 * bitmap_to_logical; // UV coordinates in the atlas page. let u0 = region.x as f32 / page_w as f32; let v0 = region.y as f32 / page_h as f32; let u1 = (region.x + region.width) as f32 / page_w as f32; let v1 = (region.y + region.height) as f32 / page_h as f32; quads.push(TexturedQuad { x: gx, y: gy, width: region.width as f32 * bitmap_to_logical, height: region.height as f32 * bitmap_to_logical, u0, v0, u1, v1, color, page: region.page, }); if synthesize_bold { // Approximate CSS bold by overstriking the glyph mask only // when font resolution fell back to a non-bold face. Advances // stay unchanged, matching browser synthetic-bold behavior for // fallback faces and web-font load failures. let offset = synthetic_bold_offset(size_px); quads.push(TexturedQuad { x: gx + offset, y: gy, width: region.width as f32 * bitmap_to_logical, height: region.height as f32 * bitmap_to_logical, u0, v0, u1, v1, color, page: region.page, }); } } quads } } fn should_synthesize_bold(line: &TextLine, font: &Font) -> bool { line.bold && resolved_font_weight_class(font) < 600 } fn resolved_font_weight_class(font: &Font) -> u16 { if let Ok(os2) = font.os2() { return os2.us_weight_class; } if let Ok(head) = font.head() { if head.mac_style & 0x01 != 0 { return 700; } } 400 } fn synthetic_bold_offset(size_px: f32) -> f32 { (size_px * 0.0125).clamp(0.35, 0.9) } impl Default for GlyphAtlas { fn default() -> Self { Self::new() } } /// Convert a CSS Color (0–255) to an [f32; 4] (0.0–1.0). fn color_to_f32(c: &Color) -> [f32; 4] { [ c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, c.a as f32 / 255.0, ] } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use we_style::computed::TextDecoration; use we_text::font::{Font, FontRegistry}; /// Create a fake glyph bitmap for testing. fn make_bitmap(w: u32, h: u32) -> GlyphBitmap { GlyphBitmap { width: w, height: h, bearing_x: 1, bearing_y: h as i32, data: vec![128; (w * h) as usize], } } fn text_line(text: &str, letter_spacing: f32) -> TextLine { TextLine { text: text.to_string(), x: 10.0, y: 20.0, width: 0.0, font_size: 18.0, baseline_offset: 18.0, line_height: 18.0 * 1.2, font_weight: 400.0, letter_spacing, font_family: String::new(), color: Color::rgb(0, 0, 0), text_shadows: Vec::new(), text_decoration: TextDecoration::None, background_color: Color::new(0, 0, 0, 0), bold: false, italic: false, } } #[test] fn empty_atlas() { let atlas = GlyphAtlas::new(); assert_eq!(atlas.page_count(), 0); assert_eq!(atlas.glyph_count(), 0); assert!(atlas.lookup(0, 0, 16).is_none()); } #[test] fn insert_single_glyph() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(10, 12); let region = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); assert_eq!(region.x, 0); assert_eq!(region.y, 0); assert_eq!(region.width, 10); assert_eq!(region.height, 12); assert_eq!(region.page, 0); assert_eq!(atlas.page_count(), 1); assert_eq!(atlas.glyph_count(), 1); } #[test] fn insert_duplicate_returns_same_region() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(10, 12); let r1 = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); let r2 = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); assert_eq!(r1.x, r2.x); assert_eq!(r1.y, r2.y); assert_eq!(r1.page, r2.page); assert_eq!(atlas.glyph_count(), 1); } #[test] fn shelf_packing_same_row() { let mut atlas = GlyphAtlas::with_page_size(256); let bm1 = make_bitmap(10, 12); let bm2 = make_bitmap(8, 10); let r1 = atlas.insert_bitmap(0, 1, 16, &bm1).unwrap(); let r2 = atlas.insert_bitmap(0, 2, 16, &bm2).unwrap(); // Both should be on the same shelf (same page). assert_eq!(r1.page, 0); assert_eq!(r2.page, 0); assert_eq!(r1.y, r2.y); // r2 should be to the right of r1 (with padding). assert_eq!(r2.x, r1.width + GLYPH_PADDING); } #[test] fn new_shelf_when_row_full() { let mut atlas = GlyphAtlas::with_page_size(32); // Each glyph is 16 + 1 padding = 17 pixels wide. // Two don't fit in 32 pixels, so the second goes to a new shelf. let bm = make_bitmap(16, 10); let r1 = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); let r2 = atlas.insert_bitmap(0, 2, 16, &bm).unwrap(); assert_eq!(r1.y, 0); assert!(r2.y > 0); assert_eq!(r1.page, r2.page); } #[test] fn new_page_when_full() { // Tiny page that can only fit one glyph. let mut atlas = GlyphAtlas::with_page_size(16); let bm = make_bitmap(14, 14); let r1 = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); let r2 = atlas.insert_bitmap(0, 2, 16, &bm).unwrap(); assert_eq!(r1.page, 0); assert_eq!(r2.page, 1); assert_eq!(atlas.page_count(), 2); } #[test] fn zero_size_bitmap_returns_none() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(0, 0); assert!(atlas.insert_bitmap(0, 1, 16, &bm).is_none()); } #[test] fn page_dirty_flag() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(10, 12); atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); assert!(atlas.is_page_dirty(0)); atlas.clear_dirty(0); assert!(!atlas.is_page_dirty(0)); // Inserting another glyph dirties it again. let bm2 = make_bitmap(8, 8); atlas.insert_bitmap(0, 2, 16, &bm2).unwrap(); assert!(atlas.is_page_dirty(0)); } #[test] fn page_pixels_correct() { let mut atlas = GlyphAtlas::with_page_size(64); let bm = make_bitmap(4, 3); let region = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); let pixels = atlas.page_pixels(0).unwrap(); // Check that the bitmap was blitted correctly. for row in 0..3u32 { for col in 0..4u32 { let idx = ((region.y + row) * 64 + region.x + col) as usize; assert_eq!(pixels[idx], 128, "pixel at ({col}, {row})"); } } } #[test] fn clear_resets_atlas() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(10, 12); atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); assert_eq!(atlas.page_count(), 1); assert_eq!(atlas.glyph_count(), 1); atlas.clear(); assert_eq!(atlas.page_count(), 0); assert_eq!(atlas.glyph_count(), 0); } #[test] fn uv_coordinates_in_range() { let mut atlas = GlyphAtlas::with_page_size(256); let bm = make_bitmap(10, 12); let region = atlas.insert_bitmap(0, 1, 16, &bm).unwrap(); let (page_w, page_h) = atlas.page_size(region.page).unwrap(); let u0 = region.x as f32 / page_w as f32; let v0 = region.y as f32 / page_h as f32; let u1 = (region.x + region.width) as f32 / page_w as f32; let v1 = (region.y + region.height) as f32 / page_h as f32; assert!(u0 >= 0.0 && u0 <= 1.0); assert!(v0 >= 0.0 && v0 <= 1.0); assert!(u1 >= 0.0 && u1 <= 1.0); assert!(v1 >= 0.0 && v1 <= 1.0); assert!(u1 > u0); assert!(v1 > v0); } #[test] fn many_glyphs_across_shelves() { let mut atlas = GlyphAtlas::with_page_size(128); // Insert many small glyphs — should fill multiple shelves. for glyph_id in 0..50u16 { let bm = make_bitmap(8, 10); let region = atlas.insert_bitmap(0, glyph_id, 16, &bm); assert!(region.is_some(), "glyph {glyph_id} should be allocated"); } // All should be findable. for glyph_id in 0..50u16 { assert!( atlas.lookup(0, glyph_id, 16).is_some(), "glyph {glyph_id} should be in atlas" ); } } #[test] fn different_sizes_different_entries() { let mut atlas = GlyphAtlas::with_page_size(256); let bm_small = make_bitmap(8, 10); let bm_large = make_bitmap(16, 20); let r1 = atlas.insert_bitmap(0, 1, 12, &bm_small).unwrap(); let r2 = atlas.insert_bitmap(0, 1, 24, &bm_large).unwrap(); // Same glyph_id but different size_key → different entries. assert_eq!(atlas.glyph_count(), 2); assert!(r1.width != r2.width || r1.height != r2.height); } #[test] fn textured_quad_color_conversion() { let c = Color { r: 255, g: 128, b: 0, a: 255, }; let f = color_to_f32(&c); assert!((f[0] - 1.0).abs() < 0.01); assert!((f[1] - 0.502).abs() < 0.01); assert!((f[2] - 0.0).abs() < 0.01); assert!((f[3] - 1.0).abs() < 0.01); } #[test] fn text_quads_apply_letter_spacing() { let font = we_text::font::load_system_font().expect("system font"); let mut atlas = GlyphAtlas::with_page_size(256); let normal = atlas.build_text_quads(&text_line("AA", 0.0), &font, 1.0); let spaced = atlas.build_text_quads(&text_line("AA", 4.0), &font, 1.0); assert!( normal.len() >= 2 && spaced.len() >= 2, "expected two visible glyph quads" ); assert!((spaced[0].x - normal[0].x).abs() < 0.01); assert!( (spaced[1].x - normal[1].x - 4.0).abs() < 0.01, "second glyph should shift by the configured letter spacing" ); } #[test] fn text_quads_scale_quantized_bitmaps_to_requested_fractional_size() { let font = we_text::font::load_system_font().expect("system font"); let mut atlas = GlyphAtlas::with_page_size(256); let mut whole = text_line("H", 0.0); whole.font_size = 16.0; whole.baseline_offset = 16.0; let whole_quads = atlas.build_text_quads(&whole, &font, 1.0); let mut fractional = text_line("H", 0.0); fractional.font_size = 15.5; fractional.baseline_offset = 15.5; let fractional_quads = atlas.build_text_quads(&fractional, &font, 1.0); if whole_quads.is_empty() || fractional_quads.is_empty() { return; } let expected = fractional.font_size / whole.font_size; let actual = fractional_quads[0].height / whole_quads[0].height; assert!( (actual - expected).abs() < 0.02, "fractional glyph quad height should scale by {expected}, got {actual}" ); } #[test] fn synthetic_bold_is_skipped_for_resolved_bold_face() { let Some((regular, bold)) = regular_and_bold_system_faces() else { return; }; let mut line = text_line("AA", 0.0); line.bold = true; line.font_weight = 700.0; assert!(should_synthesize_bold(&line, ®ular)); assert!(!should_synthesize_bold(&line, &bold)); let mut atlas = GlyphAtlas::with_page_size(512); let regular_quads = atlas.build_text_quads(&line, ®ular, 1.0); let bold_quads = atlas.build_text_quads(&line, &bold, 1.0); if regular_quads.is_empty() || bold_quads.is_empty() { return; } assert_eq!( regular_quads.len(), bold_quads.len() * 2, "fallback regular faces are overstruck, real bold faces are not" ); } fn regular_and_bold_system_faces() -> Option<(Font, Font)> { let registry = FontRegistry::new(); for family in registry.list_families() { let Some(entries) = registry.family_entries(&family) else { continue; }; let has_regular = entries .iter() .any(|entry| !entry.italic && entry.weight < 600); let has_bold = entries .iter() .any(|entry| !entry.italic && entry.weight >= 600); if !has_regular || !has_bold { continue; } let Some(regular) = registry.find_font_with_weight(&family, 400, false) else { continue; }; let Some(bold) = registry.find_font_with_weight(&family, 700, false) else { continue; }; if resolved_font_weight_class(®ular) < 600 && resolved_font_weight_class(&bold) >= 600 { return Some((regular, bold)); } } None } #[test] fn large_glyph_exceeding_page_creates_larger_page() { let mut atlas = GlyphAtlas::with_page_size(32); // Glyph larger than the default page size. let bm = make_bitmap(48, 48); let region = atlas.insert_bitmap(0, 1, 16, &bm); assert!( region.is_some(), "large glyph should create an oversized page" ); let (pw, ph) = atlas.page_size(0).unwrap(); assert!(pw >= 48); assert!(ph >= 48); } #[test] fn atlas_utilization_metrics() { let mut atlas = GlyphAtlas::with_page_size(64); // Insert a few glyphs and check counts. for i in 0..5u16 { let bm = make_bitmap(10, 10); atlas.insert_bitmap(0, i, 16, &bm).unwrap(); } assert_eq!(atlas.glyph_count(), 5); assert!(atlas.page_count() >= 1); } #[test] fn lookup_returns_none_for_missing() { let atlas = GlyphAtlas::new(); assert!(atlas.lookup(0, 42, 16).is_none()); assert!(atlas.page_pixels(0).is_none()); assert!(atlas.page_size(0).is_none()); } #[test] fn dirty_flag_nonexistent_page() { let atlas = GlyphAtlas::new(); assert!(!atlas.is_page_dirty(0)); } #[test] fn synthetic_bold_offset_scales_with_size() { assert_eq!(synthetic_bold_offset(8.0), 0.35); assert!(synthetic_bold_offset(48.0) > synthetic_bold_offset(16.0)); assert_eq!(synthetic_bold_offset(100.0), 0.9); } #[test] fn clear_dirty_nonexistent_page() { let mut atlas = GlyphAtlas::new(); // Should not panic. atlas.clear_dirty(99); } }