Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666//! 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 std::collections::HashMap;
use we_css::values::Color;use we_dom::NodeId;use we_image::pixel::Image;use we_layout::{BoxType, LayoutBox, LayoutTree, Rect, TextLine, SCROLLBAR_WIDTH};use we_style::computed::{BorderStyle, Overflow, TextDecoration, Visibility};use we_text::font::Font;
/// Scroll state: maps NodeId of scrollable boxes to their (scroll_x, scroll_y) offsets.pub type ScrollState = HashMap<NodeId, (f32, f32)>;
/// Scroll bar track color (light gray).const SCROLLBAR_TRACK_COLOR: Color = Color { r: 230, g: 230, b: 230, a: 255,};
/// Scroll bar thumb color (darker gray).const SCROLLBAR_THUMB_COLOR: Color = Color { r: 160, g: 160, b: 160, a: 255,};
/// 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, }, /// Draw an image at a position with given display dimensions. DrawImage { x: f32, y: f32, width: f32, height: f32, node_id: NodeId, }, /// Push a clip rectangle onto the clip stack. All subsequent paint /// commands are clipped to the intersection of all active clip rects. PushClip { x: f32, y: f32, width: f32, height: f32, }, /// Pop the most recent clip rectangle off the clip stack. PopClip,}
/// 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 { build_display_list_with_scroll(tree, &HashMap::new())}
/// Build a display list from a layout tree with scroll state.////// `page_scroll` is the viewport-level scroll offset (x, y) applied to all content./// `scroll_state` maps element NodeIds to their per-element scroll offsets.pub fn build_display_list_with_scroll( tree: &LayoutTree, scroll_state: &ScrollState,) -> DisplayList { let mut list = DisplayList::new(); paint_box(&tree.root, &mut list, (0.0, 0.0), scroll_state); list}
/// Build a display list with page-level scrolling.////// `page_scroll_y` shifts all content vertically (viewport-level scroll).pub fn build_display_list_with_page_scroll( tree: &LayoutTree, page_scroll_y: f32, scroll_state: &ScrollState,) -> DisplayList { let mut list = DisplayList::new(); paint_box(&tree.root, &mut list, (0.0, -page_scroll_y), scroll_state); list}
fn paint_box( layout_box: &LayoutBox, list: &mut DisplayList, translate: (f32, f32), scroll_state: &ScrollState,) { let visible = layout_box.visibility == Visibility::Visible; let tx = translate.0; let ty = translate.1;
if visible { paint_background(layout_box, list, tx, ty); paint_borders(layout_box, list, tx, ty);
// Emit image paint command for replaced elements. if let Some((rw, rh)) = layout_box.replaced_size { if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { list.push(PaintCommand::DrawImage { x: layout_box.rect.x + tx, y: layout_box.rect.y + ty, width: rw, height: rh, node_id, }); } }
paint_text(layout_box, list, tx, ty); }
// Determine if this box is scrollable. let scrollable = layout_box.overflow == Overflow::Scroll || layout_box.overflow == Overflow::Auto;
// If this box has overflow clipping, push a clip rect for the padding box. let clips = layout_box.overflow != Overflow::Visible; if clips { let clip = padding_box(layout_box); list.push(PaintCommand::PushClip { x: clip.x + tx, y: clip.y + ty, width: clip.width, height: clip.height, }); }
// Compute child translate: adds scroll offset for scrollable boxes. let mut child_translate = translate; if scrollable { if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { if let Some(&(sx, sy)) = scroll_state.get(&node_id) { child_translate.0 -= sx; child_translate.1 -= sy; } } }
// Always recurse into children — they may override visibility. for child in &layout_box.children { paint_box(child, list, child_translate, scroll_state); }
if clips { list.push(PaintCommand::PopClip); }
// Paint scroll bars after PopClip so they're not clipped by the container. if scrollable && visible { paint_scrollbars(layout_box, list, tx, ty, scroll_state); }}
/// Compute the padding box rectangle for a layout box./// The padding box is the content area expanded by padding.fn padding_box(layout_box: &LayoutBox) -> Rect { Rect { x: layout_box.rect.x - layout_box.padding.left, y: layout_box.rect.y - layout_box.padding.top, width: layout_box.rect.width + layout_box.padding.left + layout_box.padding.right, height: layout_box.rect.height + layout_box.padding.top + layout_box.padding.bottom, }}
/// Extract the NodeId from a BoxType, if it has one.fn node_id_from_box_type(box_type: &BoxType) -> Option<NodeId> { match box_type { BoxType::Block(id) | BoxType::Inline(id) => Some(*id), BoxType::TextRun { node, .. } => Some(*node), BoxType::Anonymous => None, }}
fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { 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 + tx, y: layout_box.rect.y + ty, width: layout_box.rect.width, height: layout_box.rect.height, color: bg, }); }}
fn paint_borders(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32) { 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 + tx; let by = r.y - layout_box.padding.top - b.top + ty; 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, tx: f32, ty: f32) { 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();
// Create a translated copy of the text line. let mut translated_line = line.clone(); translated_line.x += tx; translated_line.y += ty;
list.push(PaintCommand::DrawGlyphs { line: translated_line, 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 + ty + font_size; let underline_y = baseline_y + 2.0; list.push(PaintCommand::FillRect { x: line.x + tx, 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 + tx, y: line.y + ty, width: line.width, height: font_size * 1.2, color: line.background_color, }, ); } }}
/// Paint scroll bars for a scrollable box.fn paint_scrollbars( layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: f32, scroll_state: &ScrollState,) { let pad = padding_box(layout_box); let viewport_height = pad.height; let content_height = layout_box.content_height;
// Determine whether scroll bars should be shown. let show_vertical = match layout_box.overflow { Overflow::Scroll => true, Overflow::Auto => content_height > viewport_height, _ => false, };
if !show_vertical || viewport_height <= 0.0 { return; }
// Scroll bar track: right edge of the padding box. let track_x = pad.x + pad.width - SCROLLBAR_WIDTH + tx; let track_y = pad.y + ty; let track_height = viewport_height;
// Paint the track background. list.push(PaintCommand::FillRect { x: track_x, y: track_y, width: SCROLLBAR_WIDTH, height: track_height, color: SCROLLBAR_TRACK_COLOR, });
// Compute thumb size and position. let max_content = content_height.max(viewport_height); let thumb_ratio = viewport_height / max_content; let thumb_height = (thumb_ratio * track_height).max(20.0).min(track_height);
// Get current scroll offset. let scroll_y = node_id_from_box_type(&layout_box.box_type) .and_then(|id| scroll_state.get(&id)) .map(|&(_, sy)| sy) .unwrap_or(0.0);
let max_scroll = (content_height - viewport_height).max(0.0); let scroll_ratio = if max_scroll > 0.0 { scroll_y / max_scroll } else { 0.0 }; let thumb_y = track_y + scroll_ratio * (track_height - thumb_height);
// Paint the thumb. list.push(PaintCommand::FillRect { x: track_x, y: thumb_y, width: SCROLLBAR_WIDTH, height: thumb_height, color: SCROLLBAR_THUMB_COLOR, });}
/// An axis-aligned clip rectangle.#[derive(Debug, Clone, Copy)]struct ClipRect { x0: f32, y0: f32, x1: f32, y1: f32,}
impl ClipRect { /// Intersect two clip rects, returning the overlapping region. /// Returns None if they don't overlap. fn intersect(self, other: ClipRect) -> Option<ClipRect> { let x0 = self.x0.max(other.x0); let y0 = self.y0.max(other.y0); let x1 = self.x1.min(other.x1); let y1 = self.y1.min(other.y1); if x0 < x1 && y0 < y1 { Some(ClipRect { x0, y0, x1, y1 }) } else { None } }}
/// 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>, /// Stack of clip rectangles. When non-empty, all drawing is clipped to /// the intersection of all active clip rects. clip_stack: Vec<ClipRect>,}
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, clip_stack: Vec::new(), } }
/// Compute the effective clip rect from the clip stack. /// Returns None if the clip stack is empty (no clipping). fn active_clip(&self) -> Option<ClipRect> { if self.clip_stack.is_empty() { return None; } // Start with the full buffer as the initial rect, then intersect. let mut result = ClipRect { x0: 0.0, y0: 0.0, x1: self.width as f32, y1: self.height as f32, }; for clip in &self.clip_stack { match result.intersect(*clip) { Some(r) => result = r, None => { return Some(ClipRect { x0: 0.0, y0: 0.0, x1: 0.0, y1: 0.0, }) } } } Some(result) }
/// Paint a layout tree into the pixel buffer. pub fn paint( &mut self, layout_tree: &LayoutTree, font: &Font, images: &HashMap<NodeId, &Image>, ) { self.paint_with_scroll(layout_tree, font, images, 0.0, &HashMap::new()); }
/// Paint a layout tree with scroll state into the pixel buffer. pub fn paint_with_scroll( &mut self, layout_tree: &LayoutTree, font: &Font, images: &HashMap<NodeId, &Image>, page_scroll_y: f32, scroll_state: &ScrollState, ) { let display_list = build_display_list_with_page_scroll(layout_tree, page_scroll_y, scroll_state); 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); } PaintCommand::DrawImage { x, y, width, height, node_id, } => { if let Some(image) = images.get(node_id) { self.draw_image(*x, *y, *width, *height, image); } } PaintCommand::PushClip { x, y, width, height, } => { self.clip_stack.push(ClipRect { x0: *x, y0: *y, x1: *x + *width, y1: *y + *height, }); } PaintCommand::PopClip => { self.clip_stack.pop(); } } } }
/// 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 mut fx0 = x; let mut fy0 = y; let mut fx1 = x + width; let mut fy1 = y + height;
// Apply clip rect if active. if let Some(clip) = self.active_clip() { fx0 = fx0.max(clip.x0); fy0 = fy0.max(clip.y0); fx1 = fx1.min(clip.x1); fy1 = fy1.min(clip.y1); if fx0 >= fx1 || fy0 >= fy1 { return; } }
let x0 = (fx0 as i32).max(0) as u32; let y0 = (fy0 as i32).max(0) as u32; let x1 = (fx1 as i32).max(0).min(self.width as i32) as u32; let y1 = (fy1 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; let clip = self.active_clip();
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; }
// Apply clip rect. if let Some(ref c) = clip { if (px as f32) < c.x0 || (px as f32) >= c.x1 || (py as f32) < c.y0 || (py as f32) >= c.y1 { 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. } } }
/// Draw an RGBA8 image scaled to the given display dimensions. /// /// Uses nearest-neighbor sampling for scaling. The source image is in RGBA8 /// format; the buffer is BGRA. Alpha compositing is performed for semi-transparent pixels. fn draw_image(&mut self, x: f32, y: f32, width: f32, height: f32, image: &Image) { if image.width == 0 || image.height == 0 || width <= 0.0 || height <= 0.0 { return; }
let mut fx0 = x; let mut fy0 = y; let mut fx1 = x + width; let mut fy1 = y + height;
if let Some(clip) = self.active_clip() { fx0 = fx0.max(clip.x0); fy0 = fy0.max(clip.y0); fx1 = fx1.min(clip.x1); fy1 = fy1.min(clip.y1); if fx0 >= fx1 || fy0 >= fy1 { return; } }
let dst_x0 = (fx0 as i32).max(0) as u32; let dst_y0 = (fy0 as i32).max(0) as u32; let dst_x1 = (fx1 as i32).max(0).min(self.width as i32) as u32; let dst_y1 = (fy1 as i32).max(0).min(self.height as i32) as u32;
let scale_x = image.width as f32 / width; let scale_y = image.height as f32 / height;
for dst_y in dst_y0..dst_y1 { let src_y = ((dst_y as f32 - y) * scale_y) as u32; let src_y = src_y.min(image.height - 1);
for dst_x in dst_x0..dst_x1 { let src_x = ((dst_x as f32 - x) * scale_x) as u32; let src_x = src_x.min(image.width - 1);
let src_offset = ((src_y * image.width + src_x) * 4) as usize; let r = image.data[src_offset] as u32; let g = image.data[src_offset + 1] as u32; let b = image.data[src_offset + 2] as u32; let a = image.data[src_offset + 3] as u32;
if a == 0 { continue; }
let dst_offset = ((dst_y * self.width + dst_x) * 4) as usize;
if a == 255 { // Fully opaque — direct write (RGBA → BGRA). self.buffer[dst_offset] = b as u8; self.buffer[dst_offset + 1] = g as u8; self.buffer[dst_offset + 2] = r as u8; self.buffer[dst_offset + 3] = 255; } else { // Alpha blend. let inv_a = 255 - a; let dst_b = self.buffer[dst_offset] as u32; let dst_g = self.buffer[dst_offset + 1] as u32; let dst_r = self.buffer[dst_offset + 2] as u32; self.buffer[dst_offset] = ((b * a + dst_b * inv_a) / 255) as u8; self.buffer[dst_offset + 1] = ((g * a + dst_g * inv_a) / 255) as u8; self.buffer[dst_offset + 2] = ((r * a + dst_r * inv_a) / 255) as u8; self.buffer[dst_offset + 3] = 255; } } } }
/// 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, (800.0, 600.0)).unwrap(); we_layout::layout( &styled, doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ) }
#[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, (800.0, 600.0)); if let Some(styled) = styled { let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), ); 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, &HashMap::new());
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, &HashMap::new());
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, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
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, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
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, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, 800.0, 600.0, &font, &std::collections::HashMap::new(), );
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"); }
// --- Visibility painting tests ---
#[test] fn visibility_hidden_not_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }div { visibility: hidden; background-color: red; width: 100px; height: 100px; }</style></head><body><div>Hidden</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// No red background should be in the display list. let red_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(255, 0, 0)) }); assert_eq!(red_fills.count(), 0, "hidden element should not be painted");
// No text should be rendered for the hidden element. let hidden_text = list.iter().filter( |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text == "Hidden"), ); assert_eq!( hidden_text.count(), 0, "hidden element text should not be painted" ); }
#[test] fn visibility_visible_child_of_hidden_parent_is_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.parent { visibility: hidden; }.child { visibility: visible; }</style></head><body><div class="parent"><p class="child">Visible</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// The visible child's text should appear in the display list. let visible_text = list.iter().filter( |c| matches!(c, PaintCommand::DrawGlyphs { line, .. } if line.text.contains("Visible")), ); assert!( visible_text.count() > 0, "visible child of hidden parent should be painted" ); }
#[test] fn visibility_collapse_not_painted() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }div { visibility: collapse; background-color: blue; width: 50px; height: 50px; }</style></head><body><div>Collapsed</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let blue_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 0, 255)) }); assert_eq!( blue_fills.count(), 0, "collapse element should not be painted" ); }
// --- Overflow clipping tests ---
#[test] fn overflow_hidden_generates_clip_commands() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: hidden; width: 100px; height: 50px; }</style></head><body><div class="container"><p>Content</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let push_count = list .iter() .filter(|c| matches!(c, PaintCommand::PushClip { .. })) .count(); let pop_count = list .iter() .filter(|c| matches!(c, PaintCommand::PopClip)) .count();
assert!(push_count >= 1, "overflow:hidden should emit PushClip"); assert_eq!(push_count, pop_count, "PushClip/PopClip must be balanced"); }
#[test] fn overflow_visible_no_clip_commands() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: visible; width: 100px; height: 50px; }</style></head><body><div class="container"><p>Content</p></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let push_count = list .iter() .filter(|c| matches!(c, PaintCommand::PushClip { .. })) .count();
assert_eq!( push_count, 0, "overflow:visible should not emit clip commands" ); }
#[test] fn overflow_hidden_clips_child_background() { // A tall child inside a short overflow:hidden container. // The child's red background should be clipped to the container's bounds. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: hidden; width: 100px; height: 50px; }.child { background-color: red; width: 100px; height: 200px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(200, 200); renderer.paint(&tree, &font, &HashMap::new());
let pixels = renderer.pixels();
// Pixel at (50, 25) — inside the container — should be red. let inside_offset = ((25 * 200 + 50) * 4) as usize; assert_eq!(pixels[inside_offset], 0, "B inside should be 0 (red)"); assert_eq!( pixels[inside_offset + 2], 255, "R inside should be 255 (red)" );
// Pixel at (50, 100) — outside the container (below 50px) — should be white. let outside_offset = ((100 * 200 + 50) * 4) as usize; assert_eq!( pixels[outside_offset], 255, "B outside should be 255 (white)" ); assert_eq!( pixels[outside_offset + 1], 255, "G outside should be 255 (white)" ); assert_eq!( pixels[outside_offset + 2], 255, "R outside should be 255 (white)" ); }
#[test] fn overflow_auto_clips_like_hidden() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: auto; width: 100px; height: 50px; }.child { background-color: blue; width: 100px; height: 200px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(200, 200); renderer.paint(&tree, &font, &HashMap::new());
let pixels = renderer.pixels();
// Pixel at (50, 100) — below the container — should be white (clipped). let outside_offset = ((100 * 200 + 50) * 4) as usize; assert_eq!( pixels[outside_offset], 255, "overflow:auto should clip content below container" ); }
#[test] fn overflow_scroll_clips_like_hidden() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 100px; height: 50px; }.child { background-color: green; width: 100px; height: 200px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(200, 200); renderer.paint(&tree, &font, &HashMap::new());
let pixels = renderer.pixels();
// Pixel at (50, 100) — below the container — should be white (clipped). let outside_offset = ((100 * 200 + 50) * 4) as usize; assert_eq!( pixels[outside_offset], 255, "overflow:scroll should clip content below container" ); }
#[test] fn nested_overflow_clips_intersect() { // Outer: 200x200, inner: 100x100, both overflow:hidden. // A large red child should only appear in the inner 100x100 area. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.outer { overflow: hidden; width: 200px; height: 200px; }.inner { overflow: hidden; width: 100px; height: 100px; }.child { background-color: red; width: 500px; height: 500px; }</style></head><body><div class="outer"><div class="inner"><div class="child"></div></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc); let mut renderer = Renderer::new(300, 300); renderer.paint(&tree, &font, &HashMap::new());
let pixels = renderer.pixels();
// Pixel at (50, 50) — inside inner container — should be red. let inside_offset = ((50 * 300 + 50) * 4) as usize; assert_eq!(pixels[inside_offset + 2], 255, "should be red inside inner");
// Pixel at (150, 50) — inside outer but outside inner — should be white. let between_offset = ((50 * 300 + 150) * 4) as usize; assert_eq!( pixels[between_offset], 255, "should be white outside inner clip" );
// Pixel at (250, 50) — outside both — should be white. let outside_offset = ((50 * 300 + 250) * 4) as usize; assert_eq!( pixels[outside_offset], 255, "should be white outside both clips" ); }
#[test] fn clip_rect_intersect_basic() { let a = ClipRect { x0: 0.0, y0: 0.0, x1: 100.0, y1: 100.0, }; let b = ClipRect { x0: 50.0, y0: 50.0, x1: 150.0, y1: 150.0, }; let c = a.intersect(b).unwrap(); assert_eq!(c.x0, 50.0); assert_eq!(c.y0, 50.0); assert_eq!(c.x1, 100.0); assert_eq!(c.y1, 100.0); }
#[test] fn clip_rect_no_overlap() { let a = ClipRect { x0: 0.0, y0: 0.0, x1: 50.0, y1: 50.0, }; let b = ClipRect { x0: 100.0, y0: 100.0, x1: 200.0, y1: 200.0, }; assert!(a.intersect(b).is_none()); }
#[test] fn display_none_not_in_display_list() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.gone { display: none; background-color: green; }</style></head><body><p>Visible</p><div class="gone">Gone</div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let green_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == Color::rgb(0, 128, 0)) }); assert_eq!( green_fills.count(), 0, "display:none element should not be in display list" ); }
// --- Overflow scrolling tests ---
#[test] fn overflow_scroll_renders_scrollbar_always() { // overflow:scroll should always render scroll bars, even when content fits. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 200px; height: 200px; }.child { height: 50px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Should have scroll bar track and thumb FillRect commands. let scrollbar_fills: Vec<_> = list .iter() .filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) }) .collect();
assert!( scrollbar_fills.len() >= 2, "overflow:scroll should render track and thumb (got {} fills)", scrollbar_fills.len() ); }
#[test] fn overflow_auto_scrollbar_only_when_overflows() { // overflow:auto with content that fits should NOT show scroll bars. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: auto; width: 200px; height: 200px; }.child { height: 50px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let scrollbar_fills = list.iter().filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) });
assert_eq!( scrollbar_fills.count(), 0, "overflow:auto should not render scroll bars when content fits" ); }
#[test] fn overflow_auto_scrollbar_when_content_overflows() { // overflow:auto with content taller than container should show scroll bars. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: auto; width: 200px; height: 100px; }.child { background-color: red; height: 500px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
let scrollbar_fills: Vec<_> = list .iter() .filter(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_TRACK_COLOR || *color == SCROLLBAR_THUMB_COLOR) }) .collect();
assert!( scrollbar_fills.len() >= 2, "overflow:auto should render scroll bars when content overflows (got {} fills)", scrollbar_fills.len() ); }
#[test] fn scroll_offset_shifts_content() { // Scrolling should shift content within a scroll container. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 200px; height: 100px; }.child { background-color: red; width: 200px; height: 500px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc);
// Find the container's NodeId to set scroll offset. let container_id = tree .root .iter() .find_map(|b| { if b.overflow == Overflow::Scroll { node_id_from_box_type(&b.box_type) } else { None } }) .expect("should find scroll container");
// Without scroll: pixel at (50, 50) should be red (inside child). let mut renderer = Renderer::new(300, 300); renderer.paint(&tree, &font, &HashMap::new()); let pixels = renderer.pixels(); let offset_50_50 = ((50 * 300 + 50) * 4) as usize; assert_eq!( pixels[offset_50_50 + 2], 255, "before scroll: (50,50) should be red" );
// With scroll offset 60px: content shifts up, so pixel at (50, 50) should // still be red (content extends to 500px), but pixel at (50, 0) should now // show content that was at y=60. let mut scroll_state = HashMap::new(); scroll_state.insert(container_id, (0.0, 60.0)); let mut renderer2 = Renderer::new(300, 300); renderer2.paint_with_scroll(&tree, &font, &HashMap::new(), 0.0, &scroll_state); let pixels2 = renderer2.pixels();
// After scrolling 60px, content starts at y=-60 in the viewport. // The container clips to its bounds, so pixel at (50, 50) is still visible // red content (it was at y=110 in original content, now at y=50). assert_eq!( pixels2[offset_50_50 + 2], 255, "after scroll: (50,50) should still be red (content is tall)" ); }
#[test] fn scroll_bar_thumb_proportional() { // Thumb size should be proportional to viewport/content ratio. let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.container { overflow: scroll; width: 200px; height: 100px; }.child { height: 400px; }</style></head><body><div class="container"><div class="child"></div></div></body></html>"#; let doc = we_html::parse_html(html_str); let tree = layout_doc(&doc); let list = build_display_list(&tree);
// Find the thumb FillRect (SCROLLBAR_THUMB_COLOR). let thumb = list.iter().find(|c| { matches!(c, PaintCommand::FillRect { color, .. } if *color == SCROLLBAR_THUMB_COLOR) });
assert!(thumb.is_some(), "should have a scroll bar thumb"); if let Some(PaintCommand::FillRect { height, .. }) = thumb { // Container height is 100, content is 400. // Thumb ratio = 100/400 = 0.25, track height = 100. // Thumb height = max(0.25 * 100, 20) = 25. assert!( *height >= 20.0 && *height <= 100.0, "thumb height {} should be proportional and within bounds", height ); } }
#[test] fn page_scroll_shifts_all_content() { let html_str = r#"<!DOCTYPE html><html><head><style>body { margin: 0; }.block { background-color: red; width: 100px; height: 100px; }</style></head><body><div class="block"></div></body></html>"#; let doc = we_html::parse_html(html_str); let font = test_font(); let tree = layout_doc(&doc);
// Without page scroll: red at (50, 50). let mut r1 = Renderer::new(200, 200); r1.paint(&tree, &font, &HashMap::new()); let offset = ((50 * 200 + 50) * 4) as usize; assert_eq!(r1.pixels()[offset + 2], 255, "should be red without scroll");
// With page scroll 80px: the red block shifts up by 80px. // Pixel at (50, 50) was at y=130 in content, which is beyond the 100px block. let mut r2 = Renderer::new(200, 200); r2.paint_with_scroll(&tree, &font, &HashMap::new(), 80.0, &HashMap::new()); let pixels2 = r2.pixels(); // At (50, 50) with 80px scroll: this shows content at y=130, which is white. assert_eq!( pixels2[offset], 255, "should be white at (50,50) with 80px page scroll" );
// Pixel at (50, 10) with 80px scroll shows content at y=90, which is still red. let offset_10 = ((10 * 200 + 50) * 4) as usize; assert_eq!( pixels2[offset_10 + 2], 255, "should be red at (50,10) with 80px page scroll" ); }}