╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │785 const last: isize = @intCast(keys.len - 1); │ │786 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); │ │787 if (next == @as(usize, @intCast(cur))) return; │ │794 - try root_focus.setFocus(keys[next]); │ │788 + root_focus.setFocus(keys[next]); │ │789 if (lb.children.values()[next].rect) |rect| self.listScroll().scrollToRect(rect);│ │790 } │ │791 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 errdefer detail_scroll.deinit(allocator); │ │266 var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .direction = .vert }); │ │267 errdefer frame.deinit(allocator); │ │268 + // the frame's selected child is its scroll, so the focus chain │ │269 + // reaches the content box (populateDetail points the scroll's │ │270 + // inner box at it). this lets focus recovery descend into the │ │271 + // detail pane after it's laid out beside a too-narrow list. │ │272 + frame.getFocus().child_id = detail_scroll.getFocus().id; │ │273 try frame.children.put(allocator, detail_scroll.getFocus().id, .{ .widget = .{ .scroll = detail_scroll }, .rect = null, .min_size = null });│ │274 break :blk frame; │ │275 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │391 const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │392 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │393 │ │394 + // crossing panes only re-selects the content box's child (focusList / │ │395 + // focusDetail); when the window is too narrow to show both, the pane that │ │396 + // was holding focus is dropped here. the framework recovers focus after │ │397 + // the top-level build by re-deriving it down the selected-child chain, so │ │398 + // there's nothing to fix up afterward. │ │399 try self.box.build(allocator, constraint, root_focus); │ │390 - │ │391 - // if the detail pane is selected but focus is still elsewhere in this │ │392 - // view, the pane was too narrow to lay out when focus crossed over. it's │ │393 - // laid out now, so focus its content box. │ │394 - if (self.detailActive() and inner.children.count() > 0) { │ │395 - if (root_focus.grandchild_id) |g| { │ │396 - const in_view = self.box.getFocus().children.contains(g); │ │397 - const in_detail = inner.children.contains(g); │ │398 - if (in_view and !in_detail) │ │399 - try root_focus.setFocus(inner.getFocus().child_id orelse inner.children.keys()[0]); │ │400 - } │ │401 - } │ │400 } │ │401 │ │402 fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │425 } │ │426 } │ │427 } │ │428 + // point the pane at its content box so focus recovery can land here. │ │429 + if (inner.children.count() > 0) inner.getFocus().child_id = inner.children.keys()[0];│ │430 │ │431 // reset the scroll to the top for the newly-shown file: directly on the │ │432 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │534 sc.x = std.math.clamp(sc.x, 0, max_x); │ │535 } │ │536 │ │537 - // enter the detail pane by focusing its content box. the host arrives here on │ │538 - // right-arrow or Enter from the list. an empty pane (a directory or ".." row │ │539 - // is selected) can't be entered. │ │537 + // enter the detail pane. a directory or ".." row has no content, so there's │ │538 + // nothing to enter. setFocus handles the too-narrow case where the pane isn't │ │539 + // laid out yet (it gets selected, then focused after the next build). │ │540 fn focusDetail(self: *View, root_focus: *Focus) !void { │ │541 - const inner = self.detailInner(); │ │542 - if (inner.children.count() == 0) return; │ │543 - const target = inner.getFocus().child_id orelse inner.children.keys()[0]; │ │544 - try root_focus.setFocus(target); │ │545 - if (root_focus.grandchild_id == target) return; │ │546 - // the detail pane wasn't laid out last build (too narrow to show beside │ │547 - // the list), so its content isn't in the focus tree yet. select the pane │ │548 - // at the box level, and the content will be focused after the next build. │ │549 - self.contentBox().getFocus().child_id = self.detailOuter().getFocus().id; │ │541 + if (self.detailInner().children.count() == 0) return; │ │542 + root_focus.setFocus(self.detailOuter().getFocus().id); │ │543 } │ │544 │ │545 + // return to the list. │ │546 fn focusList(self: *View, root_focus: *Focus) !void { │ │553 - const lb = self.listBox(); │ │554 - const id = lb.getFocus().child_id orelse (if (lb.children.count() > 0) lb.children.keys()[0] else return);│ │555 - try root_focus.setFocus(id); │ │547 + root_focus.setFocus(self.listScroll().getFocus().id); │ │548 } │ │549 │ │550 fn moveSelection(self: *View, root_focus: *Focus, delta: isize) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │556 const last: isize = @intCast(keys.len - 1); │ │557 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); │ │558 if (next == @as(usize, @intCast(cur))) return; │ │567 - try root_focus.setFocus(keys[next]); │ │559 + root_focus.setFocus(keys[next]); │ │560 if (lb.children.values()[next].rect) |rect| self.listScroll().scrollToRect(rect);│ │561 } │ │562 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯