we (web engine): Experimental web browser project to understand the limits of Claude
at img-loading 170 lines 5.0 kB view raw
1use std::cell::RefCell; 2use std::collections::HashMap; 3 4use we_html::parse_html; 5use we_layout::layout; 6use we_platform::appkit; 7use we_platform::cg::BitmapContext; 8use we_render::Renderer; 9use we_style::computed::{extract_stylesheets, resolve_styles}; 10use we_text::font::{self, Font}; 11 12/// Default HTML page shown when no file argument is provided. 13const DEFAULT_HTML: &str = r#"<!DOCTYPE html> 14<html> 15<head> 16<title>we browser</title> 17<style> 18h1 { color: blue; } 19h2 { color: green; } 20p { color: #333333; } 21</style> 22</head> 23<body> 24<h1>Hello from we!</h1> 25<p>This is a from-scratch web browser engine written in pure Rust.</p> 26<p>Zero external crate dependencies. Every subsystem is implemented in Rust.</p> 27<h2>Features</h2> 28<p>HTML5 tokenizer, DOM tree, block layout, CSS cascade, and software rendering.</p> 29</body> 30</html>"#; 31 32/// Browser state kept in thread-local storage so the resize handler can 33/// access it. All AppKit callbacks run on the main thread. 34struct BrowserState { 35 html: String, 36 font: Font, 37 bitmap: Box<BitmapContext>, 38 view: appkit::BitmapView, 39} 40 41thread_local! { 42 static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) }; 43} 44 45/// Re-run the full pipeline: parse → extract CSS → resolve styles → layout → render → copy to bitmap. 46fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) { 47 let width = bitmap.width() as u32; 48 let height = bitmap.height() as u32; 49 if width == 0 || height == 0 { 50 return; 51 } 52 53 // Parse HTML into DOM. 54 let doc = parse_html(html); 55 56 // Extract CSS from <style> elements and resolve computed styles. 57 let stylesheets = extract_stylesheets(&doc); 58 let styled = match resolve_styles(&doc, &stylesheets) { 59 Some(s) => s, 60 None => return, 61 }; 62 63 // Layout using styled tree (CSS-driven). 64 let tree = layout( 65 &styled, 66 &doc, 67 width as f32, 68 height as f32, 69 font, 70 &HashMap::new(), 71 ); 72 73 let mut renderer = Renderer::new(width, height); 74 renderer.paint(&tree, font, &HashMap::new()); 75 76 // Copy rendered pixels into the bitmap context's buffer. 77 let src = renderer.pixels(); 78 let dst = bitmap.pixels_mut(); 79 let len = src.len().min(dst.len()); 80 dst[..len].copy_from_slice(&src[..len]); 81} 82 83/// Called by the platform crate when the window is resized. 84fn handle_resize(width: f64, height: f64) { 85 STATE.with(|state| { 86 let mut state = state.borrow_mut(); 87 let state = match state.as_mut() { 88 Some(s) => s, 89 None => return, 90 }; 91 92 let w = width as usize; 93 let h = height as usize; 94 if w == 0 || h == 0 { 95 return; 96 } 97 98 // Create a new bitmap context with the new dimensions. 99 let mut new_bitmap = match BitmapContext::new(w, h) { 100 Some(b) => Box::new(b), 101 None => return, 102 }; 103 104 render_page(&state.html, &state.font, &mut new_bitmap); 105 106 // Swap in the new bitmap and update the view's pointer. 107 state.bitmap = new_bitmap; 108 state.view.update_bitmap(&state.bitmap); 109 }); 110} 111 112fn main() { 113 // Load HTML from file argument or use default page. 114 let html = match std::env::args().nth(1) { 115 Some(path) => match std::fs::read_to_string(&path) { 116 Ok(content) => content, 117 Err(e) => { 118 eprintln!("Error reading {}: {}", path, e); 119 std::process::exit(1); 120 } 121 }, 122 None => DEFAULT_HTML.to_string(), 123 }; 124 125 // Load a system font for text rendering. 126 let font = match font::load_system_font() { 127 Ok(f) => f, 128 Err(e) => { 129 eprintln!("Error loading system font: {:?}", e); 130 std::process::exit(1); 131 } 132 }; 133 134 let _pool = appkit::AutoreleasePool::new(); 135 136 let app = appkit::App::shared(); 137 app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); 138 appkit::install_app_delegate(&app); 139 140 let window = appkit::create_standard_window("we"); 141 appkit::install_window_delegate(&window); 142 window.set_accepts_mouse_moved_events(true); 143 144 // Initial render at the default window size (800x600). 145 let mut bitmap = 146 Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); 147 render_page(&html, &font, &mut bitmap); 148 149 // Create the view backed by the rendered bitmap. 150 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); 151 let view = appkit::BitmapView::new(frame, &bitmap); 152 window.set_content_view(&view.id()); 153 154 // Store state for the resize handler. 155 STATE.with(|state| { 156 *state.borrow_mut() = Some(BrowserState { 157 html, 158 font, 159 bitmap, 160 view, 161 }); 162 }); 163 164 // Register resize handler so re-layout happens on window resize. 165 appkit::set_resize_handler(handle_resize); 166 167 window.make_key_and_order_front(); 168 app.activate(); 169 app.run(); 170}