use 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, 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, } thread_local! { static STATE: RefCell> = const { RefCell::new(None) }; } // --------------------------------------------------------------------------- // Rendering pipeline // --------------------------------------------------------------------------- /// Build a `HashMap` of display dimensions for layout. fn image_sizes(store: &ImageStore) -> HashMap { 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` reference map for the renderer. fn image_refs(store: &ImageStore) -> HashMap { 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!( "\ Error\ \

Error

{escaped}

" ); 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