we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add glyph texture atlas for GPU text rendering

Implement a shelf-packing texture atlas in the render crate that packs
rasterized glyph bitmaps into R8 grayscale texture pages. The atlas
provides UV coordinates for each glyph and can generate textured quads
from TextLine data for the Metal rendering pipeline.

- Shelf-packing allocator with configurable page size (default 1024x1024)
- Multi-page support: automatically creates new pages when full
- Dirty tracking for efficient GPU texture uploads
- TexturedQuad generation from TextLine with correct UV mapping
- Integration with existing Font glyph cache for on-demand rasterization
- 19 comprehensive unit tests

Implements issue 3mi4366fkxs2g

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+656
+654
crates/render/src/atlas.rs
··· 1 + //! Glyph texture atlas: shelf-packing allocator for GPU text rendering. 2 + //! 3 + //! Packs rasterized glyph bitmaps into one or more R8 (grayscale) texture 4 + //! pages using a shelf-packing algorithm. Returns UV coordinates for each 5 + //! glyph so the Metal renderer can emit textured quads. 6 + 7 + use std::collections::HashMap; 8 + 9 + use we_css::values::Color; 10 + use we_layout::TextLine; 11 + use we_text::font::{Font, GlyphBitmap, GlyphCache}; 12 + 13 + // --------------------------------------------------------------------------- 14 + // Atlas region 15 + // --------------------------------------------------------------------------- 16 + 17 + /// Location of a glyph bitmap within the atlas. 18 + #[derive(Debug, Clone, Copy)] 19 + pub struct AtlasRegion { 20 + /// X offset in the atlas page (pixels). 21 + pub x: u32, 22 + /// Y offset in the atlas page (pixels). 23 + pub y: u32, 24 + /// Width of the glyph bitmap (pixels). 25 + pub width: u32, 26 + /// Height of the glyph bitmap (pixels). 27 + pub height: u32, 28 + /// Horizontal bearing from the glyph origin to the left edge of the bitmap. 29 + pub bearing_x: i32, 30 + /// Vertical bearing from the glyph origin (baseline) to the top edge. 31 + pub bearing_y: i32, 32 + /// Which atlas page this glyph lives in. 33 + pub page: usize, 34 + } 35 + 36 + // --------------------------------------------------------------------------- 37 + // Textured quad 38 + // --------------------------------------------------------------------------- 39 + 40 + /// A positioned, textured quad for GPU rendering of a single glyph. 41 + #[derive(Debug, Clone, Copy)] 42 + pub struct TexturedQuad { 43 + /// Screen X position (pixels). 44 + pub x: f32, 45 + /// Screen Y position (pixels). 46 + pub y: f32, 47 + /// Quad width (pixels). 48 + pub width: f32, 49 + /// Quad height (pixels). 50 + pub height: f32, 51 + /// Left UV coordinate (0.0–1.0). 52 + pub u0: f32, 53 + /// Top UV coordinate (0.0–1.0). 54 + pub v0: f32, 55 + /// Right UV coordinate (0.0–1.0). 56 + pub u1: f32, 57 + /// Bottom UV coordinate (0.0–1.0). 58 + pub v1: f32, 59 + /// RGBA text color (0.0–1.0). 60 + pub color: [f32; 4], 61 + /// Atlas page index. 62 + pub page: usize, 63 + } 64 + 65 + // --------------------------------------------------------------------------- 66 + // Shelf 67 + // --------------------------------------------------------------------------- 68 + 69 + /// A horizontal shelf (row) in the atlas page. 70 + struct Shelf { 71 + /// Y offset of this shelf in the page. 72 + y: u32, 73 + /// Height of this shelf (determined by the tallest glyph placed on it). 74 + height: u32, 75 + /// Next free X position on this shelf. 76 + cursor_x: u32, 77 + } 78 + 79 + // --------------------------------------------------------------------------- 80 + // Atlas page 81 + // --------------------------------------------------------------------------- 82 + 83 + /// A single atlas texture page. 84 + struct AtlasPage { 85 + width: u32, 86 + height: u32, 87 + /// R8 grayscale pixel data. 88 + pixels: Vec<u8>, 89 + shelves: Vec<Shelf>, 90 + /// True when pixel data has changed since last GPU upload. 91 + dirty: bool, 92 + } 93 + 94 + impl AtlasPage { 95 + fn new(width: u32, height: u32) -> Self { 96 + AtlasPage { 97 + width, 98 + height, 99 + pixels: vec![0; (width * height) as usize], 100 + shelves: Vec::new(), 101 + dirty: true, 102 + } 103 + } 104 + 105 + /// Try to allocate a rectangle of `w × h` pixels (including padding). 106 + /// Returns `(x, y)` on success. 107 + fn allocate(&mut self, w: u32, h: u32, padding: u32) -> Option<(u32, u32)> { 108 + let padded_w = w + padding; 109 + let padded_h = h + padding; 110 + 111 + // Try to fit on an existing shelf. 112 + for shelf in &mut self.shelves { 113 + if shelf.height >= padded_h && shelf.cursor_x + padded_w <= self.width { 114 + let x = shelf.cursor_x; 115 + let y = shelf.y; 116 + shelf.cursor_x += padded_w; 117 + return Some((x, y)); 118 + } 119 + } 120 + 121 + // Open a new shelf. 122 + let shelf_y = self.shelves.last().map(|s| s.y + s.height).unwrap_or(0); 123 + 124 + if shelf_y + padded_h > self.height || padded_w > self.width { 125 + return None; // Page is full. 126 + } 127 + 128 + self.shelves.push(Shelf { 129 + y: shelf_y, 130 + height: padded_h, 131 + cursor_x: padded_w, 132 + }); 133 + 134 + Some((0, shelf_y)) 135 + } 136 + 137 + /// Copy a glyph bitmap into the page at `(x, y)`. 138 + fn blit(&mut self, x: u32, y: u32, bitmap: &GlyphBitmap) { 139 + for row in 0..bitmap.height { 140 + let src_start = (row * bitmap.width) as usize; 141 + let src_end = src_start + bitmap.width as usize; 142 + let dst_start = ((y + row) * self.width + x) as usize; 143 + let dst_end = dst_start + bitmap.width as usize; 144 + if src_end <= bitmap.data.len() && dst_end <= self.pixels.len() { 145 + self.pixels[dst_start..dst_end].copy_from_slice(&bitmap.data[src_start..src_end]); 146 + } 147 + } 148 + self.dirty = true; 149 + } 150 + } 151 + 152 + // --------------------------------------------------------------------------- 153 + // Glyph atlas 154 + // --------------------------------------------------------------------------- 155 + 156 + /// Cache key: (glyph_id, quantized_size_px). 157 + type GlyphKey = (u16, u16); 158 + 159 + /// Default atlas page dimensions. 160 + const DEFAULT_PAGE_SIZE: u32 = 1024; 161 + 162 + /// Padding between glyphs to prevent texture bleeding. 163 + const GLYPH_PADDING: u32 = 1; 164 + 165 + /// GPU glyph texture atlas using shelf-packing. 166 + /// 167 + /// Rasterized glyph bitmaps are packed into R8 (single-channel grayscale) 168 + /// texture pages. When a page fills up, a new one is created. The atlas 169 + /// tracks which glyphs are placed where so the Metal renderer can emit 170 + /// textured quads with correct UV coordinates. 171 + pub struct GlyphAtlas { 172 + pages: Vec<AtlasPage>, 173 + entries: HashMap<GlyphKey, AtlasRegion>, 174 + page_size: u32, 175 + } 176 + 177 + impl GlyphAtlas { 178 + /// Create a new empty glyph atlas with the default page size (1024×1024). 179 + pub fn new() -> Self { 180 + GlyphAtlas { 181 + pages: Vec::new(), 182 + entries: HashMap::new(), 183 + page_size: DEFAULT_PAGE_SIZE, 184 + } 185 + } 186 + 187 + /// Create a new glyph atlas with a custom page size. 188 + pub fn with_page_size(page_size: u32) -> Self { 189 + GlyphAtlas { 190 + pages: Vec::new(), 191 + entries: HashMap::new(), 192 + page_size, 193 + } 194 + } 195 + 196 + /// Look up a glyph in the atlas. Returns `None` if not yet inserted. 197 + pub fn lookup(&self, glyph_id: u16, size_key: u16) -> Option<&AtlasRegion> { 198 + self.entries.get(&(glyph_id, size_key)) 199 + } 200 + 201 + /// Insert a glyph bitmap into the atlas. Returns the region on success. 202 + /// 203 + /// If the glyph is already in the atlas, returns the existing region. 204 + /// Rasterizes the glyph via `font` if a bitmap is not provided. 205 + pub fn get_or_insert( 206 + &mut self, 207 + glyph_id: u16, 208 + size_px: f32, 209 + font: &Font, 210 + ) -> Option<AtlasRegion> { 211 + let size_key = GlyphCache::quantize_size(size_px); 212 + 213 + // Already in atlas? 214 + if let Some(region) = self.entries.get(&(glyph_id, size_key)) { 215 + return Some(*region); 216 + } 217 + 218 + // Rasterize the glyph. 219 + let bitmap = font.get_glyph_bitmap(glyph_id, size_px)?; 220 + 221 + self.insert_bitmap(glyph_id, size_key, &bitmap) 222 + } 223 + 224 + /// Insert a pre-rasterized glyph bitmap into the atlas. 225 + pub fn insert_bitmap( 226 + &mut self, 227 + glyph_id: u16, 228 + size_key: u16, 229 + bitmap: &GlyphBitmap, 230 + ) -> Option<AtlasRegion> { 231 + if let Some(region) = self.entries.get(&(glyph_id, size_key)) { 232 + return Some(*region); 233 + } 234 + 235 + if bitmap.width == 0 || bitmap.height == 0 { 236 + return None; 237 + } 238 + 239 + // Try to allocate in an existing page. 240 + for (page_idx, page) in self.pages.iter_mut().enumerate() { 241 + if let Some((x, y)) = page.allocate(bitmap.width, bitmap.height, GLYPH_PADDING) { 242 + page.blit(x, y, bitmap); 243 + let region = AtlasRegion { 244 + x, 245 + y, 246 + width: bitmap.width, 247 + height: bitmap.height, 248 + bearing_x: bitmap.bearing_x, 249 + bearing_y: bitmap.bearing_y, 250 + page: page_idx, 251 + }; 252 + self.entries.insert((glyph_id, size_key), region); 253 + return Some(region); 254 + } 255 + } 256 + 257 + // All pages full — create a new one. 258 + let page_w = self.page_size.max(bitmap.width + GLYPH_PADDING); 259 + let page_h = self.page_size.max(bitmap.height + GLYPH_PADDING); 260 + let mut page = AtlasPage::new(page_w, page_h); 261 + let (x, y) = page.allocate(bitmap.width, bitmap.height, GLYPH_PADDING)?; 262 + page.blit(x, y, bitmap); 263 + 264 + let page_idx = self.pages.len(); 265 + self.pages.push(page); 266 + 267 + let region = AtlasRegion { 268 + x, 269 + y, 270 + width: bitmap.width, 271 + height: bitmap.height, 272 + bearing_x: bitmap.bearing_x, 273 + bearing_y: bitmap.bearing_y, 274 + page: page_idx, 275 + }; 276 + self.entries.insert((glyph_id, size_key), region); 277 + Some(region) 278 + } 279 + 280 + /// Number of atlas pages. 281 + pub fn page_count(&self) -> usize { 282 + self.pages.len() 283 + } 284 + 285 + /// Dimensions of a given page. 286 + pub fn page_size(&self, page: usize) -> Option<(u32, u32)> { 287 + self.pages.get(page).map(|p| (p.width, p.height)) 288 + } 289 + 290 + /// R8 pixel data for a given page. 291 + pub fn page_pixels(&self, page: usize) -> Option<&[u8]> { 292 + self.pages.get(page).map(|p| p.pixels.as_slice()) 293 + } 294 + 295 + /// Whether a page has been modified since the last `clear_dirty` call. 296 + pub fn is_page_dirty(&self, page: usize) -> bool { 297 + self.pages.get(page).map(|p| p.dirty).unwrap_or(false) 298 + } 299 + 300 + /// Mark a page as clean (after GPU upload). 301 + pub fn clear_dirty(&mut self, page: usize) { 302 + if let Some(p) = self.pages.get_mut(page) { 303 + p.dirty = false; 304 + } 305 + } 306 + 307 + /// Number of cached glyph entries. 308 + pub fn glyph_count(&self) -> usize { 309 + self.entries.len() 310 + } 311 + 312 + /// Reset the atlas: remove all pages and cached entries. 313 + pub fn clear(&mut self) { 314 + self.pages.clear(); 315 + self.entries.clear(); 316 + } 317 + 318 + /// Build textured quads for a `TextLine`, inserting any missing glyphs. 319 + /// 320 + /// Each glyph in the text becomes a `TexturedQuad` with UV coordinates 321 + /// pointing into the atlas. Glyphs with no outline (e.g. space) are 322 + /// skipped. 323 + pub fn build_text_quads(&mut self, line: &TextLine, font: &Font) -> Vec<TexturedQuad> { 324 + let size_px = line.font_size; 325 + let shaped = font.shape_text(&line.text, size_px); 326 + let color = color_to_f32(&line.color); 327 + 328 + let mut quads = Vec::with_capacity(shaped.len()); 329 + 330 + for sg in &shaped { 331 + let region = match self.get_or_insert(sg.glyph_id, size_px, font) { 332 + Some(r) => r, 333 + None => continue, // no outline (space, etc.) 334 + }; 335 + 336 + let (page_w, page_h) = match self.page_size(region.page) { 337 + Some(s) => s, 338 + None => continue, 339 + }; 340 + 341 + // Screen position: TextLine origin + glyph offset + bearing. 342 + let gx = line.x + sg.x_offset + region.bearing_x as f32; 343 + let gy = line.y - region.bearing_y as f32; 344 + 345 + // UV coordinates in the atlas page. 346 + let u0 = region.x as f32 / page_w as f32; 347 + let v0 = region.y as f32 / page_h as f32; 348 + let u1 = (region.x + region.width) as f32 / page_w as f32; 349 + let v1 = (region.y + region.height) as f32 / page_h as f32; 350 + 351 + quads.push(TexturedQuad { 352 + x: gx, 353 + y: gy, 354 + width: region.width as f32, 355 + height: region.height as f32, 356 + u0, 357 + v0, 358 + u1, 359 + v1, 360 + color, 361 + page: region.page, 362 + }); 363 + } 364 + 365 + quads 366 + } 367 + } 368 + 369 + impl Default for GlyphAtlas { 370 + fn default() -> Self { 371 + Self::new() 372 + } 373 + } 374 + 375 + /// Convert a CSS Color (0–255) to an [f32; 4] (0.0–1.0). 376 + fn color_to_f32(c: &Color) -> [f32; 4] { 377 + [ 378 + c.r as f32 / 255.0, 379 + c.g as f32 / 255.0, 380 + c.b as f32 / 255.0, 381 + c.a as f32 / 255.0, 382 + ] 383 + } 384 + 385 + // --------------------------------------------------------------------------- 386 + // Tests 387 + // --------------------------------------------------------------------------- 388 + 389 + #[cfg(test)] 390 + mod tests { 391 + use super::*; 392 + 393 + /// Create a fake glyph bitmap for testing. 394 + fn make_bitmap(w: u32, h: u32) -> GlyphBitmap { 395 + GlyphBitmap { 396 + width: w, 397 + height: h, 398 + bearing_x: 1, 399 + bearing_y: h as i32, 400 + data: vec![128; (w * h) as usize], 401 + } 402 + } 403 + 404 + #[test] 405 + fn empty_atlas() { 406 + let atlas = GlyphAtlas::new(); 407 + assert_eq!(atlas.page_count(), 0); 408 + assert_eq!(atlas.glyph_count(), 0); 409 + assert!(atlas.lookup(0, 16).is_none()); 410 + } 411 + 412 + #[test] 413 + fn insert_single_glyph() { 414 + let mut atlas = GlyphAtlas::with_page_size(256); 415 + let bm = make_bitmap(10, 12); 416 + let region = atlas.insert_bitmap(1, 16, &bm).unwrap(); 417 + assert_eq!(region.x, 0); 418 + assert_eq!(region.y, 0); 419 + assert_eq!(region.width, 10); 420 + assert_eq!(region.height, 12); 421 + assert_eq!(region.page, 0); 422 + assert_eq!(atlas.page_count(), 1); 423 + assert_eq!(atlas.glyph_count(), 1); 424 + } 425 + 426 + #[test] 427 + fn insert_duplicate_returns_same_region() { 428 + let mut atlas = GlyphAtlas::with_page_size(256); 429 + let bm = make_bitmap(10, 12); 430 + let r1 = atlas.insert_bitmap(1, 16, &bm).unwrap(); 431 + let r2 = atlas.insert_bitmap(1, 16, &bm).unwrap(); 432 + assert_eq!(r1.x, r2.x); 433 + assert_eq!(r1.y, r2.y); 434 + assert_eq!(r1.page, r2.page); 435 + assert_eq!(atlas.glyph_count(), 1); 436 + } 437 + 438 + #[test] 439 + fn shelf_packing_same_row() { 440 + let mut atlas = GlyphAtlas::with_page_size(256); 441 + let bm1 = make_bitmap(10, 12); 442 + let bm2 = make_bitmap(8, 10); 443 + 444 + let r1 = atlas.insert_bitmap(1, 16, &bm1).unwrap(); 445 + let r2 = atlas.insert_bitmap(2, 16, &bm2).unwrap(); 446 + 447 + // Both should be on the same shelf (same page). 448 + assert_eq!(r1.page, 0); 449 + assert_eq!(r2.page, 0); 450 + assert_eq!(r1.y, r2.y); 451 + // r2 should be to the right of r1 (with padding). 452 + assert_eq!(r2.x, r1.width + GLYPH_PADDING); 453 + } 454 + 455 + #[test] 456 + fn new_shelf_when_row_full() { 457 + let mut atlas = GlyphAtlas::with_page_size(32); 458 + // Each glyph is 16 + 1 padding = 17 pixels wide. 459 + // Two don't fit in 32 pixels, so the second goes to a new shelf. 460 + let bm = make_bitmap(16, 10); 461 + 462 + let r1 = atlas.insert_bitmap(1, 16, &bm).unwrap(); 463 + let r2 = atlas.insert_bitmap(2, 16, &bm).unwrap(); 464 + 465 + assert_eq!(r1.y, 0); 466 + assert!(r2.y > 0); 467 + assert_eq!(r1.page, r2.page); 468 + } 469 + 470 + #[test] 471 + fn new_page_when_full() { 472 + // Tiny page that can only fit one glyph. 473 + let mut atlas = GlyphAtlas::with_page_size(16); 474 + let bm = make_bitmap(14, 14); 475 + 476 + let r1 = atlas.insert_bitmap(1, 16, &bm).unwrap(); 477 + let r2 = atlas.insert_bitmap(2, 16, &bm).unwrap(); 478 + 479 + assert_eq!(r1.page, 0); 480 + assert_eq!(r2.page, 1); 481 + assert_eq!(atlas.page_count(), 2); 482 + } 483 + 484 + #[test] 485 + fn zero_size_bitmap_returns_none() { 486 + let mut atlas = GlyphAtlas::with_page_size(256); 487 + let bm = make_bitmap(0, 0); 488 + assert!(atlas.insert_bitmap(1, 16, &bm).is_none()); 489 + } 490 + 491 + #[test] 492 + fn page_dirty_flag() { 493 + let mut atlas = GlyphAtlas::with_page_size(256); 494 + let bm = make_bitmap(10, 12); 495 + atlas.insert_bitmap(1, 16, &bm).unwrap(); 496 + 497 + assert!(atlas.is_page_dirty(0)); 498 + atlas.clear_dirty(0); 499 + assert!(!atlas.is_page_dirty(0)); 500 + 501 + // Inserting another glyph dirties it again. 502 + let bm2 = make_bitmap(8, 8); 503 + atlas.insert_bitmap(2, 16, &bm2).unwrap(); 504 + assert!(atlas.is_page_dirty(0)); 505 + } 506 + 507 + #[test] 508 + fn page_pixels_correct() { 509 + let mut atlas = GlyphAtlas::with_page_size(64); 510 + let bm = make_bitmap(4, 3); 511 + let region = atlas.insert_bitmap(1, 16, &bm).unwrap(); 512 + 513 + let pixels = atlas.page_pixels(0).unwrap(); 514 + // Check that the bitmap was blitted correctly. 515 + for row in 0..3u32 { 516 + for col in 0..4u32 { 517 + let idx = ((region.y + row) * 64 + region.x + col) as usize; 518 + assert_eq!(pixels[idx], 128, "pixel at ({col}, {row})"); 519 + } 520 + } 521 + } 522 + 523 + #[test] 524 + fn clear_resets_atlas() { 525 + let mut atlas = GlyphAtlas::with_page_size(256); 526 + let bm = make_bitmap(10, 12); 527 + atlas.insert_bitmap(1, 16, &bm).unwrap(); 528 + assert_eq!(atlas.page_count(), 1); 529 + assert_eq!(atlas.glyph_count(), 1); 530 + 531 + atlas.clear(); 532 + assert_eq!(atlas.page_count(), 0); 533 + assert_eq!(atlas.glyph_count(), 0); 534 + } 535 + 536 + #[test] 537 + fn uv_coordinates_in_range() { 538 + let mut atlas = GlyphAtlas::with_page_size(256); 539 + let bm = make_bitmap(10, 12); 540 + let region = atlas.insert_bitmap(1, 16, &bm).unwrap(); 541 + 542 + let (page_w, page_h) = atlas.page_size(region.page).unwrap(); 543 + let u0 = region.x as f32 / page_w as f32; 544 + let v0 = region.y as f32 / page_h as f32; 545 + let u1 = (region.x + region.width) as f32 / page_w as f32; 546 + let v1 = (region.y + region.height) as f32 / page_h as f32; 547 + 548 + assert!(u0 >= 0.0 && u0 <= 1.0); 549 + assert!(v0 >= 0.0 && v0 <= 1.0); 550 + assert!(u1 >= 0.0 && u1 <= 1.0); 551 + assert!(v1 >= 0.0 && v1 <= 1.0); 552 + assert!(u1 > u0); 553 + assert!(v1 > v0); 554 + } 555 + 556 + #[test] 557 + fn many_glyphs_across_shelves() { 558 + let mut atlas = GlyphAtlas::with_page_size(128); 559 + 560 + // Insert many small glyphs — should fill multiple shelves. 561 + for glyph_id in 0..50u16 { 562 + let bm = make_bitmap(8, 10); 563 + let region = atlas.insert_bitmap(glyph_id, 16, &bm); 564 + assert!(region.is_some(), "glyph {glyph_id} should be allocated"); 565 + } 566 + 567 + // All should be findable. 568 + for glyph_id in 0..50u16 { 569 + assert!( 570 + atlas.lookup(glyph_id, 16).is_some(), 571 + "glyph {glyph_id} should be in atlas" 572 + ); 573 + } 574 + } 575 + 576 + #[test] 577 + fn different_sizes_different_entries() { 578 + let mut atlas = GlyphAtlas::with_page_size(256); 579 + let bm_small = make_bitmap(8, 10); 580 + let bm_large = make_bitmap(16, 20); 581 + 582 + let r1 = atlas.insert_bitmap(1, 12, &bm_small).unwrap(); 583 + let r2 = atlas.insert_bitmap(1, 24, &bm_large).unwrap(); 584 + 585 + // Same glyph_id but different size_key → different entries. 586 + assert_eq!(atlas.glyph_count(), 2); 587 + assert!(r1.width != r2.width || r1.height != r2.height); 588 + } 589 + 590 + #[test] 591 + fn textured_quad_color_conversion() { 592 + let c = Color { 593 + r: 255, 594 + g: 128, 595 + b: 0, 596 + a: 255, 597 + }; 598 + let f = color_to_f32(&c); 599 + assert!((f[0] - 1.0).abs() < 0.01); 600 + assert!((f[1] - 0.502).abs() < 0.01); 601 + assert!((f[2] - 0.0).abs() < 0.01); 602 + assert!((f[3] - 1.0).abs() < 0.01); 603 + } 604 + 605 + #[test] 606 + fn large_glyph_exceeding_page_creates_larger_page() { 607 + let mut atlas = GlyphAtlas::with_page_size(32); 608 + // Glyph larger than the default page size. 609 + let bm = make_bitmap(48, 48); 610 + let region = atlas.insert_bitmap(1, 16, &bm); 611 + assert!( 612 + region.is_some(), 613 + "large glyph should create an oversized page" 614 + ); 615 + let (pw, ph) = atlas.page_size(0).unwrap(); 616 + assert!(pw >= 48); 617 + assert!(ph >= 48); 618 + } 619 + 620 + #[test] 621 + fn atlas_utilization_metrics() { 622 + let mut atlas = GlyphAtlas::with_page_size(64); 623 + 624 + // Insert a few glyphs and check counts. 625 + for i in 0..5u16 { 626 + let bm = make_bitmap(10, 10); 627 + atlas.insert_bitmap(i, 16, &bm).unwrap(); 628 + } 629 + 630 + assert_eq!(atlas.glyph_count(), 5); 631 + assert!(atlas.page_count() >= 1); 632 + } 633 + 634 + #[test] 635 + fn lookup_returns_none_for_missing() { 636 + let atlas = GlyphAtlas::new(); 637 + assert!(atlas.lookup(42, 16).is_none()); 638 + assert!(atlas.page_pixels(0).is_none()); 639 + assert!(atlas.page_size(0).is_none()); 640 + } 641 + 642 + #[test] 643 + fn dirty_flag_nonexistent_page() { 644 + let atlas = GlyphAtlas::new(); 645 + assert!(!atlas.is_page_dirty(0)); 646 + } 647 + 648 + #[test] 649 + fn clear_dirty_nonexistent_page() { 650 + let mut atlas = GlyphAtlas::new(); 651 + // Should not panic. 652 + atlas.clear_dirty(99); 653 + } 654 + }
+2
crates/render/src/lib.rs
··· 3 3 //! Walks a layout tree, generates paint commands, and rasterizes them 4 4 //! into a BGRA pixel buffer suitable for display via CoreGraphics. 5 5 6 + pub mod atlas; 7 + 6 8 use std::collections::HashMap; 7 9 8 10 use we_css::values::Color;