Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796//! Retained layer tree and damage tracking for minimal repaints.//!//! The layer tree maintains a persistent record of layout box state between//! frames. Each frame, it compares new layout boxes against previous state and//! computes damage rectangles — screen regions that need repainting.//!//! The GPU and software renderers can use these damage rects as scissor regions//! to avoid full-screen repaints when only small areas changed (e.g. cursor//! blink, hover effects).
use std::collections::HashMap;
use we_css::values::Color;use we_dom::NodeId;use we_layout::{LayoutBox, LayoutTree, Rect};use we_style::computed::{Overflow, Position, Visibility};
use crate::{ build_display_list_into, node_id_from_box_type, DisplayList, PaintCommand, ScrollState,};
// ---------------------------------------------------------------------------// Damage rectangles// ---------------------------------------------------------------------------
/// An axis-aligned damage rectangle in screen coordinates.#[derive(Debug, Clone, Copy, PartialEq)]pub struct DamageRect { pub x: f32, pub y: f32, pub width: f32, pub height: f32,}
impl DamageRect { pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { Self { x, y, width, height, } }
/// Create a damage rect from a layout `Rect`. pub fn from_rect(r: Rect) -> Self { Self { x: r.x, y: r.y, width: r.width, height: r.height, } }
/// Union of two damage rects: the smallest rect containing both. pub fn union(self, other: DamageRect) -> DamageRect { let x0 = self.x.min(other.x); let y0 = self.y.min(other.y); let x1 = (self.x + self.width).max(other.x + other.width); let y1 = (self.y + self.height).max(other.y + other.height); DamageRect { x: x0, y: y0, width: x1 - x0, height: y1 - y0, } }
/// Returns true if this rect has zero or negative area. pub fn is_empty(&self) -> bool { self.width <= 0.0 || self.height <= 0.0 }
/// Returns true if this rect intersects with another. pub fn intersects(&self, other: &DamageRect) -> bool { self.x < other.x + other.width && self.x + self.width > other.x && self.y < other.y + other.height && self.y + self.height > other.y }
/// Expand this rect by a margin on all sides. Used for merging nearby rects. pub fn expand(&self, margin: f32) -> DamageRect { DamageRect { x: self.x - margin, y: self.y - margin, width: self.width + margin * 2.0, height: self.height + margin * 2.0, } }}
/// Merge a list of damage rects, coalescing nearby or overlapping ones to/// reduce draw calls. Two rects are merged if they overlap or are within/// `merge_threshold` pixels of each other.pub fn merge_damage_rects(rects: &[DamageRect], merge_threshold: f32) -> Vec<DamageRect> { if rects.is_empty() { return Vec::new(); }
let mut merged: Vec<DamageRect> = Vec::new();
for &rect in rects { if rect.is_empty() { continue; }
let mut did_merge = false; for existing in &mut merged { if existing.expand(merge_threshold).intersects(&rect) { *existing = existing.union(rect); did_merge = true; break; } } if !did_merge { merged.push(rect); } }
// Second pass: merge any newly-overlapping rects created by the first pass. let mut stable = false; while !stable { stable = true; let mut i = 0; while i < merged.len() { let mut j = i + 1; while j < merged.len() { if merged[i].expand(merge_threshold).intersects(&merged[j]) { let b = merged.remove(j); merged[i] = merged[i].union(b); stable = false; } else { j += 1; } } i += 1; } }
merged}
// ---------------------------------------------------------------------------// Box fingerprint (for change detection)// ---------------------------------------------------------------------------
/// A lightweight fingerprint of a layout box's paint-relevant properties./// Used for fast comparison to detect changes between frames.#[derive(Debug, Clone, PartialEq)]struct BoxFingerprint { rect: Rect, background_color: Color, color: Color, opacity: f32, visibility: Visibility, overflow: Overflow, position: Position, z_index: Option<i32>, border_widths: [u32; 4], // stored as f32 bit patterns border_colors: [Color; 4], text_hash: u64, child_count: usize, content_height_bits: u32,}
impl BoxFingerprint { fn from_layout_box(b: &LayoutBox) -> Self { let text_hash = b.lines.iter().fold(0u64, |acc, line| { let mut h = acc; for byte in line.text.as_bytes() { h = h.wrapping_mul(31).wrapping_add(*byte as u64); } h = h.wrapping_mul(31).wrapping_add(line.x.to_bits() as u64); h = h.wrapping_mul(31).wrapping_add(line.y.to_bits() as u64); h });
BoxFingerprint { rect: b.rect, background_color: b.background_color, color: b.color, opacity: b.opacity, visibility: b.visibility, overflow: b.overflow, position: b.position, z_index: b.z_index, border_widths: [ b.border.top.to_bits(), b.border.right.to_bits(), b.border.bottom.to_bits(), b.border.left.to_bits(), ], border_colors: b.border_colors, text_hash, child_count: b.children.len(), content_height_bits: b.content_height.to_bits(), } }
/// Compute a damage rect from this fingerprint (border box approximation). fn damage_rect(&self) -> DamageRect { let bt = f32::from_bits(self.border_widths[0]); let br = f32::from_bits(self.border_widths[1]); let bb = f32::from_bits(self.border_widths[2]); let bl = f32::from_bits(self.border_widths[3]);
DamageRect { x: self.rect.x - bl, y: self.rect.y - bt, width: self.rect.width + bl + br, height: self.rect.height + bt + bb, } }}
// ---------------------------------------------------------------------------// Retained layer tree// ---------------------------------------------------------------------------
/// Retained layer tree that persists between frames and tracks damage.////// On each frame, call [`update`] with the new layout tree. The layer tree/// compares the new layout against the previous frame's snapshot and produces/// a set of damage rects indicating which screen regions need repainting.////// For the first frame or after a full invalidation, [`damage_rects`] returns/// `None`, indicating that the entire viewport should be repainted.pub struct LayerTree { /// Previous frame's box fingerprints, keyed by NodeId. prev_fingerprints: HashMap<NodeId, BoxFingerprint>, /// Collected damage rects from the most recent update. damage: Option<Vec<DamageRect>>, /// Viewport dimensions. viewport_width: f32, viewport_height: f32, /// Whether this is the first frame (no previous state). first_frame: bool, /// The display list from the most recent frame. cached_display_list: DisplayList, /// Repaint metrics for debugging/testing. metrics: RepaintMetrics,}
/// Metrics about what was repainted in the last frame.#[derive(Debug, Clone, Default)]pub struct RepaintMetrics { /// Number of damage rects produced. pub damage_rect_count: usize, /// Total area of damage rects (in px^2). pub damage_area: f32, /// Whether a full repaint was performed. pub full_repaint: bool, /// Number of boxes that changed between frames. pub changed_box_count: usize,}
impl LayerTree { /// Create a new layer tree for the given viewport size. pub fn new(viewport_width: f32, viewport_height: f32) -> Self { Self { prev_fingerprints: HashMap::new(), damage: None, viewport_width, viewport_height, first_frame: true, cached_display_list: Vec::new(), metrics: RepaintMetrics::default(), } }
/// Resize the viewport. Triggers a full repaint on the next frame. pub fn resize(&mut self, width: f32, height: f32) { if (self.viewport_width - width).abs() > 0.001 || (self.viewport_height - height).abs() > 0.001 { self.viewport_width = width; self.viewport_height = height; self.invalidate(); } }
/// Force a full repaint on the next frame. pub fn invalidate(&mut self) { self.first_frame = true; self.damage = None; }
/// Update the layer tree with a new layout. Computes damage rects by /// comparing the new layout against the previous frame's state. /// /// After calling this, use [`damage_rects`] to query which regions need /// repainting, and [`display_list`] to get the full display list. pub fn update(&mut self, tree: &LayoutTree, page_scroll_y: f32, scroll_state: &ScrollState) { // Reuse the display list buffer: clear and refill instead of drop and alloc. build_display_list_into( tree, page_scroll_y, scroll_state, &mut self.cached_display_list, );
if self.first_frame { let mut new_fingerprints = HashMap::new(); collect_fingerprints(&tree.root, &mut new_fingerprints); self.prev_fingerprints = new_fingerprints; self.first_frame = false; self.damage = None; self.metrics = RepaintMetrics { damage_rect_count: 0, damage_area: 0.0, full_repaint: true, changed_box_count: 0, }; return; }
let mut new_fingerprints = HashMap::new(); collect_fingerprints(&tree.root, &mut new_fingerprints);
let mut damage_rects = Vec::new(); let mut changed_count = 0;
// Changed or new boxes. for (node_id, new_fp) in &new_fingerprints { match self.prev_fingerprints.get(node_id) { Some(old_fp) if old_fp == new_fp => {} Some(old_fp) => { changed_count += 1; let old_d = old_fp.damage_rect(); let new_d = new_fp.damage_rect(); if !old_d.is_empty() { damage_rects.push(old_d); } if !new_d.is_empty() { damage_rects.push(new_d); } } None => { changed_count += 1; let d = new_fp.damage_rect(); if !d.is_empty() { damage_rects.push(d); } } } }
// Removed boxes. for (node_id, old_fp) in &self.prev_fingerprints { if !new_fingerprints.contains_key(node_id) { changed_count += 1; let d = old_fp.damage_rect(); if !d.is_empty() { damage_rects.push(d); } } }
let merged = merge_damage_rects(&damage_rects, 16.0); let total_area: f32 = merged.iter().map(|r| r.width * r.height).sum();
self.metrics = RepaintMetrics { damage_rect_count: merged.len(), damage_area: total_area, full_repaint: false, changed_box_count: changed_count, };
let viewport_area = self.viewport_width * self.viewport_height; if viewport_area > 0.0 && total_area > viewport_area * 0.75 { self.metrics.full_repaint = true; self.damage = None; } else if merged.is_empty() { self.damage = Some(Vec::new()); } else { self.damage = Some(merged); }
self.prev_fingerprints = new_fingerprints; }
/// Get the damage rects from the most recent update. /// /// Returns `None` if a full repaint is needed (first frame, resize, or /// damage covers most of the viewport). Returns `Some(empty)` if nothing /// changed. Returns `Some(rects)` for partial damage. pub fn damage_rects(&self) -> Option<&[DamageRect]> { self.damage.as_deref() }
/// Get the full display list from the most recent update. pub fn display_list(&self) -> &DisplayList { &self.cached_display_list }
/// Get repaint metrics from the most recent frame. pub fn metrics(&self) -> &RepaintMetrics { &self.metrics }
/// Check if any repaint is needed. Returns `false` only when zero damage. pub fn needs_repaint(&self) -> bool { match &self.damage { None => true, Some(rects) => !rects.is_empty(), } }
/// Number of paint commands in the cached display list. pub fn display_list_command_count(&self) -> usize { self.cached_display_list.len() }
/// Estimated heap bytes used by the layer tree. pub fn memory_usage(&self) -> usize { use std::mem::size_of; let fingerprints = self.prev_fingerprints.capacity() * (size_of::<NodeId>() + size_of::<BoxFingerprint>() + 8); let damage = self .damage .as_ref() .map_or(0, |v| v.capacity() * size_of::<DamageRect>()); let display_list = self.cached_display_list.capacity() * size_of::<PaintCommand>(); let dl_strings: usize = self .cached_display_list .iter() .map(|cmd| match cmd { PaintCommand::DrawGlyphs { line, .. } => line.text.capacity(), _ => 0, }) .sum(); fingerprints + damage + display_list + dl_strings }}
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
/// Collect box fingerprints from the layout tree, keyed by NodeId.fn collect_fingerprints(b: &LayoutBox, out: &mut HashMap<NodeId, BoxFingerprint>) { if let Some(id) = node_id_from_box_type(&b.box_type) { out.insert(id, BoxFingerprint::from_layout_box(b)); } for child in &b.children { collect_fingerprints(child, out); }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
// -- DamageRect tests ---------------------------------------------------
#[test] fn damage_rect_union() { let a = DamageRect::new(10.0, 10.0, 20.0, 20.0); let b = DamageRect::new(25.0, 25.0, 20.0, 20.0); let u = a.union(b); assert_eq!(u.x, 10.0); assert_eq!(u.y, 10.0); assert_eq!(u.width, 35.0); assert_eq!(u.height, 35.0); }
#[test] fn damage_rect_union_overlapping() { let a = DamageRect::new(0.0, 0.0, 50.0, 50.0); let b = DamageRect::new(10.0, 10.0, 20.0, 20.0); let u = a.union(b); assert_eq!(u.x, 0.0); assert_eq!(u.y, 0.0); assert_eq!(u.width, 50.0); assert_eq!(u.height, 50.0); }
#[test] fn damage_rect_is_empty() { assert!(DamageRect::new(0.0, 0.0, 0.0, 10.0).is_empty()); assert!(DamageRect::new(0.0, 0.0, 10.0, 0.0).is_empty()); assert!(DamageRect::new(0.0, 0.0, -1.0, 10.0).is_empty()); assert!(!DamageRect::new(0.0, 0.0, 1.0, 1.0).is_empty()); }
#[test] fn damage_rect_intersects() { let a = DamageRect::new(0.0, 0.0, 20.0, 20.0); let b = DamageRect::new(10.0, 10.0, 20.0, 20.0); assert!(a.intersects(&b));
let c = DamageRect::new(50.0, 50.0, 10.0, 10.0); assert!(!a.intersects(&c)); }
#[test] fn damage_rect_expand() { let r = DamageRect::new(10.0, 10.0, 20.0, 20.0); let e = r.expand(5.0); assert_eq!(e.x, 5.0); assert_eq!(e.y, 5.0); assert_eq!(e.width, 30.0); assert_eq!(e.height, 30.0); }
#[test] fn damage_rect_from_layout_rect() { let r = Rect { x: 5.0, y: 10.0, width: 100.0, height: 200.0, }; let d = DamageRect::from_rect(r); assert_eq!(d.x, 5.0); assert_eq!(d.y, 10.0); assert_eq!(d.width, 100.0); assert_eq!(d.height, 200.0); }
// -- merge_damage_rects tests -------------------------------------------
#[test] fn merge_empty_rects() { let result = merge_damage_rects(&[], 16.0); assert!(result.is_empty()); }
#[test] fn merge_single_rect() { let rects = vec![DamageRect::new(10.0, 10.0, 50.0, 50.0)]; let result = merge_damage_rects(&rects, 16.0); assert_eq!(result.len(), 1); assert_eq!(result[0].x, 10.0); }
#[test] fn merge_overlapping_rects() { let rects = vec![ DamageRect::new(0.0, 0.0, 30.0, 30.0), DamageRect::new(20.0, 20.0, 30.0, 30.0), ]; let result = merge_damage_rects(&rects, 0.0); assert_eq!(result.len(), 1); assert_eq!(result[0].x, 0.0); assert_eq!(result[0].y, 0.0); assert_eq!(result[0].width, 50.0); assert_eq!(result[0].height, 50.0); }
#[test] fn merge_nearby_rects() { let rects = vec![ DamageRect::new(0.0, 0.0, 20.0, 20.0), DamageRect::new(30.0, 0.0, 20.0, 20.0), ]; let result = merge_damage_rects(&rects, 16.0); assert_eq!(result.len(), 1); }
#[test] fn merge_distant_rects_stay_separate() { let rects = vec![ DamageRect::new(0.0, 0.0, 20.0, 20.0), DamageRect::new(200.0, 200.0, 20.0, 20.0), ]; let result = merge_damage_rects(&rects, 16.0); assert_eq!(result.len(), 2); }
#[test] fn merge_skips_empty_rects() { let rects = vec![ DamageRect::new(10.0, 10.0, 0.0, 0.0), DamageRect::new(20.0, 20.0, 30.0, 30.0), ]; let result = merge_damage_rects(&rects, 16.0); assert_eq!(result.len(), 1); assert_eq!(result[0].x, 20.0); }
#[test] fn merge_chain_of_nearby_rects() { let rects = vec![ DamageRect::new(0.0, 0.0, 20.0, 20.0), DamageRect::new(30.0, 0.0, 20.0, 20.0), DamageRect::new(60.0, 0.0, 20.0, 20.0), ]; let result = merge_damage_rects(&rects, 16.0); assert_eq!(result.len(), 1); }
// -- LayerTree tests (using real HTML parsing) --------------------------
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_html(html: &str) -> LayoutTree { let font = test_font(); let doc = we_html::parse_html(html); let sheets = extract_stylesheets(&doc); let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ) }
#[test] fn layer_tree_first_frame_full_repaint() { let lt = LayerTree::new(800.0, 600.0); assert!(lt.damage_rects().is_none()); assert!(lt.needs_repaint()); }
#[test] fn layer_tree_resize_triggers_full_repaint() { let mut lt = LayerTree::new(800.0, 600.0); lt.first_frame = false; lt.damage = Some(Vec::new());
lt.resize(1024.0, 768.0); assert!(lt.damage_rects().is_none()); assert!(lt.needs_repaint()); }
#[test] fn layer_tree_invalidate_triggers_full_repaint() { let mut lt = LayerTree::new(800.0, 600.0); lt.first_frame = false; lt.damage = Some(Vec::new());
lt.invalidate(); assert!(lt.damage_rects().is_none()); }
#[test] fn layer_tree_identical_layout_no_damage() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree1 = layout_html("<html><body><p>Hello</p></body></html>"); lt.update(&tree1, 0.0, &scroll_state); assert!(lt.metrics().full_repaint);
let tree2 = layout_html("<html><body><p>Hello</p></body></html>"); lt.update(&tree2, 0.0, &scroll_state); assert!(!lt.metrics().full_repaint); assert_eq!(lt.metrics().changed_box_count, 0); assert!(!lt.needs_repaint()); }
#[test] fn layer_tree_detects_text_change() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree1 = layout_html("<html><body><p>Hello</p></body></html>"); lt.update(&tree1, 0.0, &scroll_state);
let tree2 = layout_html("<html><body><p>World</p></body></html>"); lt.update(&tree2, 0.0, &scroll_state);
assert!(lt.needs_repaint()); assert!(lt.metrics().changed_box_count >= 1); }
#[test] fn layer_tree_detects_style_change() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree1 = layout_html( "<html><body><div style=\"background: white; width: 100px; height: 100px\">A</div></body></html>", ); lt.update(&tree1, 0.0, &scroll_state);
let tree2 = layout_html( "<html><body><div style=\"background: red; width: 100px; height: 100px\">A</div></body></html>", ); lt.update(&tree2, 0.0, &scroll_state);
assert!(lt.needs_repaint()); assert!(lt.metrics().changed_box_count >= 1); }
#[test] fn layer_tree_detects_added_element() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree1 = layout_html("<html><body><p>One</p></body></html>"); lt.update(&tree1, 0.0, &scroll_state);
let tree2 = layout_html("<html><body><p>One</p><p>Two</p></body></html>"); lt.update(&tree2, 0.0, &scroll_state);
assert!(lt.needs_repaint()); assert!(lt.metrics().changed_box_count >= 1); }
#[test] fn layer_tree_detects_removed_element() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree1 = layout_html("<html><body><p>One</p><p>Two</p></body></html>"); lt.update(&tree1, 0.0, &scroll_state);
let tree2 = layout_html("<html><body><p>One</p></body></html>"); lt.update(&tree2, 0.0, &scroll_state);
assert!(lt.needs_repaint()); assert!(lt.metrics().changed_box_count >= 1); }
#[test] fn layer_tree_display_list_always_available() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree = layout_html("<html><body><p>Test</p></body></html>"); lt.update(&tree, 0.0, &scroll_state);
assert!(!lt.display_list().is_empty()); }
#[test] fn layer_tree_damage_covers_changed_region() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
// Two distinct divs at known positions. let tree1 = layout_html(concat!( "<html><body>", "<div style=\"width:50px;height:50px;background:blue\">A</div>", "<div style=\"width:50px;height:50px;background:green\">B</div>", "</body></html>" )); lt.update(&tree1, 0.0, &scroll_state);
// Change only the second div's background. let tree2 = layout_html(concat!( "<html><body>", "<div style=\"width:50px;height:50px;background:blue\">A</div>", "<div style=\"width:50px;height:50px;background:red\">B</div>", "</body></html>" )); lt.update(&tree2, 0.0, &scroll_state);
assert!(lt.needs_repaint()); let rects = lt.damage_rects(); // Should have damage, not a full repaint for this small change. assert!(rects.is_some()); }
#[test] fn layer_tree_metrics_accurate_on_no_change() { let mut lt = LayerTree::new(800.0, 600.0); let scroll_state = HashMap::new();
let tree = layout_html("<html><body>Static</body></html>"); lt.update(&tree, 0.0, &scroll_state); assert!(lt.metrics().full_repaint);
let tree2 = layout_html("<html><body>Static</body></html>"); lt.update(&tree2, 0.0, &scroll_state); assert!(!lt.metrics().full_repaint); assert_eq!(lt.metrics().damage_rect_count, 0); assert_eq!(lt.metrics().damage_area, 0.0); }}