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