╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │169 } │ │170 │ │171 // move the focused row in `box` by `delta`, clamped to the ends, scrolling │ │172 -// `scroll` to keep it visible. │ │172 +// `scroll` to keep it visible. in the terminal, a row taller than the view is │ │173 +// scrolled through before focus leaves it. │ │174 pub fn moveRowFocus(box: *wgt.Box(Widget), scroll: *wgt.Scroll(Widget), root_focus: *Focus, delta: isize) void {│ │175 const keys = box.children.keys(); │ │176 if (keys.len == 0) return; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯