╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/Repo/Settings.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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,│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │221 if (comptime wasm) return; │ │222 const io = self.session.io orelse return; │ │223 const users_dir = self.session.users_dir orelse return; │ │144 - const actor = (try self.session.authorize(self.data.identity, .owner)) orelse return; │ │224 + const actor = (try self.session.authorize(self.data.identity, .owner, null)) orelse return;│ │225 │ │226 const name = try (try formField(self.formBox(), "name")).text(allocator); │ │227 defer allocator.free(name); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │228 const description = try (try formField(self.formBox(), "description")).text(allocator); │ │229 defer allocator.free(description); │ │150 - const access_radio = &self.formBox().children.values()[access_index].widget.radio; │ │151 - const access = std.meta.stringToEnum(evt.Repo.Access, access_radio.selected()) orelse unreachable; │ │230 + const access = std.meta.stringToEnum(evt.Repo.Access, (try formRadio(self.formBox(), "access")).selected()) orelse unreachable; │ │231 + const discuss_role = labelRole((try formRadio(self.formBox(), "discuss_role")).selected()) catch unreachable; │ │232 + const issue_role = labelRole((try formRadio(self.formBox(), "issue_role")).selected()) catch unreachable; │ │233 + const patch_role = labelRole((try formRadio(self.formBox(), "patch_role")).selected()) catch unreachable; │ │234 │ │153 - const route = update(io, allocator, users_dir, actor, self.data.identity, name, description, access) catch |err| { │ │235 + const route = update(io, allocator, users_dir, actor, self.data.identity, name, description, access, discuss_role, issue_role, patch_role) catch |err| {│ │236 const failure = ui.Session.FormFeedback.RepoFailure.fromError(err) orelse return err; │ │237 const aa = self.session.arena.allocator(); │ │238 self.session.data.form_feedback = .{ .repo_settings = .{ .failure = failure, .fields = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │239 .name = try aa.dupe(u8, name), │ │240 .description = try aa.dupe(u8, description),│ │241 .access = access, │ │242 + .discuss_role = discuss_role, │ │243 + .issue_role = issue_role, │ │244 + .patch_role = patch_role, │ │245 } } }; │ │246 return; │ │247 }; │ ╰─────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │249 } │ │250 │ │251 fn formBox(self: *View) *wgt.Box(ui.Widget) { │ │167 - return &self.center.child.box; │ │252 + return &self.scroll.child.center.child.box;│ │253 } │ │254 │ │255 pub fn clearGrid(self: *View) void { │ ╰────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │171 - self.center.clearGrid(); │ │256 + self.scroll.clearGrid(); │ │257 } │ │258 │ │259 pub fn getGrid(self: View) ?Grid {│ ╰───────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯