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