╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Undo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
554 // the fixed rows are filled first, since adding the ones below 555 // invalidates every pointer into the box 556 { 557 const button = &details.children.values()[0].widget.text_box; 558 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"); 559 button.getFocus().kind = if (enabled) .{ .custom = "submit" } else .text_box; 560 } 561 try details.children.values()[1].widget.text_box.setContent(allocator, item.timestamp); 562 563 // some actions put buttons where the description would go 564 if (item.buttons.len == 0) { 565 try addLabeled(allocator, details, " description ", item.description, null); 566 } else for (item.buttons) |detail_button| { 567 try addLabeled(allocator, details, detail_button.label, detail_button.text, detail_button.link); 568 } 569 if (failure) |message| try addText(allocator, details, try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{message}), null, .single); 570 self.shown_failure = failure; 571 details.getFocus().kind = if (enabled) .{ .custom = item.form } else .container; 572 details.getFocus().child_id = details.children.keys()[0]; 573 self.detailScroll().x = 0; 574 self.detailScroll().y = 0; 575 self.detailScroll().getFocus().version +%= 1; 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 if (self.data.clear) return try self.box.build(allocator, constraint, root_focus); 582 try self.refreshDetail(allocator); 583 const rows = &self.listScroll().child.box; 584 if (self.data.items.len == 0) { 585 const failure = self.session.data.undo_failure orelse self.data.failure; 586 if (!std.meta.eql(self.shown_failure, failure)) { 587 const message = if (failure) |name| try std.fmt.allocPrint(self.session.page_arena.allocator(), "error: {s}", .{name}) else "no undo history"; 588 try rows.children.values()[0].widget.text_box.setContent(allocator, message); 589 self.shown_failure = failure; 590 } 591 } 592 for (rows.children.keys(), rows.children.values()) |id, *child| ui.widget.markSelected(&child.widget.text_box, rows.getFocus().child_id == id); 593 const both_fit = if (constraint.max_size.width) |width| width >= list_max_width + detail_min_width else true; 594 self.contentBox().children.values()[0].max_size = if (both_fit) .{ .width = list_max_width, .height = null } else null; 595 const width = if (constraint.max_size.width) |value| if (both_fit) value - list_max_width else value else detail_min_width; 596 self.contentBox().children.values()[1].min_size = .{ .width = width, .height = null }; 597 for (self.detailScroll().child.box.children.values()) |*child| child.max_size = .{ .width = width -| 2, .height = null }; 598 try self.box.build(allocator, constraint, root_focus); 599 } 600 601 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 602 // the sub header sits above whichever content the route selects. 603 // scrolling crosses the boundary the same way the arrow keys do 604 const direction = inp.vertDirection(key); 605 if (self.headerActive()) { 606 if (direction == .down) root_focus.setFocus(self.box.children.values()[content_index].widget.getFocus().id); 607 return; 608 } 609 if (self.data.clear) { 610 if (direction == .up) root_focus.setFocus(self.headerBox().getFocus().id) else try self.clearInput(key, root_focus); 611 return; 612 } 613 if (direction == .up and self.contentAtTop()) { 614 root_focus.setFocus(self.headerBox().getFocus().id); 615 return; 616 } 617 try self.refreshDetail(allocator); 618 if (self.detailActive()) { 619 const details = &self.detailScroll().child.box; 620 if (self.selected()) |index| { 621 if (activated(root_focus, details.children.keys()[0], key) and self.data.can_undo and self.data.items[index].index > 0) { 622 self.requestUndo(ui.RoutablePage.repoUndoRoute(self.data.identity, self.data.items[index].index) orelse return error.RouteTooLong); 623 return; 624 } 625 } 626 if (key == .arrow_left) root_focus.setFocus(self.listScroll().getFocus().id) else if (inp.rowDelta(key, @intCast(details.children.count()))) |delta| ui.widget.moveRowFocus(details, self.detailScroll(), root_focus, delta); 627 } else if (inp.rowDelta(key, @intCast(self.listScroll().child.box.children.count()))) |delta| { 628 ui.widget.moveRowFocus(&self.listScroll().child.box, self.listScroll(), root_focus, delta); 629 } else switch (key) { 630 .arrow_right, .enter => if (self.selected() != null) { 631 root_focus.setFocus(self.detailScroll().getFocus().id); 632 }, 633 else => {}, 634 } 635 } 636 637 // the confirm button submits the form the web renderer posts 638 fn clearInput(self: *View, key: Key, root_focus: *Focus) !void { 639 const center = &self.box.children.values()[content_index].widget.center; 640 const button_id = center.child.box.getFocus().child_id orelse return; 641 if (!activated(root_focus, button_id, key) or !self.data.can_undo) return; 642 self.requestUndo(ui.RoutablePage.repoUndoClearRoute(self.data.identity) orelse return error.RouteTooLong); 643 } 644 645 fn contentAtTop(self: *View) bool { 646 const scroll = if (self.detailActive()) self.detailScroll() else self.listScroll(); 647 const rows = &scroll.child.box; 648 const id = rows.getFocus().child_id orelse return true; 649 return rows.children.getIndex(id) == 0 and scroll.y == 0; 650 } 651 652 // moving up from the sub header returns to the page's tabs 653 pub fn atTop(self: *View) bool { 654 return self.headerActive(); 655 } 656 657 // arriving from the page's tabs lands on the sub header, not the content 658 pub fn focusHeader(self: *View, root_focus: *Focus) bool { 659 root_focus.setFocus(self.headerBox().getFocus().id); 660 return true; 661 } 662 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 663 self.box.deinit(allocator); 664 } 665 pub fn clearGrid(self: *View) void { 666 self.box.clearGrid(); 667 } 668 pub fn getGrid(self: View) ?Grid { 669 return self.box.getGrid(); 670 } 671 pub fn getFocus(self: *View) *Focus { 672 return self.box.getFocus(); 673 } 674 }; 675