╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Comment.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
159 var author = try ui.authorBox(allocator, session.page_arena, entry.author); 160 errdefer author.deinit(allocator); 161 try bar.children.put(allocator, author.getFocus().id, .{ .widget = .{ .text_box = author }, .rect = null, .min_size = null, .flex = .shrink }); 162 163 var reply = try linkBox(allocator, session, "new reply", commentNewRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); 164 errdefer reply.deinit(allocator); 165 try bar.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = .{ .width = "new reply".len + 2, .height = null } }); 166 167 if (!entry.comment.removed) { 168 var edit = try linkBox(allocator, session, "edit comment", commentEditRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); 169 errdefer edit.deinit(allocator); 170 try bar.children.put(allocator, edit.getFocus().id, .{ .widget = .{ .text_box = edit }, .rect = null, .min_size = .{ .width = "edit comment".len + 2, .height = null } }); 171 } 172 173 var permalink = try linkBox(allocator, session, "permalink", commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong); 174 errdefer permalink.deinit(allocator); 175 try bar.children.put(allocator, permalink.getFocus().id, .{ .widget = .{ .text_box = permalink }, .rect = null, .min_size = .{ .width = "permalink".len + 2, .height = null } }); 176 177 if (!std.mem.eql(u8, &entry.comment.event.parent_id, &entry.comment.event.thread_id)) { 178 const parent_text = if (entry.parent_author) |parent_author| switch (parent_author) { 179 .unknown => "", 180 .email, .user_name => |text| text, 181 } else ""; 182 var parent = try wgt.TextBox.init(allocator, parent_text, .{ 183 .border_style = .single, 184 .rounded_corners = true, 185 .wrap_kind = .none, 186 .label = " replying to ", 187 }); 188 errdefer parent.deinit(allocator); 189 parent.getFocus().mode = .all; 190 const route = commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.comment.event.parent_id, 0) orelse return error.RouteTooLong; 191 parent.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 192 try bar.children.put(allocator, parent.getFocus().id, .{ .widget = .{ .text_box = parent }, .rect = null, .min_size = .{ .width = @max(parent_text.len, " replying to ".len) + 2, .height = null } }); 193 } 194 195 if (!entry.comment.removed and (session.data.is_local or session.data.user_id != null)) { 196 var remove = try linkBox(allocator, session, "✕", removeRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); 197 errdefer remove.deinit(allocator); 198 try bar.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); 199 } 200 201 bar.getFocus().child_id = bar.children.keys()[metadata_first_link_index]; 202 try box.children.put(allocator, bar.getFocus().id, .{ .widget = .{ .box = bar }, .rect = null, .min_size = null }); 203 } 204 205 const body_text = if (entry.comment.removed) "(removed)" else entry.comment.event.body; 206 var body_box = try wgt.TextBox.init(allocator, body_text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word }); 207 errdefer body_box.deinit(allocator); 208 body_box.getFocus().mode = .all; 209 try box.children.put(allocator, body_box.getFocus().id, .{ .widget = .{ .text_box = body_box }, .rect = null, .min_size = null }); 210 211 var gap = try wgt.Text.init(allocator, ""); 212 errdefer gap.deinit(allocator); 213 try box.children.put(allocator, gap.getFocus().id, .{ .widget = .{ .text = gap }, .rect = null, .min_size = null }); 214 215 box.getFocus().child_id = box.children.keys()[metadata_index]; 216 217 return .{ .box = box }; 218 } 219 220 fn metadata(self: *Item) *wgt.Box(ui.Widget) { 221 return &self.box.children.values()[metadata_index].widget.box; 222 } 223 224 fn body(self: *Item) *wgt.TextBox { 225 return &self.box.children.values()[body_index].widget.text_box; 226 } 227 228 pub fn bodyFocused(self: *Item) bool { 229 return self.box.getFocus().child_id == self.body().getFocus().id; 230 } 231 232 pub fn focusMetadata(self: *Item, root_focus: *Focus) void { 233 const bar = self.metadata(); 234 root_focus.setFocus(bar.getFocus().child_id orelse bar.children.keys()[metadata_first_link_index]); 235 } 236 237 pub fn focusBody(self: *Item, root_focus: *Focus) void { 238 root_focus.setFocus(self.body().getFocus().id); 239 } 240 241 pub fn moveHorizontal(self: *Item, root_focus: *Focus, right: bool) bool { 242 if (self.bodyFocused()) return false; 243 const bar = self.metadata(); 244 const selected = bar.getFocus().child_id orelse return false; 245 const selected_index = bar.children.getIndex(selected) orelse return false; 246 const target = if (right) selected_index + 1 else selected_index -| 1; 247 if (target < metadata_first_link_index or target >= bar.children.count()) return false; 248 root_focus.setFocus(bar.children.keys()[target]); 249 return true; 250 } 251 252 pub fn rowRect(self: *Item, body_row: bool) ?layout.IRect { 253 if (!body_row) return self.box.children.values()[metadata_index].rect; 254 var rect = self.box.children.values()[body_index].rect orelse return null; 255 if (self.box.children.values()[gap_index].rect) |gap| rect.size.height += gap.size.height; 256 return rect; 257 } 258 259 pub fn deinit(self: *Item, allocator: std.mem.Allocator) void { 260 self.box.deinit(allocator); 261 } 262 263 pub fn build(self: *Item, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 264 try self.box.build(allocator, constraint, root_focus); 265 } 266 267 pub fn input(self: *Item, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 268 try self.box.input(allocator, key, root_focus); 269 } 270 271 pub fn clearGrid(self: *Item) void { 272 self.box.clearGrid(); 273 } 274 275 pub fn getGrid(self: Item) ?Grid { 276 return self.box.getGrid(); 277 } 278 279 pub fn getFocus(self: *Item) *Focus { 280 return self.box.getFocus(); 281 } 282 }; 283 284 // append one comment's metadata bar and body. 285 pub fn appendComment(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, thread_kind: evt.EventKind, entry: CommentWithId) !void { 286 var item = try Item.init(allocator, session, identity, thread_kind, entry); 287 errdefer item.deinit(allocator); 288 try box.children.put(allocator, item.getFocus().id, .{ .widget = .{ .repo_comment = item }, .rect = null, .min_size = null }); 289 } 290 291 pub fn appendCount(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), count: usize, singular: []const u8, plural: []const u8) !void { 292 var text_buf: [64]u8 = undefined; 293 const text = try std.fmt.bufPrint(&text_buf, "{d} {s}", .{ count, if (count == 1) singular else plural }); 294 var count_box = try wgt.TextBox.init(allocator, text, .{ .border_style = .hidden, .wrap_kind = .none }); 295 errdefer count_box.deinit(allocator); 296 try box.children.put(allocator, count_box.getFocus().id, .{ .widget = .{ .text_box = count_box }, .rect = null, .min_size = null }); 297 } 298 299 pub fn appendWindowNav(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, thread_kind: evt.EventKind, thread_id: []const u8, selected_id: ?[]const u8, window: Window) !void { 300 const has_prev = window.start > 0; 301 const has_more = window.start < window.count and window.count - window.start > page_size; 302 if (!has_prev and !has_more) return; 303 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 304 errdefer row.deinit(allocator); 305 if (has_prev) { 306 const start = window.start -| page_size; 307 const route = commentsRoute(thread_kind, identity, thread_id, selected_id, start); 308 var previous = try linkBox(allocator, session, "← previous", route orelse return error.RouteTooLong); 309 errdefer previous.deinit(allocator); 310 try row.children.put(allocator, previous.getFocus().id, .{ .widget = .{ .text_box = previous }, .rect = null, .min_size = null }); 311 } 312 if (has_more) { 313 const start = window.start + page_size; 314 const route = commentsRoute(thread_kind, identity, thread_id, selected_id, start); 315 var next = try linkBox(allocator, session, "next →", route orelse return error.RouteTooLong); 316 errdefer next.deinit(allocator); 317 try row.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null }); 318 } 319 row.getFocus().child_id = row.children.keys()[0]; 320 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); 321 } 322 323 fn commentsRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, selected_id: ?[]const u8, start: usize) ?ui.RoutablePage { 324 return if (selected_id) |id| 325 ui.RoutablePage.repoThreadCommentRoute(kind, identity, thread_id, id, start) 326 else 327 ui.RoutablePage.repoThreadCommentsRoute(kind, identity, thread_id, start); 328 } 329 330 fn commentNewRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, parent_id: []const u8) ?ui.RoutablePage { 331 return ui.RoutablePage.repoThreadCommentNewRoute(kind, identity, thread_id, parent_id); 332 } 333 334 fn commentEditRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, comment_id: []const u8) ?ui.RoutablePage { 335 return ui.RoutablePage.repoThreadCommentEditRoute(kind, identity, thread_id, comment_id); 336 } 337 338 fn removeRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, comment_id: []const u8) ?ui.RoutablePage { 339 return ui.RoutablePage.repoThreadRemoveRoute(kind, identity, thread_id, comment_id); 340 } 341 342 pub fn linkBox(allocator: std.mem.Allocator, session: *ui.Session, text: []const u8, route: ui.RoutablePage) !wgt.TextBox { 343 var box = try wgt.TextBox.init(allocator, text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 344 errdefer box.deinit(allocator); 345 box.getFocus().mode = .all; 346 box.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 347 return box; 348 } 349