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 end-to-end page loading pipeline

Wire together all Phase 8 components into the browser's navigation flow:
- Use css_loader::collect_stylesheets for external <link> and inline <style>
- Use img_loader::collect_images to fetch and decode <img> elements
- Store pre-fetched PageState (DOM, stylesheet, images) so window resizes
only re-style/re-layout/re-render without re-fetching
- Show error page for network failures instead of process::exit
- Preserve base URL from HTTP response for resolving relative subresources
- Generate file:// base URL for local file paths

Implements issue 3mhktaciaf42q.

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

+151 -32
+151 -32
crates/browser/src/main.rs
··· 1 1 use std::cell::RefCell; 2 2 use std::collections::HashMap; 3 3 4 - use we_browser::loader::{ResourceLoader, ABOUT_BLANK_HTML}; 4 + use we_browser::css_loader::collect_stylesheets; 5 + use we_browser::img_loader::{collect_images, ImageStore}; 6 + use we_browser::loader::{Resource, ResourceLoader, ABOUT_BLANK_HTML}; 7 + use we_css::parser::Stylesheet; 8 + use we_dom::{Document, NodeId}; 5 9 use we_html::parse_html; 10 + use we_image::pixel::Image; 6 11 use we_layout::layout; 7 12 use we_platform::appkit; 8 13 use we_platform::cg::BitmapContext; 9 14 use we_render::Renderer; 10 - use we_style::computed::{extract_stylesheets, resolve_styles}; 15 + use we_style::computed::resolve_styles; 11 16 use we_text::font::{self, Font}; 17 + use we_url::Url; 18 + 19 + // --------------------------------------------------------------------------- 20 + // Page state: holds everything needed to re-render without re-fetching 21 + // --------------------------------------------------------------------------- 22 + 23 + /// Pre-fetched page data. Stored so that window resizes only re-style, 24 + /// re-layout, and re-render — no network requests. 25 + struct PageState { 26 + doc: Document, 27 + stylesheet: Stylesheet, 28 + images: ImageStore, 29 + } 12 30 13 31 /// Browser state kept in thread-local storage so the resize handler can 14 32 /// access it. All AppKit callbacks run on the main thread. 15 33 struct BrowserState { 16 - html: String, 34 + page: PageState, 17 35 font: Font, 18 36 bitmap: Box<BitmapContext>, 19 37 view: appkit::BitmapView, ··· 23 41 static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) }; 24 42 } 25 43 26 - /// Re-run the full pipeline: parse → extract CSS → resolve styles → layout → render → copy to bitmap. 27 - fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) { 44 + // --------------------------------------------------------------------------- 45 + // Rendering pipeline 46 + // --------------------------------------------------------------------------- 47 + 48 + /// Build a `HashMap<NodeId, (f32, f32)>` of display dimensions for layout. 49 + fn image_sizes(store: &ImageStore) -> HashMap<NodeId, (f32, f32)> { 50 + let mut sizes = HashMap::new(); 51 + for (node_id, res) in store { 52 + if res.display_width > 0.0 || res.display_height > 0.0 { 53 + sizes.insert(*node_id, (res.display_width, res.display_height)); 54 + } 55 + } 56 + sizes 57 + } 58 + 59 + /// Build a `HashMap<NodeId, &Image>` reference map for the renderer. 60 + fn image_refs(store: &ImageStore) -> HashMap<NodeId, &Image> { 61 + let mut refs = HashMap::new(); 62 + for (node_id, res) in store { 63 + if let Some(ref img) = res.image { 64 + refs.insert(*node_id, img); 65 + } 66 + } 67 + refs 68 + } 69 + 70 + /// Re-run the pipeline: resolve styles → layout → render → copy to bitmap. 71 + /// 72 + /// Uses pre-fetched `PageState` so no network I/O happens here. 73 + fn render_page(page: &PageState, font: &Font, bitmap: &mut BitmapContext) { 28 74 let width = bitmap.width() as u32; 29 75 let height = bitmap.height() as u32; 30 76 if width == 0 || height == 0 { 31 77 return; 32 78 } 33 79 34 - // Parse HTML into DOM. 35 - let doc = parse_html(html); 36 - 37 - // Extract CSS from <style> elements and resolve computed styles. 38 - let stylesheets = extract_stylesheets(&doc); 39 - let styled = match resolve_styles(&doc, &stylesheets) { 80 + // Resolve computed styles from DOM + stylesheet. 81 + let styled = match resolve_styles(&page.doc, std::slice::from_ref(&page.stylesheet)) { 40 82 Some(s) => s, 41 83 None => return, 42 84 }; 43 85 44 - // Layout using styled tree (CSS-driven). 86 + // Build image maps for layout (sizes) and render (pixel data). 87 + let sizes = image_sizes(&page.images); 88 + let refs = image_refs(&page.images); 89 + 90 + // Layout. 45 91 let tree = layout( 46 92 &styled, 47 - &doc, 93 + &page.doc, 48 94 width as f32, 49 95 height as f32, 50 96 font, 51 - &HashMap::new(), 97 + &sizes, 52 98 ); 53 99 100 + // Render. 54 101 let mut renderer = Renderer::new(width, height); 55 - renderer.paint(&tree, font, &HashMap::new()); 102 + renderer.paint(&tree, font, &refs); 56 103 57 104 // Copy rendered pixels into the bitmap context's buffer. 58 105 let src = renderer.pixels(); ··· 82 129 None => return, 83 130 }; 84 131 85 - render_page(&state.html, &state.font, &mut new_bitmap); 132 + render_page(&state.page, &state.font, &mut new_bitmap); 86 133 87 134 // Swap in the new bitmap and update the view's pointer. 88 135 state.bitmap = new_bitmap; ··· 90 137 }); 91 138 } 92 139 140 + // --------------------------------------------------------------------------- 141 + // Page loading 142 + // --------------------------------------------------------------------------- 143 + 144 + /// Result of loading a page: HTML text + base URL for resolving subresources. 145 + struct LoadedHtml { 146 + text: String, 147 + base_url: Url, 148 + } 149 + 93 150 /// Load content from a command-line argument. 94 151 /// 95 152 /// Tries the argument as a URL first (http://, https://, about:, data:), 96 - /// then falls back to reading it as a file path. 97 - fn load_from_arg(arg: &str) -> String { 153 + /// then falls back to reading it as a local file. 154 + /// On failure, returns an error page instead of exiting. 155 + fn load_from_arg(arg: &str) -> LoadedHtml { 98 156 // Try as URL if it has a recognized scheme. 99 157 if arg.starts_with("http://") 100 158 || arg.starts_with("https://") ··· 103 161 { 104 162 let mut loader = ResourceLoader::new(); 105 163 match loader.fetch_url(arg, None) { 106 - Ok(we_browser::loader::Resource::Html { text, .. }) => return text, 164 + Ok(Resource::Html { text, base_url, .. }) => { 165 + return LoadedHtml { text, base_url }; 166 + } 107 167 Ok(_) => { 108 - eprintln!("URL did not return HTML: {arg}"); 109 - std::process::exit(1); 168 + return error_page(&format!("URL did not return HTML: {arg}")); 110 169 } 111 170 Err(e) => { 112 - eprintln!("Error loading {arg}: {e}"); 113 - std::process::exit(1); 171 + return error_page(&format!("Failed to load {arg}: {e}")); 114 172 } 115 173 } 116 174 } 117 175 118 176 // Fall back to file path. 119 177 match std::fs::read_to_string(arg) { 120 - Ok(content) => content, 121 - Err(e) => { 122 - eprintln!("Error reading {arg}: {e}"); 123 - std::process::exit(1); 178 + Ok(content) => { 179 + // Use a file:// base URL for resolving relative paths. 180 + let abs_path = 181 + std::fs::canonicalize(arg).unwrap_or_else(|_| std::path::PathBuf::from(arg)); 182 + let base_str = format!("file://{}", abs_path.display()); 183 + let base_url = Url::parse(&base_str).unwrap_or_else(|_| { 184 + Url::parse("about:blank").expect("about:blank is always valid") 185 + }); 186 + LoadedHtml { 187 + text: content, 188 + base_url, 189 + } 124 190 } 191 + Err(e) => error_page(&format!("Error reading {arg}: {e}")), 125 192 } 126 193 } 127 194 195 + /// Generate an HTML error page for display. 196 + fn error_page(message: &str) -> LoadedHtml { 197 + eprintln!("{message}"); 198 + let escaped = message 199 + .replace('&', "&amp;") 200 + .replace('<', "&lt;") 201 + .replace('>', "&gt;"); 202 + let html = format!( 203 + "<!DOCTYPE html>\ 204 + <html><head><title>Error</title>\ 205 + <style>\ 206 + body {{ font-family: sans-serif; margin: 40px; color: #333; }}\ 207 + h1 {{ color: #c00; }}\ 208 + p {{ font-size: 16px; }}\ 209 + </style></head>\ 210 + <body><h1>Error</h1><p>{escaped}</p></body></html>" 211 + ); 212 + let base_url = Url::parse("about:blank").expect("about:blank is always valid"); 213 + LoadedHtml { 214 + text: html, 215 + base_url, 216 + } 217 + } 218 + 219 + /// Load a page: fetch HTML, parse DOM, collect CSS and images. 220 + fn load_page(loaded: LoadedHtml) -> PageState { 221 + let doc = parse_html(&loaded.text); 222 + 223 + // Fetch external stylesheets and merge with inline <style> elements. 224 + let mut loader = ResourceLoader::new(); 225 + let stylesheet = collect_stylesheets(&doc, &mut loader, &loaded.base_url); 226 + 227 + // Fetch and decode images referenced by <img> elements. 228 + let images = collect_images(&doc, &mut loader, &loaded.base_url); 229 + 230 + PageState { 231 + doc, 232 + stylesheet, 233 + images, 234 + } 235 + } 236 + 237 + // --------------------------------------------------------------------------- 238 + // Entry point 239 + // --------------------------------------------------------------------------- 240 + 128 241 fn main() { 129 - // Load HTML from argument (URL, file path) or default to about:blank. 130 - let html = match std::env::args().nth(1) { 242 + // Load page from argument (URL, file path) or default to about:blank. 243 + let loaded = match std::env::args().nth(1) { 131 244 Some(arg) => load_from_arg(&arg), 132 - None => ABOUT_BLANK_HTML.to_string(), 245 + None => LoadedHtml { 246 + text: ABOUT_BLANK_HTML.to_string(), 247 + base_url: Url::parse("about:blank").expect("about:blank is always valid"), 248 + }, 133 249 }; 250 + 251 + // Parse DOM and fetch subresources (CSS, images). 252 + let page = load_page(loaded); 134 253 135 254 // Load a system font for text rendering. 136 255 let font = match font::load_system_font() { ··· 154 273 // Initial render at the default window size (800x600). 155 274 let mut bitmap = 156 275 Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); 157 - render_page(&html, &font, &mut bitmap); 276 + render_page(&page, &font, &mut bitmap); 158 277 159 278 // Create the view backed by the rendered bitmap. 160 279 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); ··· 164 283 // Store state for the resize handler. 165 284 STATE.with(|state| { 166 285 *state.borrow_mut() = Some(BrowserState { 167 - html, 286 + page, 168 287 font, 169 288 bitmap, 170 289 view,