╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │16 │ │17 // how many issues one window shows before a "next" link appears. │ │18 pub const page_size = 20; │ │19 + │ │20 +// how many tags the tags view shows at most. │ │21 +pub const max_tags = 1000; │ │22 │ │23 // one issue from the repo's consumed event database, with its hex event id│ │24 // (the id lives in the event envelope, not the payload). │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │42 next_id: ?[]const u8, │ │43 // how many issues the listing holds across all windows. │ │44 count: usize, │ │45 +// every tag in the repo, in sorted order, for the tags view.│ │46 +tags: []const []const u8, │ │47 │ │48 const Self = @This(); │ │49 │ ╰─────────────────────────────────────────────────────────────────╯ ╭────────────────────────────╮ │57 .prev_id = null,│ │58 .next_id = null,│ │59 .count = 0, │ │60 + .tags = &.{}, │ │61 }; │ │62 } │ │63 │ ╰────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │180 .id = try aa.dupe(u8, &id_hex), │ │181 .issue = try evt.read(evt.Issue, DB, repo_opts.hash, arena, issue_map), │ │182 }); │ │183 + } │ │184 + │ │185 + // every tag in the repo, in the tag map's sorted order. │ │186 + var tags: std.ArrayList([]const u8) = .empty; │ │187 + if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag->issue-id-set"))) |tag_to_issues_cursor| {│ │188 + const tag_to_issues = try DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ │189 + var tag_iter = try tag_to_issues.iterator(); │ │190 + while (try tag_iter.next()) |kv_pair_cursor| { │ │191 + if (tags.items.len == max_tags) break; │ │192 + var kv_cursor = kv_pair_cursor; │ │193 + const kv_pair = try kv_cursor.readKeyValuePair(); │ │194 + try tags.append(aa, try kv_pair.key_cursor.readBytesAlloc(aa, null)); │ │195 + } │ │196 } │ │197 │ │198 return .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │203 .prev_id = prev_id, │ │204 .next_id = next_id, │ │205 .count = @intCast(try issue_id_set.count()),│ │206 + .tags = tags.items, │ │207 }; │ │208 } │ │209 │ ╰─────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │219 │ │220 const sub_header_index: usize = 0; │ │221 const stack_index: usize = 1; │ │222 + // indices within the stack, 1:1 with the sub header tabs. │ │223 + const results_view_index: usize = 0; │ │224 + const tags_view_index: usize = 1; │ │225 // indices within the results box (the horizontal split inside the stack).│ │226 const list_index: usize = 0; │ │227 const detail_index: usize = 1; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │234 │ │235 // the tabs at the top. │ │236 { │ │214 - var sub_header = try SubHeader.View.init(allocator, session, data.count); │ │237 + var sub_header = try SubHeader.View.init(allocator, session, data.count, data.tag); │ │238 errdefer sub_header.deinit(allocator); │ │239 try outer.children.put(allocator, sub_header.getFocus().id, .{ .widget = .{ .repo_issues_sub_header = sub_header }, .rect = null, .min_size = null });│ │240 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │303 const stack = &outer.children.values()[stack_index].widget.stack; │ │304 stack.getFocus().child_id = box.getFocus().id; │ │305 try stack.children.put(allocator, box.getFocus().id, .{ .box = box }); │ │306 + │ │307 + // the tags view │ │308 + { │ │309 + var tf = try ui.TagFlow.init(allocator); │ │310 + errdefer tf.deinit(allocator); │ │311 + var items: std.ArrayList(ui.TagFlow.Item) = .empty; │ │312 + defer items.deinit(allocator); │ │313 + // when filtered, the first item clears the filter │ │314 + if (data.tag.len != 0) │ │315 + try items.append(allocator, .{ .text = "╳", .link = try issuesLink(session.page_arena, data.identity, "", "") });│ │316 + for (data.tags) |tag| │ │317 + try items.append(allocator, .{ .text = tag, .link = try tagLink(session.page_arena, data.identity, tag) }); │ │318 + try tf.setItems(allocator, items.items); │ │319 + try stack.children.put(allocator, tf.getFocus().id, .{ .tag_flow = tf }); │ │320 + } │ │321 │ │322 // focus entering the view lands on the tabs first. │ │323 outer.getFocus().child_id = outer.children.keys()[sub_header_index]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │346 return &self.box.children.values()[sub_header_index].widget.repo_issues_sub_header; │ │347 } │ │348 │ │349 + fn viewStack(self: *View) *wgt.Stack(ui.Widget) { │ │350 + return &self.box.children.values()[stack_index].widget.stack; │ │351 + } │ │352 + │ │353 // the master-detail split inside the stack. │ │354 fn resultsBox(self: *View) *wgt.Box(ui.Widget) { │ │313 - return &self.box.children.values()[stack_index].widget.stack.children.values()[0].box;│ │355 + return &self.viewStack().children.values()[results_view_index].box; │ │356 + } │ │357 + │ │358 + // the tags view's flow inside the stack. │ │359 + fn tagsView(self: *View) *ui.TagFlow { │ │360 + return &self.viewStack().children.values()[tags_view_index].tag_flow; │ │361 } │ │362 │ │363 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │389 fn subHeaderActive(self: *View) bool { │ │390 const cid = self.box.getFocus().child_id orelse return false; │ │391 return self.box.children.getIndex(cid) == sub_header_index; │ │392 + } │ │393 + │ │394 + fn tagsViewActive(self: *View) bool { │ │395 + if (self.subHeaderActive()) return false; │ │396 + const stack = self.viewStack(); │ │397 + const cid = stack.getFocus().child_id orelse return false; │ │398 + return stack.children.getIndex(cid) == tags_view_index; │ │399 } │ │400 │ │401 // the selected issue's index, or null when a window-navigation row is│ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯