we (web engine): Experimental web browser project to understand the limits of Claude
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('&', "&")
200 .replace('<', "<")
201 .replace('>', ">");
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}