we (web engine): Experimental web browser project to understand the limits of Claude
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}