web engine - experimental web browser

Merge branch 'window-integration': Window integration - display rendered HTML page

+169 -17
+122 -16
crates/browser/src/main.rs
··· 1 + use std::cell::RefCell; 2 + 3 + use we_html::parse_html; 4 + use we_layout::layout; 1 5 use we_platform::appkit; 2 - use we_platform::cg::{BitmapContext, CGRect}; 6 + use we_platform::cg::BitmapContext; 7 + use we_render::Renderer; 8 + use we_text::font::{self, Font}; 9 + 10 + /// Default HTML page shown when no file argument is provided. 11 + const DEFAULT_HTML: &str = r#"<!DOCTYPE html> 12 + <html> 13 + <head><title>we browser</title></head> 14 + <body> 15 + <h1>Hello from we!</h1> 16 + <p>This is a from-scratch web browser engine written in pure Rust.</p> 17 + <p>Zero external crate dependencies. Every subsystem is implemented in Rust.</p> 18 + <h2>Features</h2> 19 + <p>HTML5 tokenizer, DOM tree, block layout, and software rendering.</p> 20 + </body> 21 + </html>"#; 22 + 23 + /// Browser state kept in thread-local storage so the resize handler can 24 + /// access it. All AppKit callbacks run on the main thread. 25 + struct BrowserState { 26 + html: String, 27 + font: Font, 28 + bitmap: Box<BitmapContext>, 29 + view: appkit::BitmapView, 30 + } 31 + 32 + thread_local! { 33 + static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) }; 34 + } 35 + 36 + /// Re-run the full pipeline: parse → layout → render → copy to bitmap. 37 + fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) { 38 + let width = bitmap.width() as u32; 39 + let height = bitmap.height() as u32; 40 + if width == 0 || height == 0 { 41 + return; 42 + } 43 + 44 + let doc = parse_html(html); 45 + let tree = layout(&doc, width as f32, height as f32, font); 46 + 47 + let mut renderer = Renderer::new(width, height); 48 + renderer.paint(&tree, font); 49 + 50 + // Copy rendered pixels into the bitmap context's buffer. 51 + let src = renderer.pixels(); 52 + let dst = bitmap.pixels_mut(); 53 + let len = src.len().min(dst.len()); 54 + dst[..len].copy_from_slice(&src[..len]); 55 + } 56 + 57 + /// Called by the platform crate when the window is resized. 58 + fn handle_resize(width: f64, height: f64) { 59 + STATE.with(|state| { 60 + let mut state = state.borrow_mut(); 61 + let state = match state.as_mut() { 62 + Some(s) => s, 63 + None => return, 64 + }; 65 + 66 + let w = width as usize; 67 + let h = height as usize; 68 + if w == 0 || h == 0 { 69 + return; 70 + } 71 + 72 + // Create a new bitmap context with the new dimensions. 73 + let mut new_bitmap = match BitmapContext::new(w, h) { 74 + Some(b) => Box::new(b), 75 + None => return, 76 + }; 77 + 78 + render_page(&state.html, &state.font, &mut new_bitmap); 79 + 80 + // Swap in the new bitmap and update the view's pointer. 81 + state.bitmap = new_bitmap; 82 + state.view.update_bitmap(&state.bitmap); 83 + }); 84 + } 3 85 4 86 fn main() { 87 + // Load HTML from file argument or use default page. 88 + let html = match std::env::args().nth(1) { 89 + Some(path) => match std::fs::read_to_string(&path) { 90 + Ok(content) => content, 91 + Err(e) => { 92 + eprintln!("Error reading {}: {}", path, e); 93 + std::process::exit(1); 94 + } 95 + }, 96 + None => DEFAULT_HTML.to_string(), 97 + }; 98 + 99 + // Load a system font for text rendering. 100 + let font = match font::load_system_font() { 101 + Ok(f) => f, 102 + Err(e) => { 103 + eprintln!("Error loading system font: {:?}", e); 104 + std::process::exit(1); 105 + } 106 + }; 107 + 5 108 let _pool = appkit::AutoreleasePool::new(); 6 109 7 110 let app = appkit::App::shared(); 8 111 app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); 9 - 10 112 appkit::install_app_delegate(&app); 11 113 12 114 let window = appkit::create_standard_window("we"); 13 - 14 - // Install a window delegate to handle resize events. 15 115 appkit::install_window_delegate(&window); 16 - 17 - // Enable mouse-moved event delivery so mouseMoved: fires on the view. 18 116 window.set_accepts_mouse_moved_events(true); 19 117 20 - // Create a bitmap context for software rendering. 21 - let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context"); 22 - 23 - // Draw a colored rectangle as proof of life. 24 - // Clear to dark gray background. 25 - bitmap.clear(0.15, 0.15, 0.15, 1.0); 26 - // Draw a blue rectangle in the center. 27 - bitmap.fill_rect(CGRect::new(200.0, 150.0, 400.0, 300.0), 0.2, 0.4, 0.8, 1.0); 118 + // Initial render at the default window size (800x600). 119 + let mut bitmap = 120 + Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context")); 121 + render_page(&html, &font, &mut bitmap); 28 122 29 - // Create a custom view backed by the bitmap context and set it as 30 - // the window's content view. 123 + // Create the view backed by the rendered bitmap. 31 124 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); 32 125 let view = appkit::BitmapView::new(frame, &bitmap); 33 126 window.set_content_view(&view.id()); 127 + 128 + // Store state for the resize handler. 129 + STATE.with(|state| { 130 + *state.borrow_mut() = Some(BrowserState { 131 + html, 132 + font, 133 + bitmap, 134 + view, 135 + }); 136 + }); 137 + 138 + // Register resize handler so re-layout happens on window resize. 139 + appkit::set_resize_handler(handle_resize); 34 140 35 141 window.make_key_and_order_front(); 36 142 app.activate();
+47 -1
crates/platform/src/appkit.rs
··· 458 458 BitmapView { view } 459 459 } 460 460 461 + /// Update the bitmap context pointer stored in the view. 462 + /// 463 + /// Call this when the bitmap context has been replaced (e.g., on resize). 464 + /// The new `BitmapContext` must outlive this view. 465 + pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) { 466 + unsafe { 467 + self.view.set_ivar( 468 + BITMAP_CTX_IVAR, 469 + bitmap_ctx as *const BitmapContext as *mut c_void, 470 + ); 471 + } 472 + } 473 + 461 474 /// Request the view to redraw. 462 475 /// 463 476 /// Call this after modifying the bitmap context's pixels to ··· 473 486 } 474 487 475 488 // --------------------------------------------------------------------------- 489 + // Global resize handler 490 + // --------------------------------------------------------------------------- 491 + 492 + /// Global resize callback, called from `windowDidResize:` with the new 493 + /// content view dimensions (width, height) in points. 494 + /// 495 + /// # Safety 496 + /// 497 + /// Accessed only from the main thread (the AppKit event loop). 498 + static mut RESIZE_HANDLER: Option<fn(f64, f64)> = None; 499 + 500 + /// Register a function to be called when the window is resized. 501 + /// 502 + /// The handler receives the new content view width and height in points. 503 + /// Only one handler can be active at a time; setting a new one replaces 504 + /// any previous handler. 505 + pub fn set_resize_handler(handler: fn(f64, f64)) { 506 + // SAFETY: Called from the main thread before `app.run()`. 507 + unsafe { 508 + RESIZE_HANDLER = Some(handler); 509 + } 510 + } 511 + 512 + // --------------------------------------------------------------------------- 476 513 // Window delegate for handling resize and close events 477 514 // --------------------------------------------------------------------------- 478 515 ··· 490 527 let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) 491 528 .expect("failed to allocate WeWindowDelegate class"); 492 529 493 - // windowDidResize: — mark the content view as needing display 530 + // windowDidResize: — call resize handler and mark view as needing display 494 531 extern "C" fn window_did_resize( 495 532 _this: *mut c_void, 496 533 _sel: *mut c_void, ··· 503 540 let content_view: *mut c_void = msg_send![win, contentView]; 504 541 if content_view.is_null() { 505 542 return; 543 + } 544 + // Get the content view's bounds to determine new dimensions. 545 + let bounds: NSRect = msg_send![content_view, bounds]; 546 + // Call the resize handler if one has been registered. 547 + // SAFETY: We are on the main thread (AppKit event loop). 548 + unsafe { 549 + if let Some(handler) = RESIZE_HANDLER { 550 + handler(bounds.size.width, bounds.size.height); 551 + } 506 552 } 507 553 let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; 508 554 }