╭ Header.zig ───────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const ui = @import("../../ui.zig"); 4 const inp = @import("../input.zig"); 5 const xit = @import("xit"); 6 const xitui = xit.xitui; 7 const wgt = xitui.widget; 8 const layout = xitui.layout; 9 const Key = xitui.input.Key; 10 const Grid = xitui.grid.Grid; 11 const Focus = xitui.focus.Focus; 12 13 const commits_tab_label = "commits"; 14 const patch_tab_label = "patch"; 15 const diff_tab_label = "diff"; 16 const files_tab_label = "files"; 17 const settings_tab_label = "settings"; 18 19 pub const AuthTab = @import("../AuthTab.zig"); 20 21 name: []const u8, 22 owner_name: []const u8, 23 title: ui.Title, 24 id: []const u8, 25 oid: []const u8, 26 27 const Self = @This(); 28 29 pub fn init(arena: *std.heap.ArenaAllocator, name: []const u8, owner_name: []const u8, id: []const u8, oid: []const u8) !Self { 30 return .{ 31 .name = name, 32 .owner_name = owner_name, 33 .title = try ui.Title.init(arena, name), 34 .id = try arena.allocator().dupe(u8, id), 35 .oid = try arena.allocator().dupe(u8, oid), 36 }; 37 } 38 39 pub const View = struct { 40 scroll: wgt.Scroll(ui.Widget), 41 tab_ids: std.AutoArrayHashMapUnmanaged(usize, void), 42 tabs_id: usize, 43 gap_id: usize, 44 first_group_width: usize, 45 session: *ui.Session, 46 47 pub fn init(allocator: std.mem.Allocator, data: *const Self, commit_count: ?u64, session: *ui.Session) !View { 48 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .rounded_corners = true, .direction = .horiz }); 49 errdefer box.deinit(allocator); 50 51 var title_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 52 var title_box_owned = false; 53 errdefer if (!title_box_owned) title_box.deinit(allocator); 54 55 var tabs_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 56 var tabs_box_owned = false; 57 errdefer if (!tabs_box_owned) tabs_box.deinit(allocator); 58 59 var tab_ids: std.AutoArrayHashMapUnmanaged(usize, void) = .empty; 60 errdefer tab_ids.deinit(allocator); 61 62 const aa = session.page_arena.allocator(); 63 const identity = try std.fmt.allocPrint(aa, "{s}/{s}", .{ data.owner_name, data.name }); 64 const commits_label = if (commit_count) |count| try std.fmt.allocPrint(aa, commits_tab_label ++ " ({d})", .{count}) else commits_tab_label; 65 var first_group_width = try data.title.width(); 66 67 try ui.widget.addBackButton(allocator, &title_box, session); 68 69 // the user's name links to their page. 70 { 71 var text_buf: [evt.User.name_max_len + 1]u8 = undefined; 72 const text = try std.fmt.bufPrint(&text_buf, "{s}/", .{data.owner_name}); 73 var owner = try wgt.TextBox.init(allocator, text, .{ .border_style = .hidden, .wrap_kind = .none }); 74 errdefer owner.deinit(allocator); 75 owner.getFocus().mode = .all; 76 owner.getFocus().kind = .{ .custom = try std.fmt.allocPrint(aa, "a:/user/{s}", .{data.owner_name}) }; 77 first_group_width += try xitui.width.displayWidth(text) + 2; 78 try title_box.children.put(allocator, owner.getFocus().id, .{ .widget = .{ .text_box = owner }, .rect = null, .min_size = null }); 79 } 80 81 // the title links to the target repository. 82 { 83 const route = ui.RoutablePage.repoFilesRoute(identity, null, "", "", 0) orelse return error.RouteTooLong; 84 var title = try ui.Title.View.init(allocator, &data.title); 85 errdefer title.deinit(allocator); 86 title.getFocus().mode = .all; 87 title.getFocus().kind = .{ .custom = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(session.page_arena)}) }; 88 try title_box.children.put(allocator, title.getFocus().id, .{ .widget = .{ .title = title }, .rect = null, .min_size = null }); 89 } 90 91 // separate the title from the tabs. 92 { 93 var spacer = try wgt.Text.init(allocator, " "); 94 errdefer spacer.deinit(allocator); 95 try tabs_box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .text = spacer }, .rect = null, .min_size = .{ .width = 1, .height = null } }); 96 } 97 98 const current_tag = std.meta.activeTag(session.data.current_page); 99 const routes = [_]ui.RoutablePage{ 100 ui.RoutablePage.forkPatchRoute(identity, data.id) orelse return error.RouteTooLong, 101 ui.RoutablePage.forkDiffRoute(identity, data.id, 0, "") orelse return error.RouteTooLong, 102 ui.RoutablePage.forkFilesRoute(identity, data.id, data.oid, "", 0) orelse return error.RouteTooLong, 103 ui.RoutablePage.forkCommitsRoute(identity, data.id, data.oid, 0, "") orelse return error.RouteTooLong, 104 }; 105 const tags = [_]std.meta.Tag(ui.RoutablePage){ .fork_patch, .fork_diff, .fork_files, .fork_commits }; 106 const labels = [_][]const u8{ patch_tab_label, diff_tab_label, files_tab_label, commits_label }; 107 var selected_tab: ?usize = null; 108 109 for (routes, tags, labels) |route, tag, label| { 110 const selected = current_tag == tag; 111 var tab = try wgt.TextBox.init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 112 errdefer tab.deinit(allocator); 113 tab.getFocus().mode = .all; 114 tab.getFocus().kind = .{ .custom = try ui.inPageTabLink(session, route, selected) }; 115 try tab_ids.put(allocator, tab.getFocus().id, {}); 116 if (selected) selected_tab = tab.getFocus().id; 117 try tabs_box.children.put(allocator, tab.getFocus().id, .{ 118 .widget = .{ .text_box = tab }, 119 .rect = null, 120 .min_size = .{ .width = label.len + 2, .height = null }, 121 }); 122 } 123 124 // spacer pushes settings + auth to the right. 125 { 126 var spacer = try ui.widget.Spacer.init(allocator); 127 errdefer spacer.deinit(allocator); 128 try tabs_box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null, .flex = .grow }); 129 } 130 131 const settings_route = ui.RoutablePage.forkSettingsRoute(identity, data.id) orelse return error.RouteTooLong; 132 const settings_link = try ui.inPageTabLink(session, settings_route, current_tag == .fork_settings); 133 const auth_route = ui.RoutablePage.forkAuthRoute(identity, data.id) orelse return error.RouteTooLong; 134 const auth_link = try ui.inPageTabLink(session, auth_route, current_tag == .fork_auth); 135 136 // settings are account preferences, so they require a login. 137 if (session.data.user_id != null) { 138 var settings = try wgt.TextBox.init(allocator, settings_tab_label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 139 errdefer settings.deinit(allocator); 140 settings.getFocus().mode = .all; 141 settings.getFocus().kind = .{ .custom = settings_link }; 142 try tab_ids.put(allocator, settings.getFocus().id, {}); 143 if (current_tag == .fork_settings) selected_tab = settings.getFocus().id; 144 try tabs_box.children.put(allocator, settings.getFocus().id, .{ .widget = .{ .text_box = settings }, .rect = null, .min_size = .{ .width = settings_tab_label.len + 2, .height = null } }); 145 } 146 147 // keep authentication within the fork page. 148 if (session.data.host_kind == .server) { 149 var auth_tab = try AuthTab.View.init(allocator, session); 150 errdefer auth_tab.deinit(allocator); 151 auth_tab.text_box.getFocus().kind = .{ .custom = auth_link }; 152 try tab_ids.put(allocator, auth_tab.getFocus().id, {}); 153 if (current_tag == .fork_auth) selected_tab = auth_tab.getFocus().id; 154 try tabs_box.children.put(allocator, auth_tab.getFocus().id, .{ .widget = .{ .auth_tab = auth_tab }, .rect = null, .min_size = .{ .width = auth_tab.minWidth(), .height = null } }); 155 } 156 157 if (session.is_terminal) { 158 var quit = try wgt.TextBox.init(allocator, ui.Quit.tab_label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 159 errdefer quit.deinit(allocator); 160 quit.getFocus().mode = .all; 161 quit.getFocus().kind = .{ .custom = ui.Quit.tab_kind }; 162 try tab_ids.put(allocator, quit.getFocus().id, {}); 163 try tabs_box.children.put(allocator, quit.getFocus().id, .{ .widget = .{ .text_box = quit }, .rect = null, .min_size = .{ .width = 3, .height = null } }); 164 } 165 166 tabs_box.getFocus().child_id = selected_tab orelse tab_ids.keys()[0]; 167 const title_id = title_box.getFocus().id; 168 const tabs_id = tabs_box.getFocus().id; 169 try box.children.put(allocator, title_id, .{ .widget = .{ .box = title_box }, .rect = null, .min_size = null }); 170 title_box_owned = true; 171 // a blank row between the title and the tabs, shown only when they wrap 172 const gap_id = blk: { 173 var gap = try wgt.Text.init(allocator, " "); 174 errdefer gap.deinit(allocator); 175 const id = gap.getFocus().id; 176 try box.children.put(allocator, id, .{ .widget = .{ .text = gap }, .rect = null, .min_size = null, .hidden = true }); 177 break :blk id; 178 }; 179 try box.children.put(allocator, tabs_id, .{ .widget = .{ .box = tabs_box }, .rect = null, .min_size = null }); 180 tabs_box_owned = true; 181 box.getFocus().child_id = tabs_id; 182 183 return .{ 184 .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = box }, .{ .direction = .horiz, .show_bar = false, .web_native = !session.is_terminal }), 185 .tab_ids = tab_ids, 186 .tabs_id = tabs_id, 187 .gap_id = gap_id, 188 .first_group_width = first_group_width, 189 .session = session, 190 }; 191 } 192 193 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 194 self.scroll.deinit(allocator); 195 self.tab_ids.deinit(allocator); 196 } 197 198 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 199 self.clearGrid(); 200 const box = &self.scroll.child.box; 201 const back_visible = self.session.back == .available; 202 ui.widget.setBackButtonVisible(box, back_visible); 203 const tabs_child = self.tabsChild(); 204 const tabs_box = &tabs_child.widget.box; 205 206 const selected_tab = if (box.getFocus().child_id == self.tabs_id) tabs_box.getFocus().child_id else null; 207 var tabs_width: usize = 0; 208 for (tabs_box.children.keys(), tabs_box.children.values()) |id, *child| { 209 const text_box: ?*wgt.TextBox = switch (child.widget) { 210 .text_box => |*value| value, 211 .auth_tab => |*auth_tab| blk: { 212 child.min_size = .{ .width = auth_tab.minWidth(), .height = null }; 213 break :blk &auth_tab.text_box; 214 }, 215 else => null, 216 }; 217 if (text_box) |tab| ui.widget.markSelected(tab, selected_tab == id); 218 tabs_width += if (child.min_size) |min_size| min_size.width orelse 0 else 0; 219 } 220 221 const viewport_width = constraint.max_size.width orelse constraint.min_size.width; 222 const content_width = if (viewport_width) |width| width -| 2 else null; 223 const back_width: usize = if (back_visible) ui.widget.back_button_width else 0; 224 const wrap = if (content_width) |width| self.first_group_width + back_width + tabs_width > width else false; 225 box.options.direction = if (wrap) .vert else .horiz; 226 tabs_child.min_size = if (wrap) .{ .width = content_width, .height = null } else null; 227 (box.children.getPtr(self.gap_id) orelse unreachable).hidden = !wrap; 228 229 var scroll_constraint = constraint; 230 scroll_constraint.min_size.width = viewport_width; 231 try self.scroll.build(allocator, scroll_constraint, root_focus); 232 } 233 234 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 235 _ = allocator; 236 const current_tab = self.currentTabIndex() orelse return; 237 if (inp.moveTab(key, current_tab, self.tab_ids.count())) |new_tab| { 238 const tab_id = self.tab_ids.keys()[new_tab]; 239 root_focus.setFocus(tab_id); 240 const tabs_child = self.tabsChild(); 241 const tabs_rect = tabs_child.rect orelse return; 242 const tab = tabs_child.widget.box.children.get(tab_id) orelse return; 243 var rect = tab.rect orelse return; 244 rect.x += tabs_rect.x; 245 rect.y += tabs_rect.y; 246 self.scroll.scrollToRect(rect); 247 } 248 } 249 250 pub fn clearGrid(self: *View) void { 251 self.scroll.clearGrid(); 252 } 253 254 pub fn getGrid(self: View) ?Grid { 255 return self.scroll.getGrid(); 256 } 257 258 pub fn getFocus(self: *View) *Focus { 259 return self.scroll.getFocus(); 260 } 261 262 pub fn getSelectedIndex(self: View) ?usize { 263 return self.currentTabIndex(); 264 } 265 266 fn currentTabIndex(self: View) ?usize { 267 if (self.scroll.child.box.focus.child_id != self.tabs_id) return null; 268 const child_id = self.tabsChild().widget.box.focus.child_id orelse return null; 269 return self.tab_ids.getIndex(child_id); 270 } 271 272 fn tabsChild(self: *const View) *wgt.Box(ui.Widget).Child { 273 return self.scroll.child.box.children.getPtr(self.tabs_id) orelse unreachable; 274 } 275 }; 276