we (web engine): Experimental web browser project to understand the limits of Claude
at margin-collapsing 299 lines 9.6 kB view raw
1use std::cell::RefCell; 2use std::collections::HashMap; 3 4use we_browser::css_loader::collect_stylesheets; 5use we_browser::img_loader::{collect_images, ImageStore}; 6use we_browser::loader::{Resource, ResourceLoader, ABOUT_BLANK_HTML}; 7use we_css::parser::Stylesheet; 8use we_dom::{Document, NodeId}; 9use we_html::parse_html; 10use we_image::pixel::Image; 11use we_layout::layout; 12use we_platform::appkit; 13use we_platform::cg::BitmapContext; 14use we_render::Renderer; 15use we_style::computed::resolve_styles; 16use we_text::font::{self, Font}; 17use 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. 25struct PageState { 26 doc: Document, 27 stylesheet: Stylesheet, 28 images: ImageStore, 29} 30 31/// Browser state kept in thread-local storage so the resize handler can 32/// access it. All AppKit callbacks run on the main thread. 33struct BrowserState { 34 page: PageState, 35 font: Font, 36 bitmap: Box<BitmapContext>, 37 view: appkit::BitmapView, 38} 39 40thread_local! { 41 static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) }; 42} 43 44// --------------------------------------------------------------------------- 45// Rendering pipeline 46// --------------------------------------------------------------------------- 47 48/// Build a `HashMap<NodeId, (f32, f32)>` of display dimensions for layout. 49fn 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. 60fn 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. 73fn render_page(page: &PageState, font: &Font, bitmap: &mut BitmapContext) { 74 let width = bitmap.width() as u32; 75 let height = bitmap.height() as u32; 76 if width == 0 || height == 0 { 77 return; 78 } 79 80 // Resolve computed styles from DOM + stylesheet. 81 let styled = match resolve_styles(&page.doc, std::slice::from_ref(&page.stylesheet)) { 82 Some(s) => s, 83 None => return, 84 }; 85 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. 91 let tree = layout( 92 &styled, 93 &page.doc, 94 width as f32, 95 height as f32, 96 font, 97 &sizes, 98 ); 99 100 // Render. 101 let mut renderer = Renderer::new(width, height); 102 renderer.paint(&tree, font, &refs); 103 104 // Copy rendered pixels into the bitmap context's buffer. 105 let src = renderer.pixels(); 106 let dst = bitmap.pixels_mut(); 107 let len = src.len().min(dst.len()); 108 dst[..len].copy_from_slice(&src[..len]); 109} 110 111/// Called by the platform crate when the window is resized. 112fn handle_resize(width: f64, height: f64) { 113 STATE.with(|state| { 114 let mut state = state.borrow_mut(); 115 let state = match state.as_mut() { 116 Some(s) => s, 117 None => return, 118 }; 119 120 let w = width as usize; 121 let h = height as usize; 122 if w == 0 || h == 0 { 123 return; 124 } 125 126 // Create a new bitmap context with the new dimensions. 127 let mut new_bitmap = match BitmapContext::new(w, h) { 128 Some(b) => Box::new(b), 129 None => return, 130 }; 131 132 render_page(&state.page, &state.font, &mut new_bitmap); 133 134 // Swap in the new bitmap and update the view's pointer. 135 state.bitmap = new_bitmap; 136 state.view.update_bitmap(&state.bitmap); 137 }); 138} 139 140// --------------------------------------------------------------------------- 141// Page loading 142// --------------------------------------------------------------------------- 143 144/// Result of loading a page: HTML text + base URL for resolving subresources. 145struct LoadedHtml { 146 text: String, 147 base_url: Url, 148} 149 150/// Load content from a command-line argument. 151/// 152/// Tries the argument as a URL first (http://, https://, about:, data:), 153/// then falls back to reading it as a local file. 154/// On failure, returns an error page instead of exiting. 155fn load_from_arg(arg: &str) -> LoadedHtml { 156 // Try as URL if it has a recognized scheme. 157 if arg.starts_with("http://") 158 || arg.starts_with("https://") 159 || arg.starts_with("about:") 160 || arg.starts_with("data:") 161 { 162 let mut loader = ResourceLoader::new(); 163 match loader.fetch_url(arg, None) { 164 Ok(Resource::Html { text, base_url, .. }) => { 165 return LoadedHtml { text, base_url }; 166 } 167 Ok(_) => { 168 return error_page(&format!("URL did not return HTML: {arg}")); 169 } 170 Err(e) => { 171 return error_page(&format!("Failed to load {arg}: {e}")); 172 } 173 } 174 } 175 176 // Fall back to file path. 177 match std::fs::read_to_string(arg) { 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 } 190 } 191 Err(e) => error_page(&format!("Error reading {arg}: {e}")), 192 } 193} 194 195/// Generate an HTML error page for display. 196fn 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. 220fn 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 241fn main() { 242 // Load page from argument (URL, file path) or default to about:blank. 243 let loaded = match std::env::args().nth(1) { 244 Some(arg) => load_from_arg(&arg), 245 None => LoadedHtml { 246 text: ABOUT_BLANK_HTML.to_string(), 247 base_url: Url::parse("about:blank").expect("about:blank is always valid"), 248 }, 249 }; 250 251 // Parse DOM and fetch subresources (CSS, images). 252 let page = load_page(loaded); 253 254 // Load a system font for text rendering. 255 let font = match font::load_system_font() { 256 Ok(f) => f, 257 Err(e) => { 258 eprintln!("Error loading system font: {:?}", e); 259 std::process::exit(1); 260 } 261 }; 262 263 let _pool = appkit::AutoreleasePool::new(); 264 265 let app = appkit::App::shared(); 266 app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); 267 appkit::install_app_delegate(&app); 268 269 let window = appkit::create_standard_window("we"); 270 appkit::install_window_delegate(&window); 271 window.set_accepts_mouse_moved_events(true); 272 273 // Initial render at the default window size (800x600). 274 let mut bitmap = 275 Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); 276 render_page(&page, &font, &mut bitmap); 277 278 // Create the view backed by the rendered bitmap. 279 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); 280 let view = appkit::BitmapView::new(frame, &bitmap); 281 window.set_content_view(&view.id()); 282 283 // Store state for the resize handler. 284 STATE.with(|state| { 285 *state.borrow_mut() = Some(BrowserState { 286 page, 287 font, 288 bitmap, 289 view, 290 }); 291 }); 292 293 // Register resize handler so re-layout happens on window resize. 294 appkit::set_resize_handler(handle_resize); 295 296 window.make_key_and_order_front(); 297 app.activate(); 298 app.run(); 299}