╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │333 try template_repo.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" });│ │334 _ = try template_repo.commit(io, allocator, .{ .message = "let there be light" });│ │335 │ │336 - // tag every commit in creation order as v1, v2, and so on │ │336 + // tag every commit in creation order, three versions at a time │ │337 var tag_num: usize = 1; │ │338 - try addNextTag(&template_repo, io, allocator, &tag_num); │ │338 + try addNextTags(&template_repo, io, allocator, &tag_num); │ │339 │ │340 // a batch of commits so the commits tab has more than one page to │ │341 // paginate through. each rewrites a few files with scattered line │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │387 } │ │388 const message = try arena.allocator().dupe(u8, msg_writer.written()); │ │389 _ = try template_repo.commit(io, allocator, .{ .message = message, .timestamp = base_ts + c * std.time.s_per_day });│ │390 - try addNextTag(&template_repo, io, allocator, &tag_num); │ │390 + try addNextTags(&template_repo, io, allocator, &tag_num); │ │391 } │ │392 │ │393 // two more branches forked off master, each adding a single commit │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │418 } │ │419 try template_repo.add(io, allocator, &.{b.file}); │ │420 _ = try template_repo.commit(io, allocator, .{ .message = b.message, .timestamp = base_ts + b.rev * std.time.s_per_day }); │ │421 - try addNextTag(&template_repo, io, allocator, &tag_num); │ │421 + try addNextTags(&template_repo, io, allocator, &tag_num); │ │422 │ │423 { │ │424 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } });│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 997 return writer.toOwnedSlice(); │ │ 998 } │ │ 999 │ │1000 -// tag the current HEAD as the next sequential version: v1, v2, and so on │ │1001 -fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { │ │1002 - var buf: [16]u8 = undefined; │ │1003 - const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); │ │1004 - _ = try repo.addTag(io, allocator, .{ .name = name }); │ │1005 - n.* += 1; │ │1000 +// tag the current HEAD with the next three sequential versions, so the refs │ │1001 +// tab has enough tags to page through │ │1002 +fn addNextTags(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void {│ │1003 + for (0..3) |_| { │ │1004 + var buf: [16]u8 = undefined; │ │1005 + const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); │ │1006 + _ = try repo.addTag(io, allocator, .{ .name = name }); │ │1007 + n.* += 1; │ │1008 + } │ │1009 } │ │1010 │ │1011 fn commitTree( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Refs.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │12 const inp = @import("../input.zig"); │ │13 │ │14 // how many refs one window of a column shows. │ │15 -pub const page_size = 20; │ │15 +pub const page_size = 50; │ │16 │ │17 // "owner/name", needed to build the columns' window-navigation links.│ │18 identity: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │123 } │ │124 │ │125 pub const View = struct { │ │126 - // a horizontal box of two vertical columns. each column is a fixed, │ │127 - // centered, non-focusable label above a Scroll of focusable rows, so the │ │128 - // label stays visible while the rows scroll. focus tracks a single selected │ │129 - // row across both columns: the box's focus path points at the active column,│ │130 - // that column's at its scroll, and the scroll's (its rows') at the row. │ │126 + // a horizontal box of two columns, each a fixed label above a Scroll of a │ │127 + // TagFlow, so the names wrap across the column and scroll beneath the │ │128 + // label. focus points at the active column and, in its flow, the selected │ │129 + // name. │ │130 box: wgt.Box(ui.Widget), │ │132 - data: *const Self, │ │131 + session: *ui.Session, │ │132 │ │133 const left_col = 0; │ │134 const right_col = 1; │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │140 try addColumn(allocator, &box, session, data.identity, .branch, &data.branches);│ │141 try addColumn(allocator, &box, session, data.identity, .tag, &data.tags); │ │142 │ │144 - var self = View{ .box = box, .data = data }; │ │145 - // select the first row of the first column that has one. │ │146 - for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │143 + // select the first name of the first column that has one │ │144 + for (box.children.keys(), box.children.values()) |id, *child| { │ │145 if (child.widget.box.getFocus().child_id != null) { │ │148 - self.box.getFocus().child_id = id; │ │146 + box.getFocus().child_id = id; │ │147 break; │ │148 } │ │149 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │152 - return self; │ │150 + return .{ .box = box, .session = session };│ │151 } │ │152 │ │153 fn addColumn( │ ╰────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │162 errdefer column.deinit(allocator); │ │163 │ │164 // a fixed, non-focusable header in the SubTitle font, nudged off the │ │167 - // left edge by a one-column space. it stays visible while the rows │ │168 - // scroll below, and declares its height (2 rows) as a min so the (fill) │ │169 - // scroll reserves room for it rather than consuming the whole column. │ │165 + // left edge by a one-column space. it declares its height (2 rows plus │ │166 + // a blank one beneath) as a min so the (fill) scroll reserves room for it. │ │167 { │ │168 var header_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz });│ │169 errdefer header_box.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │177 errdefer header.deinit(allocator); │ │178 try header_box.children.put(allocator, header.getFocus().id, .{ .widget = .{ .sub_title = header }, .rect = null, .min_size = null }); │ │179 } │ │183 - try column.children.put(allocator, header_box.getFocus().id, .{ .widget = .{ .box = header_box }, .rect = null, .min_size = .{ .width = null, .height = 2 } });│ │180 + try column.children.put(allocator, header_box.getFocus().id, .{ .widget = .{ .box = header_box }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │181 } │ │182 │ │186 - // a long ref list overflows the page height; the rows scroll on their │ │187 - // own (the label above stays put) so they lay out under a fixed height. │ │183 + // each name links to the files tab at that ref's root. window │ │184 + // navigation brackets them: "← previous" off the first window, "next →" │ │185 + // when more remain, each a full reload. │ │186 { │ │189 - var scroll = blk: { │ │190 - var rows = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │191 - errdefer rows.deinit(allocator); │ │192 - │ │193 - // window-navigation rows bracket the names: "← previous" off the │ │194 - // first window, "next →" when more remain. each is a full reload. │ │195 - if (data.prev) |p| try addRow(allocator, &rows, "← previous", try windowLink(session.page_arena, identity, kind, p)); │ │196 - // each ref name links to the files tab at that ref's root. │ │197 - for (data.names) |name| try addRow(allocator, &rows, name, try refLink(session.page_arena, identity, kind, name)); │ │198 - if (data.next) |n| try addRow(allocator, &rows, "next →", try windowLink(session.page_arena, identity, kind, n)); │ │199 - │ │200 - if (rows.children.count() > 0) rows.getFocus().child_id = rows.children.keys()[0]; │ │187 + var items: std.ArrayList(ui.widget.TagFlow.Item) = .empty; │ │188 + defer items.deinit(allocator); │ │189 + if (data.prev) |p| try items.append(allocator, .{ .text = "← previous", .link = try windowLink(session.page_arena, identity, kind, p) }); │ │190 + for (data.names) |name| try items.append(allocator, .{ .text = name, .link = try refLink(session.page_arena, identity, kind, name) }); │ │191 + if (data.next) |n| try items.append(allocator, .{ .text = "next →", .link = try windowLink(session.page_arena, identity, kind, n) }); │ │192 │ │202 - // fill the column (rows top-aligned, scroll bar pinned to the │ │203 - // edge) rather than shrinking to the widest row. │ │204 - break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .box = rows }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }); │ │193 + var scroll = blk: { │ │194 + var flow = try ui.widget.TagFlow.init(allocator); │ │195 + errdefer flow.deinit(allocator); │ │196 + try flow.setItems(allocator, items.items); │ │197 + break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .tag_flow = flow }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }); │ │198 }; │ │199 errdefer scroll.deinit(allocator); │ │200 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯