we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement canvas element & 2D rendering context infrastructure (Phase 18)

Add HTMLCanvasElement support with backing pixel buffer, layout integration
as a replaced element, render pipeline integration, and JS bindings for
getContext('2d'), width/height properties.

- DOM: canvas backing buffer storage (init, resize, clear per spec)
- Layout: canvas intrinsic sizing (default 300x150, from width/height attrs)
- Render: canvas buffers composited as images via DrawImage pipeline
- JS: getContext('2d') returns CanvasRenderingContext2D with canvas back-ref
- JS: width/height getters/setters that resize and clear the buffer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+395 -2
+24 -1
crates/browser/src/main.rs
··· 337 337 } 338 338 } 339 339 340 + /// Build `Image` objects from all canvas backing buffers in the document. 341 + fn collect_canvas_images(doc: &Document) -> HashMap<NodeId, Image> { 342 + let mut images = HashMap::new(); 343 + for (node_id, (w, h)) in doc.canvas_entries() { 344 + if w == 0 || h == 0 { 345 + continue; 346 + } 347 + if let Some(buf) = doc.canvas_buffer(node_id) { 348 + // Image::new validates length, so only insert if it matches. 349 + if let Ok(img) = Image::new(w, h, buf.to_vec()) { 350 + images.insert(node_id, img); 351 + } 352 + } 353 + } 354 + images 355 + } 356 + 340 357 /// Rasterize all SVG elements in the DOM to RGBA images. 341 358 fn rasterize_svgs(doc: &Document, font: &Font) -> HashMap<NodeId, Image> { 342 359 let mut images = HashMap::new(); ··· 767 784 // Add iframe sizes so layout treats them as replaced elements. 768 785 we_browser::iframe_loader::collect_iframe_sizes(&page.doc, &mut sizes); 769 786 787 + // Build Image objects from canvas backing buffers for the renderer. 788 + let canvas_images = collect_canvas_images(&page.doc); 789 + 770 790 let svg_images = rasterize_svgs(&page.doc, font); 771 - let refs = image_refs(&page.images, &svg_images); 791 + let mut refs = image_refs(&page.images, &svg_images); 792 + for (node_id, img) in &canvas_images { 793 + refs.insert(*node_id, img); 794 + } 772 795 773 796 // Layout with the reduced viewport height (below chrome). 774 797 let tree = layout(
+104
crates/dom/src/lib.rs
··· 110 110 select_type_ahead: HashMap<NodeId, (String, u64)>, 111 111 /// Custom validity messages set via `setCustomValidity()`. 112 112 pub custom_validity: CustomValidityMap, 113 + /// Canvas element backing pixel buffers, keyed by the `<canvas>` element's NodeId. 114 + /// Each buffer is RGBA8 data (4 bytes per pixel), stored row-major, top-to-bottom. 115 + canvas_buffers: HashMap<NodeId, Vec<u8>>, 116 + /// Canvas element dimensions (width, height) in CSS pixels. 117 + canvas_sizes: HashMap<NodeId, (u32, u32)>, 113 118 } 114 119 115 120 impl fmt::Debug for Document { ··· 143 148 select_highlighted: HashMap::new(), 144 149 select_type_ahead: HashMap::new(), 145 150 custom_validity: CustomValidityMap::new(), 151 + canvas_buffers: HashMap::new(), 152 + canvas_sizes: HashMap::new(), 146 153 } 147 154 } 148 155 ··· 996 1003 .iter() 997 1004 .filter(|n| matches!(n.data, NodeData::Text { .. })) 998 1005 .count() 1006 + } 1007 + 1008 + // ── Canvas backing buffer management ────────────────────────────── 1009 + 1010 + /// Initialize a canvas backing buffer for the given node. 1011 + /// If the node already has a buffer, this replaces it. 1012 + /// The buffer is initialized to transparent black (all zeros) per spec. 1013 + pub fn init_canvas(&mut self, node: NodeId, width: u32, height: u32) { 1014 + let len = (width as usize) * (height as usize) * 4; 1015 + self.canvas_buffers.insert(node, vec![0u8; len]); 1016 + self.canvas_sizes.insert(node, (width, height)); 1017 + } 1018 + 1019 + /// Resize a canvas backing buffer, clearing its contents per spec. 1020 + /// If the canvas has no buffer yet, one is created. 1021 + pub fn resize_canvas(&mut self, node: NodeId, width: u32, height: u32) { 1022 + self.init_canvas(node, width, height); 1023 + } 1024 + 1025 + /// Returns the canvas dimensions (width, height) for a node, if it has a canvas buffer. 1026 + pub fn canvas_size(&self, node: NodeId) -> Option<(u32, u32)> { 1027 + self.canvas_sizes.get(&node).copied() 1028 + } 1029 + 1030 + /// Returns a read-only reference to a canvas's RGBA8 pixel buffer. 1031 + pub fn canvas_buffer(&self, node: NodeId) -> Option<&[u8]> { 1032 + self.canvas_buffers.get(&node).map(|v| v.as_slice()) 1033 + } 1034 + 1035 + /// Returns a mutable reference to a canvas's RGBA8 pixel buffer. 1036 + pub fn canvas_buffer_mut(&mut self, node: NodeId) -> Option<&mut Vec<u8>> { 1037 + self.canvas_buffers.get_mut(&node) 1038 + } 1039 + 1040 + /// Returns true if this node has a canvas backing buffer. 1041 + pub fn has_canvas(&self, node: NodeId) -> bool { 1042 + self.canvas_buffers.contains_key(&node) 1043 + } 1044 + 1045 + /// Iterate over all canvas node IDs and their sizes. 1046 + pub fn canvas_entries(&self) -> impl Iterator<Item = (NodeId, (u32, u32))> + '_ { 1047 + self.canvas_sizes.iter().map(|(&k, &v)| (k, v)) 999 1048 } 1000 1049 } 1001 1050 ··· 2223 2272 assert_eq!(doc.get_element_by_id("target"), Some(div)); 2224 2273 // find_anchor_by_name should find the anchor. 2225 2274 assert_eq!(doc.find_anchor_by_name("target"), Some(anchor)); 2275 + } 2276 + 2277 + // ── Canvas tests ────────────────────────────────────────────────── 2278 + 2279 + #[test] 2280 + fn canvas_init_creates_buffer() { 2281 + let mut doc = Document::new(); 2282 + let canvas = doc.create_element("canvas"); 2283 + doc.init_canvas(canvas, 100, 50); 2284 + assert_eq!(doc.canvas_size(canvas), Some((100, 50))); 2285 + let buf = doc.canvas_buffer(canvas).unwrap(); 2286 + assert_eq!(buf.len(), 100 * 50 * 4); 2287 + // Should be transparent black (all zeros). 2288 + assert!(buf.iter().all(|&b| b == 0)); 2289 + } 2290 + 2291 + #[test] 2292 + fn canvas_resize_clears_buffer() { 2293 + let mut doc = Document::new(); 2294 + let canvas = doc.create_element("canvas"); 2295 + doc.init_canvas(canvas, 10, 10); 2296 + // Write some data. 2297 + doc.canvas_buffer_mut(canvas).unwrap()[0] = 255; 2298 + assert_eq!(doc.canvas_buffer(canvas).unwrap()[0], 255); 2299 + // Resize should clear. 2300 + doc.resize_canvas(canvas, 20, 20); 2301 + assert_eq!(doc.canvas_size(canvas), Some((20, 20))); 2302 + let buf = doc.canvas_buffer(canvas).unwrap(); 2303 + assert_eq!(buf.len(), 20 * 20 * 4); 2304 + assert!(buf.iter().all(|&b| b == 0)); 2305 + } 2306 + 2307 + #[test] 2308 + fn canvas_has_canvas() { 2309 + let mut doc = Document::new(); 2310 + let canvas = doc.create_element("canvas"); 2311 + let div = doc.create_element("div"); 2312 + assert!(!doc.has_canvas(canvas)); 2313 + assert!(!doc.has_canvas(div)); 2314 + doc.init_canvas(canvas, 10, 10); 2315 + assert!(doc.has_canvas(canvas)); 2316 + assert!(!doc.has_canvas(div)); 2317 + } 2318 + 2319 + #[test] 2320 + fn canvas_entries_iterates() { 2321 + let mut doc = Document::new(); 2322 + let c1 = doc.create_element("canvas"); 2323 + let c2 = doc.create_element("canvas"); 2324 + doc.init_canvas(c1, 100, 50); 2325 + doc.init_canvas(c2, 200, 100); 2326 + let entries: Vec<_> = doc.canvas_entries().collect(); 2327 + assert_eq!(entries.len(), 2); 2328 + assert!(entries.contains(&(c1, (100, 50)))); 2329 + assert!(entries.contains(&(c2, (200, 100)))); 2226 2330 } 2227 2331 }
+209
crates/js/src/dom_bridge.rs
··· 677 677 set_builtin_prop(gc, shapes, wrapper, name, Value::Function(func)); 678 678 } 679 679 } 680 + 681 + // Canvas methods (only for <canvas> elements). 682 + if doc.tag_name(node_id) == Some("canvas") { 683 + let canvas_methods: &[NativeMethod] = &[("getContext", canvas_get_context)]; 684 + for &(name, callback) in canvas_methods { 685 + let func = make_native(gc, name, callback); 686 + set_builtin_prop(gc, shapes, wrapper, name, Value::Function(func)); 687 + } 688 + } 680 689 } 681 690 682 691 // ── Helper: extract NodeId from a wrapper ─────────────────────────── ··· 1187 1196 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(marker)))) 1188 1197 } 1189 1198 1199 + // ── Canvas methods ───────────────────────────────────────────────── 1200 + 1201 + /// Internal property key for storing the canvas NodeId on a context2d wrapper. 1202 + const CANVAS_NODE_ID_KEY: &str = "__canvas_node_id__"; 1203 + 1204 + /// `canvas.getContext(contextType)` — returns a CanvasRenderingContext2D for 1205 + /// "2d", or null for unsupported types. 1206 + fn canvas_get_context(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1207 + let canvas_node_id = match &ctx.this { 1208 + Value::Object(r) => get_node_id(ctx.gc, ctx.shapes, *r), 1209 + _ => None, 1210 + } 1211 + .ok_or_else(|| RuntimeError::type_error("getContext called on non-canvas element"))?; 1212 + 1213 + let context_type = args 1214 + .first() 1215 + .map(|v| v.to_js_string(ctx.gc)) 1216 + .unwrap_or_default(); 1217 + if context_type != "2d" { 1218 + return Ok(Value::Null); 1219 + } 1220 + 1221 + let bridge = ctx 1222 + .dom_bridge 1223 + .as_ref() 1224 + .ok_or_else(|| RuntimeError::type_error("no DOM bridge"))?; 1225 + 1226 + // Initialize the canvas backing buffer if it doesn't exist yet. 1227 + { 1228 + let mut doc = bridge.document.borrow_mut(); 1229 + if !doc.has_canvas(canvas_node_id) { 1230 + let w: u32 = doc 1231 + .get_attribute(canvas_node_id, "width") 1232 + .and_then(|s| s.parse().ok()) 1233 + .unwrap_or(300); 1234 + let h: u32 = doc 1235 + .get_attribute(canvas_node_id, "height") 1236 + .and_then(|s| s.parse().ok()) 1237 + .unwrap_or(150); 1238 + doc.init_canvas(canvas_node_id, w, h); 1239 + } 1240 + } 1241 + 1242 + // Create the CanvasRenderingContext2D wrapper object. 1243 + let mut ctx_data = ObjectData::new(); 1244 + ctx_data.insert_property( 1245 + CANVAS_NODE_ID_KEY.to_string(), 1246 + Property { 1247 + value: Value::Number(canvas_node_id.index() as f64), 1248 + writable: false, 1249 + enumerable: false, 1250 + configurable: false, 1251 + }, 1252 + ctx.shapes, 1253 + ); 1254 + 1255 + let ctx_ref = ctx.gc.alloc(HeapObject::Object(ctx_data)); 1256 + 1257 + // Set the `canvas` back-reference so context.canvas returns the canvas element. 1258 + if let Value::Object(r) = &ctx.this { 1259 + set_builtin_prop(ctx.gc, ctx.shapes, ctx_ref, "canvas", Value::Object(*r)); 1260 + } 1261 + 1262 + Ok(Value::Object(ctx_ref)) 1263 + } 1264 + 1190 1265 // ── HTML serialization ────────────────────────────────────────────── 1191 1266 1192 1267 /// Serialize the children of a node to HTML (for innerHTML getter). ··· 1681 1756 )) 1682 1757 } 1683 1758 1759 + // ── Canvas properties ── 1760 + "width" if doc.tag_name(node_id) == Some("canvas") => { 1761 + let w: f64 = doc 1762 + .get_attribute(node_id, "width") 1763 + .and_then(|s| s.parse().ok()) 1764 + .unwrap_or(300.0); 1765 + Some(Value::Number(w)) 1766 + } 1767 + "height" if doc.tag_name(node_id) == Some("canvas") => { 1768 + let h: f64 = doc 1769 + .get_attribute(node_id, "height") 1770 + .and_then(|s| s.parse().ok()) 1771 + .unwrap_or(150.0); 1772 + Some(Value::Number(h)) 1773 + } 1774 + 1684 1775 _ => None, 1685 1776 } 1686 1777 } ··· 2033 2124 let mut doc = bridge.document.borrow_mut(); 2034 2125 let name_val = val.to_js_string(gc); 2035 2126 doc.set_attribute(node_id, "name", &name_val); 2127 + true 2128 + } 2129 + // ── Canvas width/height setters ── 2130 + "width" => { 2131 + let mut doc = bridge.document.borrow_mut(); 2132 + if doc.tag_name(node_id) != Some("canvas") { 2133 + return false; 2134 + } 2135 + let w = val.to_number() as u32; 2136 + doc.set_attribute(node_id, "width", &w.to_string()); 2137 + // Resize (and clear) the backing buffer per spec. 2138 + let h: u32 = doc 2139 + .get_attribute(node_id, "height") 2140 + .and_then(|s| s.parse().ok()) 2141 + .unwrap_or(150); 2142 + doc.resize_canvas(node_id, w, h); 2143 + true 2144 + } 2145 + "height" => { 2146 + let mut doc = bridge.document.borrow_mut(); 2147 + if doc.tag_name(node_id) != Some("canvas") { 2148 + return false; 2149 + } 2150 + let h = val.to_number() as u32; 2151 + doc.set_attribute(node_id, "height", &h.to_string()); 2152 + // Resize (and clear) the backing buffer per spec. 2153 + let w: u32 = doc 2154 + .get_attribute(node_id, "width") 2155 + .and_then(|s| s.parse().ok()) 2156 + .unwrap_or(300); 2157 + doc.resize_canvas(node_id, w, h); 2036 2158 true 2037 2159 } 2038 2160 _ => false, ··· 5884 6006 match vm.execute(&func).unwrap() { 5885 6007 Value::String(s) => assert_eq!(s, "interactive,complete"), 5886 6008 v => panic!("expected \"interactive,complete\", got {v:?}"), 6009 + } 6010 + } 6011 + 6012 + // ── Canvas tests ────────────────────────────────────────────────── 6013 + 6014 + #[test] 6015 + fn canvas_get_context_2d() { 6016 + let result = eval_with_doc( 6017 + r#"<html><body><canvas id="c"></canvas></body></html>"#, 6018 + r#" 6019 + var c = document.getElementById("c"); 6020 + var ctx = c.getContext("2d"); 6021 + ctx !== null ? "ok" : "null" 6022 + "#, 6023 + ) 6024 + .unwrap(); 6025 + assert_eq!(result.to_js_string(&crate::gc::Gc::new()), "ok"); 6026 + } 6027 + 6028 + #[test] 6029 + fn canvas_get_context_unsupported() { 6030 + let result = eval_with_doc( 6031 + r#"<html><body><canvas id="c"></canvas></body></html>"#, 6032 + r#" 6033 + var c = document.getElementById("c"); 6034 + c.getContext("webgl") 6035 + "#, 6036 + ) 6037 + .unwrap(); 6038 + assert!(matches!(result, Value::Null)); 6039 + } 6040 + 6041 + #[test] 6042 + fn canvas_default_width_height() { 6043 + let result = eval_with_doc( 6044 + r#"<html><body><canvas id="c"></canvas></body></html>"#, 6045 + r#" 6046 + var c = document.getElementById("c"); 6047 + c.width + "," + c.height 6048 + "#, 6049 + ) 6050 + .unwrap(); 6051 + assert_eq!(result.to_js_string(&crate::gc::Gc::new()), "300,150"); 6052 + } 6053 + 6054 + #[test] 6055 + fn canvas_custom_width_height() { 6056 + let result = eval_with_doc( 6057 + r#"<html><body><canvas id="c" width="400" height="200"></canvas></body></html>"#, 6058 + r#" 6059 + var c = document.getElementById("c"); 6060 + c.width + "," + c.height 6061 + "#, 6062 + ) 6063 + .unwrap(); 6064 + assert_eq!(result.to_js_string(&crate::gc::Gc::new()), "400,200"); 6065 + } 6066 + 6067 + #[test] 6068 + fn canvas_set_width_height() { 6069 + let result = eval_with_doc( 6070 + r#"<html><body><canvas id="c"></canvas></body></html>"#, 6071 + r#" 6072 + var c = document.getElementById("c"); 6073 + c.width = 500; 6074 + c.height = 250; 6075 + c.width + "," + c.height 6076 + "#, 6077 + ) 6078 + .unwrap(); 6079 + assert_eq!(result.to_js_string(&crate::gc::Gc::new()), "500,250"); 6080 + } 6081 + 6082 + #[test] 6083 + fn canvas_context_has_canvas_ref() { 6084 + let result = eval_with_doc( 6085 + r#"<html><body><canvas id="c"></canvas></body></html>"#, 6086 + r#" 6087 + var c = document.getElementById("c"); 6088 + var ctx = c.getContext("2d"); 6089 + ctx.canvas === c 6090 + "#, 6091 + ) 6092 + .unwrap(); 6093 + match result { 6094 + Value::Boolean(b) => assert!(b, "ctx.canvas === c should be true"), 6095 + v => panic!("expected true, got {v:?}"), 5887 6096 } 5888 6097 } 5889 6098 }
+58 -1
crates/layout/src/lib.rs
··· 544 544 /// Fixed size for checkboxes and radio buttons (CSS px). 545 545 const CHECK_RADIO_SIZE: f32 = 13.0; 546 546 547 + /// Compute intrinsic (width, height) for a `<canvas>` element, or `None` if 548 + /// the node is not a canvas. 549 + /// 550 + /// Per the HTML spec, canvas has a default intrinsic size of 300×150 CSS pixels. 551 + /// The `width` and `height` attributes override these defaults. 552 + fn canvas_intrinsic_size(node: NodeId, doc: &Document) -> Option<(f32, f32)> { 553 + if doc.tag_name(node)? != "canvas" { 554 + return None; 555 + } 556 + let width: f32 = doc 557 + .get_attribute(node, "width") 558 + .and_then(|s| s.parse().ok()) 559 + .unwrap_or(300.0); 560 + let height: f32 = doc 561 + .get_attribute(node, "height") 562 + .and_then(|s| s.parse().ok()) 563 + .unwrap_or(150.0); 564 + Some((width, height)) 565 + } 566 + 547 567 /// Compute intrinsic (width, height) for a form control element, or `None` if 548 568 /// the node is not a form control that needs intrinsic sizing. 549 569 /// ··· 1123 1143 b.border = border; 1124 1144 b.children = children; 1125 1145 1126 - // Check for replaced element (e.g., <img>). 1146 + // Check for replaced element (e.g., <img>, <canvas>). 1127 1147 if let Some(&(w, h)) = image_sizes.get(&node) { 1128 1148 b.replaced_size = Some((w, h)); 1149 + } 1150 + 1151 + // Canvas elements are replaced elements with intrinsic size from 1152 + // their width/height attributes (default 300×150 per spec). 1153 + if b.replaced_size.is_none() { 1154 + if let Some(size) = canvas_intrinsic_size(node, doc) { 1155 + b.replaced_size = Some(size); 1156 + } 1129 1157 } 1130 1158 1131 1159 // Form controls are atomic inline-level boxes with intrinsic sizes. ··· 8468 8496 doc.append_child(root, btn); 8469 8497 let info = build_form_control_info(btn, &doc).expect("should produce info"); 8470 8498 assert_eq!(info.value, "Submit"); 8499 + } 8500 + 8501 + // ── Canvas intrinsic size tests ────────────────────────────────── 8502 + 8503 + #[test] 8504 + fn canvas_default_intrinsic_size() { 8505 + let mut doc = Document::new(); 8506 + let canvas = doc.create_element("canvas"); 8507 + let (w, h) = canvas_intrinsic_size(canvas, &doc).unwrap(); 8508 + assert_eq!(w, 300.0); 8509 + assert_eq!(h, 150.0); 8510 + } 8511 + 8512 + #[test] 8513 + fn canvas_custom_intrinsic_size() { 8514 + let mut doc = Document::new(); 8515 + let canvas = doc.create_element("canvas"); 8516 + doc.set_attribute(canvas, "width", "400"); 8517 + doc.set_attribute(canvas, "height", "200"); 8518 + let (w, h) = canvas_intrinsic_size(canvas, &doc).unwrap(); 8519 + assert_eq!(w, 400.0); 8520 + assert_eq!(h, 200.0); 8521 + } 8522 + 8523 + #[test] 8524 + fn canvas_intrinsic_size_not_canvas() { 8525 + let mut doc = Document::new(); 8526 + let div = doc.create_element("div"); 8527 + assert!(canvas_intrinsic_size(div, &doc).is_none()); 8471 8528 } 8472 8529 }