╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │543 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │544 self.clearGrid(); │ │545 │ │416 - // each header tab maps 1:1 to a stack child by position │ │546 + // each header tab maps 1:1 to a stack child by position. mirror the │ │547 + // selection into the url (the list routes keep the tag filter). │ │548 if (self.header().getSelectedIndex()) |index| { │ │549 const stack = self.viewStack(); │ │550 stack.getFocus().child_id = stack.children.keys()[index]; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │551 self.session.data.current_page = (if (index == tags_view_index) │ │421 - ui.RoutablePage.repoIssuesTagsRoute(self.data.identity) │ │552 + ui.RoutablePage.repoIssuesTagsRoute(self.data.identity, self.data.tag) │ │553 else │ │423 - ui.RoutablePage.repoIssuesRoute(self.data.identity, self.data.tag, "")) orelse self.session.data.current_page; │ │554 + ui.RoutablePage.repoIssuesRoute(self.data.identity, splitStatus(index), self.data.tag, "")) orelse self.session.data.current_page; │ │555 } │ │556 │ │426 - // swap the detail pane to the selected issue when it changes. │ │427 - try self.refreshDetail(allocator); │ │557 + if (self.selectedSplitIndex()) |i| { │ │558 + // swap the detail pane to the selected issue when it changes. │ │559 + try self.refreshDetail(allocator, i); │ │560 │ │429 - // mirror the focused issue into the url so it updates as the selection │ │430 - // moves, but only while focus is inside this view (the list or detail). │ │431 - // when focus sits on the header tab, use the page's base /issues route. │ │432 - // an issue's url is the same whether or not the list is filtered, so │ │433 - // the mirror drops the tag. │ │434 - if (root_focus.grandchild_id) |g| { │ │435 - if (self.resultsBox().getFocus().children.contains(g)) { │ │436 - if (self.selectedIssueIndex()) |sel| { │ │437 - if (ui.RoutablePage.repoIssuesRoute(self.data.identity, "", self.data.issues[sel].id)) |route| │ │438 - self.session.data.current_page = route; │ │561 + // mirror the focused issue into the url, but only while focus is │ │562 + // inside the split. an issue's url is the same whether or not the │ │563 + // list is filtered, so the mirror drops the tag. │ │564 + if (root_focus.grandchild_id) |g| { │ │565 + if (self.resultsBox(i).getFocus().children.contains(g)) { │ │566 + if (self.selectedIssueIndex(i)) |sel| { │ │567 + if (ui.RoutablePage.repoIssuesRoute(self.data.identity, splitStatus(i), "", self.window(i).issues[sel].id)) |route| │ │568 + self.session.data.current_page = route; │ │569 + } │ │570 } │ │571 } │ │441 - } │ │572 │ │443 - // the selected list row shows a border (the focused TextBox upgrades it │ │444 - // to a double border itself); the rest stay borderless. │ │445 - const lb = self.listBox(); │ │446 - for (lb.children.keys(), lb.children.values()) |id, *child| { │ │447 - switch (child.widget) { │ │448 - .text_box => |*tb| tb.options.border_style = if (lb.getFocus().child_id == id) .single else .hidden, │ │449 - else => {}, │ │573 + // the selected list row shows a border (the focused TextBox │ │574 + // upgrades it to a double border itself); the rest stay borderless. │ │575 + const lb = self.listBox(i); │ │576 + for (lb.children.keys(), lb.children.values()) |id, *child| { │ │577 + switch (child.widget) { │ │578 + .text_box => |*tb| tb.options.border_style = if (lb.getFocus().child_id == id) .single else .hidden, │ │579 + else => {}, │ │580 + } │ │581 } │ │451 - } │ │582 │ │453 - // the pane's selected child shows the selection border: the description │ │454 - // directly, the tags via the flow's selected item. │ │455 - const inner = self.detailInner(); │ │456 - for (inner.children.keys(), inner.children.values()) |id, *child| { │ │457 - switch (child.widget) { │ │458 - .text_box => |*tb| tb.options.border_style = if (inner.getFocus().child_id == id) .single else .hidden, │ │459 - else => {}, │ │583 + // the pane's selected child shows the selection border: the │ │584 + // description directly, the tags via the flow's selected item. │ │585 + const inner = self.detailInner(i); │ │586 + for (inner.children.keys(), inner.children.values()) |id, *child| { │ │587 + switch (child.widget) { │ │588 + .text_box => |*tb| tb.options.border_style = if (inner.getFocus().child_id == id) .single else .hidden, │ │589 + else => {}, │ │590 + } │ │591 } │ │461 - } │ │462 - if (self.tagFlow()) |tf| tf.selected = self.tagsFocused(); │ │463 - self.tagsView().selected = self.tagsViewActive(); │ │592 + if (self.tagFlow(i)) |tf| tf.selected = self.tagsFocused(i); │ │593 │ │465 - // cap the list at list_max_width only while the detail pane fits beside │ │466 - // it. the box drops the detail when the width can't hold both minimums, │ │467 - // so when it's that narrow we lift the cap and let the list fill the │ │468 - // whole width. │ │469 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │470 - self.resultsBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │594 + // cap the list at list_max_width only while the detail pane fits │ │595 + // beside it. the box drops the detail when the width can't hold │ │596 + // both minimums, so when it's that narrow we lift the cap and let │ │597 + // the list fill the whole width. │ │598 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │599 + self.resultsBox(i).children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │600 │ │472 - // stretch the detail pane across the rest of the width so it fills the │ │473 - // area rather than shrinking to its content; its scroll fills the pane. │ │474 - if (constraint.max_size.width) |w| { │ │475 - self.resultsBox().children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null }; │ │476 - } else { │ │477 - self.resultsBox().children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │601 + // stretch the detail pane across the rest of the width so it fills │ │602 + // the area rather than shrinking to its content; its scroll fills │ │603 + // the pane. │ │604 + if (constraint.max_size.width) |w| { │ │605 + self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null };│ │606 + } else { │ │607 + self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │608 + } │ │609 } │ │610 + │ │611 + self.tagsView().selected = self.tagsViewActive(); │ │612 │ │613 try self.box.build(allocator, constraint, root_focus); │ │614 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │615 │ │483 - fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ │484 - const sel = self.selectedIssueIndex() orelse return; │ │485 - if (self.detailed_index) |d| if (d == sel) return; │ │486 - try self.populateDetail(allocator, sel); │ │487 - self.detailed_index = sel; │ │616 + fn refreshDetail(self: *View, allocator: std.mem.Allocator, index: usize) !void { │ │617 + const sel = self.selectedIssueIndex(index) orelse return; │ │618 + if (self.detailed_index[index]) |d| if (d == sel) return; │ │619 + try self.populateDetail(allocator, index, sel); │ │620 + self.detailed_index[index] = sel; │ │621 } │ │622 │ │490 - fn populateDetail(self: *View, allocator: std.mem.Allocator, sel: usize) !void { │ │491 - const entry = self.data.issues[sel]; │ │492 - const inner = self.detailInner(); │ │623 + fn populateDetail(self: *View, allocator: std.mem.Allocator, index: usize, sel: usize) !void {│ │624 + const entry = self.window(index).issues[sel]; │ │625 + const inner = self.detailInner(index); │ │626 │ │627 for (inner.children.values()) |*child| child.widget.deinit(allocator); │ │628 inner.children.clearAndFree(allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │629 inner.getFocus().child_id = null; │ │630 │ │498 - // the issue's tags, each linking to the list filtered to that tag.│ │631 + // the issue's tags, each linking to this status's list filtered to│ │632 + // that tag. │ │633 { │ │634 var items: std.ArrayList(ui.TagFlow.Item) = .empty; │ │635 defer items.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │636 var tag_iter = evt.Issue.tagIterator(entry.issue.tags); │ │637 while (tag_iter.next()) |tag| { │ │638 if (tag.len == 0) continue; │ │505 - try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, tag) }); │ │639 + try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, splitStatus(index), tag) });│ │640 } │ │641 if (items.items.len > 0) { │ │642 var tf = try ui.TagFlow.init(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │649 // the description as a focusable word-wrapped text box. its hidden │ │650 // border reserves the space the border occupies when focused, so │ │651 // focusing doesn't shift layout. │ │518 - self.description_id = blk: { │ │652 + self.description_id[index] = blk: { │ │653 var tb = try wgt.TextBox(ui.Widget).init(allocator, entry.issue.description, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .word });│ │654 errdefer tb.deinit(allocator); │ │655 tb.getFocus().focusable = true; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │658 }; │ │659 │ │660 // select the description by default │ │527 - inner.getFocus().child_id = self.description_id; │ │661 + inner.getFocus().child_id = self.description_id[index]; │ │662 │ │663 // reset the scroll to the top for the newly-shown issue: directly on the│ │664 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │665 // renderer's scroll id changes and JS drops the preserved position).│ │532 - const sc = self.detailScroll(); │ │666 + const sc = self.detailScroll(index); │ │667 sc.x = 0; │ │668 sc.y = 0; │ │669 sc.getFocus().version +%= 1; │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │671 │ │672 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │673 if (self.headerActive()) { │ │540 - // down from the tabs re-enters the split; other keys move the tabs. │ │674 + // down from the tabs re-enters the stack if the selected view has │ │675 + // something to focus; other keys move the tabs. │ │676 if (inp.vertDirection(key) == .down) { │ │542 - root_focus.setFocus(self.box.children.keys()[stack_index]); │ │677 + const enterable = if (self.viewStack().getSelected()) |selected| switch (selected.*) {│ │678 + .tag_flow => |*tf| tf.text_boxes.items.len > 0, │ │679 + else => true, │ │680 + } else false; │ │681 + if (enterable) root_focus.setFocus(self.box.children.keys()[stack_index]); │ │682 } else { │ │683 try self.header().input(allocator, key, root_focus); │ │684 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │686 } │ │687 if (self.tagsViewActive()) { │ │688 self.tagsViewInput(key, root_focus); │ │550 - } else if (self.detailActive()) { │ │551 - try self.detailInput(key, root_focus); │ │689 + return; │ │690 + } │ │691 + const i = self.selectedSplitIndex() orelse return;│ │692 + if (self.detailActive(i)) { │ │693 + try self.detailInput(i, key, root_focus); │ │694 } else { │ │553 - try self.listInput(key, root_focus); │ │695 + try self.listInput(i, key, root_focus); │ │696 } │ │697 } │ │698 │ ╰───────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯