╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │273 - return &self.box.children.values()[list_index].widget.scroll; │ │317 + return &self.resultsBox().children.values()[list_index].widget.scroll;│ │318 } │ │319 │ │320 fn listBox(self: *View) *wgt.Box(ui.Widget) { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │322 } │ │323 │ │324 fn detailOuter(self: *View) *wgt.Box(ui.Widget) { │ │281 - return &self.box.children.values()[detail_index].widget.box; │ │325 + return &self.resultsBox().children.values()[detail_index].widget.box;│ │326 } │ │327 │ │328 fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │334 } │ │335 │ │336 fn detailActive(self: *View) bool { │ │337 + const rb = self.resultsBox(); │ │338 + const cid = rb.getFocus().child_id orelse return false; │ │339 + return rb.children.getIndex(cid) == detail_index; │ │340 + } │ │341 + │ │342 + fn subHeaderActive(self: *View) bool { │ │343 const cid = self.box.getFocus().child_id orelse return false; │ │294 - return self.box.children.getIndex(cid) == detail_index; │ │344 + return self.box.children.getIndex(cid) == sub_header_index; │ │345 } │ │346 │ │347 // the selected issue's index, or null when a window-navigation row is│ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │367 // an issue's url is the same whether or not the list is filtered, so │ │368 // the mirror drops the tag. │ │369 if (root_focus.grandchild_id) |g| { │ │320 - if (self.box.getFocus().children.contains(g)) { │ │370 + if (self.resultsBox().getFocus().children.contains(g)) { │ │371 if (self.selectedIssueIndex()) |sel| { │ │372 if (ui.RoutablePage.repoIssuesRoute(self.data.identity, "", self.data.issues[sel].id)) |route|│ │373 self.session.data.current_page = route; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │401 // so when it's that narrow we lift the cap and let the list fill the │ │402 // whole width. │ │403 const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │354 - self.box.children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │404 + self.resultsBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │405 │ │406 // stretch the detail pane across the rest of the width so it fills the │ │407 // area rather than shrinking to its content; its scroll fills the pane. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │408 if (constraint.max_size.width) |w| { │ │359 - self.box.children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null }; │ │409 + self.resultsBox().children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null };│ │410 } else { │ │361 - self.box.children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │411 + self.resultsBox().children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │412 } │ │413 │ │414 try self.box.build(allocator, constraint, root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │456 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null });│ │457 } │ │458 │ │409 - // point the pane at its first row so focus recovery can land here. │ │410 - inner.getFocus().child_id = inner.children.keys()[0]; │ │459 + // select the description by default │ │460 + inner.getFocus().child_id = inner.children.keys()[inner.children.count() - 1]; │ │461 │ │462 // reset the scroll to the top for the newly-shown issue: directly on the │ │463 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │469 } │ │470 │ │471 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │422 - _ = allocator; │ │472 + if (self.subHeaderActive()) { │ │473 + // down from the tabs re-enters the split; other keys move the tabs. │ │474 + if (inp.vertDirection(key) == .down) { │ │475 + root_focus.setFocus(self.box.children.keys()[stack_index]); │ │476 + } else { │ │477 + try self.subHeader().input(allocator, key, root_focus); │ │478 + } │ │479 + return; │ │480 + } │ │481 if (self.detailActive()) { │ │482 try self.detailInput(key, root_focus); │ │483 } else { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │487 │ │488 fn listInput(self: *View, key: Key, root_focus: *Focus) !void { │ │489 // up/down (and the scroll wheel) move the selection a row; page up/down │ │432 - // jump a fixed amount. right/Enter cross into the detail pane. │ │490 + // jump a fixed amount. right/Enter cross into the detail pane. up from │ │491 + // the top row crosses into the sub header tabs. │ │492 if (inp.rowDelta(key, @intCast(self.listBox().children.count()))) |delta| { │ │434 - ui.moveRowFocus(self.listBox(), self.listScroll(), root_focus, delta); │ │493 + const lb = self.listBox(); │ │494 + const at_top = if (lb.getFocus().child_id) |cid| lb.children.getIndex(cid) == 0 else true;│ │495 + if (delta < 0 and at_top) return self.focusSubHeader(root_focus); │ │496 + ui.moveRowFocus(lb, self.listScroll(), root_focus, delta); │ │497 return; │ │498 } │ │499 switch (key) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │514 const sc = self.detailScroll(); │ │515 switch (key) { │ │516 .arrow_left => return self.focusList(root_focus), │ │455 - // once the scroll can't move further, cross into the tags. │ │517 + // once the scroll can't move further, cross into the tags (or the│ │518 + // sub header tabs when the issue has none). │ │519 .arrow_up => { │ │520 const before = sc.y; │ │521 sc.y -= 1; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯