Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365use std::cell::RefCell;use std::collections::HashMap;
use we_browser::css_loader::collect_stylesheets;use we_browser::img_loader::{collect_images, ImageStore};use we_browser::loader::{Resource, ResourceLoader, ABOUT_BLANK_HTML};use we_css::parser::Stylesheet;use we_dom::{Document, NodeId};use we_html::parse_html;use we_image::pixel::Image;use we_layout::layout;use we_platform::appkit;use we_platform::cg::BitmapContext;use we_render::{Renderer, ScrollState};use we_style::computed::resolve_styles;use we_text::font::{self, Font};use we_url::Url;
// ---------------------------------------------------------------------------// Page state: holds everything needed to re-render without re-fetching// ---------------------------------------------------------------------------
/// Pre-fetched page data. Stored so that window resizes only re-style,/// re-layout, and re-render — no network requests.struct PageState { doc: Document, stylesheet: Stylesheet, images: ImageStore,}
/// Browser state kept in thread-local storage so the resize handler can/// access it. All AppKit callbacks run on the main thread.struct BrowserState { page: PageState, font: Font, bitmap: Box<BitmapContext>, view: appkit::BitmapView, /// Page-level scroll offset (vertical). page_scroll_y: f32, /// Total content height from the last layout (for scroll clamping). content_height: f32, /// Per-element scroll offsets for overflow:scroll/auto containers. scroll_offsets: HashMap<NodeId, (f32, f32)>,}
thread_local! { static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) };}
// ---------------------------------------------------------------------------// Rendering pipeline// ---------------------------------------------------------------------------
/// Build a `HashMap<NodeId, (f32, f32)>` of display dimensions for layout.fn image_sizes(store: &ImageStore) -> HashMap<NodeId, (f32, f32)> { let mut sizes = HashMap::new(); for (node_id, res) in store { if res.display_width > 0.0 || res.display_height > 0.0 { sizes.insert(*node_id, (res.display_width, res.display_height)); } } sizes}
/// Build a `HashMap<NodeId, &Image>` reference map for the renderer.fn image_refs(store: &ImageStore) -> HashMap<NodeId, &Image> { let mut refs = HashMap::new(); for (node_id, res) in store { if let Some(ref img) = res.image { refs.insert(*node_id, img); } } refs}
/// Re-run the pipeline: resolve styles → layout → render → copy to bitmap.////// Uses pre-fetched `PageState` so no network I/O happens here./// Returns the total content height for scroll clamping.fn render_page( page: &PageState, font: &Font, bitmap: &mut BitmapContext, page_scroll_y: f32, scroll_offsets: &ScrollState,) -> f32 { let width = bitmap.width() as u32; let height = bitmap.height() as u32; if width == 0 || height == 0 { return 0.0; }
// Resolve computed styles from DOM + stylesheet. let styled = match resolve_styles( &page.doc, std::slice::from_ref(&page.stylesheet), (width as f32, height as f32), ) { Some(s) => s, None => return 0.0, };
// Build image maps for layout (sizes) and render (pixel data). let sizes = image_sizes(&page.images); let refs = image_refs(&page.images);
// Layout. let tree = layout( &styled, &page.doc, width as f32, height as f32, font, &sizes, );
// Render with scroll state. let mut renderer = Renderer::new(width, height); renderer.paint_with_scroll(&tree, font, &refs, page_scroll_y, scroll_offsets);
// Copy rendered pixels into the bitmap context's buffer. let src = renderer.pixels(); let dst = bitmap.pixels_mut(); let len = src.len().min(dst.len()); dst[..len].copy_from_slice(&src[..len]);
// Return total content height for scroll clamping. tree.root.content_height}
/// Called by the platform crate when the window is resized.fn handle_resize(width: f64, height: f64) { STATE.with(|state| { let mut state = state.borrow_mut(); let state = match state.as_mut() { Some(s) => s, None => return, };
let w = width as usize; let h = height as usize; if w == 0 || h == 0 { return; }
// Create a new bitmap context with the new dimensions. let mut new_bitmap = match BitmapContext::new(w, h) { Some(b) => Box::new(b), None => return, };
let content_height = render_page( &state.page, &state.font, &mut new_bitmap, state.page_scroll_y, &state.scroll_offsets, ); state.content_height = content_height;
// Clamp scroll position after resize (viewport may have grown). let viewport_height = h as f32; let max_scroll = (state.content_height - viewport_height).max(0.0); state.page_scroll_y = state.page_scroll_y.clamp(0.0, max_scroll);
// Swap in the new bitmap and update the view's pointer. state.bitmap = new_bitmap; state.view.update_bitmap(&state.bitmap); });}
/// Called by the platform crate on scroll wheel events.fn handle_scroll(_dx: f64, dy: f64, _mouse_x: f64, _mouse_y: f64) { STATE.with(|state| { let mut state = state.borrow_mut(); let state = match state.as_mut() { Some(s) => s, None => return, };
let viewport_height = state.bitmap.height() as f32; let max_scroll = (state.content_height - viewport_height).max(0.0);
// Apply scroll delta (negative dy = scroll down). state.page_scroll_y = (state.page_scroll_y - dy as f32).clamp(0.0, max_scroll);
// Re-render with updated scroll position. let content_height = render_page( &state.page, &state.font, &mut state.bitmap, state.page_scroll_y, &state.scroll_offsets, ); state.content_height = content_height; state.view.set_needs_display(); });}
// ---------------------------------------------------------------------------// Page loading// ---------------------------------------------------------------------------
/// Result of loading a page: HTML text + base URL for resolving subresources.struct LoadedHtml { text: String, base_url: Url,}
/// Load content from a command-line argument.////// Tries the argument as a URL first (http://, https://, about:, data:),/// then falls back to reading it as a local file./// On failure, returns an error page instead of exiting.fn load_from_arg(arg: &str) -> LoadedHtml { // Try as URL if it has a recognized scheme. if arg.starts_with("http://") || arg.starts_with("https://") || arg.starts_with("about:") || arg.starts_with("data:") { let mut loader = ResourceLoader::new(); match loader.fetch_url(arg, None) { Ok(Resource::Html { text, base_url, .. }) => { return LoadedHtml { text, base_url }; } Ok(_) => { return error_page(&format!("URL did not return HTML: {arg}")); } Err(e) => { return error_page(&format!("Failed to load {arg}: {e}")); } } }
// Fall back to file path. match std::fs::read_to_string(arg) { Ok(content) => { // Use a file:// base URL for resolving relative paths. let abs_path = std::fs::canonicalize(arg).unwrap_or_else(|_| std::path::PathBuf::from(arg)); let base_str = format!("file://{}", abs_path.display()); let base_url = Url::parse(&base_str).unwrap_or_else(|_| { Url::parse("about:blank").expect("about:blank is always valid") }); LoadedHtml { text: content, base_url, } } Err(e) => error_page(&format!("Error reading {arg}: {e}")), }}
/// Generate an HTML error page for display.fn error_page(message: &str) -> LoadedHtml { eprintln!("{message}"); let escaped = message .replace('&', "&") .replace('<', "<") .replace('>', ">"); let html = format!( "<!DOCTYPE html>\ <html><head><title>Error</title>\ <style>\ body {{ font-family: sans-serif; margin: 40px; color: #333; }}\ h1 {{ color: #c00; }}\ p {{ font-size: 16px; }}\ </style></head>\ <body><h1>Error</h1><p>{escaped}</p></body></html>" ); let base_url = Url::parse("about:blank").expect("about:blank is always valid"); LoadedHtml { text: html, base_url, }}
/// Load a page: fetch HTML, parse DOM, collect CSS and images.fn load_page(loaded: LoadedHtml) -> PageState { let doc = parse_html(&loaded.text);
// Fetch external stylesheets and merge with inline <style> elements. let mut loader = ResourceLoader::new(); let stylesheet = collect_stylesheets(&doc, &mut loader, &loaded.base_url);
// Fetch and decode images referenced by <img> elements. let images = collect_images(&doc, &mut loader, &loaded.base_url);
PageState { doc, stylesheet, images, }}
// ---------------------------------------------------------------------------// Entry point// ---------------------------------------------------------------------------
fn main() { // Load page from argument (URL, file path) or default to about:blank. let loaded = match std::env::args().nth(1) { Some(arg) => load_from_arg(&arg), None => LoadedHtml { text: ABOUT_BLANK_HTML.to_string(), base_url: Url::parse("about:blank").expect("about:blank is always valid"), }, };
// Parse DOM and fetch subresources (CSS, images). let page = load_page(loaded);
// Load a system font for text rendering. let font = match font::load_system_font() { Ok(f) => f, Err(e) => { eprintln!("Error loading system font: {:?}", e); std::process::exit(1); } };
let _pool = appkit::AutoreleasePool::new();
let app = appkit::App::shared(); app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); appkit::install_app_delegate(&app);
let window = appkit::create_standard_window("we"); appkit::install_window_delegate(&window); window.set_accepts_mouse_moved_events(true);
// Initial render at the default window size (800x600). let mut bitmap = Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); let scroll_offsets: HashMap<NodeId, (f32, f32)> = HashMap::new(); let content_height = render_page(&page, &font, &mut bitmap, 0.0, &scroll_offsets);
// Create the view backed by the rendered bitmap. let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); let view = appkit::BitmapView::new(frame, &bitmap); window.set_content_view(&view.id());
// Store state for the resize handler. STATE.with(|state| { *state.borrow_mut() = Some(BrowserState { page, font, bitmap, view, page_scroll_y: 0.0, content_height, scroll_offsets, }); });
// Register resize and scroll handlers. appkit::set_resize_handler(handle_resize); appkit::set_scroll_handler(handle_scroll);
window.make_key_and_order_front(); app.activate(); app.run();}