╭ changed ────────────╮ │src/ui/Repo/Roles.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────╮ │121 } │ │122 │ │123 pub const View = struct { │ │124 - box: wgt.Box(ui.Widget), │ │124 + scroll: wgt.Scroll(ui.Widget),│ │125 data: *const Self, │ │126 session: *ui.Session, │ │127 │ ╰───────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │128 const add_index = 0; │ │129 - const list_index = 1; │ │129 │ │130 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │131 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert });│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │148 } │ │149 │ │151 - { │ │152 - var rows = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │153 - errdefer rows.deinit(allocator); │ │154 - if (data.start > 0) { │ │155 - var previous = try Comment.linkBox(allocator, session, "← previous", ui.RoutablePage.repoRolesRoute(data.identity, data.start -| page_size) orelse return error.RouteTooLong);│ │156 - errdefer previous.deinit(allocator); │ │157 - try rows.children.put(allocator, previous.getFocus().id, .{ .widget = .{ .text_box = previous }, .rect = null, .min_size = null }); │ │158 - } │ │159 - for (data.rows) |row| { │ │160 - var row_route = route; │ │161 - row_route.repo_roles.user = ui.RoutablePage.Array(evt.User.name_max_len).from(row.name) orelse return error.RouteTooLong; │ │162 - const row_url = try row_route.toUrl(session.page_arena); │ │150 + // the user rows follow the add row in the same box │ │151 + if (data.start > 0) { │ │152 + var previous = try Comment.linkBox(allocator, session, "← previous", ui.RoutablePage.repoRolesRoute(data.identity, data.start -| page_size) orelse return error.RouteTooLong); │ │153 + errdefer previous.deinit(allocator); │ │154 + try box.children.put(allocator, previous.getFocus().id, .{ .widget = .{ .text_box = previous }, .rect = null, .min_size = null }); │ │155 + } │ │156 + for (data.rows) |row| { │ │157 + var row_route = route; │ │158 + row_route.repo_roles.user = ui.RoutablePage.Array(evt.User.name_max_len).from(row.name) orelse return error.RouteTooLong; │ │159 + const row_url = try row_route.toUrl(session.page_arena); │ │160 │ │164 - var row_box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │165 - errdefer row_box.deinit(allocator); │ │166 - row_box.getFocus().kind = .{ .custom = try aa.print("form:{s}", .{row_url}) }; │ │167 - { │ │168 - const user_route = ui.RoutablePage{ .user_repos = .{ .name = ui.RoutablePage.Array(evt.User.name_max_len).from(row.name) orelse return error.RouteTooLong } }; │ │169 - var name = try Comment.linkBox(allocator, session, row.name, user_route); │ │170 - errdefer name.deinit(allocator); │ │171 - name.options.bottom_label.text = if (row.role == .owner) " owner " else " collaborator "; │ │172 - try row_box.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_box = name }, .rect = null, .min_size = null }); │ │173 - } │ │174 - try addButton(allocator, &row_box, "remove", try aa.print("submit:{s}/revoke", .{row_url})); │ │175 - if (row.role == .owner) │ │176 - try addButton(allocator, &row_box, "demote to collaborator", try aa.print("submit:{s}/demote", .{row_url})) │ │177 - else │ │178 - try addButton(allocator, &row_box, "promote to owner", try aa.print("submit:{s}/promote", .{row_url})); │ │179 - row_box.getFocus().child_id = row_box.children.keys()[0]; │ │180 - try rows.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null }); │ │161 + var row_box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │162 + errdefer row_box.deinit(allocator); │ │163 + row_box.getFocus().kind = .{ .custom = try aa.print("form:{s}", .{row_url}) }; │ │164 + { │ │165 + const user_route = ui.RoutablePage{ .user_repos = .{ .name = ui.RoutablePage.Array(evt.User.name_max_len).from(row.name) orelse return error.RouteTooLong } }; │ │166 + var name = try Comment.linkBox(allocator, session, row.name, user_route); │ │167 + errdefer name.deinit(allocator); │ │168 + name.options.bottom_label.text = if (row.role == .owner) " owner " else " collaborator "; │ │169 + try row_box.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_box = name }, .rect = null, .min_size = null }); │ │170 } │ │182 - if (data.next_start) |next_start| { │ │183 - var next = try Comment.linkBox(allocator, session, "next →", ui.RoutablePage.repoRolesRoute(data.identity, next_start) orelse return error.RouteTooLong); │ │184 - errdefer next.deinit(allocator); │ │185 - try rows.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null }); │ │186 - } │ │187 - if (rows.children.count() > 0) rows.getFocus().child_id = rows.children.keys()[0]; │ │188 - var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = rows }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }); │ │189 - errdefer scroll.deinit(allocator); │ │190 - try box.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null }); │ │171 + try addButton(allocator, &row_box, "remove", try aa.print("submit:{s}/revoke", .{row_url})); │ │172 + if (row.role == .owner) │ │173 + try addButton(allocator, &row_box, "demote to collaborator", try aa.print("submit:{s}/demote", .{row_url})) │ │174 + else │ │175 + try addButton(allocator, &row_box, "promote to owner", try aa.print("submit:{s}/promote", .{row_url})); │ │176 + row_box.getFocus().child_id = row_box.children.keys()[0]; │ │177 + try box.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null }); │ │178 + } │ │179 + if (data.next_start) |next_start| { │ │180 + var next = try Comment.linkBox(allocator, session, "next →", ui.RoutablePage.repoRolesRoute(data.identity, next_start) orelse return error.RouteTooLong); │ │181 + errdefer next.deinit(allocator); │ │182 + try box.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null }); │ │183 } │ │184 │ │185 box.getFocus().child_id = box.children.keys()[add_index]; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │194 - return .{ .box = box, .data = data, .session = session }; │ │186 + var center = try ui.widget.Center.init(allocator, .{ .box = box }); │ │187 + errdefer center.deinit(allocator); │ │188 + return .{ │ │189 + .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .center = center }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }),│ │190 + .data = data, │ │191 + .session = session, │ │192 + }; │ │193 } │ │194 │ │195 fn addButton(allocator: std.mem.Allocator, row: *wgt.Box(ui.Widget), text: []const u8, kind: []const u8) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │201 } │ │202 │ │203 pub fn deinit(self: *View, allocator: std.mem.Allocator) void {│ │206 - self.box.deinit(allocator); │ │204 + self.scroll.deinit(allocator); │ │205 + } │ │206 + │ │207 + fn contentBox(self: *View) *wgt.Box(ui.Widget) { │ │208 + return &self.scroll.child.center.child.box; │ │209 } │ │210 │ │211 fn addBox(self: *View) *wgt.Box(ui.Widget) { │ ╰────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │210 - return &self.box.children.values()[add_index].widget.box; │ │212 + return &self.contentBox().children.values()[add_index].widget.box;│ │213 } │ │214 │ │215 fn nameInput(self: *View) *wgt.TextInput { │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │216 return &self.addBox().children.values()[0].widget.text_input; │ │217 } │ │218 │ │217 - fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │218 - return &self.box.children.values()[list_index].widget.scroll; │ │219 - } │ │220 - │ │219 fn addActive(self: *View) bool { │ │222 - return self.box.getFocus().child_id == self.addBox().getFocus().id; │ │220 + return self.contentBox().getFocus().child_id == self.addBox().getFocus().id; │ │221 } │ │222 │ │223 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │231 name.options.bottom_label.text = if (root_focus.grandchild_id == name.getFocus().id) " press enter " else ""; │ │232 // the web form handling finds the input by focus id │ │233 try self.session.text_inputs.put(self.session.arena.allocator(), name.getFocus().id, name); │ │236 - try self.box.build(allocator, constraint, root_focus); │ │234 + // the window's height as a minimum centers rows that fit │ │235 + try self.scroll.build(allocator, .{ │ │236 + .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ │237 + .max_size = constraint.max_size, │ │238 + }, root_focus); │ │239 } │ │240 │ │241 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │240 - if (self.addActive()) { │ │241 - switch (inp.vertDirection(key)) { │ │242 - .down => root_focus.setFocus(self.listScroll().getFocus().id), │ │243 - .up => {}, │ │244 - .none => if (key == .enter) { │ │245 - const name = try self.nameInput().text(allocator); │ │246 - defer allocator.free(name); │ │247 - try self.act(allocator, .grant, name); │ │248 - } else try self.nameInput().input(allocator, key, root_focus), │ │249 - } │ │242 + // the add row keeps every key but the vertical ones, which move between rows│ │243 + if (self.addActive() and inp.vertDirection(key) == .none) { │ │244 + if (key == .enter) { │ │245 + const name = try self.nameInput().text(allocator); │ │246 + defer allocator.free(name); │ │247 + try self.act(allocator, .grant, name); │ │248 + } else try self.nameInput().input(allocator, key, root_focus); │ │249 return; │ │250 } │ │251 │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │253 - const rows = &self.listScroll().child.box; │ │252 + const rows = self.contentBox(); │ │253 const current = rows.children.getIndex(rows.getFocus().child_id orelse return) orelse return;│ │254 if (inp.rowDelta(key, @intCast(rows.children.count()))) |delta| { │ │256 - if (delta < 0 and current == 0) │ │257 - root_focus.setFocus(self.addBox().getFocus().id) │ │258 - else │ │259 - ui.widget.moveRowFocus(rows, self.listScroll(), root_focus, delta); │ │255 + ui.widget.moveRowFocus(rows, &self.scroll, root_focus, delta); │ │256 return; │ │257 } │ │258 switch (key) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯