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];
}