Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
at url-parser
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668//! Display list, software rasterizer, Metal GPU compositor.//!//! Walks a layout tree, generates paint commands, and rasterizes them//! into a BGRA pixel buffer suitable for display via CoreGraphics.
use we_css::values::Color;use we_layout::{LayoutBox, LayoutTree, TextLine};use we_style::computed::{BorderStyle, TextDecoration};use we_text::font::Font;
/// A paint command in the display list.#[derive(Debug)]pub enum PaintCommand { /// Fill a rectangle with a solid color. FillRect { x: f32, y: f32, width: f32, height: f32, color: Color, }, /// Draw a text fragment at a position with styling. DrawGlyphs { line: TextLine, font_size: f32, color: Color, },}
/// A flat list of paint commands in painter's order.pub type DisplayList = Vec<PaintCommand>;
/// Build a display list from a layout tree.////// Walks the tree in depth-first pre-order (painter's order):/// backgrounds first, then borders, then text on top.pub fn build_display_list(tree: &LayoutTree) -> DisplayList { let mut list = DisplayList::new(); paint_box(&tree.root, &mut list); list}
fn paint_box(layout_box: &LayoutBox, list: &mut DisplayList) { paint_background(layout_box, list); paint_borders(layout_box, list); paint_text(layout_box, list);
// Recurse into children. for child in &layout_box.children { paint_box(child, list); }}
fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList) { let bg = layout_box.background_color; // Only paint if the background is not fully transparent and the box has area. if bg.a == 0 { return; } if layout_box.rect.width > 0.0 && layout_box.rect.height > 0.0 { // Background covers the padding box (content + padding), not including border. list.push(PaintCommand::FillRect { x: layout_box.rect.x, y: layout_box.rect.y, width: layout_box.rect.width, height: layout_box.rect.height, color: bg, }); }}
fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList) { let b = &layout_box.border; let r = &layout_box.rect; let styles = &layout_box.border_styles; let colors = &layout_box.border_colors;
// Border box starts at content origin minus padding and border. let bx = r.x - layout_box.padding.left - b.left; let by = r.y - layout_box.padding.top - b.top; let bw = b.left + layout_box.padding.left + r.width + layout_box.padding.right + b.right; let bh = b.top + layout_box.padding.top + r.height + layout_box.padding.bottom + b.bottom;
// Top border if b.top > 0.0 && styles[0] != BorderStyle::None && styles[0] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by, width: bw, height: b.top, color: colors[0], }); } // Right border if b.right > 0.0 && styles[1] != BorderStyle::None && styles[1] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx + bw - b.right, y: by, width: b.right, height: bh, color: colors[1], }); } // Bottom border if b.bottom > 0.0 && styles[2] != BorderStyle::None && styles[2] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by + bh - b.bottom, width: bw, height: b.bottom, color: colors[2], }); } // Left border if b.left > 0.0 && styles[3] != BorderStyle::None && styles[3] != BorderStyle::Hidden { list.push(PaintCommand::FillRect { x: bx, y: by, width: b.left, height: bh, color: colors[3], }); }}
fn paint_text(layout_box: &LayoutBox, list: &mut DisplayList) { for line in &layout_box.lines { let color = line.color; let font_size = line.font_size;
// Record index before pushing glyphs so we can insert the background // before the text in painter's order. let glyph_idx = list.len();
list.push(PaintCommand::DrawGlyphs { line: line.clone(), font_size, color, });
// Draw underline as a 1px line below the baseline. if line.text_decoration == TextDecoration::Underline && line.width > 0.0 { let baseline_y = line.y + font_size; let underline_y = baseline_y + 2.0; list.push(PaintCommand::FillRect { x: line.x, y: underline_y, width: line.width, height: 1.0, color, }); }
// Draw inline background if not transparent. if line.background_color.a > 0 && line.width > 0.0 { list.insert( glyph_idx, PaintCommand::FillRect { x: line.x, y: line.y, width: line.width, height: font_size * 1.2, color: line.background_color, }, ); } }}
/// Software renderer that paints a display list into a BGRA pixel buffer.pub struct Renderer { width: u32, height: u32, /// BGRA pixel data, row-major, top-to-bottom. buffer: Vec<u8>,}
impl Renderer { /// Create a new renderer with the given dimensions. /// The buffer is initialized to white. pub fn new(width: u32, height: u32) -> Renderer { let size = (width as usize) * (height as usize) * 4; let mut buffer = vec![0u8; size]; // Fill with white (BGRA: B=255, G=255, R=255, A=255). for pixel in buffer.chunks_exact_mut(4) { pixel[0] = 255; // B pixel[1] = 255; // G pixel[2] = 255; // R pixel[3] = 255; // A } Renderer { width, height, buffer, } }
/// Paint a layout tree into the pixel buffer. pub fn paint(&mut self, layout_tree: &LayoutTree, font: &Font) { let display_list = build_display_list(layout_tree); for cmd in &display_list { match cmd { PaintCommand::FillRect { x, y, width, height, color, } => { self.fill_rect(*x, *y, *width, *height, *color); } PaintCommand::DrawGlyphs { line, font_size, color, } => { self.draw_text_line(line, *font_size, *color, font); } } } }
/// Get the BGRA pixel data. pub fn pixels(&self) -> &[u8] { &self.buffer }
/// Width in pixels. pub fn width(&self) -> u32 { self.width }
/// Height in pixels. pub fn height(&self) -> u32 { self.height }
/// Fill a rectangle with a solid color. pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) { let x0 = (x as i32).max(0) as u32; let y0 = (y as i32).max(0) as u32; let x1 = ((x + width) as i32).max(0).min(self.width as i32) as u32; let y1 = ((y + height) as i32).max(0).min(self.height as i32) as u32;
if color.a == 255 { // Fully opaque — direct write. for py in y0..y1 { for px in x0..x1 { self.set_pixel(px, py, color); } } } else if color.a > 0 { // Semi-transparent — alpha blend. let alpha = color.a as u32; let inv_alpha = 255 - alpha; for py in y0..y1 { for px in x0..x1 { let offset = ((py * self.width + px) * 4) as usize; let dst_b = self.buffer[offset] as u32; let dst_g = self.buffer[offset + 1] as u32; let dst_r = self.buffer[offset + 2] as u32; self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; self.buffer[offset + 1] = ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; self.buffer[offset + 2] = ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; self.buffer[offset + 3] = 255; } } } }
/// Draw a line of text using the font to render glyphs. fn draw_text_line(&mut self, line: &TextLine, font_size: f32, color: Color, font: &Font) { let positioned = font.render_text(&line.text, font_size);
for glyph in &positioned { if let Some(ref bitmap) = glyph.bitmap { // Glyph origin: line position + glyph horizontal offset. let gx = line.x + glyph.x + bitmap.bearing_x as f32; // Baseline is at line.y + font_size (approximate: baseline at ~80% of font_size). // bearing_y is distance from baseline to top of glyph (positive = above baseline). let baseline_y = line.y + font_size; let gy = baseline_y - bitmap.bearing_y as f32;
self.composite_glyph(gx, gy, bitmap, color); } } }
/// Composite a grayscale glyph bitmap onto the buffer with anti-aliasing. fn composite_glyph( &mut self, x: f32, y: f32, bitmap: &we_text::font::rasterizer::GlyphBitmap, color: Color, ) { let x0 = x as i32; let y0 = y as i32;
for by in 0..bitmap.height { for bx in 0..bitmap.width { let px = x0 + bx as i32; let py = y0 + by as i32;
if px < 0 || py < 0 || px >= self.width as i32 || py >= self.height as i32 { continue; }
let coverage = bitmap.data[(by * bitmap.width + bx) as usize]; if coverage == 0 { continue; }
let px = px as u32; let py = py as u32;
// Alpha-blend the glyph coverage with the text color onto the background. let alpha = (coverage as u32 * color.a as u32) / 255; if alpha == 0 { continue; }
let offset = ((py * self.width + px) * 4) as usize; let dst_b = self.buffer[offset] as u32; let dst_g = self.buffer[offset + 1] as u32; let dst_r = self.buffer[offset + 2] as u32;
// Source-over compositing. let inv_alpha = 255 - alpha; self.buffer[offset] = ((color.b as u32 * alpha + dst_b * inv_alpha) / 255) as u8; self.buffer[offset + 1] = ((color.g as u32 * alpha + dst_g * inv_alpha) / 255) as u8; self.buffer[offset + 2] = ((color.r as u32 * alpha + dst_r * inv_alpha) / 255) as u8; self.buffer[offset + 3] = 255; // Fully opaque. } } }
/// Set a single pixel to the given color (no blending). fn set_pixel(&mut self, x: u32, y: u32, color: Color) { if x >= self.width || y >= self.height { return; } let offset = ((y * self.width + x) * 4) as usize; self.buffer[offset] = color.b; // B self.buffer[offset + 1] = color.g; // G self.buffer[offset + 2] = color.r; // R self.buffer[offset + 3] = color.a; // A }}
#[cfg(test)]mod tests { use super::*; use we_dom::Document; use we_style::computed::{extract_stylesheets, resolve_styles}; use we_text::font::Font;
fn test_font() -> Font { let paths = [ "/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Monaco.ttf", ]; for path in &paths { let p = std::path::Path::new(path); if p.exists() { return Font::from_file(p).expect("failed to parse font"); } } panic!("no test font found"); }
fn layout_doc(doc: &Document) -> we_layout::LayoutTree { let font = test_font(); let sheets = extract_stylesheets(doc); let styled = resolve_styles(doc, &sheets).unwrap(); we_layout::layout(&styled, doc, 800.0, 600.0, &font) }
#[test] fn renderer_new_white_background() { let r = Renderer::new(10, 10); assert_eq!(r.width(), 10); assert_eq!(r.height(), 10); assert_eq!(r.pixels().len(), 10 * 10 * 4); // All pixels should be white (BGRA: 255, 255, 255, 255). for pixel in r.pixels().chunks_exact(4) { assert_eq!(pixel, [255, 255, 255, 255]); } }
#[test] fn fill_rect_basic() { let mut r = Renderer::new(20, 20); let red = Color::new(255, 0, 0, 255); r.fill_rect(5.0, 5.0, 10.0, 10.0, red);
// Pixel at (7, 7) should be red (BGRA: 0, 0, 255, 255). let offset = ((7 * 20 + 7) * 4) as usize; assert_eq!(r.pixels()[offset], 0); // B assert_eq!(r.pixels()[offset + 1], 0); // G assert_eq!(r.pixels()[offset + 2], 255); // R assert_eq!(r.pixels()[offset + 3], 255); // A
// Pixel at (0, 0) should still be white. assert_eq!(r.pixels()[0], 255); // B assert_eq!(r.pixels()[1], 255); // G assert_eq!(r.pixels()[2], 255); // R assert_eq!(r.pixels()[3], 255); // A }
#[test] fn fill_rect_clipping() { let mut r = Renderer::new(10, 10); let blue = Color::new(0, 0, 255, 255); // Rect extends beyond the buffer — should not panic. r.fill_rect(-5.0, -5.0, 20.0, 20.0, blue);
// All pixels should be blue (BGRA: 255, 0, 0, 255). for pixel in r.pixels().chunks_exact(4) { assert_eq!(pixel, [255, 0, 0, 255]); } }
#[test] fn display_list_from_empty_layout() { let doc = Document::new(); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets); if let Some(styled) = styled { let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font); let list = build_display_list(&tree); assert!(list.len() <= 1); } }
#[test] fn display_list_has_background_and_text() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("Hello world"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text);
let tree = layout_doc(&doc); let list = build_display_list(&tree);
let has_text = list .iter() .any(|c| matches!(c, PaintCommand::DrawGlyphs { .. }));
assert!(has_text, "should have at least one DrawGlyphs"); }
#[test] fn paint_simple_page() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("Hello"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text);
let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font);
let pixels = renderer.pixels();
// The buffer should have some non-white pixels (from text rendering). let has_non_white = pixels.chunks_exact(4).any(|p| p != [255, 255, 255, 255]); assert!( has_non_white, "rendered page should have non-white pixels from text" ); }
#[test] fn bgra_format_correct() { let mut r = Renderer::new(1, 1); let color = Color::new(100, 150, 200, 255); r.set_pixel(0, 0, color); let pixels = r.pixels(); // BGRA format. assert_eq!(pixels[0], 200); // B assert_eq!(pixels[1], 150); // G assert_eq!(pixels[2], 100); // R assert_eq!(pixels[3], 255); // A }
#[test] fn paint_heading_produces_larger_glyphs() { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let h1 = doc.create_element("h1"); let h1_text = doc.create_text("Big"); let p = doc.create_element("p"); let p_text = doc.create_text("Small"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, h1); doc.append_child(h1, h1_text); doc.append_child(body, p); doc.append_child(p, p_text);
let tree = layout_doc(&doc); let list = build_display_list(&tree);
// There should be DrawGlyphs commands with different font sizes. let font_sizes: Vec<f32> = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { font_size, .. } => Some(*font_size), _ => None, }) .collect();
assert!(font_sizes.len() >= 2, "should have at least 2 text lines"); // h1 has font_size 32, p has font_size 16. assert!( font_sizes.iter().any(|&s| s > 20.0), "should have a heading with large font size" ); assert!( font_sizes.iter().any(|&s| s < 20.0), "should have a paragraph with normal font size" ); }
#[test] fn renderer_zero_size() { let r = Renderer::new(0, 0); assert_eq!(r.pixels().len(), 0); }
#[test] fn glyph_compositing_anti_aliased() { // Render text and verify we get anti-aliased (partially transparent) pixels. let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); let body = doc.create_element("body"); let p = doc.create_element("p"); let text = doc.create_text("MMMMM"); doc.append_child(root, html); doc.append_child(html, body); doc.append_child(body, p); doc.append_child(p, text);
let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(800, 600); renderer.paint(&tree, &font);
let pixels = renderer.pixels(); // Find pixels that are not pure white and not pure black. // These represent anti-aliased edges of glyphs. let mut has_intermediate = false; for pixel in pixels.chunks_exact(4) { let b = pixel[0]; let g = pixel[1]; let r = pixel[2]; // Anti-aliased pixel: gray between white and black. if r > 0 && r < 255 && r == g && g == b { has_intermediate = true; break; } } assert!( has_intermediate, "should have anti-aliased (gray) pixels from glyph compositing" ); }
#[test] fn css_color_renders_correctly() { let html_str = r#"<!DOCTYPE html><html><head><style>p { color: red; }</style></head><body><p>Red text</p></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font);
let list = build_display_list(&tree); let text_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::DrawGlyphs { color, .. } => Some(color), _ => None, }) .collect();
assert!(!text_colors.is_empty()); // Text should be red. assert_eq!(*text_colors[0], Color::rgb(255, 0, 0)); }
#[test] fn css_background_color_renders() { let html_str = r#"<!DOCTYPE html><html><head><style>div { background-color: yellow; }</style></head><body><div>Content</div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font);
let list = build_display_list(&tree); let fill_colors: Vec<&Color> = list .iter() .filter_map(|c| match c { PaintCommand::FillRect { color, .. } => Some(color), _ => None, }) .collect();
// Should have a yellow fill rect for the div background. assert!(fill_colors.iter().any(|c| **c == Color::rgb(255, 255, 0))); }
#[test] fn border_rendering() { let html_str = r#"<!DOCTYPE html><html><head><style>div { border: 2px solid red; }</style></head><body><div>Bordered</div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets).unwrap(); let tree = we_layout::layout(&styled, &doc, 800.0, 600.0, &font);
let list = build_display_list(&tree); let red_fills: Vec<_> = list .iter() .filter(|c| matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0))) .collect();
// Should have 4 border fills (top, right, bottom, left). assert_eq!(red_fills.len(), 4, "should have 4 border edges"); }}