╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │119 return try out.toOwnedSlice(allocator); │ │120 } │ │121 │ │122 +fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { │ │123 + var iter = focus.children.iterator(); │ │124 + while (iter.next()) |entry| { │ │125 + const child = entry.value_ptr.*; │ │126 + if (!child.focus.focusable) continue; │ │127 + const r = child.rect; │ │128 + if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { │ │129 + return entry.key_ptr.*; │ │130 + } │ │131 + } │ │132 + return null; │ │133 +} │ │134 + │ │135 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void {│ │136 for (input) |ch| { │ │137 switch (ch) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │418 │ │419 pub fn build(self: *UserRepoView, constraint: layout.Constraint, root_focus: *Focus) !void {│ │420 self.clearGrid(); │ │421 + // refresh the repo list here (rather than in input) so it stays in │ │422 + // sync regardless of how the user selection changed — including │ │423 + // clicks that go straight through setFocus without touching input. │ │424 + // updateRepos is a no-op when the selected user hasn't changed. │ │425 + try self.updateRepos(); │ │426 try self.box.build(constraint, root_focus); │ │427 } │ │428 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────╮ │461 else => {}, │ │462 } │ │463 try child.input(key, root_focus); │ │430 - if (current_index == user_list_index) {│ │431 - try self.updateRepos(); │ │432 - } │ │464 break :blk current_index; │ │465 }; │ │466 │ ╰────────────────────────────────────────────────────────────────╯