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