╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │360 box: wgt.Box(ui.Widget), // vert: [header_index] = clone url row, [content_index] = split│ │361 data: *const Self, │ │362 session: *ui.Session, │ │363 - // the commit the pane currently shows (index into data.commits). │ │364 - detailed_index: ?usize, │ │363 + // what the pane was last filled for: the commit (index into data.commits), │ │364 + // and whether the window was too narrow to show the list beside it │ │365 + shown: ?Shown = null, │ │366 + // the detail's way back to a list that isn't shown beside it │ │367 + back_id: ?usize = null, │ │368 + │ │369 + const Shown = struct { index: usize, narrow: bool }; │ │370 │ │371 // the sub-header leads the box where there is one; local mode has none │ │372 const header_index: usize = 0; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │385 errdefer center.deinit(allocator); │ │386 outer.getFocus().child_id = center.getFocus().id; │ │387 try outer.children.put(allocator, center.getFocus().id, .{ .widget = .{ .center = center }, .rect = null, .min_size = null });│ │383 - return .{ .box = outer, .data = data, .session = session, .detailed_index = null }; │ │388 + return .{ .box = outer, .data = data, .session = session }; │ │389 } │ │390 │ │391 // the search box and the clone url at the top. local mode has neither. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │452 // search box so the query can be refined right away. │ │453 outer.getFocus().child_id = outer.children.keys()[if (data.search != null) header_index else outer.children.count() - 1]; │ │454 │ │450 - return .{ │ │451 - .box = outer, │ │452 - .data = data, │ │453 - .session = session, │ │454 - .detailed_index = null, │ │455 - }; │ │455 + return .{ .box = outer, .data = data, .session = session }; │ │456 } │ │457 │ │458 fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8, bottom_label: []const u8) !void {│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │463 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null, .max_size = .{ .width = null, .height = 5 } });│ │464 } │ │465 │ │466 - // a focusable row following the "a:" `link`. │ │467 - fn addLink(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { │ │466 + // a focusable row following `link`, returning its focus id │ │467 + fn addLink(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !usize { │ │468 var tb = try wgt.TextBox.init(allocator, label, .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │469 errdefer tb.deinit(allocator); │ │470 tb.getFocus().mode = .all; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │471 tb.getFocus().kind = .{ .custom = link }; │ │472 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null });│ │473 + return tb.getFocus().id; │ │474 } │ │475 │ │476 // how much of the message a pane's box holds: a truncated preview links to │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │553 self.clearGrid(); │ │554 if (self.data.no_commits) return self.box.build(allocator, constraint, root_focus); │ │555 │ │556 + // whether the detail pane fits beside the list, which decides the list │ │557 + // cap below and whether the detail carries a way back to the list │ │558 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true;│ │559 + │ │560 // swap the detail pane to the selected commit when it changes. │ │556 - try self.refreshDetail(allocator); │ │561 + try self.refreshDetail(allocator, !both_panes_fit); │ │562 │ │563 // the selected list row shows a border (the focused TextBox upgrades it │ │564 // to a double border itself); the rest stay borderless. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │573 // cap the list at list_max_width only while the detail pane fits beside it. │ │574 // the box drops the pane when the width can't hold both minimums, so when │ │575 // it's that narrow we lift the cap and let the list fill the whole width. │ │571 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │576 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │577 │ │578 // stretch the detail pane across the rest of the width so it fills the area │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │596 try self.box.build(allocator, constraint, root_focus); │ │597 } │ │598 │ │595 - fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ │596 - const sel = self.selectedCommitIndex() orelse return; │ │597 - if (self.detailed_index) |d| if (d == sel) return; │ │598 - try self.populateDetail(allocator, sel); │ │599 - self.detailed_index = sel; │ │599 + fn refreshDetail(self: *View, allocator: std.mem.Allocator, narrow: bool) !void { │ │600 + const next: Shown = .{ .index = self.selectedCommitIndex() orelse return, .narrow = narrow };│ │601 + if (self.shown) |shown| if (std.meta.eql(shown, next)) return; │ │602 + try self.populateDetail(allocator, next); │ │603 + self.shown = next; │ │604 } │ │605 │ │602 - fn populateDetail(self: *View, allocator: std.mem.Allocator, sel: usize) !void { │ │606 + fn populateDetail(self: *View, allocator: std.mem.Allocator, next: Shown) !void { │ │607 + const sel = next.index; │ │608 const commit = self.data.commits[sel]; │ │609 const inner = self.detailInner(); │ │610 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │611 for (inner.children.values()) |*child| child.widget.deinit(allocator); │ │612 inner.children.clearAndFree(allocator); │ │613 inner.getFocus().child_id = null; │ │614 + self.back_id = null; │ │615 │ │616 + // with the list hidden beside it, the detail leads with the way back. │ │617 + // an in-page link to this very page, so both hosts hand its press and │ │618 + // enter to input instead of navigating. │ │619 + if (next.narrow) { │ │620 + const pa = self.session.page_arena; │ │621 + const link = try pa.allocator().print("ai:{s}", .{try self.session.data.current_page.toUrl(pa)}); │ │622 + self.back_id = try addLink(allocator, inner, "← back to commit list", link); │ │623 + } │ │624 + │ │625 if (sel == 0 and self.data.message) { │ │611 - try addLink(allocator, inner, "← back to commit", try commitsLink(self.session.page_arena, self.data, commit.oid)); │ │626 + _ = try addLink(allocator, inner, "← back to commit", try commitsLink(self.session.page_arena, self.data, commit.oid));│ │627 try self.addMessageBox(allocator, inner, commit, .whole); │ │628 } else { │ │629 const pa = self.session.page_arena; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │631 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │632 errdefer row.deinit(allocator); │ │633 const diff_route = self.data.handle.location.commitDiffRoute(commit.oid, commit.parent_oid) orelse return error.RouteTooLong; │ │619 - try addLink(allocator, &row, "view diff", try pa.allocator().print("a:{s}", .{try diff_route.toUrl(pa)})); │ │620 - try addLink(allocator, &row, "view files at this commit", try filesObjectLink(pa, self.data.handle.location, commit.oid)); │ │634 + _ = try addLink(allocator, &row, "view diff", try pa.allocator().print("a:{s}", .{try diff_route.toUrl(pa)})); │ │635 + _ = try addLink(allocator, &row, "view files at this commit", try filesObjectLink(pa, self.data.handle.location, commit.oid));│ │636 row.getFocus().child_id = row.children.keys()[0]; │ │637 try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │638 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯