╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │366 errdefer diff_scroll.deinit(allocator); │ │367 var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .direction = .vert }); │ │368 errdefer frame.deinit(allocator); │ │369 + // the frame's selected child is its scroll, so the focus chain │ │370 + // reaches a hunk (populateDiff points the scroll's inner box at │ │371 + // one), letting focus recovery descend into the diff pane after │ │372 + // it's laid out beside a too-narrow list. │ │373 + frame.getFocus().child_id = diff_scroll.getFocus().id; │ │374 try frame.children.put(allocator, diff_scroll.getFocus().id, .{ .widget = .{ .scroll = diff_scroll }, .rect = null, .min_size = null });│ │375 break :blk frame; │ │376 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │539 // the web bounds the layout to the browser viewport like the terminal; │ │540 // each Scroll's web-native mode hands its full content to a real │ │541 // scrollable element, so we no longer build unbounded here. │ │542 + // │ │543 + // crossing panes only re-selects the content box's child (focusDiff / │ │544 + // focusList); when the window is too narrow to show both, the pane that │ │545 + // held focus is dropped here. the framework recovers focus after the │ │546 + // top-level build by re-deriving it down the selected-child chain, so │ │547 + // there's nothing to fix up afterward. │ │548 try self.box.build(allocator, constraint, root_focus); │ │538 - │ │539 - // if the diff pane is selected but focus is still elsewhere in this view, │ │540 - // the pane was too narrow to lay out when focus crossed over. it's laid │ │541 - // out now, so focus its hunk. │ │542 - if (self.diffActive() and inner.children.count() > 0) { │ │543 - if (root_focus.grandchild_id) |g| { │ │544 - const in_view = self.box.getFocus().children.contains(g); │ │545 - const in_diff = inner.children.contains(g); │ │546 - if (in_view and !in_diff) │ │547 - try root_focus.setFocus(inner.getFocus().child_id orelse inner.children.keys()[0]);│ │548 - } │ │549 - } │ │549 } │ │550 │ │551 fn refreshDiff(self: *View, allocator: std.mem.Allocator) !void { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │574 try self.addHunkBox(allocator, inner, hunk.lines); │ │575 } │ │576 if (commit.has_more) try self.addNavLink(allocator, inner, "next →", commit.oid, commit.window_start + diff_page);│ │577 + │ │578 + // point the pane at its first row so focus recovery can land here. │ │579 + if (inner.children.count() > 0) inner.getFocus().child_id = inner.children.keys()[0]; │ │580 │ │581 // reset the scroll to the top for the newly-shown commit: directly on the │ │582 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │665 │ │666 if (self.session.is_terminal) { │ │667 if (in_range and self.hunkVisible(@intCast(target))) {│ │666 - try root_focus.setFocus(keys[@intCast(target)]); │ │668 + root_focus.setFocus(keys[@intCast(target)]); │ │669 return; │ │670 } │ │671 │ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │673 sc.y += delta * 5; // magnify because it's a line count│ │674 self.clampDiffScroll(); │ │675 if (in_range and self.hunkVisible(@intCast(target))) { │ │674 - try root_focus.setFocus(keys[@intCast(target)]); │ │676 + root_focus.setFocus(keys[@intCast(target)]); │ │677 } │ │678 } else { │ │679 if (in_range) { │ ╰────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │678 - try root_focus.setFocus(keys[@intCast(target)]);│ │680 + root_focus.setFocus(keys[@intCast(target)]); │ │681 } │ │682 } │ │683 } │ ╰─────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │697 if (keys.len == 0) return; │ │698 const cur: isize = if (root_focus.grandchild_id) |g| @intCast(inner.children.getIndex(g) orelse 0) else 0;│ │699 const target: usize = @intCast(std.math.clamp(cur + delta, 0, @as(isize, @intCast(keys.len - 1)))); │ │698 - try root_focus.setFocus(keys[target]); │ │700 + root_focus.setFocus(keys[target]); │ │701 } │ │702 } │ │703 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │712 sc.y = if (to_end) std.math.maxInt(isize) else 0; │ │713 self.clampDiffScroll(); │ │714 } │ │713 - try root_focus.setFocus(if (to_end) keys[keys.len - 1] else keys[0]); │ │715 + root_focus.setFocus(if (to_end) keys[keys.len - 1] else keys[0]); │ │716 } │ │717 │ │718 // whether hunk `index` is at least partly within the diff viewport, per the│ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │744 chosen = id; │ │745 if (!prefer_last) break; // first visible │ │746 } │ │745 - if (chosen) |id| try root_focus.setFocus(id); │ │747 + if (chosen) |id| root_focus.setFocus(id); │ │748 } │ │749 │ │750 // keep the diff scroll within its content, using the last build's grids. the│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │762 sc.x = std.math.clamp(sc.x, 0, max_x); │ │763 } │ │764 │ │763 - // enter the diff pane by focusing the hunk that was focused last (the box's │ │764 - // child_id remembers it across the trip to the commit list); fall back to the │ │765 - // first hunk on a fresh diff. the host arrives here on right-arrow or Enter │ │766 - // from the list. a diff with no hunks can't be entered. │ │765 + // enter the diff pane. the host arrives here on right-arrow or Enter from the │ │766 + // list. a diff with no rows can't be entered. setFocus handles the too-narrow │ │767 + // case where the pane isn't laid out yet (it gets selected, then focused after │ │768 + // the next build, landing on its remembered hunk). │ │769 fn focusDiff(self: *View, root_focus: *Focus) !void { │ │768 - const inner = self.diffInner(); │ │769 - if (inner.children.count() == 0) return; │ │770 - const target = inner.getFocus().child_id orelse inner.children.keys()[0]; │ │771 - try root_focus.setFocus(target); │ │772 - if (root_focus.grandchild_id == target) return; │ │773 - // the diff pane wasn't laid out last build (too narrow to show beside │ │774 - // the list), so its hunks aren't in the focus tree yet. select the pane │ │775 - // at the box level, and the hunk will be focused after the next build. │ │776 - self.contentBox().getFocus().child_id = self.diffOuter().getFocus().id; │ │770 + if (self.diffInner().children.count() == 0) return; │ │771 + root_focus.setFocus(self.diffOuter().getFocus().id); │ │772 } │ │773 │ │774 + // return to the list. │ │775 fn focusList(self: *View, root_focus: *Focus) !void { │ │780 - const lb = self.listBox(); │ │781 - const id = lb.getFocus().child_id orelse (if (lb.children.count() > 0) lb.children.keys()[0] else return);│ │782 - try root_focus.setFocus(id); │ │776 + root_focus.setFocus(self.listScroll().getFocus().id); │ │777 } │ │778 │ │779 fn moveSelection(self: *View, root_focus: *Focus, delta: isize) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯