use std::cell::{Cell, RefCell};
use std::rc::Rc;
use js_sys::{Function, Object, Reflect, WebAssembly};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{Clamped, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
use super::dom;
use super::templates;
const FB_W: u32 = 256;
const FB_H: u32 = 144;
const FB_BYTES: usize = (FB_W * FB_H * 4) as usize;
type Framebuffer = Rc<RefCell<Vec<u8>>>;
type FrameLoopHolder = Rc<RefCell<Option<Closure<dyn FnMut()>>>>;
thread_local! {
static FRAME_GEN: Cell<u32> = const { Cell::new(0) };
static RUNTIME: RefCell<Option<CartridgeRuntime>> = const { RefCell::new(None) };
static POINTER: Cell<(i32, i32)> = const { Cell::new((0, 0)) };
static POINTER_DOWN: Cell<i32> = const { Cell::new(0) };
static STATE: RefCell<[i32; 64]> = const { RefCell::new([0; 64]) };
}
#[allow(dead_code)]
struct CartridgeRuntime {
clear: Closure<dyn FnMut(i32)>,
set_pixel: Closure<dyn FnMut(i32, i32, i32)>,
fill_rect: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
draw_char: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
draw_number: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
present: Closure<dyn FnMut()>,
width: Closure<dyn FnMut() -> i32>,
height: Closure<dyn FnMut() -> i32>,
pointer_x: Closure<dyn FnMut() -> i32>,
pointer_y: Closure<dyn FnMut() -> i32>,
pointer_down: Closure<dyn FnMut() -> i32>,
state_get: Closure<dyn FnMut(i32) -> i32>,
state_set: Closure<dyn FnMut(i32, i32)>,
}
pub(crate) async fn run_wasm(wasm_bytes: &[u8]) -> Result<(), JsValue> {
let ctx = mount_canvas()?;
run_with_ctx(wasm_bytes, ctx).await
}
pub(crate) async fn run_in_root_canvas(wasm_bytes: &[u8]) -> Result<(), JsValue> {
let ctx = size_and_get_ctx()?;
run_with_ctx(wasm_bytes, ctx).await
}
pub(crate) fn render_html(source: &str) -> Result<(), JsValue> {
stop();
let ctx = mount_canvas()?;
let blocks = html_to_blocks(source);
let buf = paint_html_fb(&blocks);
let img = ImageData::new_with_u8_clamped_array_and_sh(Clamped(&buf[..]), FB_W, FB_H)?;
ctx.put_image_data(&img, 0.0, 0.0)?;
Ok(())
}
async fn run_with_ctx(
wasm_bytes: &[u8],
ctx: CanvasRenderingContext2d,
) -> Result<(), JsValue> {
let generation = FRAME_GEN.with(|g| {
let n = g.get().wrapping_add(1);
g.set(n);
n
});
let fb: Framebuffer = Rc::new(RefCell::new(black_framebuffer()));
POINTER_DOWN.with(|d| d.set(0));
STATE.with(|s| *s.borrow_mut() = [0; 64]);
let (imports, runtime) = build_host_display(&fb, &ctx)?;
RUNTIME.with(|cell| *cell.borrow_mut() = Some(runtime));
let result = JsFuture::from(WebAssembly::instantiate_buffer(wasm_bytes, &imports)).await?;
let instance = Reflect::get(&result, &JsValue::from_str("instance"))?;
let exports = Reflect::get(&instance, &JsValue::from_str("exports"))?;
if let Some(frame) = export_fn(&exports, "frame") {
start_frame_loop(frame, generation);
} else if let Some(render) = export_fn(&exports, "render") {
render.call0(&JsValue::NULL)?;
} else {
return Err(JsValue::from_str("cartridge exports neither frame nor render"));
}
Ok(())
}
fn build_host_display(
fb: &Framebuffer,
ctx: &CanvasRenderingContext2d,
) -> Result<(Object, CartridgeRuntime), JsValue> {
let clear = {
let fb = fb.clone();
Closure::<dyn FnMut(i32)>::new(move |rgb: i32| {
let (r, g, b) = rgb_components(rgb);
let mut buf = fb.borrow_mut();
let mut i = 0;
while i < buf.len() {
buf[i] = r;
buf[i + 1] = g;
buf[i + 2] = b;
buf[i + 3] = 255;
i += 4;
}
})
};
let set_pixel = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32)>::new(move |x: i32, y: i32, rgb: i32| {
if x < 0 || y < 0 || x >= FB_W as i32 || y >= FB_H as i32 {
return;
}
let (r, g, b) = rgb_components(rgb);
let mut buf = fb.borrow_mut();
let idx = ((y as usize) * (FB_W as usize) + (x as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
})
};
let fill_rect = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, w: i32, h: i32, rgb: i32| {
let (r, g, b) = rgb_components(rgb);
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = x.saturating_add(w).min(FB_W as i32);
let y1 = y.saturating_add(h).min(FB_H as i32);
let mut buf = fb.borrow_mut();
let mut yy = y0;
while yy < y1 {
let mut xx = x0;
while xx < x1 {
let idx = ((yy as usize) * (FB_W as usize) + (xx as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
xx += 1;
}
yy += 1;
}
},
)
};
let present = {
let fb = fb.clone();
let ctx = ctx.clone();
Closure::<dyn FnMut()>::new(move || {
let buf = fb.borrow();
if let Ok(img) =
ImageData::new_with_u8_clamped_array_and_sh(Clamped(&buf[..]), FB_W, FB_H)
{
let _ = ctx.put_image_data(&img, 0.0, 0.0);
}
})
};
let draw_char = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, code: i32, rgb: i32, scale: i32| {
let mut buf = fb.borrow_mut();
blit_glyph(&mut buf, x, y, code as u32, rgb_components(rgb), scale.max(1));
},
)
};
let draw_number = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, value: i32, rgb: i32, scale: i32| {
let color = rgb_components(rgb);
let s = scale.max(1);
let advance = 6 * s; let mut buf = fb.borrow_mut();
let mut cx = x;
let mut n = (value as i64).unsigned_abs();
if value < 0 {
blit_glyph(&mut buf, cx, y, '-' as u32, color, s);
cx += advance;
}
let mut digits = [0u8; 20];
let mut count = 0;
if n == 0 {
digits[0] = b'0';
count = 1;
} else {
while n > 0 {
digits[count] = b'0' + (n % 10) as u8;
n /= 10;
count += 1;
}
}
for i in (0..count).rev() {
blit_glyph(&mut buf, cx, y, digits[i] as u32, color, s);
cx += advance;
}
},
)
};
let width = Closure::<dyn FnMut() -> i32>::new(move || FB_W as i32);
let height = Closure::<dyn FnMut() -> i32>::new(move || FB_H as i32);
let pointer_x = Closure::<dyn FnMut() -> i32>::new(move || POINTER.with(|p| p.get().0));
let pointer_y = Closure::<dyn FnMut() -> i32>::new(move || POINTER.with(|p| p.get().1));
let pointer_down = Closure::<dyn FnMut() -> i32>::new(move || POINTER_DOWN.with(|d| d.get()));
let state_get = Closure::<dyn FnMut(i32) -> i32>::new(move |slot: i32| {
if !(0..64).contains(&slot) {
return 0;
}
STATE.with(|s| s.borrow()[slot as usize])
});
let state_set = Closure::<dyn FnMut(i32, i32)>::new(move |slot: i32, value: i32| {
if !(0..64).contains(&slot) {
return;
}
STATE.with(|s| s.borrow_mut()[slot as usize] = value);
});
let host_display = Object::new();
set_fn(&host_display, "clear", &clear)?;
set_fn(&host_display, "set_pixel", &set_pixel)?;
set_fn(&host_display, "fill_rect", &fill_rect)?;
set_fn(&host_display, "draw_char", &draw_char)?;
set_fn(&host_display, "draw_number", &draw_number)?;
set_fn(&host_display, "present", &present)?;
set_fn(&host_display, "width", &width)?;
set_fn(&host_display, "height", &height)?;
set_fn(&host_display, "pointer_x", &pointer_x)?;
set_fn(&host_display, "pointer_y", &pointer_y)?;
set_fn(&host_display, "pointer_down", &pointer_down)?;
set_fn(&host_display, "state_get", &state_get)?;
set_fn(&host_display, "state_set", &state_set)?;
let imports = Object::new();
Reflect::set(&imports, &JsValue::from_str("host_display"), &host_display)?;
Ok((
imports,
CartridgeRuntime {
clear, set_pixel, fill_rect, draw_char, draw_number, present, width, height,
pointer_x, pointer_y, pointer_down, state_get, state_set,
},
))
}
fn blit_glyph(buf: &mut [u8], x: i32, y: i32, code: u32, color: (u8, u8, u8), scale: i32) {
let glyph = glyph_5x7(code);
let (r, g, b) = color;
for (row, bits) in glyph.iter().enumerate() {
for col in 0..5 {
if (bits >> (4 - col)) & 1 == 0 {
continue;
}
for dy in 0..scale {
for dx in 0..scale {
let px = x + col * scale + dx;
let py = y + row as i32 * scale + dy;
if px < 0 || py < 0 || px >= FB_W as i32 || py >= FB_H as i32 {
continue;
}
let idx = ((py as usize) * (FB_W as usize) + (px as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
}
}
}
}
}
fn glyph_5x7(c: u32) -> [u8; 7] {
match c {
0x20 => [0, 0, 0, 0, 0, 0, 0], 0x30 => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], 0x31 => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x32 => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], 0x33 => [0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E], 0x34 => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], 0x35 => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E], 0x36 => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E], 0x37 => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08], 0x38 => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], 0x39 => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E], 0x21 => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04], 0x22 => [0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00], 0x23 => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A], 0x25 => [0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03], 0x26 => [0x0C, 0x12, 0x14, 0x08, 0x15, 0x12, 0x0D], 0x27 => [0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00], 0x28 => [0x04, 0x08, 0x10, 0x10, 0x10, 0x08, 0x04], 0x29 => [0x04, 0x02, 0x01, 0x01, 0x01, 0x02, 0x04], 0x2A => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00], 0x2B => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00], 0x2C => [0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x08], 0x2D => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00], 0x2E => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06], 0x2F => [0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10], 0x3A => [0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00], 0x3B => [0x00, 0x06, 0x06, 0x00, 0x06, 0x04, 0x08], 0x3C => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02], 0x3D => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00], 0x3E => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08], 0x3F => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04], 0x40 => [0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0E], 0x5B => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E], 0x5D => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E], 0x5F => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F], 0x41 => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x42 => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E], 0x43 => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], 0x44 => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E], 0x45 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], 0x46 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10], 0x47 => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E], 0x48 => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x49 => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x4A => [0x07, 0x02, 0x02, 0x02, 0x12, 0x12, 0x0C], 0x4B => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], 0x4C => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F], 0x4D => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11], 0x4E => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11], 0x4F => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x50 => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10], 0x51 => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], 0x52 => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11], 0x53 => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E], 0x54 => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], 0x55 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x56 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04], 0x57 => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11], 0x58 => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], 0x59 => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], 0x5A => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], 0x61 => [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F], 0x62 => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1E], 0x63 => [0x00, 0x00, 0x0E, 0x10, 0x10, 0x11, 0x0E], 0x64 => [0x01, 0x01, 0x0D, 0x13, 0x11, 0x11, 0x0F], 0x65 => [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E], 0x66 => [0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08], 0x67 => [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x0E], 0x68 => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11], 0x69 => [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E], 0x6A => [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C], 0x6B => [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12], 0x6C => [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x6D => [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11], 0x6E => [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11], 0x6F => [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E], 0x70 => [0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10], 0x71 => [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01], 0x72 => [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10], 0x73 => [0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E], 0x74 => [0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06], 0x75 => [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D], 0x76 => [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04], 0x77 => [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A], 0x78 => [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11], 0x79 => [0x00, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x0E], 0x7A => [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F], _ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F], }
}
pub(crate) fn set_pointer_down(down: bool) {
POINTER_DOWN.with(|d| d.set(if down { 1 } else { 0 }));
}
pub(crate) fn set_pointer(client_x: f64, client_y: f64) {
let Some(el) = dom::by_id("display-canvas") else { return };
let Ok(canvas) = el.dyn_into::<HtmlCanvasElement>() else { return };
let rect = canvas.get_bounding_client_rect();
let (w, h) = (rect.width(), rect.height());
if w <= 0.0 || h <= 0.0 {
return;
}
let fx = (((client_x - rect.left()) / w) * FB_W as f64).clamp(0.0, (FB_W - 1) as f64) as i32;
let fy = (((client_y - rect.top()) / h) * FB_H as f64).clamp(0.0, (FB_H - 1) as f64) as i32;
POINTER.with(|p| p.set((fx, fy)));
}
fn set_fn<T: ?Sized>(obj: &Object, name: &str, closure: &Closure<T>) -> Result<(), JsValue> {
Reflect::set(obj, &JsValue::from_str(name), closure.as_ref().unchecked_ref())?;
Ok(())
}
fn rgb_components(c: i32) -> (u8, u8, u8) {
let u = c as u32;
(((u >> 16) & 0xff) as u8, ((u >> 8) & 0xff) as u8, (u & 0xff) as u8)
}
fn black_framebuffer() -> Vec<u8> {
let mut buf = vec![0u8; FB_BYTES];
let mut i = 3;
while i < buf.len() {
buf[i] = 255;
i += 4;
}
buf
}
fn export_fn(exports: &JsValue, name: &str) -> Option<Function> {
Reflect::get(exports, &JsValue::from_str(name))
.ok()
.and_then(|v| v.dyn_into::<Function>().ok())
}
fn start_frame_loop(frame: Function, generation: u32) {
let start = js_sys::Date::now();
let holder: FrameLoopHolder = Rc::new(RefCell::new(None));
let holder2 = holder.clone();
*holder.borrow_mut() = Some(Closure::wrap(Box::new(move || {
if FRAME_GEN.with(|g| g.get()) != generation {
let _ = holder2.borrow_mut().take();
return;
}
let t = (js_sys::Date::now() - start) as i32;
let _ = frame.call1(&JsValue::NULL, &JsValue::from(t));
if let Some(cb) = holder2.borrow().as_ref() {
let _ = request_af(cb);
}
}) as Box<dyn FnMut()>));
if let Some(cb) = holder.borrow().as_ref() {
let _ = request_af(cb);
}
}
fn request_af(cb: &Closure<dyn FnMut()>) -> Result<i32, JsValue> {
dom::window()?.request_animation_frame(cb.as_ref().unchecked_ref())
}
pub(crate) fn stop() {
FRAME_GEN.with(|g| g.set(g.get().wrapping_add(1)));
RUNTIME.with(|cell| *cell.borrow_mut() = None);
}
fn mount_canvas() -> Result<CanvasRenderingContext2d, JsValue> {
dom::swap_inner("view-content", &templates::display_surface().into_string());
super::opfs::set_view_collapsed(false);
size_and_get_ctx()
}
fn size_and_get_ctx() -> Result<CanvasRenderingContext2d, JsValue> {
let canvas = dom::by_id("display-canvas")
.ok_or_else(|| JsValue::from_str("display-canvas missing"))?
.dyn_into::<HtmlCanvasElement>()?;
canvas.set_width(FB_W);
canvas.set_height(FB_H);
let ctx = canvas
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("no 2d context"))?
.dyn_into::<CanvasRenderingContext2d>()?;
Ok(ctx)
}
struct HtmlBlock {
text: String,
scale: i32,
bullet: bool,
}
fn tag_name(inner: &str) -> String {
let t = inner.trim().trim_start_matches('/').trim_start();
let end = t
.find(|ch: char| ch.is_whitespace() || ch == '/')
.unwrap_or(t.len());
t[..end].to_ascii_lowercase()
}
fn decode_entities(s: &str) -> String {
s.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("'", "'")
.replace(" ", " ")
.replace("&", "&")
}
fn collapse_ws(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_space = false;
for ch in s.chars() {
if ch.is_whitespace() {
if !prev_space && !out.is_empty() {
out.push(' ');
}
prev_space = true;
} else {
out.push(ch);
prev_space = false;
}
}
out.trim_end().to_string()
}
fn flush_block(blocks: &mut Vec<HtmlBlock>, cur: &mut String, scale: i32, bullet: bool) {
let text = collapse_ws(&decode_entities(cur));
cur.clear();
if !text.is_empty() {
blocks.push(HtmlBlock { text, scale, bullet });
}
}
fn html_to_blocks(src: &str) -> Vec<HtmlBlock> {
let chars: Vec<char> = src.chars().collect();
let mut blocks: Vec<HtmlBlock> = Vec::new();
let mut cur = String::new();
let mut scale: i32 = 1;
let mut bullet = false;
let mut skip_tag: Option<String> = None;
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '<' {
let mut j = i + 1;
let mut inner = String::new();
while j < chars.len() && chars[j] != '>' {
inner.push(chars[j]);
j += 1;
}
i = if j < chars.len() { j + 1 } else { j };
let closing = inner.trim_start().starts_with('/');
let name = tag_name(&inner);
if let Some(skip) = skip_tag.clone() {
if closing && name == skip {
skip_tag = None;
}
continue;
}
match name.as_str() {
"script" | "style" | "head" => {
if !closing {
skip_tag = Some(name);
}
}
"br" | "hr" => {
flush_block(&mut blocks, &mut cur, scale, bullet);
bullet = false;
}
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
flush_block(&mut blocks, &mut cur, scale, bullet);
bullet = false;
scale = if closing {
1
} else if name == "h1" {
3
} else {
2
};
}
"li" => {
flush_block(&mut blocks, &mut cur, scale, bullet);
scale = 1;
bullet = !closing;
}
"p" | "div" | "ul" | "ol" | "section" | "article" | "header" | "footer"
| "nav" | "main" | "blockquote" | "pre" | "table" | "tr" | "title" | "body"
| "html" | "figure" | "figcaption" => {
flush_block(&mut blocks, &mut cur, scale, bullet);
bullet = false;
scale = 1;
}
_ => { }
}
continue;
}
if skip_tag.is_some() {
i += 1;
continue;
}
cur.push(c);
i += 1;
}
flush_block(&mut blocks, &mut cur, scale, bullet);
blocks
}
fn wrap_text(text: &str, max_chars: usize) -> Vec<String> {
let max_chars = max_chars.max(1);
let mut lines: Vec<String> = Vec::new();
let mut line = String::new();
for word in text.split_whitespace() {
if line.is_empty() {
line.push_str(word);
} else if line.chars().count() + 1 + word.chars().count() <= max_chars {
line.push(' ');
line.push_str(word);
} else {
lines.push(std::mem::take(&mut line));
line.push_str(word);
}
while line.chars().count() > max_chars {
let head: String = line.chars().take(max_chars).collect();
let tail: String = line.chars().skip(max_chars).collect();
lines.push(head);
line = tail;
}
}
if !line.is_empty() {
lines.push(line);
}
lines
}
fn filled_framebuffer(color: (u8, u8, u8)) -> Vec<u8> {
let (r, g, b) = color;
let mut buf = vec![0u8; FB_BYTES];
let mut i = 0;
while i + 3 < buf.len() {
buf[i] = r;
buf[i + 1] = g;
buf[i + 2] = b;
buf[i + 3] = 255;
i += 4;
}
buf
}
fn paint_html_fb(blocks: &[HtmlBlock]) -> Vec<u8> {
let mut buf = filled_framebuffer((13, 13, 13));
let left = 6i32;
let right = FB_W as i32 - 6;
let mut y = 6i32;
for block in blocks {
let scale = block.scale.clamp(1, 3);
let advance = 6 * scale; let line_h = 8 * scale; let max_chars = (((right - left) / advance).max(1)) as usize;
let color = if scale > 1 { (245, 245, 245) } else { (205, 205, 205) };
let text = if block.bullet {
format!("- {}", block.text)
} else {
block.text.clone()
};
for line in wrap_text(&text, max_chars) {
if y + line_h > FB_H as i32 {
return buf; }
let mut x = left;
for ch in line.chars() {
blit_glyph(&mut buf, x, y, ch as u32, color, scale);
x += advance;
}
y += line_h;
}
y += 3; }
buf
}