╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────╮ │10 const inp = xitui.input; │ │11 const Grid = xitui.grid.Grid; │ │12 const Focus = xitui.focus.Focus; │ │13 + │ │14 +const SubHeader = @import("SubHeader.zig");│ │15 │ │16 const RefOrOid = ui.RoutablePage.RefOrOid; │ │17 │ ╰───────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │30 // the directory being viewed, relative to the repo root ("" at the root).│ │31 dir: []const u8, │ │32 entries: []const Entry, │ │33 +// the "viewing <ref> <value>" banner shown above the listing. │ │34 +sub_header: SubHeader, │ │35 │ │36 const Self = @This(); │ │37 │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │121 .ref_or_oid_value = resolved.value, │ │122 .dir = try aa.dupe(u8, dir), │ │123 .entries = entries, │ │124 + .sub_header = try SubHeader.init(aa, resolved.ref_or_oid, resolved.value),│ │125 }; │ │126 } │ │127 │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │133 .ref_or_oid_value = try aa.dupe(u8, ref_or_oid_value), │ │134 .dir = try aa.dupe(u8, dir), │ │135 .entries = &.{}, │ │136 + .sub_header = try SubHeader.init(aa, ref_or_oid, ref_or_oid_value),│ │137 }; │ │138 } │ │139 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │140 pub const View = struct { │ │135 - // a vertical list: one focusable row per entry. each subdirectory row is an │ │136 - // `a:` link to its /repo/.../files/<path> route, so following it (click or │ │137 - // Enter) navigates through the host's link handling, like the Users/Repos │ │138 - // lists. files are unlinked. a ".." row at the top links to the parent. │ │139 - scroll: wgt.Scroll(ui.Widget), // wraps a vertical Box of rows │ │141 + // a vertical stack: the "viewing <ref>" banner on top, then a list with one │ │142 + // focusable row per entry. each subdirectory row is an `a:` link to its │ │143 + // /repo/.../files/<path> route, so following it (click or Enter) navigates │ │144 + // through the host's link handling, like the Users/Repos lists. files are │ │145 + // unlinked. a ".." row at the top links to the parent. │ │146 + box: wgt.Box(ui.Widget), // vert: [sub_header_index] = banner, [scroll_index] = list scroll │ │147 data: *const Self, │ │148 │ │149 + const sub_header_index: usize = 0; │ │150 + const scroll_index: usize = 1; │ │151 + │ │152 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │143 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │144 - errdefer box.deinit(allocator); │ │153 + var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │154 + errdefer outer.deinit(allocator); │ │155 │ │146 - // labels and link kinds are borrowed by the rows, so they live in the │ │147 - // page arena (as long as this page's widget tree). │ │148 - const aa = session.page_arena.allocator(); │ │149 - if (data.dir.len != 0) { │ │150 - try addRow(allocator, &box, "..", try dirLink(session.page_arena, data, parentDir(data.dir))); │ │156 + // the ref banner at the top. │ │157 + { │ │158 + var sub_header = try SubHeader.View.init(allocator, &data.sub_header, session); │ │159 + errdefer sub_header.deinit(allocator); │ │160 + try outer.children.put(allocator, sub_header.getFocus().id, .{ .widget = .{ .repo_sub_header = sub_header }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │161 } │ │152 - for (data.entries) |entry| { │ │153 - const label = if (entry.is_dir) try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) else entry.name; │ │154 - const link = if (entry.is_dir) try dirLink(session.page_arena, data, try childDir(aa, data.dir, entry.name)) else ""; │ │155 - try addRow(allocator, &box, label, link); │ │162 + │ │163 + // the directory listing. │ │164 + { │ │165 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │166 + errdefer box.deinit(allocator); │ │167 + │ │168 + // labels and link kinds are borrowed by the rows, so they live in the │ │169 + // page arena (as long as this page's widget tree). │ │170 + const aa = session.page_arena.allocator(); │ │171 + if (data.dir.len != 0) { │ │172 + try addRow(allocator, &box, "..", try dirLink(session.page_arena, data, parentDir(data.dir))); │ │173 + } │ │174 + for (data.entries) |entry| { │ │175 + const label = if (entry.is_dir) try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) else entry.name; │ │176 + const link = if (entry.is_dir) try dirLink(session.page_arena, data, try childDir(aa, data.dir, entry.name)) else ""; │ │177 + try addRow(allocator, &box, label, link); │ │178 + } │ │179 + if (box.children.count() > 0) box.getFocus().child_id = box.children.keys()[0]; │ │180 + │ │181 + var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = box }, .{ .direction = .vert, .web_native = !session.is_terminal }); │ │182 + errdefer scroll.deinit(allocator); │ │183 + try outer.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null }); │ │184 } │ │157 - if (box.children.count() > 0) box.getFocus().child_id = box.children.keys()[0]; │ │185 │ │159 - var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = box }, .{ .direction = .vert, .web_native = !session.is_terminal }); │ │160 - errdefer scroll.deinit(allocator); │ │161 - return .{ .scroll = scroll, .data = data }; │ │186 + // focus lives on the list; the banner isn't focusable. │ │187 + outer.getFocus().child_id = outer.children.keys()[scroll_index]; │ │188 + return .{ .box = outer, .data = data }; │ │189 } │ │190 │ │191 fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │197 } │ │198 │ │199 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ │173 - self.scroll.deinit(allocator); │ │200 + self.box.deinit(allocator); │ │201 + } │ │202 + │ │203 + fn scrollWidget(self: *View) *wgt.Scroll(ui.Widget) { │ │204 + return &self.box.children.values()[scroll_index].widget.scroll;│ │205 } │ │206 │ │207 fn innerBox(self: *View) *wgt.Box(ui.Widget) { │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │177 - return &self.scroll.child.box; │ │208 + return &self.scrollWidget().child.box; │ │209 } │ │210 │ │211 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │222 // clear the incoming min_size so rows size to their content rather than│ │223 // stretching to fill the page height; max_size still bounds the scroll │ │224 // viewport so a long listing clips and scrolls. │ │194 - try self.scroll.build(allocator, .{ │ │225 + try self.box.build(allocator, .{ │ │226 .min_size = .{ .width = null, .height = null }, │ │227 .max_size = constraint.max_size, │ │228 }, root_focus); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │249 if (delta > 0 and cur + 1 >= keys.len) return; │ │250 const next = if (delta < 0) cur - 1 else cur + 1; │ │251 try root_focus.setFocus(keys[next]); │ │221 - if (box.children.values()[next].rect) |rect| self.scroll.scrollToRect(rect); │ │252 + if (box.children.values()[next].rect) |rect| self.scrollWidget().scrollToRect(rect);│ │253 } │ │254 │ │255 pub fn clearGrid(self: *View) void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │225 - self.scroll.clearGrid(); │ │256 + self.box.clearGrid(); │ │257 } │ │258 │ │259 pub fn getGrid(self: View) ?Grid {│ ╰───────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯