From 0de30018b3597f9274181b367b62090486fa84ae Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Wed, 4 Mar 2026 19:21:06 +0100 Subject: [PATCH] Implement window integration: display rendered HTML page Connect all Phase 3 components to display a rendered HTML page in the AppKit window. The browser now runs the full pipeline: parse HTML into a DOM, run block layout, paint via the software renderer, and display the result in the window. - Default "Hello from we!" page when run with no arguments - Load HTML from a file path via command-line argument - Window resize triggers full re-layout and re-render at new dimensions - Platform crate: add set_resize_handler() and BitmapView::update_bitmap() Co-Authored-By: Claude Opus 4.6 --- crates/browser/src/main.rs | 138 ++++++++++++++++++++++++++++++---- crates/platform/src/appkit.rs | 48 +++++++++++- 2 files changed, 169 insertions(+), 17 deletions(-) diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index a32972a..48e7920 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -1,37 +1,143 @@ +use std::cell::RefCell; + +use we_html::parse_html; +use we_layout::layout; use we_platform::appkit; -use we_platform::cg::{BitmapContext, CGRect}; +use we_platform::cg::BitmapContext; +use we_render::Renderer; +use we_text::font::{self, Font}; + +/// Default HTML page shown when no file argument is provided. +const DEFAULT_HTML: &str = r#" + +we browser + +

Hello from we!

+

This is a from-scratch web browser engine written in pure Rust.

+

Zero external crate dependencies. Every subsystem is implemented in Rust.

+

Features

+

HTML5 tokenizer, DOM tree, block layout, and software rendering.

+ +"#; + +/// Browser state kept in thread-local storage so the resize handler can +/// access it. All AppKit callbacks run on the main thread. +struct BrowserState { + html: String, + font: Font, + bitmap: Box, + view: appkit::BitmapView, +} + +thread_local! { + static STATE: RefCell> = const { RefCell::new(None) }; +} + +/// Re-run the full pipeline: parse → layout → render → copy to bitmap. +fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) { + let width = bitmap.width() as u32; + let height = bitmap.height() as u32; + if width == 0 || height == 0 { + return; + } + + let doc = parse_html(html); + let tree = layout(&doc, width as f32, height as f32, font); + + let mut renderer = Renderer::new(width, height); + renderer.paint(&tree, font); + + // 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]); +} + +/// 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, + }; + + render_page(&state.html, &state.font, &mut new_bitmap); + + // Swap in the new bitmap and update the view's pointer. + state.bitmap = new_bitmap; + state.view.update_bitmap(&state.bitmap); + }); +} fn main() { + // Load HTML from file argument or use default page. + let html = match std::env::args().nth(1) { + Some(path) => match std::fs::read_to_string(&path) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading {}: {}", path, e); + std::process::exit(1); + } + }, + None => DEFAULT_HTML.to_string(), + }; + + // 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"); - - // Install a window delegate to handle resize events. appkit::install_window_delegate(&window); - - // Enable mouse-moved event delivery so mouseMoved: fires on the view. window.set_accepts_mouse_moved_events(true); - // Create a bitmap context for software rendering. - let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context"); + // Initial render at the default window size (800x600). + let mut bitmap = + Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); + render_page(&html, &font, &mut bitmap); - // Draw a colored rectangle as proof of life. - // Clear to dark gray background. - bitmap.clear(0.15, 0.15, 0.15, 1.0); - // Draw a blue rectangle in the center. - bitmap.fill_rect(CGRect::new(200.0, 150.0, 400.0, 300.0), 0.2, 0.4, 0.8, 1.0); - - // Create a custom view backed by the bitmap context and set it as - // the window's content view. + // 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 { + html, + font, + bitmap, + view, + }); + }); + + // Register resize handler so re-layout happens on window resize. + appkit::set_resize_handler(handle_resize); + window.make_key_and_order_front(); app.activate(); app.run(); diff --git a/crates/platform/src/appkit.rs b/crates/platform/src/appkit.rs index e2775d8..17a9975 100644 --- a/crates/platform/src/appkit.rs +++ b/crates/platform/src/appkit.rs @@ -458,6 +458,19 @@ impl BitmapView { BitmapView { view } } + /// Update the bitmap context pointer stored in the view. + /// + /// Call this when the bitmap context has been replaced (e.g., on resize). + /// The new `BitmapContext` must outlive this view. + pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) { + unsafe { + self.view.set_ivar( + BITMAP_CTX_IVAR, + bitmap_ctx as *const BitmapContext as *mut c_void, + ); + } + } + /// Request the view to redraw. /// /// Call this after modifying the bitmap context's pixels to @@ -472,6 +485,30 @@ impl BitmapView { } } +// --------------------------------------------------------------------------- +// Global resize handler +// --------------------------------------------------------------------------- + +/// Global resize callback, called from `windowDidResize:` with the new +/// content view dimensions (width, height) in points. +/// +/// # Safety +/// +/// Accessed only from the main thread (the AppKit event loop). +static mut RESIZE_HANDLER: Option = None; + +/// Register a function to be called when the window is resized. +/// +/// The handler receives the new content view width and height in points. +/// Only one handler can be active at a time; setting a new one replaces +/// any previous handler. +pub fn set_resize_handler(handler: fn(f64, f64)) { + // SAFETY: Called from the main thread before `app.run()`. + unsafe { + RESIZE_HANDLER = Some(handler); + } +} + // --------------------------------------------------------------------------- // Window delegate for handling resize and close events // --------------------------------------------------------------------------- @@ -490,7 +527,7 @@ fn register_we_window_delegate_class() { let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) .expect("failed to allocate WeWindowDelegate class"); - // windowDidResize: — mark the content view as needing display + // windowDidResize: — call resize handler and mark view as needing display extern "C" fn window_did_resize( _this: *mut c_void, _sel: *mut c_void, @@ -504,6 +541,15 @@ fn register_we_window_delegate_class() { if content_view.is_null() { return; } + // Get the content view's bounds to determine new dimensions. + let bounds: NSRect = msg_send![content_view, bounds]; + // Call the resize handler if one has been registered. + // SAFETY: We are on the main thread (AppKit event loop). + unsafe { + if let Some(handler) = RESIZE_HANDLER { + handler(bounds.size.width, bounds.size.height); + } + } let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; } -- 2.51.2