╭ changed ──────────────╮ │src/ui/Repo/Members.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │145 data: *const Self, │ │146 session: *ui.Session, │ │147 │ │148 - const add_index = 0; │ │148 + const about_index = 0; │ │149 + const add_index = 1; │ │150 │ │151 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View {│ │152 const aa = session.page_arena.allocator(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 const url = try route.toUrl(session.page_arena); │ │171 │ │172 { │ │172 - // the gap leaves a blank line between the text and the input │ │173 - var add = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert, .gap = 1 }); │ │173 + var about = try ui.widget.initAbout(allocator, &.{ │ │174 + .{ .text = "members", .style = .{ .bold = true } }, │ │175 + .{ .text = " can push to this repo.\n" }, │ │176 + .{ .text = "owners", .style = .{ .bold = true } }, │ │177 + .{ .text = " can add and remove members." }, │ │178 + }); │ │179 + errdefer about.deinit(allocator); │ │180 + try box.children.put(allocator, about.getFocus().id, .{ .widget = .{ .box = about }, .rect = null, .min_size = null }); │ │181 + } │ │182 + { │ │183 + var add = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │184 errdefer add.deinit(allocator); │ │185 add.getFocus().kind = .{ .custom = try aa.print("form:{s}/grant", .{url}) }; │ │176 - { │ │177 - var about = try ui.widget.RichText.init(allocator, &.{ │ │178 - .{ .text = "members", .style = .{ .bold = true } }, │ │179 - .{ .text = " can push to this repo.\n" }, │ │180 - .{ .text = "owners", .style = .{ .bold = true } }, │ │181 - .{ .text = " can add and remove members." }, │ │182 - }); │ │183 - errdefer about.deinit(allocator); │ │184 - try add.children.put(allocator, about.getFocus().id, .{ .widget = .{ .rich_text = about }, .rect = null, .min_size = null }); │ │185 - } │ │186 var name = try wgt.TextInput.init(allocator, .{ .top_label = .{ .text = " add a member " }, .name = "name", .visible_width = 30, .round_corners = true, .render_content = session.is_terminal });│ │187 errdefer name.deinit(allocator); │ │188 name.getFocus().mode = .all; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │189 if (session.formFeedback(.repo_members)) |saved| try name.setContent(allocator, saved.name); │ │190 add.getFocus().child_id = name.getFocus().id; │ │191 try add.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_input = name }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │192 - try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 6 } }); │ │192 + try box.children.put(allocator, add.getFocus().id, .{ .widget = .{ .box = add }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │193 } │ │194 │ │195 // the user rows follow the add row in the same box │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │235 try box.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null });│ │236 } │ │237 │ │238 - box.getFocus().child_id = box.children.keys()[add_index]; │ │238 + box.getFocus().child_id = box.children.keys()[about_index]; │ │239 var center = try ui.widget.Center.init(allocator, .{ .box = box }); │ │240 errdefer center.deinit(allocator); │ │241 return .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │266 } │ │267 │ │268 fn nameInput(self: *View) *wgt.TextInput { │ │269 - return &self.addBox().children.values()[1].widget.text_input;│ │269 + return &self.addBox().children.values()[0].widget.text_input;│ │270 } │ │271 │ │272 fn addActive(self: *View) bool { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │273 return self.contentBox().getFocus().child_id == self.addBox().getFocus().id; │ │274 + } │ │275 + │ │276 + fn aboutBox(self: *View) *wgt.Box(ui.Widget) { │ │277 + return &self.contentBox().children.values()[about_index].widget.box; │ │278 } │ │279 │ │280 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │281 self.clearGrid(); │ │278 - if (self.data.confirm == null) try self.labelAddInput(root_focus); │ │282 + if (self.data.confirm == null) { │ │283 + try self.labelAddInput(root_focus); │ │284 + ui.widget.markAboutFocused(self.aboutBox(), root_focus); │ │285 + } │ │286 // the window's height as a minimum centers rows that fit │ │287 try self.scroll.build(allocator, .{ │ │288 .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height },│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │328 switch (key) { │ │329 .arrow_left, .arrow_right => _ = ui.widget.moveInSelectedRow(rows, root_focus, key == .arrow_right),│ │330 else => { │ │324 - // the add row and the previous link, when there is one, sit above the rows │ │325 - const offset: usize = add_index + 1 + @intFromBool(self.data.start > 0); │ │331 + // the text, the add row and the previous link, when there is one, sit above the rows │ │332 + const offset: usize = add_index + 1 + @as(usize, @intFromBool(self.data.start > 0)); │ │333 if (current < offset or current - offset >= self.data.rows.len) return; │ │334 const row = self.data.rows[current - offset]; │ │335 const row_box = &rows.children.values()[current].widget.box; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │374 return self.scroll.getFocus(); │ │375 } │ │376 │ │370 - // up leaves the view from the add row or the confirmation │ │377 + // up leaves the view from the explanatory text or the confirmation │ │378 pub fn atTop(self: *View) bool { │ │372 - return self.data.confirm != null or self.addActive(); │ │379 + return self.data.confirm != null or self.contentBox().getFocus().child_id == self.aboutBox().getFocus().id;│ │380 } │ │381 }; │ │382 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │182 text_box.options.invert = selected; │ │183 } │ │184 │ │185 +// explanatory text atop a view's rows: a frame that takes focus like a row, so │ │186 +// the arrows scroll it into view, then a blank line │ │187 +pub fn initAbout(allocator: std.mem.Allocator, spans: []const RichText.Span) !wgt.Box(Widget) { │ │188 + var row = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .vert }); │ │189 + errdefer row.deinit(allocator); │ │190 + { │ │191 + var frame = try wgt.Box(Widget).init(allocator, .{ .border = .single, .round_corners = true, .direction = .vert, .top_label = .{ .text = " note " } });│ │192 + errdefer frame.deinit(allocator); │ │193 + frame.getFocus().mode = .all; │ │194 + { │ │195 + var text = try RichText.init(allocator, spans); │ │196 + errdefer text.deinit(allocator); │ │197 + try frame.children.put(allocator, text.getFocus().id, .{ .widget = .{ .rich_text = text }, .rect = null, .min_size = null }); │ │198 + } │ │199 + row.getFocus().child_id = frame.getFocus().id; │ │200 + try row.children.put(allocator, frame.getFocus().id, .{ .widget = .{ .box = frame }, .rect = null, .min_size = null }); │ │201 + } │ │202 + { │ │203 + var blank = try wgt.Text.init(allocator, " "); │ │204 + errdefer blank.deinit(allocator); │ │205 + try row.children.put(allocator, blank.getFocus().id, .{ .widget = .{ .text = blank }, .rect = null, .min_size = null }); │ │206 + } │ │207 + return row; │ │208 +} │ │209 + │ │210 +// double the about frame's border while it has focus │ │211 +pub fn markAboutFocused(row: *wgt.Box(Widget), root_focus: *const Focus) void { │ │212 + const frame = &row.children.values()[0].widget.box; │ │213 + frame.options.border = if (root_focus.grandchild_id == frame.getFocus().id) .double else .single; │ │214 +} │ │215 + │ │216 pub fn addBackButton(allocator: std.mem.Allocator, box: *wgt.Box(Widget), session: *ui.Session) !void { │ │217 if (!session.is_terminal) return; │ │218 var back = try wgt.TextBox.init(allocator, "←", .{ .border = .hidden, .wrap_kind = .none }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯