···11+use std::cell::RefCell;
22+33+use we_html::parse_html;
44+use we_layout::layout;
15use we_platform::appkit;
22-use we_platform::cg::{BitmapContext, CGRect};
66+use we_platform::cg::BitmapContext;
77+use we_render::Renderer;
88+use we_text::font::{self, Font};
99+1010+/// Default HTML page shown when no file argument is provided.
1111+const DEFAULT_HTML: &str = r#"<!DOCTYPE html>
1212+<html>
1313+<head><title>we browser</title></head>
1414+<body>
1515+<h1>Hello from we!</h1>
1616+<p>This is a from-scratch web browser engine written in pure Rust.</p>
1717+<p>Zero external crate dependencies. Every subsystem is implemented in Rust.</p>
1818+<h2>Features</h2>
1919+<p>HTML5 tokenizer, DOM tree, block layout, and software rendering.</p>
2020+</body>
2121+</html>"#;
2222+2323+/// Browser state kept in thread-local storage so the resize handler can
2424+/// access it. All AppKit callbacks run on the main thread.
2525+struct BrowserState {
2626+ html: String,
2727+ font: Font,
2828+ bitmap: Box<BitmapContext>,
2929+ view: appkit::BitmapView,
3030+}
3131+3232+thread_local! {
3333+ static STATE: RefCell<Option<BrowserState>> = const { RefCell::new(None) };
3434+}
3535+3636+/// Re-run the full pipeline: parse → layout → render → copy to bitmap.
3737+fn render_page(html: &str, font: &Font, bitmap: &mut BitmapContext) {
3838+ let width = bitmap.width() as u32;
3939+ let height = bitmap.height() as u32;
4040+ if width == 0 || height == 0 {
4141+ return;
4242+ }
4343+4444+ let doc = parse_html(html);
4545+ let tree = layout(&doc, width as f32, height as f32, font);
4646+4747+ let mut renderer = Renderer::new(width, height);
4848+ renderer.paint(&tree, font);
4949+5050+ // Copy rendered pixels into the bitmap context's buffer.
5151+ let src = renderer.pixels();
5252+ let dst = bitmap.pixels_mut();
5353+ let len = src.len().min(dst.len());
5454+ dst[..len].copy_from_slice(&src[..len]);
5555+}
5656+5757+/// Called by the platform crate when the window is resized.
5858+fn handle_resize(width: f64, height: f64) {
5959+ STATE.with(|state| {
6060+ let mut state = state.borrow_mut();
6161+ let state = match state.as_mut() {
6262+ Some(s) => s,
6363+ None => return,
6464+ };
6565+6666+ let w = width as usize;
6767+ let h = height as usize;
6868+ if w == 0 || h == 0 {
6969+ return;
7070+ }
7171+7272+ // Create a new bitmap context with the new dimensions.
7373+ let mut new_bitmap = match BitmapContext::new(w, h) {
7474+ Some(b) => Box::new(b),
7575+ None => return,
7676+ };
7777+7878+ render_page(&state.html, &state.font, &mut new_bitmap);
7979+8080+ // Swap in the new bitmap and update the view's pointer.
8181+ state.bitmap = new_bitmap;
8282+ state.view.update_bitmap(&state.bitmap);
8383+ });
8484+}
385486fn main() {
8787+ // Load HTML from file argument or use default page.
8888+ let html = match std::env::args().nth(1) {
8989+ Some(path) => match std::fs::read_to_string(&path) {
9090+ Ok(content) => content,
9191+ Err(e) => {
9292+ eprintln!("Error reading {}: {}", path, e);
9393+ std::process::exit(1);
9494+ }
9595+ },
9696+ None => DEFAULT_HTML.to_string(),
9797+ };
9898+9999+ // Load a system font for text rendering.
100100+ let font = match font::load_system_font() {
101101+ Ok(f) => f,
102102+ Err(e) => {
103103+ eprintln!("Error loading system font: {:?}", e);
104104+ std::process::exit(1);
105105+ }
106106+ };
107107+5108 let _pool = appkit::AutoreleasePool::new();
61097110 let app = appkit::App::shared();
8111 app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR);
99-10112 appkit::install_app_delegate(&app);
1111312114 let window = appkit::create_standard_window("we");
1313-1414- // Install a window delegate to handle resize events.
15115 appkit::install_window_delegate(&window);
1616-1717- // Enable mouse-moved event delivery so mouseMoved: fires on the view.
18116 window.set_accepts_mouse_moved_events(true);
191172020- // Create a bitmap context for software rendering.
2121- let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context");
2222-2323- // Draw a colored rectangle as proof of life.
2424- // Clear to dark gray background.
2525- bitmap.clear(0.15, 0.15, 0.15, 1.0);
2626- // Draw a blue rectangle in the center.
2727- bitmap.fill_rect(CGRect::new(200.0, 150.0, 400.0, 300.0), 0.2, 0.4, 0.8, 1.0);
118118+ // Initial render at the default window size (800x600).
119119+ let mut bitmap =
120120+ Box::new(BitmapContext::new(800, 600).expect("failed to create bitmap context"));
121121+ render_page(&html, &font, &mut bitmap);
281222929- // Create a custom view backed by the bitmap context and set it as
3030- // the window's content view.
123123+ // Create the view backed by the rendered bitmap.
31124 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0);
32125 let view = appkit::BitmapView::new(frame, &bitmap);
33126 window.set_content_view(&view.id());
127127+128128+ // Store state for the resize handler.
129129+ STATE.with(|state| {
130130+ *state.borrow_mut() = Some(BrowserState {
131131+ html,
132132+ font,
133133+ bitmap,
134134+ view,
135135+ });
136136+ });
137137+138138+ // Register resize handler so re-layout happens on window resize.
139139+ appkit::set_resize_handler(handle_resize);
3414035141 window.make_key_and_order_front();
36142 app.activate();
+47-1
crates/platform/src/appkit.rs
···458458 BitmapView { view }
459459 }
460460461461+ /// Update the bitmap context pointer stored in the view.
462462+ ///
463463+ /// Call this when the bitmap context has been replaced (e.g., on resize).
464464+ /// The new `BitmapContext` must outlive this view.
465465+ pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) {
466466+ unsafe {
467467+ self.view.set_ivar(
468468+ BITMAP_CTX_IVAR,
469469+ bitmap_ctx as *const BitmapContext as *mut c_void,
470470+ );
471471+ }
472472+ }
473473+461474 /// Request the view to redraw.
462475 ///
463476 /// Call this after modifying the bitmap context's pixels to
···473486}
474487475488// ---------------------------------------------------------------------------
489489+// Global resize handler
490490+// ---------------------------------------------------------------------------
491491+492492+/// Global resize callback, called from `windowDidResize:` with the new
493493+/// content view dimensions (width, height) in points.
494494+///
495495+/// # Safety
496496+///
497497+/// Accessed only from the main thread (the AppKit event loop).
498498+static mut RESIZE_HANDLER: Option<fn(f64, f64)> = None;
499499+500500+/// Register a function to be called when the window is resized.
501501+///
502502+/// The handler receives the new content view width and height in points.
503503+/// Only one handler can be active at a time; setting a new one replaces
504504+/// any previous handler.
505505+pub fn set_resize_handler(handler: fn(f64, f64)) {
506506+ // SAFETY: Called from the main thread before `app.run()`.
507507+ unsafe {
508508+ RESIZE_HANDLER = Some(handler);
509509+ }
510510+}
511511+512512+// ---------------------------------------------------------------------------
476513// Window delegate for handling resize and close events
477514// ---------------------------------------------------------------------------
478515···490527 let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0)
491528 .expect("failed to allocate WeWindowDelegate class");
492529493493- // windowDidResize: — mark the content view as needing display
530530+ // windowDidResize: — call resize handler and mark view as needing display
494531 extern "C" fn window_did_resize(
495532 _this: *mut c_void,
496533 _sel: *mut c_void,
···503540 let content_view: *mut c_void = msg_send![win, contentView];
504541 if content_view.is_null() {
505542 return;
543543+ }
544544+ // Get the content view's bounds to determine new dimensions.
545545+ let bounds: NSRect = msg_send![content_view, bounds];
546546+ // Call the resize handler if one has been registered.
547547+ // SAFETY: We are on the main thread (AppKit event loop).
548548+ unsafe {
549549+ if let Some(handler) = RESIZE_HANDLER {
550550+ handler(bounds.size.width, bounds.size.height);
551551+ }
506552 }
507553 let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true];
508554 }