╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Issues.zig ───────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
422 // up/down (and the scroll wheel) move the selection a row; page up/down 423 // jump a fixed amount. right/Enter cross into the detail pane. 424 if (inp.rowDelta(key, @intCast(self.listBox().children.count()))) |delta| { 425 ui.moveRowFocus(self.listBox(), self.listScroll(), root_focus, delta); 426 return; 427 } 428 switch (key) { 429 .enter, .arrow_right => try self.focusDetail(root_focus), 430 else => {}, 431 } 432 } 433 434 fn detailInput(self: *View, key: Key, root_focus: *Focus) !void { 435 if (self.tagsFocused()) { 436 try self.tagsInput(key, root_focus); 437 } else { 438 try self.descriptionInput(key, root_focus); 439 } 440 } 441 442 fn descriptionInput(self: *View, key: Key, root_focus: *Focus) !void { 443 const sc = self.detailScroll(); 444 switch (key) { 445 .arrow_left => return self.focusList(root_focus), 446 // once the scroll can't move further, cross into the tags. 447 .arrow_up => { 448 const before = sc.y; 449 sc.y -= 1; 450 sc.clampToContent(); 451 if (sc.y == before) try self.focusTags(root_focus); 452 return; 453 }, 454 .arrow_down => sc.y += 1, 455 .page_up => sc.y -= 10, 456 .page_down => sc.y += 10, 457 .home => sc.y = 0, 458 .end => sc.y = std.math.maxInt(isize), 459 .mouse => |mouse| switch (mouse.action) { 460 .scroll => |dir| sc.y += if (dir == .up) @as(isize, -1) else 1, 461 else => {}, 462 }, 463 else => return, 464 } 465 sc.clampToContent(); 466 } 467 468 // arrow keys move the tag selection; at the flow's edges focus crosses to 469 // the neighboring widgets. 470 fn tagsInput(self: *View, key: Key, root_focus: *Focus) !void { 471 const tf = self.tagFlow() orelse return; 472 const cid = tf.focus.child_id orelse return; 473 const cur = tf.indexOfFocusId(cid) orelse return; 474 const count = tf.text_boxes.items.len; 475 const sc = self.detailScroll(); 476 switch (key) { 477 .arrow_left => if (cur > 0) self.focusTag(tf, root_focus, cur - 1) else try self.focusList(root_focus), 478 .arrow_right => if (cur + 1 < count) self.focusTag(tf, root_focus, cur + 1), 479 .arrow_up => if (tf.rowStep(cur, false)) |i| self.focusTag(tf, root_focus, i), 480 .arrow_down => if (tf.rowStep(cur, true)) |i| self.focusTag(tf, root_focus, i) else try self.focusDescription(root_focus), 481 .home => self.focusTag(tf, root_focus, 0), 482 .end => self.focusTag(tf, root_focus, count - 1), 483 .mouse => |mouse| switch (mouse.action) { 484 .scroll => |dir| { 485 sc.y += if (dir == .up) @as(isize, -1) else 1; 486 sc.clampToContent(); 487 }, 488 else => {}, 489 }, 490 else => {}, 491 } 492 } 493 494 const tags_child_index: usize = 0; 495 496 fn tagFlow(self: *View) ?*ui.TagFlow { 497 const inner = self.detailInner(); 498 if (inner.children.count() == 0) return null; 499 return switch (inner.children.values()[tags_child_index].widget) { 500 .tag_flow => |*tf| tf, 501 else => null, 502 }; 503 } 504 505 fn tagsFocused(self: *View) bool { 506 const inner = self.detailInner(); 507 const cid = inner.getFocus().child_id orelse return false; 508 return inner.children.getIndex(cid) == tags_child_index and self.tagFlow() != null; 509 } 510 511 fn focusTag(self: *View, tf: *ui.TagFlow, root_focus: *Focus, index: usize) void { 512 root_focus.setFocus(tf.text_boxes.items[index].getFocus().id); 513 // keep the tag visible on the terminal: its rect offset by the flow's 514 // position in the pane. 515 if (self.session.is_terminal and index < tf.rects.items.len) { 516 if (self.detailInner().children.values()[tags_child_index].rect) |flow_rect| { 517 var rect = tf.rects.items[index]; 518 rect.x += flow_rect.x; 519 rect.y += flow_rect.y; 520 self.detailScroll().scrollToRect(rect); 521 } 522 } 523 } 524 525 fn focusTags(self: *View, root_focus: *Focus) !void { 526 const tf = self.tagFlow() orelse return; 527 if (tf.text_boxes.items.len == 0) return; 528 const cid = tf.focus.child_id orelse tf.text_boxes.items[0].getFocus().id; 529 const index = tf.indexOfFocusId(cid) orelse 0; 530 self.focusTag(tf, root_focus, index); 531 } 532 533 fn focusDescription(self: *View, root_focus: *Focus) !void { 534 const inner = self.detailInner(); 535 const index: usize = if (self.tagFlow() != null) 1 else 0; 536 if (inner.children.count() <= index) return; 537 root_focus.setFocus(inner.children.keys()[index]); 538 } 539 540 // enter the detail pane. an empty pane (no issues) can't be entered. 541 fn focusDetail(self: *View, root_focus: *Focus) !void { 542 if (self.detailInner().children.count() == 0) return; 543 root_focus.setFocus(self.detailOuter().getFocus().id); 544 } 545 546 // return to the list. 547 fn focusList(self: *View, root_focus: *Focus) !void { 548 root_focus.setFocus(self.listScroll().getFocus().id); 549 } 550 551 pub fn clearGrid(self: *View) void { 552 self.box.clearGrid(); 553 } 554 555 pub fn getGrid(self: View) ?Grid { 556 return self.box.getGrid(); 557 } 558 559 pub fn getFocus(self: *View) *Focus { 560 return self.box.getFocus(); 561 } 562 563 // for the parent's "scroll up at the top jumps to the header" check. when 564 // the list holds focus, report its selected row directly. 565 pub fn getSelectedIndex(self: *View) ?usize { 566 if (self.detailActive()) { 567 return if (self.detailScroll().y == 0 and self.paneTopFocused()) 0 else 1; 568 } 569 const lb = self.listBox(); 570 const cid = lb.getFocus().child_id orelse return null; 571 return lb.children.getIndex(cid); 572 } 573 574 // whether the focused element is the detail pane's topmost one, so up has 575 // nowhere further to go within the pane. 576 fn paneTopFocused(self: *View) bool { 577 const tf = self.tagFlow() orelse return true; 578 if (!self.tagsFocused()) return false; 579 const cid = tf.focus.child_id orelse return true; 580 const cur = tf.indexOfFocusId(cid) orelse return true; 581 return tf.rowStep(cur, false) == null; 582 } 583 }; 584 585 // the "a:" navigation link for the issues page filtered to the url-encoded 586 // `tag` and rooted at issue `id` within `identity` ("owner/name"). 587 fn issuesLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8, id: []const u8) ![]const u8 { 588 const route = ui.RoutablePage.repoIssuesRoute(identity, tag, id) orelse return error.RouteTooLong; 589 const url = try route.urlAlloc(page_arena); 590 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 591 } 592 593 // the in-page "ai:" anchor for selecting issue `id` in `identity`'s list; the 594 // href is only followed with js off. 595 fn issueRowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, id: []const u8) ![]const u8 { 596 const route = ui.RoutablePage.repoIssuesRoute(identity, "", id) orelse return error.RouteTooLong; 597 const url = try route.urlAlloc(page_arena); 598 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); 599 } 600 601 // the "a:" link to the issues list filtered to `tag` (raw; encoded here). 602 fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8) ![]const u8 { 603 const encoded = try ui.urlEncodeRef(page_arena.allocator(), tag); 604 const route = ui.RoutablePage.repoIssuesRoute(identity, encoded, "") orelse return error.RouteTooLong; 605 const url = try route.urlAlloc(page_arena); 606 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 607 } 608