we (web engine): Experimental web browser project to understand the limits of Claude
at e2e-page-loading 811 lines 27 kB view raw
1//! Display list, software rasterizer, Metal GPU compositor. 2//! 3//! Walks a layout tree, generates paint commands, and rasterizes them 4//! into a BGRA pixel buffer suitable for display via CoreGraphics. 5 6use std::collections::HashMap; 7 8use we_css::values::Color; 9use we_dom::NodeId; 10use we_image::pixel::Image; 11use we_layout::{BoxType, LayoutBox, LayoutTree, TextLine}; 12use we_style::computed::{BorderStyle, TextDecoration}; 13use we_text::font::Font; 14 15/// A paint command in the display list. 16#[derive(Debug)] 17pub enum PaintCommand { 18 /// Fill a rectangle with a solid color. 19 FillRect { 20 x: f32, 21 y: f32, 22 width: f32, 23 height: f32, 24 color: Color, 25 }, 26 /// Draw a text fragment at a position with styling. 27 DrawGlyphs { 28 line: TextLine, 29 font_size: f32, 30 color: Color, 31 }, 32 /// Draw an image at a position with given display dimensions. 33 DrawImage { 34 x: f32, 35 y: f32, 36 width: f32, 37 height: f32, 38 node_id: NodeId, 39 }, 40} 41 42/// A flat list of paint commands in painter's order. 43pub type DisplayList = Vec<PaintCommand>; 44 45/// Build a display list from a layout tree. 46/// 47/// Walks the tree in depth-first pre-order (painter's order): 48/// backgrounds first, then borders, then text on top. 49pub fn build_display_list(tree: &LayoutTree) -> DisplayList { 50 let mut list = DisplayList::new(); 51 paint_box(&tree.root, &mut list); 52 list 53} 54 55fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { 56 paint_background(layout_box, list); 57 paint_borders(layout_box, list); 58 59 // Emit image paint command for replaced elements. 60 if let Some((rw, rh)) = layout_box.replaced_size { 61 if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { 62 list.push(PaintCommand::DrawImage { 63 x: layout_box.rect.x, 64 y: layout_box.rect.y, 65 width: rw, 66 height: rh, 67 node_id, 68 }); 69 } 70 } 71 72 paint_text(layout_box, list); 73 74 // Recurse into children. 75 for child in &layout_box.children { 76 paint_box(child, list); 77 } 78} 79 80/// Extract the NodeId from a BoxType, if it has one. 81fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> { 82 match box_type { 83 BoxType::Block(id) | BoxType::Inline(id) => Some(*id), 84 BoxType::TextRun { node, .. } => Some(*node), 85 BoxType::Anonymous => None, 86 } 87} 88 89fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList) { 90 let bg = layout_box.background_color; 91 // Only paint if the background is not fully transparent and the box has area. 92 if bg.a == 0 { 93 return; 94 } 95 if layout_box.rect.width > 0.0 && layout_box.rect.height > 0.0 { 96 // Background covers the padding box (content + padding), not including border. 97 list.push(PaintCommand::FillRect { 98 x: layout_box.rect.x, 99 y: layout_box.rect.y, 100 width: layout_box.rect.width, 101 height: layout_box.rect.height, 102 color: bg, 103 }); 104 } 105} 106 107fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList) { 108 let b = &layout_box.border; 109 let r = &layout_box.rect; 110 let styles = &layout_box.border_styles; 111 let colors = &layout_box.border_colors; 112 113 // Border box starts at content origin minus padding and border. 114 let bx = r.x - layout_box.padding.left - b.left; 115 let by = r.y - layout_box.padding.top - b.top; 116 let bw = b.left + layout_box.padding.left + r.width + layout_box.padding.right + b.right; 117 let bh = b.top + layout_box.padding.top + r.height + layout_box.padding.bottom + b.bottom; 118 119 // Top border 120 if b.top > 0.0 && styles[0] != BorderStyle::None && styles[0] != BorderStyle::Hidden { 121 list.push(PaintCommand::FillRect { 122 x: bx, 123 y: by, 124 width: bw, 125 height: b.top, 126 color: colors[0], 127 }); 128 } 129 // Right border 130 if b.right > 0.0 && styles[1] != BorderStyle::None && styles[1] != BorderStyle::Hidden { 131 list.push(PaintCommand::FillRect { 132 x: bx + bw - b.right, 133 y: by, 134 width: b.right, 135 height: bh, 136 color: colors[1], 137 }); 138 } 139 // Bottom border 140 if b.bottom > 0.0 && styles[2] != BorderStyle::None && styles[2] != BorderStyle::Hidden { 141 list.push(PaintCommand::FillRect { 142 x: bx, 143 y: by + bh - b.bottom, 144 width: bw, 145 height: b.bottom, 146 color: colors[2], 147 }); 148 } 149 // Left border 150 if b.left > 0.0 && styles[3] != BorderStyle::None && styles[3] != BorderStyle::Hidden { 151 list.push(PaintCommand::FillRect { 152 x: bx, 153 y: by, 154 width: b.left, 155 height: bh, 156 color: colors[3], 157 }); 158 } 159} 160 161fn paint_text(layout_box: &LayoutBox, list: &mut DisplayList) { 162 for line in &layout_box.lines { 163 let color = line.color; 164 let font_size = line.font_size; 165 166 // Record index before pushing glyphs so we can insert the background 167 // before the text in painter's order. 168 let glyph_idx = list.len(); 169 170 list.push(PaintCommand::DrawGlyphs { 171 line: line.clone(), 172 font_size, 173 color, 174 }); 175 176 // Draw underline as a 1px line below the baseline. 177 if line.text_decoration == TextDecoration::Underline && line.width > 0.0 { 178 let baseline_y = line.y + font_size; 179 let underline_y = baseline_y + 2.0; 180 list.push(PaintCommand::FillRect { 181 x: line.x, 182 y: underline_y, 183 width: line.width, 184 height: 1.0, 185 color, 186 }); 187 } 188 189 // Draw inline background if not transparent. 190 if line.background_color.a > 0 && line.width > 0.0 { 191 list.insert( 192 glyph_idx, 193 PaintCommand::FillRect { 194 x: line.x, 195 y: line.y, 196 width: line.width, 197 height: font_size * 1.2, 198 color: line.background_color, 199 }, 200 ); 201 } 202 } 203} 204 205/// Software renderer that paints a display list into a BGRA pixel buffer. 206pub struct Renderer { 207 width: u32, 208 height: u32, 209 /// BGRA pixel data, row-major, top-to-bottom. 210 buffer: Vec<u8>, 211} 212 213impl Renderer { 214 /// Create a new renderer with the given dimensions. 215 /// The buffer is initialized to white. 216 pub fn new(width: u32, height: u32) -> Renderer { 217 let size = (width as usize) * (height as usize) * 4; 218 let mut buffer = vec![0u8; size]; 219 // Fill with white (BGRA: B=255, G=255, R=255, A=255). 220 for pixel in buffer.chunks_exact_mut(4) { 221 pixel[0] = 255; // B 222 pixel[1] = 255; // G 223 pixel[2] = 255; // R 224 pixel[3] = 255; // A 225 } 226 Renderer { 227 width, 228 height, 229 buffer, 230 } 231 } 232 233 /// Paint a layout tree into the pixel buffer. 234 pub fn paint( 235 &mut self, 236 layout_tree: &LayoutTree, 237 font: &Font, 238 images: &HashMap<NodeId, &Image>, 239 ) { 240 let display_list = build_display_list(layout_tree); 241 for cmd in &display_list { 242 match cmd { 243 PaintCommand::FillRect { 244 x, 245 y, 246 width, 247 height, 248 color, 249 } => { 250 self.fill_rect(*x, *y, *width, *height, *color); 251 } 252 PaintCommand::DrawGlyphs { 253 line, 254 font_size, 255 color, 256 } => { 257 self.draw_text_line(line, *font_size, *color, font); 258 } 259 PaintCommand::DrawImage { 260 x, 261 y, 262 width, 263 height, 264 node_id, 265 } => { 266 if let Some(image) = images.get(node_id) { 267 self.draw_image(*x, *y, *width, *height, image); 268 } 269 } 270 } 271 } 272 } 273 274 /// Get the BGRA pixel data. 275 pub fn pixels(&self) -> &[u8] { 276 &self.buffer 277 } 278 279 /// Width in pixels. 280 pub fn width(&self) -> u32 { 281 self.width 282 } 283 284 /// Height in pixels. 285 pub fn height(&self) -> u32 { 286 self.height 287 } 288 289 /// Fill a rectangle with a solid color. 290 pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) { 291 let x0 = (x as i32).max(0) as u32; 292 let y0 = (y as i32).max(0) as u32; 293 let x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32; 294 let y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32; 295 296 if color.a == 255 { 297 // Fully opaque — direct write. 298 for py in y0..y1 { 299 for px in x0..x1 { 300 self.set_pixel(px, py, color); 301 } 302 } 303 } else if color.a > 0 { 304 // Semi-transparent — alpha blend. 305 let alpha = color.a as u32; 306 let inv_alpha = 255 - alpha; 307 for py in y0..y1 { 308 for px in x0..x1 { 309 let offset = ((py * self.width + px) * 4) as usize; 310 let dst_b = self.buffer[offset] as u32; 311 let dst_g = self.buffer[offset + 1] as u32; 312 let dst_r = self.buffer[offset + 2] as u32; 313 self.buffer[offset] = 314 ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; 315 self.buffer[offset + 1] = 316 ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; 317 self.buffer[offset + 2] = 318 ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; 319 self.buffer[offset + 3] = 255; 320 } 321 } 322 } 323 } 324 325 /// Draw a line of text using the font to render glyphs. 326 fn draw_text_line(&mut self, line: &TextLine, font_size: f32, color: Color, font: &Font) { 327 let positioned = font.render_text(&line.text, font_size); 328 329 for glyph in &positioned { 330 if let Some(ref bitmap) = glyph.bitmap { 331 // Glyph origin: line position + glyph horizontal offset. 332 let gx = line.x + glyph.x + bitmap.bearing_x as f32; 333 // Baseline is at line.y + font_size (approximate: baseline at ~80% of font_size). 334 // bearing_y is distance from baseline to top of glyph (positive = above baseline). 335 let baseline_y = line.y + font_size; 336 let gy = baseline_y - bitmap.bearing_y as f32; 337 338 self.composite_glyph(gx, gy, bitmap, color); 339 } 340 } 341 } 342 343 /// Composite a grayscale glyph bitmap onto the buffer with anti-aliasing. 344 fn composite_glyph( 345 &mut self, 346 x: f32, 347 y: f32, 348 bitmap: &we_text::font::rasterizer::GlyphBitmap, 349 color: Color, 350 ) { 351 let x0 = x as i32; 352 let y0 = y as i32; 353 354 for by in 0..bitmap.height { 355 for bx in 0..bitmap.width { 356 let px = x0 + bx as i32; 357 let py = y0 + by as i32; 358 359 if px < 0 || py < 0 || px >= self.width as i32 || py >= self.height as i32 { 360 continue; 361 } 362 363 let coverage = bitmap.data[(by * bitmap.width + bx) as usize]; 364 if coverage == 0 { 365 continue; 366 } 367 368 let px = px as u32; 369 let py = py as u32; 370 371 // Alpha-blend the glyph coverage with the text color onto the background. 372 let alpha = (coverage as u32 * color.a as u32) / 255; 373 if alpha == 0 { 374 continue; 375 } 376 377 let offset = ((py * self.width + px) * 4) as usize; 378 let dst_b = self.buffer[offset] as u32; 379 let dst_g = self.buffer[offset + 1] as u32; 380 let dst_r = self.buffer[offset + 2] as u32; 381 382 // Source-over compositing. 383 let inv_alpha = 255 - alpha; 384 self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; 385 self.buffer[offset + 1] = 386 ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; 387 self.buffer[offset + 2] = 388 ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; 389 self.buffer[offset + 3] = 255; // Fully opaque. 390 } 391 } 392 } 393 394 /// Draw an RGBA8 image scaled to the given display dimensions. 395 /// 396 /// Uses nearest-neighbor sampling for scaling. The source image is in RGBA8 397 /// format; the buffer is BGRA. Alpha compositing is performed for semi-transparent pixels. 398 fn draw_image(&mut self, x: f32, y: f32, width: f32, height: f32, image: &Image) { 399 if image.width == 0 || image.height == 0 || width <= 0.0 || height <= 0.0 { 400 return; 401 } 402 403 let dst_x0 = (x as i32).max(0) as u32; 404 let dst_y0 = (y as i32).max(0) as u32; 405 let dst_x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32; 406 let dst_y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32; 407 408 let scale_x = image.width as f32 / width; 409 let scale_y = image.height as f32 / height; 410 411 for dst_y in dst_y0..dst_y1 { 412 let src_y = ((dst_y as f32 - y) * scale_y) as u32; 413 let src_y = src_y.min(image.height - 1); 414 415 for dst_x in dst_x0..dst_x1 { 416 let src_x = ((dst_x as f32 - x) * scale_x) as u32; 417 let src_x = src_x.min(image.width - 1); 418 419 let src_offset = ((src_y * image.width + src_x) * 4) as usize; 420 let r = image.data[src_offset] as u32; 421 let g = image.data[src_offset + 1] as u32; 422 let b = image.data[src_offset + 2] as u32; 423 let a = image.data[src_offset + 3] as u32; 424 425 if a == 0 { 426 continue; 427 } 428 429 let dst_offset = ((dst_y * self.width + dst_x) * 4) as usize; 430 431 if a == 255 { 432 // Fully opaque — direct write (RGBA → BGRA). 433 self.buffer[dst_offset] = b as u8; 434 self.buffer[dst_offset + 1] = g as u8; 435 self.buffer[dst_offset + 2] = r as u8; 436 self.buffer[dst_offset + 3] = 255; 437 } else { 438 // Alpha blend. 439 let inv_a = 255 - a; 440 let dst_b = self.buffer[dst_offset] as u32; 441 let dst_g = self.buffer[dst_offset + 1] as u32; 442 let dst_r = self.buffer[dst_offset + 2] as u32; 443 self.buffer[dst_offset] = ((b * a + dst_b * inv_a) / 255) as u8; 444 self.buffer[dst_offset + 1] = ((g * a + dst_g * inv_a) / 255) as u8; 445 self.buffer[dst_offset + 2] = ((r * a + dst_r * inv_a) / 255) as u8; 446 self.buffer[dst_offset + 3] = 255; 447 } 448 } 449 } 450 } 451 452 /// Set a single pixel to the given color (no blending). 453 fn set_pixel(&mut self, x: u32, y: u32, color: Color) { 454 if x >= self.width || y >= self.height { 455 return; 456 } 457 let offset = ((y * self.width + x) * 4) as usize; 458 self.buffer[offset] = color.b; // B 459 self.buffer[offset + 1] = color.g; // G 460 self.buffer[offset + 2] = color.r; // R 461 self.buffer[offset + 3] = color.a; // A 462 } 463} 464 465#[cfg(test)] 466mod tests { 467 use super::*; 468 use we_dom::Document; 469 use we_style::computed::{extract_stylesheets, resolve_styles}; 470 use we_text::font::Font; 471 472 fn test_font() -> Font { 473 let paths = [ 474 "/System/Library/Fonts/Geneva.ttf", 475 "/System/Library/Fonts/Monaco.ttf", 476 ]; 477 for path in &paths { 478 let p = std::path::Path::new(path); 479 if p.exists() { 480 return Font::from_file(p).expect("failed to parse font"); 481 } 482 } 483 panic!("no test font found"); 484 } 485 486 fn layout_doc(doc: &Document) -> we_layout::LayoutTree { 487 let font = test_font(); 488 let sheets = extract_stylesheets(doc); 489 let styled = resolve_styles(doc, &sheets).unwrap(); 490 we_layout::layout( 491 &styled, 492 doc, 493 800.0, 494 600.0, 495 &font, 496 &std::collections::HashMap::new(), 497 ) 498 } 499 500 #[test] 501 fn renderer_new_white_background() { 502 let r = Renderer::new(10, 10); 503 assert_eq!(r.width(), 10); 504 assert_eq!(r.height(), 10); 505 assert_eq!(r.pixels().len(), 10 * 10 * 4); 506 // All pixels should be white (BGRA: 255, 255, 255, 255). 507 for pixel in r.pixels().chunks_exact(4) { 508 assert_eq!(pixel, [255, 255, 255, 255]); 509 } 510 } 511 512 #[test] 513 fn fill_rect_basic() { 514 let mut r = Renderer::new(20, 20); 515 let red = Color::new(255, 0, 0, 255); 516 r.fill_rect(5.0, 5.0, 10.0, 10.0, red); 517 518 // Pixel at (7, 7) should be red (BGRA: 0, 0, 255, 255). 519 let offset = ((7 * 20 + 7) * 4) as usize; 520 assert_eq!(r.pixels()[offset], 0); // B 521 assert_eq!(r.pixels()[offset + 1], 0); // G 522 assert_eq!(r.pixels()[offset + 2], 255); // R 523 assert_eq!(r.pixels()[offset + 3], 255); // A 524 525 // Pixel at (0, 0) should still be white. 526 assert_eq!(r.pixels()[0], 255); // B 527 assert_eq!(r.pixels()[1], 255); // G 528 assert_eq!(r.pixels()[2], 255); // R 529 assert_eq!(r.pixels()[3], 255); // A 530 } 531 532 #[test] 533 fn fill_rect_clipping() { 534 let mut r = Renderer::new(10, 10); 535 let blue = Color::new(0, 0, 255, 255); 536 // Rect extends beyond the buffer — should not panic. 537 r.fill_rect(-5.0, -5.0, 20.0, 20.0, blue); 538 539 // All pixels should be blue (BGRA: 255, 0, 0, 255). 540 for pixel in r.pixels().chunks_exact(4) { 541 assert_eq!(pixel, [255, 0, 0, 255]); 542 } 543 } 544 545 #[test] 546 fn display_list_from_empty_layout() { 547 let doc = Document::new(); 548 let font = test_font(); 549 let sheets = extract_stylesheets(&doc); 550 let styled = resolve_styles(&doc, &sheets); 551 if let Some(styled) = styled { 552 let tree = we_layout::layout( 553 &styled, 554 &doc, 555 800.0, 556 600.0, 557 &font, 558 &std::collections::HashMap::new(), 559 ); 560 let list = build_display_list(&tree); 561 assert!(list.len() <= 1); 562 } 563 } 564 565 #[test] 566 fn display_list_has_background_and_text() { 567 let mut doc = Document::new(); 568 let root = doc.root(); 569 let html = doc.create_element("html"); 570 let body = doc.create_element("body"); 571 let p = doc.create_element("p"); 572 let text = doc.create_text("Hello world"); 573 doc.append_child(root, html); 574 doc.append_child(html, body); 575 doc.append_child(body, p); 576 doc.append_child(p, text); 577 578 let tree = layout_doc(&doc); 579 let list = build_display_list(&tree); 580 581 let has_text = list 582 .iter() 583 .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. })); 584 585 assert!(has_text, "should have at least one DrawGlyphs"); 586 } 587 588 #[test] 589 fn paint_simple_page() { 590 let mut doc = Document::new(); 591 let root = doc.root(); 592 let html = doc.create_element("html"); 593 let body = doc.create_element("body"); 594 let p = doc.create_element("p"); 595 let text = doc.create_text("Hello"); 596 doc.append_child(root, html); 597 doc.append_child(html, body); 598 doc.append_child(body, p); 599 doc.append_child(p, text); 600 601 let font = test_font(); 602 let tree = layout_doc(&doc); 603 let mut renderer = Renderer::new(800, 600); 604 renderer.paint(&tree, &font, &HashMap::new()); 605 606 let pixels = renderer.pixels(); 607 608 // The buffer should have some non-white pixels (from text rendering). 609 let has_non_white = pixels.chunks_exact(4).any(|p| p != [255, 255, 255, 255]); 610 assert!( 611 has_non_white, 612 "rendered page should have non-white pixels from text" 613 ); 614 } 615 616 #[test] 617 fn bgra_format_correct() { 618 let mut r = Renderer::new(1, 1); 619 let color = Color::new(100, 150, 200, 255); 620 r.set_pixel(0, 0, color); 621 let pixels = r.pixels(); 622 // BGRA format. 623 assert_eq!(pixels[0], 200); // B 624 assert_eq!(pixels[1], 150); // G 625 assert_eq!(pixels[2], 100); // R 626 assert_eq!(pixels[3], 255); // A 627 } 628 629 #[test] 630 fn paint_heading_produces_larger_glyphs() { 631 let mut doc = Document::new(); 632 let root = doc.root(); 633 let html = doc.create_element("html"); 634 let body = doc.create_element("body"); 635 let h1 = doc.create_element("h1"); 636 let h1_text = doc.create_text("Big"); 637 let p = doc.create_element("p"); 638 let p_text = doc.create_text("Small"); 639 doc.append_child(root, html); 640 doc.append_child(html, body); 641 doc.append_child(body, h1); 642 doc.append_child(h1, h1_text); 643 doc.append_child(body, p); 644 doc.append_child(p, p_text); 645 646 let tree = layout_doc(&doc); 647 let list = build_display_list(&tree); 648 649 // There should be DrawGlyphs commands with different font sizes. 650 let font_sizes: Vec<f32> = list 651 .iter() 652 .filter_map(|c| match c { 653 PaintCommand::DrawGlyphs { font_size, .. } => Some(*font_size), 654 _ => None, 655 }) 656 .collect(); 657 658 assert!(font_sizes.len() >= 2, "should have at least 2 text lines"); 659 // h1 has font_size 32, p has font_size 16. 660 assert!( 661 font_sizes.iter().any(|&s| s > 20.0), 662 "should have a heading with large font size" 663 ); 664 assert!( 665 font_sizes.iter().any(|&s| s < 20.0), 666 "should have a paragraph with normal font size" 667 ); 668 } 669 670 #[test] 671 fn renderer_zero_size() { 672 let r = Renderer::new(0, 0); 673 assert_eq!(r.pixels().len(), 0); 674 } 675 676 #[test] 677 fn glyph_compositing_anti_aliased() { 678 // Render text and verify we get anti-aliased (partially transparent) pixels. 679 let mut doc = Document::new(); 680 let root = doc.root(); 681 let html = doc.create_element("html"); 682 let body = doc.create_element("body"); 683 let p = doc.create_element("p"); 684 let text = doc.create_text("MMMMM"); 685 doc.append_child(root, html); 686 doc.append_child(html, body); 687 doc.append_child(body, p); 688 doc.append_child(p, text); 689 690 let font = test_font(); 691 let tree = layout_doc(&doc); 692 let mut renderer = Renderer::new(800, 600); 693 renderer.paint(&tree, &font, &HashMap::new()); 694 695 let pixels = renderer.pixels(); 696 // Find pixels that are not pure white and not pure black. 697 // These represent anti-aliased edges of glyphs. 698 let mut has_intermediate = false; 699 for pixel in pixels.chunks_exact(4) { 700 let b = pixel[0]; 701 let g = pixel[1]; 702 let r = pixel[2]; 703 // Anti-aliased pixel: gray between white and black. 704 if r > 0 && r < 255 && r == g && g == b { 705 has_intermediate = true; 706 break; 707 } 708 } 709 assert!( 710 has_intermediate, 711 "should have anti-aliased (gray) pixels from glyph compositing" 712 ); 713 } 714 715 #[test] 716 fn css_color_renders_correctly() { 717 let html_str = r#"<!DOCTYPE html> 718<html> 719<head><style>p { color: red; }</style></head> 720<body><p>Red text</p></body> 721</html>"#; 722 let doc = we_html::parse_html(html_str); 723 let font = test_font(); 724 let sheets = extract_stylesheets(&doc); 725 let styled = resolve_styles(&doc, &sheets).unwrap(); 726 let tree = we_layout::layout( 727 &styled, 728 &doc, 729 800.0, 730 600.0, 731 &font, 732 &std::collections::HashMap::new(), 733 ); 734 735 let list = build_display_list(&tree); 736 let text_colors: Vec<&Color> = list 737 .iter() 738 .filter_map(|c| match c { 739 PaintCommand::DrawGlyphs { color, .. } => Some(color), 740 _ => None, 741 }) 742 .collect(); 743 744 assert!(!text_colors.is_empty()); 745 // Text should be red. 746 assert_eq!(*text_colors[0], Color::rgb(255, 0, 0)); 747 } 748 749 #[test] 750 fn css_background_color_renders() { 751 let html_str = r#"<!DOCTYPE html> 752<html> 753<head><style>div { background-color: yellow; }</style></head> 754<body><div>Content</div></body> 755</html>"#; 756 let doc = we_html::parse_html(html_str); 757 let font = test_font(); 758 let sheets = extract_stylesheets(&doc); 759 let styled = resolve_styles(&doc, &sheets).unwrap(); 760 let tree = we_layout::layout( 761 &styled, 762 &doc, 763 800.0, 764 600.0, 765 &font, 766 &std::collections::HashMap::new(), 767 ); 768 769 let list = build_display_list(&tree); 770 let fill_colors: Vec<&Color> = list 771 .iter() 772 .filter_map(|c| match c { 773 PaintCommand::FillRect { color, .. } => Some(color), 774 _ => None, 775 }) 776 .collect(); 777 778 // Should have a yellow fill rect for the div background. 779 assert!(fill_colors.iter().any(|c| **c == Color::rgb(255, 255, 0))); 780 } 781 782 #[test] 783 fn border_rendering() { 784 let html_str = r#"<!DOCTYPE html> 785<html> 786<head><style>div { border: 2px solid red; }</style></head> 787<body><div>Bordered</div></body> 788</html>"#; 789 let doc = we_html::parse_html(html_str); 790 let font = test_font(); 791 let sheets = extract_stylesheets(&doc); 792 let styled = resolve_styles(&doc, &sheets).unwrap(); 793 let tree = we_layout::layout( 794 &styled, 795 &doc, 796 800.0, 797 600.0, 798 &font, 799 &std::collections::HashMap::new(), 800 ); 801 802 let list = build_display_list(&tree); 803 let red_fills: Vec<_> = list 804 .iter() 805 .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))) 806 .collect(); 807 808 // Should have 4 border fills (top, right, bottom, left). 809 assert_eq!(red_fills.len(), 4, "should have 4 border edges"); 810 } 811}