╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │177 const cur_id = box.getFocus().child_id orelse return; │ │178 const cur: isize = @intCast(box.children.getIndex(cur_id) orelse return); │ │179 + // home and end jump straight to the ends │ │180 + if (@abs(delta) < keys.len) if (box.children.values()[@intCast(cur)].rect) |rect| if (scrollThroughRow(scroll, rect, delta)) return;│ │181 + const view_h = viewHeight(scroll); │ │182 const last: isize = @intCast(keys.len - 1); │ │183 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); │ │184 if (next == @as(usize, @intCast(cur))) return; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │185 root_focus.setFocus(keys[next]); │ │182 - if (box.children.values()[next].rect) |rect| scroll.scrollToRect(rect); │ │186 + const rect = box.children.values()[next].rect orelse return; │ │187 + // a tall row shows the end it was entered from │ │188 + if (view_h) |h| if (@as(isize, @intCast(rect.size.height)) > h) { │ │189 + scroll.y = if (delta > 0) rect.y else rectBottom(rect) - h; │ │190 + return; │ │191 + }; │ │192 + scroll.scrollToRect(rect); │ │193 +} │ │194 + │ │195 +// step across the boxes of `box`'s selected row, false when it isn't a row or │ │196 +// has no box that way. │ │197 +pub fn moveInSelectedRow(box: *wgt.Box(Widget), root_focus: *Focus, right: bool) bool { │ │198 + const selected = box.getFocus().child_id orelse return false; │ │199 + return switch ((box.children.getPtr(selected) orelse return false).widget) { │ │200 + .box => |*row| moveInRow(row, root_focus, right), │ │201 + else => false, │ │202 + }; │ │203 +} │ │204 + │ │205 +// scroll a step through `rect`, a row in `scroll`'s content, when it extends │ │206 +// past the view in `delta`'s direction. false when it doesn't, and on the web, │ │207 +// which scrolls natively. │ │208 +pub fn scrollThroughRow(scroll: *wgt.Scroll(Widget), rect: layout.IRect, delta: isize) bool {│ │209 + // how far one step scrolls │ │210 + const step_lines = 5; │ │211 + const h = viewHeight(scroll) orelse return false; │ │212 + const hidden = if (delta > 0) rectBottom(rect) - (scroll.y + h) else scroll.y - rect.y; │ │213 + if (hidden <= 0) return false; │ │214 + scroll.y += std.math.clamp(delta * step_lines, -hidden, hidden); │ │215 + return true; │ │216 +} │ │217 + │ │218 +// the terminal view's height, or null on the web │ │219 +fn viewHeight(scroll: *wgt.Scroll(Widget)) ?isize { │ │220 + if (scroll.options.web_native) return null; │ │221 + const grid = scroll.grid orelse return null; │ │222 + return @intCast(grid.size.height - scroll.bar_h); │ │223 +} │ │224 + │ │225 +fn rectBottom(rect: layout.IRect) isize { │ │226 + return rect.y + @as(isize, @intCast(rect.size.height)); │ │227 } │ │228 │ │229 // focus the box beside the selected one in `row`, false when there is none that way. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │1041 } │ │1042 │ │1043 fn descriptionInput(self: *This, key: Key, root_focus: *Focus) void { │ │1044 - const detail_scroll = &self.scroll; │ │1044 switch (key) { │ │1045 .arrow_left => self.exit = .list, │ │1047 - .arrow_up => { │ │1048 - if (!self.session.is_terminal) { │ │1049 - _ = self.moveVertical(root_focus, false); │ │1050 - return; │ │1051 - } │ │1052 - if (self.focusAdjacentVisible(root_focus, false)) return; │ │1053 - const before = detail_scroll.y; │ │1054 - detail_scroll.y -= 1; │ │1055 - detail_scroll.clampToContent(); │ │1056 - if (detail_scroll.y == before) _ = self.moveVertical(root_focus, false);│ │1057 - }, │ │1058 - .arrow_down => { │ │1059 - if (!self.session.is_terminal) { │ │1060 - _ = self.moveVertical(root_focus, true); │ │1061 - return; │ │1062 - } │ │1063 - if (self.focusAdjacentVisible(root_focus, true)) return; │ │1064 - const before = detail_scroll.y; │ │1065 - detail_scroll.y += 1; │ │1066 - detail_scroll.clampToContent(); │ │1067 - if (detail_scroll.y == before) _ = self.moveVertical(root_focus, true); │ │1068 - }, │ │1046 + .arrow_up => _ = self.moveVertical(root_focus, false), │ │1047 + .arrow_down => _ = self.moveVertical(root_focus, true), │ │1048 else => {}, │ │1049 } │ │1050 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │1136 return false; │ │1137 } │ │1138 │ │1160 - fn focusAdjacentVisible(self: *This, root_focus: *Focus, down: bool) bool {│ │1161 - const inner_box = self.inner(); │ │1162 - var next = self.focusedChild(root_focus) orelse return false; │ │1163 - while (true) { │ │1164 - if (down) { │ │1165 - next += 1; │ │1166 - if (next >= inner_box.children.count()) return false; │ │1167 - } else { │ │1168 - if (next == 0) return false; │ │1169 - next -= 1; │ │1170 - } │ │1171 - if (self.focusVisibleChild(next, root_focus, !down)) { │ │1172 - self.focusChild(next, !down, root_focus); │ │1173 - return true; │ │1174 - } │ │1175 - } │ │1176 - } │ │1177 - │ │1139 fn focusedChild(self: *This, root_focus: *Focus) ?usize { │ │1140 const inner_box = self.inner(); │ │1141 var id = root_focus.grandchild_id orelse return null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1194 fn moveVertical(self: *This, root_focus: *Focus, down: bool) bool { │ │1195 const inner_box = self.inner(); │ │1196 const current = self.focusedChild(root_focus) orelse return false; │ │1197 + // a tall description or comment body is scrolled through before focus │ │1198 + // leaves it │ │1199 + if (self.focusedRect(current)) |rect| if (widget.scrollThroughRow(&self.scroll, rect, if (down) 1 else -1)) return true;│ │1200 if (inner_box.children.values()[current].widget == .repo_comment) { │ │1201 const comment = &inner_box.children.values()[current].widget.repo_comment; │ │1202 if (down and !comment.bodyFocused()) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │1224 } │ │1225 } │ │1226 │ │1227 + // the focused part of child `child_index`, in the scroll's content: a │ │1228 + // comment's metadata or body row, else the whole child │ │1229 + fn focusedRect(self: *This, child_index: usize) ?layout.IRect { │ │1230 + const child = &self.inner().children.values()[child_index]; │ │1231 + var rect = child.rect orelse return null; │ │1232 + if (child.widget == .repo_comment) { │ │1233 + const comment = &child.widget.repo_comment; │ │1234 + const row = comment.rowRect(comment.bodyFocused()) orelse return null;│ │1235 + rect.x += row.x; │ │1236 + rect.y += row.y; │ │1237 + rect.size = row.size; │ │1238 + } │ │1239 + return rect; │ │1240 + } │ │1241 + │ │1242 fn moveHorizontal(self: *This, root_focus: *Focus, right: bool) bool { │ │1243 const inner_box = self.inner(); │ │1244 const current = self.focusedChild(root_focus) orelse return false; │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯