╭ changed ───────────╮ │src/assets/script.js│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │ 4 let wasmInstance; │ │ 5 const decoder = new TextDecoder(); │ │ 6 let currentHtml = ""; │ │ 7 + │ │ 8 +const MIN_COLS = 30; │ │ 9 │ │10 +// measured once after the font is loaded. recomputed lazily if it ever │ │11 +// reads back as zero (e.g. measured before the font was actually ready). │ │12 +let cellWidth = null; │ │13 + │ │14 +function measureCellWidth() { │ │15 + const probe = document.createElement("span"); │ │16 + probe.style.position = "absolute"; │ │17 + probe.style.visibility = "hidden"; │ │18 + probe.style.whiteSpace = "pre"; │ │19 + probe.textContent = "X".repeat(100); │ │20 + grid.appendChild(probe); │ │21 + const w = probe.getBoundingClientRect().width / 100; │ │22 + probe.remove(); │ │23 + return w; │ │24 +} │ │25 + │ │26 +function maxCols() { │ │27 + if (!cellWidth) cellWidth = measureCellWidth(); │ │28 + return Math.max(MIN_COLS, Math.floor(document.body.clientWidth / cellWidth));│ │29 +} │ │30 + │ │31 function readWasmString(ptr, len) { │ │32 const memory = wasmInstance.exports.memory; │ │33 return decoder.decode(new Uint8Array(memory.buffer, ptr, len)); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │34 } │ │35 │ │13 -var importObject = { │ │36 +const importObject = { │ │37 env: { │ │15 - _consoleLog: function(ptr, len) { │ │38 + _consoleLog: function (ptr, len) { │ │39 console.log(readWasmString(ptr, len));│ │40 }, │ │18 - _setHtml: function(ptr, len) { │ │41 + _setHtml: function (ptr, len) { │ │42 const html = readWasmString(ptr, len);│ │43 if (html !== currentHtml) { │ │44 currentHtml = html; │ ╰──────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │48 }, │ │49 }; │ │50 │ │28 -WebAssembly.instantiateStreaming(fetch("haxy.wasm"), importObject).then((result) => { │ │51 +WebAssembly.instantiateStreaming(fetch("haxy.wasm"), importObject).then(async (result) => {│ │52 wasmInstance = result.instance; │ │53 + │ │54 + // wait for the @font-face to load before measuring cell width, otherwise │ │55 + // we get the fallback font's metrics. │ │56 + if (document.fonts && document.fonts.ready) { │ │57 + await document.fonts.ready; │ │58 + } │ │59 │ │60 // the page is embedded as base64 so it can sit inside the host html │ │61 // without worrying about characters that would terminate the script tag. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 const jsonBytes = Uint8Array.from(atob(pageJsonBase64), (c) => c.charCodeAt(0)); │ │64 const ptr = wasmInstance.exports._alloc(jsonBytes.length); │ │65 new Uint8Array(wasmInstance.exports.memory.buffer, ptr, jsonBytes.length).set(jsonBytes); │ │37 - wasmInstance.exports._start(ptr, jsonBytes.length); │ │66 + wasmInstance.exports._start(ptr, jsonBytes.length, maxCols()); │ │67 │ │68 document.addEventListener("keydown", (event) => { │ │69 if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"].includes(event.key)) {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │70 event.preventDefault(); │ │71 } │ │72 wasmInstance.exports._onKeyDown(event.keyCode);│ │44 - wasmInstance.exports._tick(); │ │73 + wasmInstance.exports._tick(maxCols()); │ │74 + }); │ │75 + │ │76 + let resizeTimer = null; │ │77 + window.addEventListener("resize", () => { │ │78 + clearTimeout(resizeTimer); │ │79 + resizeTimer = setTimeout(() => { │ │80 + wasmInstance.exports._tick(maxCols()); │ │81 + }, 100); │ │82 }); │ │83 }); │ │84 │ ╰───────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │20 var page_arena: ?std.heap.ArenaAllocator = null; │ │21 var page: ?pg.Page = null; │ │22 │ │23 -fn start(json: []const u8) !void { │ │23 +fn start(json: []const u8, max_width: u32) !void { │ │24 if (page_arena) |*a| a.deinit(); │ │25 page_arena = std.heap.ArenaAllocator.init(allocator);│ │26 │ ╰─────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │36 if (root) |*old_root| old_root.deinit(); │ │37 root = next_root; │ │38 │ │39 - try updateHtml(); │ │39 + try tick(max_width); │ │40 } │ │41 │ │42 -fn tick() !void { │ │42 +fn tick(max_width: u32) !void { │ │43 const root_ptr = if (root) |*root_value| root_value else return error.NotStarted;│ │44 - // TODO: make this fit the current size of the browser window │ │44 try root_ptr.build(.{ │ │45 .min_size = .{ .width = null, .height = null }, │ │47 - .max_size = .{ .width = 80, .height = 24 }, │ │46 + // height is null so the TUI grows to fit all its content; the browser │ │47 + // page handles vertical scrolling. │ │48 + .max_size = .{ .width = max_width, .height = null }, │ │49 }, root_ptr.getFocus()); │ │50 try updateHtml(); │ │51 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │83 return slice.ptr; │ │84 } │ │85 │ │85 -export fn _start(json_ptr: [*]u8, json_len: u32) void { │ │86 +export fn _start(json_ptr: [*]u8, json_len: u32, max_width: u32) void { │ │87 const json = json_ptr[0..json_len]; │ │88 defer allocator.free(json); │ │88 - start(json) catch |err| { │ │89 + start(json, max_width) catch |err| { │ │90 var buf: [256]u8 = undefined; │ │91 const str = std.fmt.bufPrint(&buf, "start: {}", .{err}) catch unreachable;│ │92 consoleLog(str); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │ 93 }; │ │ 94 } │ │ 95 │ │ 95 -export fn _tick() bool { │ │ 96 - tick() catch |err| { │ │ 96 +export fn _tick(max_width: u32) bool { │ │ 97 + tick(max_width) catch |err| { │ │ 98 var buf: [256]u8 = undefined; │ │ 99 const str = std.fmt.bufPrint(&buf, "tick: {}", .{err}) catch unreachable;│ │100 consoleLog(str); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────╮ │58 │ │59 try root.build(.{ │ │60 .min_size = .{ .width = null, .height = null },│ │61 - .max_size = .{ .width = 80, .height = 24 }, │ │61 + .max_size = .{ .width = 80, .height = null }, │ │62 }, root.getFocus()); │ │63 if (root.getFocus().child_id) |child_id| { │ │64 try root.getFocus().setFocus(child_id); │ ╰───────────────────────────────────────────────────────────╯