web engine - experimental web browser
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}