╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │340 box: wgt.Box(ui.Widget), // vert: [header_index] = header row, [content_index] = split│ │341 data: *const Self, │ │342 session: *ui.Session, │ │343 - // the list row whose contents the detail pane currently shows. │ │344 - shown_index: ?usize, │ │343 + // what the detail pane was last filled for: the selected list row, and │ │344 + // whether the window was too narrow to show the list beside it │ │345 + shown: Shown = .{ .index = null, .narrow = false }, │ │346 + // the detail's way back to a list that isn't shown beside it │ │347 + back_id: ?usize = null, │ │348 + │ │349 + const Shown = struct { index: ?usize, narrow: bool }; │ │350 │ │351 // the sub-header leads the box where there is one; local mode has none │ │352 const header_index: usize = 0; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │370 errdefer center.deinit(allocator); │ │371 outer.getFocus().child_id = center.getFocus().id; │ │372 try outer.children.put(allocator, center.getFocus().id, .{ .widget = .{ .center = center }, .rect = null, .min_size = null });│ │368 - return .{ .box = outer, .data = data, .session = session, .shown_index = null }; │ │373 + return .{ .box = outer, .data = data, .session = session }; │ │374 } │ │375 │ │376 // the search box and the clone url at the top. local mode has neither. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │497 // focus lives in the split, except that search results start in the │ │498 // search box so the term can be refined right away. │ │499 outer.getFocus().child_id = outer.children.keys()[if (data.find != null) header_index else outer.children.count() - 1]; │ │495 - return .{ .box = outer, .data = data, .session = session, .shown_index = null }; │ │500 + return .{ .box = outer, .data = data, .session = session }; │ │501 } │ │502 │ │503 fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, bottom_label: []const u8, link: []const u8) !void {│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │604 self.clearGrid(); │ │605 if (self.data.no_commits) return self.box.build(allocator, constraint, root_focus); │ │606 │ │607 + // whether the detail pane fits beside the list, which decides the list │ │608 + // cap below and whether the detail carries a way back to the list │ │609 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true;│ │610 + │ │611 // swap the detail pane to the selected entry when the selection changes. │ │603 - try self.refreshDetail(allocator); │ │612 + try self.refreshDetail(allocator, !both_panes_fit); │ │613 │ │614 // the selected list row shows a border (the focused TextBox upgrades it │ │615 // to a double border itself); the rest stay borderless. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │630 // it. the box drops the detail pane when the width can't hold both │ │631 // minimums, so when it's that narrow we lift the cap and let the list │ │632 // fill the whole width. │ │624 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │633 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │634 │ │635 // stretch the detail pane and its bordered scroll frame across the rest │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │662 if (constraint.max_size.height != null) self.listScroll().clampToContent(); │ │663 } │ │664 │ │657 - fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ │658 - const cur = self.selectedRowIndex(); │ │659 - if (std.meta.eql(cur, self.shown_index)) return; │ │660 - try self.populateDetail(allocator); │ │661 - self.shown_index = cur; │ │665 + fn refreshDetail(self: *View, allocator: std.mem.Allocator, narrow: bool) !void { │ │666 + const next: Shown = .{ .index = self.selectedRowIndex(), .narrow = narrow }; │ │667 + if (std.meta.eql(next, self.shown)) return; │ │668 + try self.populateDetail(allocator, narrow); │ │669 + self.shown = next; │ │670 } │ │671 │ │664 - fn populateDetail(self: *View, allocator: std.mem.Allocator) !void { │ │672 + fn populateDetail(self: *View, allocator: std.mem.Allocator, narrow: bool) !void {│ │673 const nav_box = self.navBox(); │ │674 const inner = self.detailInner(); │ │675 │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │679 for (inner.children.values()) |*child| child.widget.deinit(allocator); │ │680 inner.children.clearAndFree(allocator); │ │681 inner.getFocus().child_id = null; │ │682 + self.back_id = null; │ │683 │ │684 // only files have contents to show; directories and the ".." row leave│ │685 // the pane empty. │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │687 if (self.selectedEntry()) |entry| { │ │688 if (!entry.is_dir) { │ │689 const path = try childDir(self.session.page_arena.allocator(), self.data.dir, entry.name); │ │690 + // with the list hidden beside it, the detail leads with the way │ │691 + // back. an in-page link to this very page, so both hosts hand │ │692 + // its press and enter to detailInput instead of navigating. │ │693 + if (narrow) { │ │694 + const page_arena = self.session.page_arena; │ │695 + const link = try page_arena.allocator().print("ai:{s}", .{try self.session.data.current_page.toUrl(page_arena)});│ │696 + self.back_id = try addNavButton(allocator, nav_box, "← back to file list", link); │ │697 + } │ │698 // the window links sit above the scroll, so they stay put │ │699 // while the content scrolls. │ │700 if (entry.window_start > 0) try self.addNavLink(allocator, nav_box, "scroll to top", entry, 0); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │718 } │ │719 } │ │720 } │ │704 - // point each box at its first child so focus can descend into it. │ │721 + // point each box at its first child so focus can descend into it, and │ │722 + // the pane at its nav row when it has one, else at the content │ │723 if (nav_box.children.count() > 0) nav_box.getFocus().child_id = nav_box.children.keys()[0]; │ │724 if (inner.children.count() > 0) inner.getFocus().child_id = inner.children.keys()[0]; │ │725 + self.detailOuter().getFocus().child_id = if (nav_box.children.count() > 0) nav_box.getFocus().id else self.detailScrollFrame().getFocus().id;│ │726 │ │727 // reserve the nav row's height (the shrink scroll frame yields it). │ │728 self.detailOuter().children.values()[detail_nav_index].min_size = │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │748 const path = try childDir(page_arena.allocator(), self.data.dir, entry.name); │ │749 const route = self.data.filesRoute(path, target_start) orelse return error.RouteTooLong; │ │750 const link = try page_arena.allocator().print("a:{s}", .{try route.toUrl(page_arena)}); │ │751 + _ = try addNavButton(allocator, box, label, link); │ │752 + } │ │753 + │ │754 + // a bordered button in the nav row carrying `link`, returning its focus id │ │755 + fn addNavButton(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !usize { │ │756 var tb = try wgt.TextBox.init(allocator, label, .{ .border = .single, .round_corners = true, .wrap_kind = .none });│ │757 errdefer tb.deinit(allocator); │ │758 tb.getFocus().mode = .all; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯