╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Undo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
546 entry.value.widget.deinit(allocator); 547 } 548 const item = self.data.items[index]; 549 const enabled = self.data.handle.canUndo() and item.index > 0; 550 551 // the fixed rows are filled first, since adding the ones below 552 // invalidates every pointer into the box 553 { 554 const button = &details.children.values()[0].widget.text_box; 555 try button.setContent(allocator, if (item.index == 0) "initial state cannot be undone" else if (item.index == self.data.count - 1) "undo this" else "undo this and all above it"); 556 button.getFocus().kind = if (enabled) .{ .custom = "submit" } else .text_box; 557 } 558 const timestamp = try ui.timestamp.format(self.session.page_arena.allocator(), item.timestamp, try self.session.nowSeconds()); 559 try details.children.values()[1].widget.text_box.setContent(allocator, timestamp.relative); 560 details.children.values()[1].widget.text_box.options.bottom_label.text = timestamp.exact; 561 562 // some actions put buttons where the description would go 563 if (item.buttons.len == 0) { 564 try addLabeled(allocator, details, " description ", item.description, null); 565 } else for (item.buttons) |detail_button| { 566 try addLabeled(allocator, details, detail_button.label, detail_button.text, detail_button.link); 567 } 568 if (failure) |message| try addText(allocator, details, try self.session.page_arena.allocator().print("error: {s}", .{message}), null, .single); 569 self.shown_failure = failure; 570 details.getFocus().kind = if (enabled) .{ .custom = item.form } else .container; 571 details.getFocus().child_id = details.children.keys()[0]; 572 self.detailScroll().x = 0; 573 self.detailScroll().y = 0; 574 self.detailScroll().getFocus().version +%= 1; 575 ui.widget.setListBackLink(self.detailFrame(), item.link); 576 self.detailed_index = index; 577 } 578 579 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 580 self.clearGrid(); 581 // the clear form or the empty message stands in for the list 582 if (self.data.clear or self.data.isEmpty(self.session)) return try self.box.build(allocator, constraint, root_focus); 583 try self.refreshDetail(allocator); 584 const rows = &self.listScroll().child.box; 585 if (self.data.items.len == 0) { 586 // an empty list only shows to report its failure 587 const failure = self.session.data.undo_failure orelse self.data.failure orelse unreachable; 588 if (!std.meta.eql(self.shown_failure, failure)) { 589 try rows.children.values()[0].widget.text_box.setContent(allocator, try self.session.page_arena.allocator().print("error: {s}", .{failure})); 590 self.shown_failure = failure; 591 } 592 } 593 for (rows.children.keys(), rows.children.values()) |id, *child| ui.widget.markSelected(&child.widget.text_box, rows.getFocus().child_id == id); 594 const both_fit = if (constraint.max_size.width) |width| width >= list_max_width + detail_min_width else true; 595 ui.widget.showListBack(self.detailFrame(), !both_fit); 596 self.contentBox().children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; 597 const width = if (constraint.max_size.width) |value| if (both_fit) value - list_max_width else value else detail_min_width; 598 self.contentBox().children.values()[1].min_size = .{ .width = width, .height = null }; 599 for (self.detailScroll().child.box.children.values()) |*child| child.max_size = .{ .width = width -| 2, .height = null }; 600 try self.box.build(allocator, constraint, root_focus); 601 } 602 603 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 604 // the sub header sits above whichever content the route selects. 605 // scrolling crosses the boundary the same way the arrow keys do 606 const direction = inp.vertDirection(key); 607 if (self.headerActive()) { 608 // the empty message has nothing to focus 609 if (direction == .down and !self.data.isEmpty(self.session)) root_focus.setFocus(self.box.children.values()[content_index].widget.getFocus().id); 610 return; 611 } 612 if (self.data.clear) { 613 if (direction == .up) root_focus.setFocus(self.headerBox().getFocus().id) else try self.clearInput(key, root_focus); 614 return; 615 } 616 // the way back to the list, by enter or a click 617 if (self.detailActive()) if (ui.widget.listBackId(self.detailFrame())) |id| { 618 if (inp.activated(root_focus, id, key)) return self.focusList(root_focus); 619 if (root_focus.grandchild_id == id) return switch (direction) { 620 .up => root_focus.setFocus(self.headerBox().getFocus().id), 621 .down => root_focus.setFocus(self.detailScroll().getFocus().id), 622 .none => if (key == .arrow_left) self.focusList(root_focus), 623 }; 624 if (direction == .up and self.contentAtTop()) return root_focus.setFocus(id); 625 }; 626 if (direction == .up and self.contentAtTop()) { 627 root_focus.setFocus(self.headerBox().getFocus().id); 628 return; 629 } 630 try self.refreshDetail(allocator); 631 if (self.detailActive()) { 632 const details = &self.detailScroll().child.box; 633 if (self.selected()) |index| { 634 if (inp.activated(root_focus, details.children.keys()[0], key) and self.data.handle.canUndo() and self.data.items[index].index > 0) { 635 self.requestUndo(ui.RoutablePage.repoUndoRoute(self.data.identity, self.data.items[index].index) orelse return error.RouteTooLong); 636 return; 637 } 638 } 639 if (inp.rowDelta(key, @intCast(details.children.count()))) |delta| { 640 ui.widget.moveRowFocus(details, self.detailScroll(), root_focus, delta); 641 } else if (key == .arrow_left) { 642 self.focusList(root_focus); 643 } 644 } else if (inp.rowDelta(key, @intCast(self.listScroll().child.box.children.count()))) |delta| { 645 ui.widget.moveRowFocus(&self.listScroll().child.box, self.listScroll(), root_focus, delta); 646 } else switch (key) { 647 .arrow_right, .enter => if (self.selected() != null) self.focusDetail(root_focus), 648 // when the window is too narrow to lay out the detail pane beside 649 // the list, a click on a row opens it like enter. the detail was 650 // swapped to the just-selected row above. 651 .mouse => |mouse| if (self.selected() != null and self.contentBox().children.values()[1].rect == null and ui.widget.clickOnSelectedRow(&self.listScroll().child.box, root_focus, mouse)) { 652 self.focusDetail(root_focus); 653 }, 654 else => {}, 655 } 656 } 657 658 // enter the detail pane, landing on the way back to the list when it shows 659 fn focusDetail(self: *View, root_focus: *Focus) void { 660 if (ui.widget.listBackId(self.detailFrame()) != null) return ui.widget.focusListBack(self.detailFrame(), root_focus); 661 root_focus.setFocus(self.detailScroll().getFocus().id); 662 } 663 664 fn focusList(self: *View, root_focus: *Focus) void { 665 root_focus.setFocus(self.listScroll().getFocus().id); 666 } 667 668 // the confirm button submits the form the web renderer posts 669 fn clearInput(self: *View, key: Key, root_focus: *Focus) !void { 670 const center = &self.box.children.values()[content_index].widget.center; 671 const button_id = center.child.box.getFocus().child_id orelse return; 672 if (!inp.activated(root_focus, button_id, key) or !self.data.handle.canUndo()) return; 673 self.requestUndo(ui.RoutablePage.repoUndoClearRoute(self.data.identity) orelse return error.RouteTooLong); 674 } 675 676 fn contentAtTop(self: *View) bool { 677 const scroll = if (self.detailActive()) self.detailScroll() else self.listScroll(); 678 const rows = &scroll.child.box; 679 const id = rows.getFocus().child_id orelse return true; 680 return rows.children.getIndex(id) == 0 and scroll.y == 0; 681 } 682 683 // moving up from the sub header returns to the page's tabs 684 pub fn atTop(self: *View) bool { 685 return self.headerActive(); 686 } 687 688 // arriving from the page's tabs lands on the sub header, not the content 689 pub fn focusHeader(self: *View, root_focus: *Focus) bool { 690 root_focus.setFocus(self.headerBox().getFocus().id); 691 return true; 692 } 693 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 694 self.box.deinit(allocator); 695 } 696 pub fn clearGrid(self: *View) void { 697 self.box.clearGrid(); 698 } 699 pub fn getGrid(self: View) ?Grid { 700 return self.box.getGrid(); 701 } 702 pub fn getFocus(self: *View) *Focus { 703 return self.box.getFocus(); 704 } 705 }; 706