╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │520 - // reaches a hunk (populateDiff points the scroll's inner box at │ │521 - // one), letting focus recovery descend into the diff pane after │ │442 + // reaches a row (populateDetail points the scroll's inner box at │ │443 + // one), letting focus recovery descend into the pane after │ │444 // it's laid out beside a too-narrow list. │ │523 - frame.getFocus().child_id = diff_scroll.getFocus().id; │ │524 - try frame.children.put(allocator, diff_scroll.getFocus().id, .{ .widget = .{ .diff_view = diff_scroll }, .rect = null, .min_size = null }); │ │445 + frame.getFocus().child_id = detail_scroll.getFocus().id; │ │446 + try frame.children.put(allocator, detail_scroll.getFocus().id, .{ .widget = .{ .diff_view = detail_scroll }, .rect = null, .min_size = null }); │ │447 break :blk frame; │ │448 }; │ │527 - errdefer diff_outer.deinit(allocator); │ │528 - diff_outer.getFocus().mode = .mouse; │ │529 - try box.children.put(allocator, diff_outer.getFocus().id, .{ .widget = .{ .box = diff_outer }, .rect = null, .min_size = .{ .width = diff_min_width, .height = null } }); │ │449 + errdefer detail_outer.deinit(allocator); │ │450 + detail_outer.getFocus().mode = .mouse; │ │451 + try box.children.put(allocator, detail_outer.getFocus().id, .{ .widget = .{ .box = detail_outer }, .rect = null, .min_size = .{ .width = detail_min_width, .height = null } });│ │452 } │ │453 │ │454 box.getFocus().child_id = box.children.keys()[list_index]; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │462 .box = outer, │ │463 .data = data, │ │464 .session = session, │ │543 - .diffed_index = null, │ │465 + .detailed_index = null,│ │466 }; │ │467 } │ │468 │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │532 return &self.listScroll().child.box; │ │533 } │ │534 │ │613 - fn diffOuter(self: *View) *wgt.Box(ui.Widget) { │ │614 - return &self.contentBox().children.values()[diff_index].widget.box; │ │535 + fn detailOuter(self: *View) *wgt.Box(ui.Widget) { │ │536 + return &self.contentBox().children.values()[detail_index].widget.box; │ │537 } │ │538 │ │617 - fn diffScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │618 - return &self.diffOuter().children.values()[0].widget.diff_view.scroll; │ │539 + fn detailScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │540 + return &self.detailOuter().children.values()[0].widget.diff_view.scroll;│ │541 } │ │542 │ │621 - fn diffInner(self: *View) *wgt.Box(ui.Widget) { │ │622 - return &self.diffScroll().child.box; │ │543 + fn detailInner(self: *View) *wgt.Box(ui.Widget) { │ │544 + return &self.detailScroll().child.box; │ │545 } │ │546 │ │625 - fn diffActive(self: *View) bool { │ │547 + fn detailActive(self: *View) bool { │ │548 const content = self.contentBox(); │ │549 const cid = content.getFocus().child_id orelse return false; │ │628 - return content.children.getIndex(cid) == diff_index; │ │550 + return content.children.getIndex(cid) == detail_index; │ │551 } │ │552 │ │631 - // what the pane shows for the commit at `sel`. the page's content only │ │632 - // applies to the commit it walks from; the rest show their plain diff. │ │633 - fn paneContent(self: *View, sel: usize) Self.Content { │ │634 - return if (sel == 0) self.data.content else .{ .diff = .{} }; │ │635 - } │ │636 - │ │553 // the selected commit's index, or null when the "next" row is selected. │ │554 fn selectedCommitIndex(self: *View) ?usize { │ │555 const lb = self.listBox(); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │563 self.clearGrid(); │ │564 if (self.data.no_commits) return self.box.build(allocator, constraint, root_focus);│ │565 │ │650 - // swap the diff pane to the selected commit when it changes. │ │651 - try self.refreshDiff(allocator); │ │566 + // swap the detail pane to the selected commit when it changes. │ │567 + try self.refreshDetail(allocator); │ │568 │ │569 // the selected list row shows a border (the focused TextBox upgrades it │ │570 // to a double border itself); the rest stay borderless. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │576 } │ │577 } │ │578 │ │663 - // cap the list at list_max_width only while the diff pane fits beside it. │ │664 - // the box drops the diff when the width can't hold both minimums, so when │ │579 + // cap the list at list_max_width only while the detail pane fits beside it. │ │580 + // the box drops the pane when the width can't hold both minimums, so when │ │581 // it's that narrow we lift the cap and let the list fill the whole width. │ │666 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + diff_min_width else true; │ │582 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │583 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │584 │ │669 - // stretch the diff pane across the rest of the width so it fills the area │ │585 + // stretch the detail pane across the rest of the width so it fills the area │ │586 // rather than shrinking to its content; its scroll fills the pane. when │ │587 // too narrow for both, it fills the whole width. │ │672 - const diff_width: usize = if (constraint.max_size.width) |w| │ │588 + const detail_width: usize = if (constraint.max_size.width) |w| │ │589 (if (both_panes_fit) w - list_max_width else w) │ │590 else │ │675 - diff_min_width; │ │676 - self.contentBox().children.values()[diff_index].min_size = .{ .width = diff_width, .height = null }; │ │591 + detail_min_width; │ │592 + self.contentBox().children.values()[detail_index].min_size = .{ .width = detail_width, .height = null }; │ │593 │ │594 // the message is the pane's only wrapping row, and wrapping needs a │ │595 // bounded width, which the pane's scroll doesn't grant (it scrolls │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │596 // horizontally too). cap it to the pane, leaving room for the border │ │597 // and the scrollbar column. re-capped each build so it tracks resizes. │ │682 - for (self.diffInner().children.values()) |*child| switch (child.widget) { │ │598 + for (self.detailInner().children.values()) |*child| switch (child.widget) {│ │599 .text_box => |text_box| if (text_box.options.wrap_kind == .word) { │ │684 - child.max_size = .{ .width = diff_width -| 3, .height = null }; │ │600 + child.max_size = .{ .width = detail_width -| 3, .height = null }; │ │601 }, │ │602 else => {}, │ │603 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │606 // each Scroll's web-native mode hands its full content to a real │ │607 // scrollable element, so we no longer build unbounded here. │ │608 // │ │693 - // crossing panes only re-selects the content box's child (focusDiff / │ │609 + // crossing panes only re-selects the content box's child (focusDetail /│ │610 // focusList); when the window is too narrow to show both, the pane that│ │611 // held focus is dropped here. the framework recovers focus after the │ │612 // top-level build by re-deriving it down the selected-child chain, so │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │614 try self.box.build(allocator, constraint, root_focus); │ │615 } │ │616 │ │701 - fn refreshDiff(self: *View, allocator: std.mem.Allocator) !void { │ │617 + fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ │618 const sel = self.selectedCommitIndex() orelse return; │ │703 - if (self.diffed_index) |d| if (d == sel) return; │ │704 - try self.populateDiff(allocator, sel); │ │705 - self.diffed_index = sel; │ │619 + if (self.detailed_index) |d| if (d == sel) return; │ │620 + try self.populateDetail(allocator, sel); │ │621 + self.detailed_index = sel; │ │622 } │ │623 │ │708 - fn populateDiff(self: *View, allocator: std.mem.Allocator, sel: usize) !void { │ │624 + fn populateDetail(self: *View, allocator: std.mem.Allocator, sel: usize) !void {│ │625 const commit = self.data.commits[sel]; │ │710 - const inner = self.diffInner(); │ │626 + const inner = self.detailInner(); │ │627 │ │628 for (inner.children.values()) |*child| child.widget.deinit(allocator); │ │629 inner.children.clearAndFree(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │630 inner.getFocus().child_id = null; │ │631 │ │716 - switch (self.paneContent(sel)) { │ │717 - .message => { │ │718 - try addLink(allocator, inner, "← back to diff", try commitsLink(self.session.page_arena, self.data, commit.oid, 0, "")); │ │719 - try self.addMessageBox(allocator, inner, commit, .whole); │ │720 - }, │ │721 - .diff => |d| { │ │722 - if (d.path.len == 0) { │ │723 - try self.addMessageBox(allocator, inner, commit, .preview); │ │724 - if (commit.author != .unknown or commit.committer != .unknown) { │ │725 - var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │726 - errdefer row.deinit(allocator); │ │727 - if (commit.author != .unknown) { │ │728 - var tb = try ui.authorBox(allocator, self.session.page_arena, commit.author); │ │729 - errdefer tb.deinit(allocator); │ │730 - try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │731 - } │ │732 - if (commit.committer != .unknown) { │ │733 - var tb = try ui.authorBox(allocator, self.session.page_arena, commit.committer); │ │734 - errdefer tb.deinit(allocator); │ │735 - tb.options.label = " committer "; │ │736 - try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │737 - } │ │738 - row.getFocus().child_id = row.children.keys()[0]; │ │739 - try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │740 - } │ │741 - { │ │742 - var tb = try wgt.TextBox.init(allocator, commit.timestamp, .{ .border_style = .single, .round_corners = true, .wrap_kind = .none, .label = " timestamp " }); │ │743 - errdefer tb.deinit(allocator); │ │744 - tb.getFocus().mode = .all; │ │745 - try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │746 - } │ │747 - if (commit.merge) if (self.data.handle.location.commitsMergeRoute(commit.oid)) |route| { │ │748 - try addLink(allocator, inner, "view commits from this merge", try std.fmt.allocPrint(self.session.page_arena.allocator(), "a:{s}", .{try route.toUrl(self.session.page_arena)}));│ │749 - }; │ │750 - if (commit.stats) |stats| { │ │751 - var text: std.Io.Writer.Allocating = .init(allocator); │ │752 - defer text.deinit(); │ │753 - try text.writer.print("lines changed: {d}", .{stats.lines_added + stats.lines_changed}); │ │754 - if (stats.lines_removed != 0) try text.writer.print("\nlines removed: {d}", .{stats.lines_removed}); │ │755 - const bytes_added = stats.bytes_added >= stats.bytes_removed; │ │756 - const bytes = if (bytes_added) stats.bytes_added - stats.bytes_removed else stats.bytes_removed - stats.bytes_added; │ │757 - try text.writer.print("\nbytes {s}: {d}\nfiles changed: {d}", .{ │ │758 - if (bytes_added) "added" else "removed", bytes, stats.files_added + stats.files_changed, │ │759 - }); │ │760 - if (stats.files_removed != 0) try text.writer.print("\nfiles removed: {d}", .{stats.files_removed}); │ │761 - var tb = try wgt.TextBox.init(allocator, text.written(), .{ .border_style = .single, .round_corners = true, .wrap_kind = .none, .label = " stats " }); │ │762 - errdefer tb.deinit(allocator); │ │763 - tb.getFocus().mode = .all; │ │764 - try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │765 - } │ │766 - try addLink(allocator, inner, "view files at this commit", try filesObjectLink(self.session.page_arena, self.data.handle.location, commit.oid)); │ │632 + if (sel == 0 and self.data.message) { │ │633 + try addLink(allocator, inner, "← back to commit", try commitsLink(self.session.page_arena, self.data, commit.oid)); │ │634 + try self.addMessageBox(allocator, inner, commit, .whole); │ │635 + } else { │ │636 + const pa = self.session.page_arena; │ │637 + { │ │638 + var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │639 + errdefer row.deinit(allocator); │ │640 + const diff_route = self.data.handle.location.commitDiffRoute(commit.oid, commit.parent_oid) orelse return error.RouteTooLong; │ │641 + try addLink(allocator, &row, "view diff", try std.fmt.allocPrint(pa.allocator(), "a:{s}", .{try diff_route.toUrl(pa)})); │ │642 + try addLink(allocator, &row, "view files at this commit", try filesObjectLink(pa, self.data.handle.location, commit.oid)); │ │643 + row.getFocus().child_id = row.children.keys()[0]; │ │644 + try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │645 + } │ │646 + if (commit.merge) if (self.data.handle.location.commitsMergeRoute(commit.oid)) |route| { │ │647 + try addLink(allocator, inner, "view commits from this merge", try std.fmt.allocPrint(pa.allocator(), "a:{s}", .{try route.toUrl(pa)})); │ │648 + }; │ │649 + if (commit.author != .unknown or commit.committer != .unknown) { │ │650 + var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │651 + errdefer row.deinit(allocator); │ │652 + if (commit.author != .unknown) { │ │653 + var tb = try ui.authorBox(allocator, pa, commit.author); │ │654 + errdefer tb.deinit(allocator); │ │655 + try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │656 } │ │768 - │ │769 - try (Diff{ │ │770 - .route = .{ .commit = .{ .location = self.data.handle.location, .oid = commit.oid, .base_oid = self.data.base_oid } }, │ │771 - .path = d.path, │ │772 - .window = commit.window, │ │773 - }).appendWindow(allocator, self.session, inner); │ │774 - }, │ │657 + if (commit.committer != .unknown) { │ │658 + var tb = try ui.authorBox(allocator, pa, commit.committer); │ │659 + errdefer tb.deinit(allocator); │ │660 + tb.options.label = " committer "; │ │661 + try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │662 + } │ │663 + row.getFocus().child_id = row.children.keys()[0]; │ │664 + try inner.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │665 + } │ │666 + { │ │667 + var tb = try wgt.TextBox.init(allocator, commit.timestamp, .{ .border_style = .single, .round_corners = true, .wrap_kind = .none, .label = " timestamp " }); │ │668 + errdefer tb.deinit(allocator); │ │669 + tb.getFocus().mode = .all; │ │670 + try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │671 + } │ │672 + if (commit.stats) |stats| { │ │673 + var text: std.Io.Writer.Allocating = .init(allocator); │ │674 + defer text.deinit(); │ │675 + try text.writer.print("lines changed: {d}", .{stats.lines_added + stats.lines_changed}); │ │676 + if (stats.lines_removed != 0) try text.writer.print("\nlines removed: {d}", .{stats.lines_removed}); │ │677 + const bytes_added = stats.bytes_added >= stats.bytes_removed; │ │678 + const bytes = if (bytes_added) stats.bytes_added - stats.bytes_removed else stats.bytes_removed - stats.bytes_added; │ │679 + try text.writer.print("\nbytes {s}: {d}\nfiles changed: {d}", .{ │ │680 + if (bytes_added) "added" else "removed", bytes, stats.files_added + stats.files_changed, │ │681 + }); │ │682 + if (stats.files_removed != 0) try text.writer.print("\nfiles removed: {d}", .{stats.files_removed}); │ │683 + var tb = try wgt.TextBox.init(allocator, text.written(), .{ .border_style = .single, .round_corners = true, .wrap_kind = .none, .label = " stats " }); │ │684 + errdefer tb.deinit(allocator); │ │685 + tb.getFocus().mode = .all; │ │686 + try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │687 + } │ │688 + try self.addMessageBox(allocator, inner, commit, .preview); │ │689 } │ │690 │ │691 // point the pane at its first row so focus recovery can land here. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │694 // reset the scroll to the top for the newly-shown commit: directly on the│ │695 // terminal (the wasm offset), and via a version bump on the web (so the │ │696 // renderer's scroll id changes and JS drops the preserved position). │ │783 - const sc = self.diffScroll(); │ │697 + const sc = self.detailScroll(); │ │698 sc.x = 0; │ │699 sc.y = 0; │ │700 sc.getFocus().version +%= 1; │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯