╭ changed ────────────╮ │src/assets/index.html│ ╰─────────────────────╯ ╭────────────────────────────────╮ │24 │ │25 #grid a { │ │26 color: white; │ │27 + } │ │28 + │ │29 + #grid .clickable {│ │30 + cursor: pointer;│ │31 } │ │32 </style> │ │33 </head> │ ╰────────────────────────────────╯ ╭ changed ───────────╮ │src/assets/script.js│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │ 7 │ │ 8 const MIN_COLS = 30; │ │ 9 │ │10 -// measured once after the font is loaded. recomputed lazily if they ever│ │11 -// read back as zero (e.g. measured before the font was actually ready). │ │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 -let cellHeight = null; │ │13 │ │15 -function measureCellSize() { │ │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); │ │22 - const rect = probe.getBoundingClientRect(); │ │21 + const w = probe.getBoundingClientRect().width / 100;│ │22 probe.remove(); │ │24 - cellWidth = rect.width / 100; │ │25 - cellHeight = rect.height; │ │23 + return w; │ │24 } │ │25 │ │26 function maxCols() { │ ╰────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │29 - if (!cellWidth) measureCellSize(); │ │27 + if (!cellWidth) cellWidth = measureCellWidth(); │ │28 return Math.max(MIN_COLS, Math.floor(document.body.clientWidth / cellWidth));│ │29 } │ │30 │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │74 }); │ │75 │ │76 grid.addEventListener("click", (event) => { │ │79 - // ensure cellWidth/cellHeight are measured before converting pixels to grid cells│ │80 - maxCols(); │ │81 - const rect = grid.getBoundingClientRect(); │ │82 - const col = Math.floor((event.clientX - rect.left) / cellWidth); │ │83 - const row = Math.floor((event.clientY - rect.top) / cellHeight); │ │84 - if (col < 0 || row < 0) return; │ │85 - wasmInstance.exports._onMouseClick(col, row); │ │77 + const span = event.target.closest(".clickable"); │ │78 + if (!span) return; │ │79 + const focusId = Number(span.dataset.focusId); │ │80 + wasmInstance.exports._onMouseClick(focusId); │ │81 wasmInstance.exports._tick(maxCols()); │ │82 }); │ │83 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │66 try root_ptr.input(key, root_ptr.getFocus()); │ │67 } │ │68 │ │69 -fn onMouseClick(x: usize, y: usize) !void { │ │69 +fn onMouseClick(focus_id: usize) !void { │ │70 const root_ptr = if (root) |*root_value| root_value else return error.NotStarted; │ │71 - const root_focus = root_ptr.getFocus(); │ │72 - var iter = root_focus.children.iterator(); │ │73 - while (iter.next()) |entry| { │ │74 - const child = entry.value_ptr.*; │ │75 - if (!child.focus.focusable) continue; │ │76 - const r = child.rect; │ │77 - if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) {│ │78 - try root_focus.setFocus(entry.key_ptr.*); │ │79 - break; │ │80 - } │ │81 - } │ │71 + try root_ptr.getFocus().setFocus(focus_id); │ │72 } │ │73 │ │74 fn consoleLog(arg: []const u8) void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │116 }; │ │117 } │ │118 │ │129 -export fn _onMouseClick(x: u32, y: u32) void { │ │130 - onMouseClick(x, y) catch |err| { │ │119 +export fn _onMouseClick(focus_id: u32) void { │ │120 + onMouseClick(focus_id) catch |err| { │ │121 var buf: [256]u8 = undefined; │ │122 const str = std.fmt.bufPrint(&buf, "onMouseClick: {}", .{err}) catch unreachable;│ │123 consoleLog(str); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │88 │ │89 pub fn generateHtml(allocator: std.mem.Allocator, root: *Widget) ![]const u8 {│ │90 const grid = root.getGrid() orelse return error.MissingGrid; │ │91 + const root_focus = root.getFocus(); │ │92 │ │93 var out: std.ArrayList(u8) = .empty; │ │94 errdefer out.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 │ │ 96 for (0..grid.size.height) |y| { │ │ 97 + // wrap runs of cells belonging to a focusable widget in a span tagged │ │ 98 + // with that widget's focus id. CSS paints them with a pointer cursor; │ │ 99 + // JS reads the id on click and dispatches focus directly. │ │100 + var current_id: ?usize = null; │ │101 for (0..grid.size.width) |x| { │ │102 + const cell_id = cellFocusId(root_focus, x, y); │ │103 + if (cell_id != current_id) { │ │104 + if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │105 + if (cell_id) |id| { │ │106 + var buf: [64]u8 = undefined; │ │107 + const tag = try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{}\">", .{id});│ │108 + try out.appendSlice(allocator, tag); │ │109 + } │ │110 + current_id = cell_id; │ │111 + } │ │112 const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; │ │113 try appendEscapedHtml(allocator, &out, rune); │ │114 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │115 + if (current_id != null) try out.appendSlice(allocator, "</span>");│ │116 try out.append(allocator, '\n'); │ │117 } │ │118 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯