╭ changed ───────────────╮ │src/ui/Repo/Settings.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────╮ │11 const Key = xitui.input.Key; │ │12 const Grid = xitui.grid.Grid; │ │13 const Focus = xitui.focus.Focus; │ │14 + │ │15 +const Roles = @import("./Roles.zig"); │ │16 │ │17 const wasm = builtin.target.cpu.arch == .wasm32;│ │18 │ ╰────────────────────────────────────────────────────╯ ╭───────────────────────────────────╮ │28 issue_role: ?evt.Repo.Role, │ │29 patch_role: ?evt.Repo.Role, │ │30 hash_kind: hash.HashKind, │ │31 +// the sub tab the route names │ │32 +view: enum { settings, roles },│ │33 +roles: Roles, │ │34 │ │35 const Self = @This(); │ │36 │ ╰───────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 60 return error.InvalidRole; │ │ 61 } │ │ 62 │ │ 63 +// the repo settings and user roles sub tabs over their content │ │ 64 pub const View = struct { │ │ 65 + box: wgt.Box(ui.Widget), │ │ 66 + │ │ 67 + const header_index = 0; │ │ 68 + const content_index = 1; │ │ 69 + │ │ 70 + pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │ 71 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │ 72 + errdefer box.deinit(allocator); │ │ 73 + │ │ 74 + { │ │ 75 + var header = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │ 76 + errdefer header.deinit(allocator); │ │ 77 + const on_tab = switch (session.data.current_page) { │ │ 78 + .repo_settings, .repo_roles => true, │ │ 79 + else => false, │ │ 80 + }; │ │ 81 + const routes = [_]?ui.RoutablePage{ ui.RoutablePage.repoSettingsRoute(data.identity), ui.RoutablePage.repoRolesRoute(data.identity, 0) }; │ │ 82 + for ([_]@FieldType(Self, "view"){ .settings, .roles }, [_][]const u8{ "repo settings", "user roles" }, routes) |view, label, route| { │ │ 83 + var tab = try wgt.TextBox.init(allocator, label, .{ .border = .hidden, .round_corners = true, .wrap_kind = .none }); │ │ 84 + errdefer tab.deinit(allocator); │ │ 85 + tab.getFocus().mode = .all; │ │ 86 + tab.getFocus().kind = .{ .custom = try ui.inPageTabLink(session, route orelse return error.RouteTooLong, on_tab and data.view == view) }; │ │ 87 + try header.children.put(allocator, tab.getFocus().id, .{ .widget = .{ .text_box = tab }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } });│ │ 88 + } │ │ 89 + header.getFocus().child_id = header.children.keys()[@backingInt(data.view)]; │ │ 90 + // a row taller than the header, leaving a blank line beneath it │ │ 91 + try box.children.put(allocator, header.getFocus().id, .{ .widget = .{ .box = header }, .rect = null, .min_size = .{ .width = null, .height = 4 } }); │ │ 92 + } │ │ 93 + │ │ 94 + { │ │ 95 + var stack = try wgt.Stack(ui.Widget).init(allocator); │ │ 96 + errdefer stack.deinit(allocator); │ │ 97 + { │ │ 98 + var form = try Form.init(allocator, data, session); │ │ 99 + errdefer form.deinit(allocator); │ │100 + try stack.children.put(allocator, form.getFocus().id, .{ .repo_settings_form = form }); │ │101 + } │ │102 + { │ │103 + var roles = try Roles.View.init(allocator, &data.roles, session); │ │104 + errdefer roles.deinit(allocator); │ │105 + try stack.children.put(allocator, roles.getFocus().id, .{ .repo_roles = roles }); │ │106 + } │ │107 + stack.getFocus().child_id = stack.children.keys()[@backingInt(data.view)]; │ │108 + try box.children.put(allocator, stack.getFocus().id, .{ .widget = .{ .stack = stack }, .rect = null, .min_size = null }); │ │109 + } │ │110 + box.getFocus().child_id = box.children.keys()[content_index]; │ │111 + return .{ .box = box }; │ │112 + } │ │113 + │ │114 + pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ │115 + self.box.deinit(allocator); │ │116 + } │ │117 + │ │118 + fn headerBox(self: *View) *wgt.Box(ui.Widget) { │ │119 + return &self.box.children.values()[header_index].widget.box; │ │120 + } │ │121 + │ │122 + fn contentStack(self: *View) *wgt.Stack(ui.Widget) { │ │123 + return &self.box.children.values()[content_index].widget.stack; │ │124 + } │ │125 + │ │126 + fn headerActive(self: *View) bool { │ │127 + return self.box.getFocus().child_id == self.headerBox().getFocus().id; │ │128 + } │ │129 + │ │130 + pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │131 + self.clearGrid(); │ │132 + const header = self.headerBox(); │ │133 + if (header.getFocus().child_id) |selected_id| { │ │134 + for (header.children.keys(), header.children.values(), 0..) |id, *child, index| { │ │135 + ui.widget.markSelected(&child.widget.text_box, id == selected_id); │ │136 + if (id == selected_id) self.contentStack().getFocus().child_id = self.contentStack().children.keys()[index]; │ │137 + } │ │138 + } │ │139 + try self.box.build(allocator, constraint, root_focus); │ │140 + } │ │141 + │ │142 + pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │143 + const direction = inp.vertDirection(key); │ │144 + if (self.headerActive()) { │ │145 + const header = self.headerBox(); │ │146 + if (direction == .down) { │ │147 + root_focus.setFocus(self.contentStack().getFocus().id); │ │148 + } else if (header.children.getIndex(header.getFocus().child_id orelse return)) |current| { │ │149 + if (inp.moveTab(key, current, header.children.count())) |next| root_focus.setFocus(header.children.keys()[next]); │ │150 + } │ │151 + return; │ │152 + } │ │153 + if (direction == .up) if (self.contentStack().getSelected()) |selected| if (selected.atTop(root_focus)) { │ │154 + root_focus.setFocus(self.headerBox().getFocus().id); │ │155 + return; │ │156 + }; │ │157 + try self.contentStack().input(allocator, key, root_focus); │ │158 + } │ │159 + │ │160 + // moving up from the sub header returns to the page's tabs │ │161 + pub fn atTop(self: *View) bool { │ │162 + return self.headerActive(); │ │163 + } │ │164 + │ │165 + // arriving from the page's tabs lands on the sub header, not the content │ │166 + pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ │167 + root_focus.setFocus(self.headerBox().getFocus().id); │ │168 + return true; │ │169 + } │ │170 + │ │171 + pub fn clearGrid(self: *View) void { │ │172 + self.box.clearGrid(); │ │173 + } │ │174 + │ │175 + pub fn getGrid(self: View) ?Grid { │ │176 + return self.box.getGrid(); │ │177 + } │ │178 + │ │179 + pub fn getFocus(self: *View) *Focus { │ │180 + return self.box.getFocus(); │ │181 + } │ │182 +}; │ │183 + │ │184 +pub const Form = struct { │ │185 // a centered form, scrolled when it outgrows the window │ │186 scroll: wgt.Scroll(ui.Widget), │ │187 data: *const Self, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │188 session: *ui.Session, │ │189 │ │ 64 - pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │190 + pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !Form { │ │191 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │192 errdefer box.deinit(allocator); │ │ 67 - const route = ui.RoutablePage.repoRepoRoute(data.identity) orelse return error.RouteTooLong; │ │193 + const route = ui.RoutablePage.repoSettingsRoute(data.identity) orelse return error.RouteTooLong; │ │194 box.getFocus().kind = .{ .custom = try session.page_arena.allocator().print("form:{s}", .{try route.toUrl(session.page_arena)}) };│ │195 │ │196 const saved_fields = if (session.formFeedback(.repo_settings)) |saved| saved.fields else null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │255 }; │ │256 } │ │257 │ │132 - pub fn deinit(self: *View, allocator: std.mem.Allocator) void {│ │258 + pub fn deinit(self: *Form, allocator: std.mem.Allocator) void {│ │259 self.scroll.deinit(allocator); │ │260 } │ │261 │ ╰────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │136 - pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │262 + pub fn build(self: *Form, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │263 self.clearGrid(); │ │264 const failure = if (self.session.formFeedback(.repo_settings)) |saved| saved.failure else null; │ │265 (try formField(self.formBox(), "name")).options.top_label.text = if (failure) |value| switch (value) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │280 }, root_focus); │ │281 } │ │282 │ │157 - pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │283 + pub fn input(self: *Form, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │284 const cid = self.formBox().getFocus().child_id orelse return; │ │285 const cur = self.formBox().children.getIndex(cid) orelse return; │ │286 const child = &self.formBox().children.values()[cur]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │334 │ │335 // update the repo and navigate to its new url. this is the terminal path;│ │336 // the web posts the form to the settings route. │ │211 - fn submitForm(self: *View, allocator: std.mem.Allocator) !void { │ │337 + fn submitForm(self: *Form, allocator: std.mem.Allocator) !void { │ │338 if (comptime wasm) return; │ │339 const io = self.session.io orelse return; │ │340 const users_dir = self.session.users_dir orelse return; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │365 try self.session.navigate(route); │ │366 } │ │367 │ │242 - fn formBox(self: *View) *wgt.Box(ui.Widget) { │ │368 + fn formBox(self: *Form) *wgt.Box(ui.Widget) { │ │369 return &self.scroll.child.center.child.box;│ │370 } │ │371 │ ╰────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────╮ │246 - pub fn clearGrid(self: *View) void {│ │372 + pub fn clearGrid(self: *Form) void {│ │373 self.scroll.clearGrid(); │ │374 } │ │375 │ ╰─────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯