╭ changed ───────────────╮ │src/ui/Repo/Settings.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────╮ │22 name: []const u8, │ │23 description: []const u8, │ │24 access: evt.Repo.Access, │ │25 +discuss_role: ?evt.Repo.Role,│ │26 +issue_role: ?evt.Repo.Role, │ │27 +patch_role: ?evt.Repo.Role, │ │28 hash_kind: hash.HashKind, │ │29 │ │30 const Self = @This(); │ ╰─────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │31 │ │32 +// the tab role choices, in the order the form shows them │ │33 +const role_choices = [_]?evt.Repo.Role{ null, .owner, .write, .read }; │ │34 + │ │35 +const role_labels = blk: { │ │36 + var labels: [role_choices.len][]const u8 = undefined; │ │37 + for (&labels, role_choices) |*label, role| label.* = roleLabel(role);│ │38 + break :blk labels; │ │39 +}; │ │40 + │ │41 +pub fn roleLabel(role_maybe: ?evt.Repo.Role) []const u8 { │ │42 + const role = role_maybe orelse return "nobody"; │ │43 + return switch (role) { │ │44 + .owner => "owners", │ │45 + .write => "collaborators", │ │46 + .read => "anybody", │ │47 + }; │ │48 +} │ │49 + │ │50 +// the tab role a label names │ │51 +pub fn labelRole(label: []const u8) error{InvalidRole}!?evt.Repo.Role { │ │52 + for (role_choices) |role| { │ │53 + if (std.mem.eql(u8, label, roleLabel(role))) return role; │ │54 + } │ │55 + return error.InvalidRole; │ │56 +} │ │57 + │ │58 pub const View = struct { │ │30 - center: ui.widget.Center, │ │59 + // a centered form, scrolled when it outgrows the window │ │60 + scroll: wgt.Scroll(ui.Widget), │ │61 data: *const Self, │ │62 session: *ui.Session, │ │63 │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │34 - const access_index: usize = 2; │ │35 - const hash_index: usize = 3; │ │36 - │ │64 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │65 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert });│ │66 errdefer box.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │92 │ │93 { │ │94 const access = if (saved_fields) |saved| saved.access else data.access; │ │68 - var access_radio = try ui.widget.Radio.init(allocator, session, "access", &.{ "private", "public" }, @tagName(access)); │ │95 + var access_radio = try ui.widget.Radio.init(allocator, session, "access", &.{ "private", "public" }, @tagName(access), null); │ │96 errdefer access_radio.deinit(allocator); │ │97 try box.children.put(allocator, access_radio.getFocus().id, .{ .widget = .{ .radio = access_radio }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │98 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │101 var hash_box = try wgt.TextBox.init(allocator, @tagName(data.hash_kind), .{ .border = .single, .round_corners = true, .wrap_kind = .none, .top_label = .{ .text = " hash " } });│ │102 errdefer hash_box.deinit(allocator); │ │103 try box.children.put(allocator, hash_box.getFocus().id, .{ .widget = .{ .text_box = hash_box }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │104 + } │ │105 + │ │106 + const roles = [_]struct { name: []const u8, label: []const u8, role: ?evt.Repo.Role }{ │ │107 + .{ .name = "discuss_role", .label = " who can create discussions ", .role = if (saved_fields) |saved| saved.discuss_role else data.discuss_role }, │ │108 + .{ .name = "issue_role", .label = " who can create issues ", .role = if (saved_fields) |saved| saved.issue_role else data.issue_role }, │ │109 + .{ .name = "patch_role", .label = " who can create patches ", .role = if (saved_fields) |saved| saved.patch_role else data.patch_role }, │ │110 + }; │ │111 + for (roles) |role| { │ │112 + var role_radio = try ui.widget.Radio.init(allocator, session, role.name, &role_labels, roleLabel(role.role), role.label); │ │113 + errdefer role_radio.deinit(allocator); │ │114 + try box.children.put(allocator, role_radio.getFocus().id, .{ .widget = .{ .radio = role_radio }, .rect = null, .min_size = null }); │ │115 } │ │116 │ │117 { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │120 try box.children.put(allocator, submit.getFocus().id, .{ .widget = .{ .submit_button = submit }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │121 } │ │122 │ │ 85 - return .{ .center = try ui.widget.Center.init(allocator, .{ .box = box }), .data = data, .session = session }; │ │123 + var center = try ui.widget.Center.init(allocator, .{ .box = box }); │ │124 + errdefer center.deinit(allocator); │ │125 + return .{ │ │126 + .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .center = center }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }), │ │127 + .data = data, │ │128 + .session = session, │ │129 + }; │ │130 } │ │131 │ │132 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 89 - self.center.deinit(allocator); │ │133 + self.scroll.deinit(allocator); │ │134 } │ │135 │ │136 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 .text_input => |*ti| try self.session.text_inputs.put(inputs_arena, ti.getFocus().id, ti), │ │148 else => {}, │ │149 }; │ │106 - try self.center.build(allocator, constraint, root_focus); │ │150 + // the window's height as a minimum centers a form that fits │ │151 + try self.scroll.build(allocator, .{ │ │152 + .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ │153 + .max_size = constraint.max_size, │ │154 + }, root_focus); │ │155 } │ │156 │ │157 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │161 const keys = self.formBox().children.keys(); │ │162 │ │163 const on_submit = child.widget == .submit_button; │ │116 - // the read-only hash row is stepped over │ │164 const direction = inp.vertDirection(key); │ │118 - if (direction == .up or key == .back_tab) return if (cur > 0) root_focus.setFocus(keys[if (cur - 1 == hash_index) access_index else cur - 1]); │ │119 - if (direction == .down or key == .tab) return if (cur + 1 < keys.len) root_focus.setFocus(keys[if (cur + 1 == hash_index) hash_index + 1 else cur + 1]);│ │165 + const up = direction == .up or key == .back_tab; │ │166 + if (up or direction == .down or key == .tab) { │ │167 + var next = cur; │ │168 + while (true) { │ │169 + if (up) { │ │170 + if (next == 0) return; │ │171 + next -= 1; │ │172 + } else { │ │173 + next += 1; │ │174 + if (next == keys.len) return; │ │175 + } │ │176 + // the read-only hash row is stepped over │ │177 + if (self.formBox().children.values()[next].widget != .text_box) break; │ │178 + } │ │179 + root_focus.setFocus(keys[next]); │ │180 + if (self.session.is_terminal) self.scrollToChild(next); │ │181 + return; │ │182 + } │ │183 switch (key) { │ │184 .enter => if (on_submit) return self.submitForm(allocator), │ │185 .mouse => |mouse| if (on_submit) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │188 else => {}, │ │189 } │ │190 try child.widget.input(allocator, key, root_focus); │ │191 + } │ │192 + │ │193 + // keep the form child at `index` in view, its rect shifted by the centering │ │194 + fn scrollToChild(self: *View, index: usize) void { │ │195 + const rect = self.formBox().children.values()[index].rect orelse return; │ │196 + const center_grid = self.scroll.child.center.getGrid() orelse return; │ │197 + const form_grid = self.formBox().getGrid() orelse return; │ │198 + const offset_y: isize = @intCast((center_grid.size.height -| form_grid.size.height) / 2); │ │199 + self.scroll.scrollToRect(.{ .x = rect.x, .y = rect.y + offset_y, .size = rect.size }); │ │200 } │ │201 │ │202 + fn formRadio(form: *wgt.Box(ui.Widget), name: []const u8) !*ui.widget.Radio { │ │203 + for (form.children.values()) |*child| switch (child.widget) { │ │204 + .radio => |*radio| if (std.mem.eql(u8, radio.name, name)) return radio, │ │205 + else => {}, │ │206 + }; │ │207 + return error.MissingFormField; │ │208 + } │ │209 + │ │210 fn formField(form: *wgt.Box(ui.Widget), name: []const u8) !*wgt.TextInput { │ │211 for (form.children.values()) |*child| switch (child.widget) { │ │212 .text_input => |*text_input| if (std.mem.eql(u8, text_input.options.name, name)) return text_input,│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯