╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1774 - if (sc.y == before) _ = self.moveCommentVertical(index, root_focus, true);│ │1779 + if (sc.y == before) _ = self.moveDetailVertical(index, root_focus, true); │ │1780 return; │ │1781 }, │ │1782 else => return, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1788 switch (key) { │ │1789 .arrow_left => if (!self.moveCommentHorizontal(index, root_focus, false)) return self.focusList(index, root_focus),│ │1790 .arrow_right => _ = self.moveCommentHorizontal(index, root_focus, true), │ │1786 - .arrow_up => _ = self.moveCommentVertical(index, root_focus, false), │ │1787 - .arrow_down => _ = self.moveCommentVertical(index, root_focus, true), │ │1791 + .arrow_up => _ = self.moveDetailVertical(index, root_focus, false), │ │1792 + .arrow_down => _ = self.moveDetailVertical(index, root_focus, true), │ │1793 else => return, │ │1794 } │ │1795 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │1796 │ │1797 // page the detail scroll, then focus the first or last visible widget. │ │1798 fn pageDetail(self: *View, index: usize, root_focus: *Focus, delta: isize) void {│ │1799 + if (!self.session.is_terminal) return; │ │1800 const sc = self.detailScroll(index); │ │1801 sc.y += delta; │ │1802 sc.clampToContent(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │1803 self.focusVisible(index, root_focus, delta > 0); │ │1804 } │ │1805 │ │1800 - // jump to the detail's top or bottom and focus that edge's visible widget. │ │1806 + // jump to the detail's first or last focusable widget. the terminal pins │ │1807 + // its synthetic scroll too; the browser scrolls to the new focus itself. │ │1808 fn jumpDetail(self: *View, index: usize, root_focus: *Focus, to_end: bool) void { │ │1802 - const sc = self.detailScroll(index); │ │1803 - sc.y = if (to_end) std.math.maxInt(isize) else 0; │ │1804 - sc.clampToContent(); │ │1805 - self.focusVisible(index, root_focus, to_end); │ │1809 + if (self.session.is_terminal) { │ │1810 + const sc = self.detailScroll(index); │ │1811 + sc.y = if (to_end) std.math.maxInt(isize) else 0; │ │1812 + sc.clampToContent(); │ │1813 + self.focusVisible(index, root_focus, to_end); │ │1814 + } else { │ │1815 + self.focusDetailEdge(index, root_focus, to_end); │ │1816 + } │ │1817 + } │ │1818 + │ │1819 + fn focusDetailEdge(self: *View, index: usize, root_focus: *Focus, last: bool) void {│ │1820 + const focus = self.detailInner(index).getFocus(); │ │1821 + var chosen: ?usize = null; │ │1822 + for (focus.children.keys(), focus.children.values()) |id, child| { │ │1823 + if (!child.focus.focusable) continue; │ │1824 + chosen = id; │ │1825 + if (!last) break; │ │1826 + } │ │1827 + if (chosen) |id| root_focus.setFocus(id); │ │1828 } │ │1829 │ │1830 fn detailRectVisible(self: *View, index: usize, rect: layout.IRect) bool { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │1874 return true; │ │1875 } │ │1876 return false; │ │1877 + } │ │1878 + │ │1879 + fn focusAdjacentVisible(self: *View, index: usize, root_focus: *Focus, down: bool) bool {│ │1880 + const inner = self.detailInner(index); │ │1881 + var next = self.focusedDetailChild(index, root_focus) orelse return false; │ │1882 + while (true) { │ │1883 + if (down) { │ │1884 + next += 1; │ │1885 + if (next >= inner.children.count()) return false; │ │1886 + } else { │ │1887 + if (next == 0) return false; │ │1888 + next -= 1; │ │1889 + } │ │1890 + if (self.focusVisibleChild(index, next, root_focus, !down)) return true; │ │1891 + } │ │1892 } │ │1893 │ │1894 fn focusedDetailChild(self: *View, index: usize, root_focus: *Focus) ?usize { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1903 │ │1904 fn detailChildFocusable(self: *View, index: usize, child_index: usize) bool { │ │1905 const child = &self.detailInner(index).children.values()[child_index].widget; │ │1869 - return switch (child.*) { │ │1870 - .repo_comment => true, │ │1871 - .box => |*box| for (box.children.values()) |*item| { │ │1872 - if (item.widget.getFocus().focusable) break true; │ │1873 - } else false, │ │1874 - else => child.getFocus().focusable, │ │1875 - }; │ │1906 + const focus = child.getFocus(); │ │1907 + if (focus.focusable) return true; │ │1908 + for (focus.children.values()) |item| { │ │1909 + if (item.focus.focusable) return true; │ │1910 + } │ │1911 + return false; │ │1912 } │ │1913 │ │1914 fn focusDetailChild(self: *View, index: usize, child_index: usize, last: bool, root_focus: *Focus) void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1931 }, │ │1932 else => root_focus.setFocus(target), │ │1933 } │ │1898 - self.detailScroll(index).scrollToRect(rect); │ │1934 + const sc = self.detailScroll(index); │ │1935 + if (sc.grid) |viewport| { │ │1936 + const viewport_height = viewport.size.height - sc.bar_h; │ │1937 + if (rect.size.height > viewport_height) { │ │1938 + const bottom_aligned = rect.y + @as(isize, @intCast(rect.size.height - viewport_height));│ │1939 + sc.y = std.math.clamp(sc.y, rect.y, bottom_aligned); │ │1940 + sc.clampToContent(); │ │1941 + return; │ │1942 + } │ │1943 + } │ │1944 + sc.scrollToRect(rect); │ │1945 } │ │1946 │ │1901 - fn moveCommentVertical(self: *View, index: usize, root_focus: *Focus, down: bool) bool { │ │1947 + fn moveDetailVertical(self: *View, index: usize, root_focus: *Focus, down: bool) bool { │ │1948 const inner = self.detailInner(index); │ │1949 const current = self.focusedDetailChild(index, root_focus) orelse return false; │ │1950 if (inner.children.values()[current].widget == .repo_comment) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2397 switch (key) { │ │2398 .arrow_left => if (cur > 0) self.focusTag(index, tf, root_focus, cur - 1) else try self.focusList(index, root_focus), │ │2399 .arrow_right => if (cur + 1 < count) self.focusTag(index, tf, root_focus, cur + 1), │ │2354 - .arrow_up => if (tf.rowStep(cur, false)) |i| self.focusTag(index, tf, root_focus, i) else self.focusAuthor(index, root_focus), │ │2355 - .arrow_down => if (tf.rowStep(cur, true)) |i| self.focusTag(index, tf, root_focus, i) else try self.focusDescription(index, root_focus),│ │2400 + .arrow_up => if (tf.rowStep(cur, false)) |i| { │ │2401 + self.focusTag(index, tf, root_focus, i); │ │2402 + } else { │ │2403 + _ = self.moveDetailVertical(index, root_focus, false); │ │2404 + }, │ │2405 + .arrow_down => if (tf.rowStep(cur, true)) |i| { │ │2406 + self.focusTag(index, tf, root_focus, i); │ │2407 + } else { │ │2408 + _ = self.moveDetailVertical(index, root_focus, true); │ │2409 + }, │ │2410 .home => self.focusTag(index, tf, root_focus, 0), │ │2411 .end => self.focusTag(index, tf, root_focus, count - 1), │ │2412 else => {}, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │2474 return inner.children.getIndex(cid) == title_child_index; │ │2475 } │ │2476 │ │2423 - fn authorPresent(self: *View, index: usize) bool { │ │2424 - return self.author_id[index] != null; │ │2425 - } │ │2426 - │ │2477 fn authorFocused(self: *View, index: usize) bool { │ │2478 const inner = self.detailInner(index); │ │2479 const cid = inner.getFocus().child_id orelse return false;│ ╰────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │2483 fn descriptionFocused(self: *View, index: usize) bool { │ │2484 const cid = self.detailInner(index).getFocus().child_id orelse return false;│ │2485 return cid == self.description_id[index]; │ │2436 - } │ │2437 - │ │2438 - // the author sits right under the title at the top of the pane, so │ │2439 - // focusing it scrolls there. │ │2440 - fn focusAuthor(self: *View, index: usize, root_focus: *Focus) void { │ │2441 - const id = self.author_id[index] orelse return; │ │2442 - root_focus.setFocus(id); │ │2443 - self.detailScroll(index).y = 0; │ │2444 - } │ │2445 - │ │2446 - // the title sits at the top of the pane, so focusing it scrolls there. │ │2447 - fn focusTitle(self: *View, index: usize, root_focus: *Focus) void { │ │2448 - const inner = self.detailInner(index); │ │2449 - if (inner.children.count() == 0) return; │ │2450 - root_focus.setFocus(inner.children.keys()[title_child_index]); │ │2451 - self.detailScroll(index).y = 0; │ │2486 } │ │2487 │ │2454 - fn focusTags(self: *View, index: usize, root_focus: *Focus) !void { │ │2455 - const tf = self.tagFlow(index) orelse return; │ │2456 - if (tf.text_boxes.items.len == 0) return; │ │2457 - const cid = tf.focus.child_id orelse tf.text_boxes.items[0].getFocus().id; │ │2458 - const item = tf.indexOfFocusId(cid) orelse 0; │ │2459 - self.focusTag(index, tf, root_focus, item); │ │2460 - } │ │2461 - │ │2462 - fn focusDescription(self: *View, index: usize, root_focus: *Focus) !void { │ │2463 - if (self.description_id[index]) |id| root_focus.setFocus(id); │ │2464 - } │ │2465 - │ │2488 // return to the tool row's last-focused button (the header when the │ │2489 // description page shows no row). │ │2490 fn focusToolRow(self: *View, index: usize, root_focus: *Focus) void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯