╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │915 self.arena.deinit(); │ │916 } │ │917 │ │918 - // `links`, when given, is parallel to `items`: a non-empty entry sets that │ │919 - // item's focus kind to `.{ .custom = link }` (e.g. "a:/user/foo"), which the │ │920 - // web renderer turns into an anchor. pass null when items aren't links. │ │918 + // one flow item: its display text plus an optional link. a non-empty `link` │ │919 + // sets the item's focus kind such as `.{ .custom = "a:/user/foo" }`, which │ │920 + // the web renderer turns into an anchor. │ │921 + pub const Item = struct { text: []const u8, link: []const u8 = "" }; │ │922 + │ │923 // both the item text and the link are copied into the arena, so the caller's │ │924 // slices needn't outlive this call. │ │923 - pub fn setItems(self: *FlowBox, allocator: std.mem.Allocator, items: []const []const u8, links: ?[]const []const u8) !void {│ │925 + pub fn setItems(self: *FlowBox, allocator: std.mem.Allocator, items: []const Item) !void { │ │926 for (self.text_boxes.items) |*tb| tb.deinit(allocator); │ │927 self.text_boxes.clearAndFree(allocator); │ │928 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │934 self.focus.clear(); │ │935 self.focus.child_id = null; │ │936 │ │935 - for (items, 0..) |item, i| { │ │936 - const line = try aa.dupe(u8, item); │ │937 + for (items) |item| { │ │938 + const line = try aa.dupe(u8, item.text); │ │939 │ │940 var text_box = try wgt.TextBox(Widget).init(allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .word });│ │941 errdefer text_box.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │942 text_box.getFocus().focusable = true; │ │943 │ │942 - if (links) |link_items| { │ │943 - if (i < link_items.len and link_items[i].len > 0) { │ │944 - text_box.getFocus().kind = .{ .custom = try aa.dupe(u8, link_items[i]) };│ │945 - } │ │944 + if (item.link.len > 0) { │ │945 + text_box.getFocus().kind = .{ .custom = try aa.dupe(u8, item.link) }; │ │946 } │ │947 │ │948 try self.text_boxes.append(allocator, text_box); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1054 self.scroll.deinit(allocator); │ │1055 } │ │1056 │ │1057 - pub fn setItems(self: *Scroll, allocator: std.mem.Allocator, items: []const []const u8, links: ?[]const []const u8) !void {│ │1057 + pub fn setItems(self: *Scroll, allocator: std.mem.Allocator, items: []const Item) !void { │ │1058 self.scroll.x = 0; │ │1059 self.scroll.y = 0; │ │1060 - try self.inner().setItems(allocator, items, links); │ │1060 + try self.inner().setItems(allocator, items); │ │1061 } │ │1062 │ │1063 pub fn build(self: *Scroll, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Home/Repos.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 89 // a leading "previous" row off the first window, one row per repo, then │ │ 90 // a trailing "next" row when more remain. each window row navigates to the │ │ 91 // adjacent window (full reload on web, Nav rebuild on the TUI). │ │ 92 - const lead: usize = if (data.after > 0) 1 else 0; │ │ 93 - const trail: usize = if (data.next_after != null) 1 else 0; │ │ 94 - const lines = try aa.alloc([]const u8, lead + data.repos.len + trail); │ │ 95 - const links = try aa.alloc([]const u8, lead + data.repos.len + trail); │ │ 96 - if (data.after > 0) { │ │ 97 - lines[0] = "← previous"; │ │ 98 - links[0] = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{data.after -| page_size}); │ │ 99 - } │ │ 92 + var items: std.ArrayList(ui.FlowBox.Item) = .empty; │ │ 93 + if (data.after > 0) │ │ 94 + try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{data.after -| page_size}) });│ │ 95 for (data.repos, 0..) |repo, i| { │ │101 - lines[lead + i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }); │ │ 96 // clicking a repo opens its page; the "a:" prefix makes the web │ │ 97 // renderer emit an <a href="/repo/alice/foo"> anchor. skip the link │ │ 98 // when the owner is unknown so it isn't a dead route. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 const owner = data.owner_names[i]; │ │106 - links[lead + i] = if (owner.len > 0) try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ owner, repo.name }) else ""; │ │107 - } │ │108 - if (data.next_after) |next_after| { │ │109 - lines[lead + data.repos.len] = "next →"; │ │110 - links[lead + data.repos.len] = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{next_after}); │ │100 + try items.append(aa, .{ │ │101 + .text = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }), │ │102 + .link = if (owner.len > 0) try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ owner, repo.name }) else "", │ │103 + }); │ │104 } │ │112 - try self.list.setItems(allocator, lines, links); │ │105 + if (data.next_after) |next_after| │ │106 + try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{next_after}) });│ │107 + try self.list.setItems(allocator, items.items); │ │108 │ │109 return self; │ │110 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Home/Users.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 83 // a leading "previous" row off the first window, one row per user, then │ │ 84 // a trailing "next" row when more remain. each window row navigates to the │ │ 85 // adjacent window (full reload on web, Nav rebuild on the TUI). │ │ 86 - const lead: usize = if (data.after > 0) 1 else 0; │ │ 87 - const trail: usize = if (data.next_after != null) 1 else 0; │ │ 88 - const lines = try aa.alloc([]const u8, lead + data.users.len + trail); │ │ 89 - const links = try aa.alloc([]const u8, lead + data.users.len + trail); │ │ 90 - if (data.after > 0) { │ │ 91 - lines[0] = "← previous"; │ │ 92 - links[0] = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{data.after -| page_size}); │ │ 93 - } │ │ 94 - for (data.users, 0..) |user, i| { │ │ 95 - lines[lead + i] = try std.fmt.allocPrint(aa, "{s} ({s})", .{ user.name, user.display_name }); │ │ 86 + var items: std.ArrayList(ui.FlowBox.Item) = .empty; │ │ 87 + if (data.after > 0) │ │ 88 + try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{data.after -| page_size}) });│ │ 89 + for (data.users) |user| │ │ 90 // clicking a user opens their page; the "a:" prefix makes the web │ │ 91 // renderer emit an <a href="/user/foo"> anchor. │ │ 98 - links[lead + i] = try std.fmt.allocPrint(aa, "a:/user/{s}", .{user.name}); │ │ 99 - } │ │100 - if (data.next_after) |next_after| { │ │101 - lines[lead + data.users.len] = "next →"; │ │102 - links[lead + data.users.len] = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{next_after}); │ │103 - } │ │104 - try self.list.setItems(allocator, lines, links); │ │ 92 + try items.append(aa, .{ │ │ 93 + .text = try std.fmt.allocPrint(aa, "{s} ({s})", .{ user.name, user.display_name }), │ │ 94 + .link = try std.fmt.allocPrint(aa, "a:/user/{s}", .{user.name}), │ │ 95 + }); │ │ 96 + if (data.next_after) |next_after| │ │ 97 + try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{next_after}) }); │ │ 98 + try self.list.setItems(allocator, items.items); │ │ 99 │ │100 return self; │ │101 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 defer arena.deinit(); │ │115 const aa = arena.allocator(); │ │116 │ │117 - const lines = try aa.alloc([]const u8, data.repos.len); │ │118 - const links = try aa.alloc([]const u8, data.repos.len); │ │119 - for (data.repos, 0..) |repo, i| { │ │120 - lines[i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }); │ │117 + var items: std.ArrayList(ui.FlowBox.Item) = .empty; │ │118 + for (data.repos) |repo| │ │119 // clicking a repo opens its page; the "a:" prefix makes the web │ │120 // renderer emit an <a href="/repo/alice/foo"> anchor. │ │123 - links[i] = try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ data.user.name, repo.name }); │ │124 - } │ │125 - try list.setItems(allocator, lines, links); │ │121 + try items.append(aa, .{ │ │122 + .text = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }), │ │123 + .link = try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ data.user.name, repo.name }),│ │124 + }); │ │125 + try list.setItems(allocator, items.items); │ │126 │ │127 try stack.children.put(allocator, list.getFocus().id, .{ .flow_box_scroll = list }); │ │128 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯