╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │473 self.session.host_request = .{ .undo = route.repo_undo }; │ │474 } │ │475 │ │515 - fn headerBox(self: *View) *wgt.Box(ui.Widget) { │ │516 - return self.box.children.values()[header_index].widget.header_scroll.row(); │ │476 + fn header(self: *View) ?*ui.widget.CopyableText { │ │477 + return switch (self.box.children.values()[header_index].widget) { │ │478 + .header_scroll => |*header_scroll| &header_scroll.row().children.values()[0].widget.copyable_text,│ │479 + else => null, │ │480 + }; │ │481 } │ │482 fn headerActive(self: *View) bool { │ │483 + if (self.header() == null) return false; │ │484 return self.box.getFocus().child_id == self.box.children.keys()[header_index]; │ │485 } │ │486 + // the content is last, below the sub header where there is one │ │487 fn contentBox(self: *View) *wgt.Box(ui.Widget) { │ │522 - return &self.box.children.values()[content_index].widget.box; │ │488 + return &self.box.children.values()[self.box.children.count() - 1].widget.box; │ │489 } │ │490 fn listScroll(self: *View) *wgt.Scroll(ui.Widget) { │ │491 return &self.contentBox().children.values()[0].widget.scroll; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │547 │ │548 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │549 self.clearGrid(); │ │584 - // the clear form or the empty message stands in for the list │ │585 - if (self.data.clear or self.data.isEmpty(self.session)) return try self.box.build(allocator, constraint, root_focus);│ │550 + // the empty message stands in for the list │ │551 + if (self.data.isEmpty(self.session)) return try self.box.build(allocator, constraint, root_focus); │ │552 try self.refreshDetail(allocator); │ │553 const rows = &self.listScroll().child.box; │ │554 if (self.data.items.len == 0) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │570 } │ │571 │ │572 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │607 - // the sub header sits above whichever content the route selects. │ │608 - // scrolling crosses the boundary the same way the arrow keys do │ │573 + // scrolling crosses the sub header boundary the same way the arrow keys do │ │574 const direction = inp.vertDirection(key); │ │575 if (self.headerActive()) { │ │611 - // the empty message has nothing to focus │ │612 - if (direction == .down and !self.data.isEmpty(self.session)) root_focus.setFocus(self.box.children.values()[content_index].widget.getFocus().id);│ │613 - return; │ │614 - } │ │615 - if (self.data.clear) { │ │616 - if (direction == .up) root_focus.setFocus(self.headerBox().getFocus().id) else try self.clearInput(key, root_focus); │ │617 - return; │ │576 + // active means there is one │ │577 + const copyable_text = self.header() orelse unreachable; │ │578 + if (direction == .down) { │ │579 + // the empty message has nothing to focus │ │580 + if (!self.data.isEmpty(self.session)) root_focus.setFocus(self.contentBox().getFocus().id); │ │581 + return; │ │582 + } │ │583 + return copyable_text.input(allocator, key, root_focus); │ │584 } │ │585 + // the empty message has nothing to focus │ │586 + if (self.data.isEmpty(self.session)) return; │ │587 // the way back to the list, by enter or a click │ │588 if (self.detailActive()) if (ui.widget.listBackId(self.detailFrame())) |id| { │ │589 if (inp.activated(root_focus, id, key)) return self.focusList(root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │590 if (root_focus.grandchild_id == id) return switch (direction) { │ │623 - .up => root_focus.setFocus(self.headerBox().getFocus().id), │ │591 + // without a sub header, atTop hands it to the page's tabs │ │592 + .up => _ = self.focusHeader(root_focus), │ │593 .down => root_focus.setFocus(self.detailScroll().getFocus().id),│ │594 .none => if (key == .arrow_left) self.focusList(root_focus), │ │595 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │596 if (direction == .up and self.contentAtTop()) return root_focus.setFocus(id); │ │597 }; │ │629 - if (direction == .up and self.contentAtTop()) { │ │630 - root_focus.setFocus(self.headerBox().getFocus().id); │ │631 - return; │ │632 - } │ │598 + if (direction == .up and self.contentAtTop() and self.focusHeader(root_focus)) return;│ │599 try self.refreshDetail(allocator); │ │600 if (self.detailActive()) { │ │601 const details = &self.detailScroll().child.box; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │632 │ │633 fn focusList(self: *View, root_focus: *Focus) void { │ │634 root_focus.setFocus(self.listScroll().getFocus().id); │ │669 - } │ │670 - │ │671 - // the confirm button submits the form the web renderer posts │ │672 - fn clearInput(self: *View, key: Key, root_focus: *Focus) !void { │ │673 - const center = &self.box.children.values()[content_index].widget.center; │ │674 - const button_id = center.child.box.getFocus().child_id orelse return; │ │675 - if (!inp.activated(root_focus, button_id, key) or !self.data.handle.canUndo()) return; │ │676 - self.requestUndo(ui.RoutablePage.repoUndoClearRoute(self.data.identity) orelse return error.RouteTooLong);│ │635 } │ │636 │ │637 fn contentAtTop(self: *View) bool { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │641 return rows.children.getIndex(id) == 0 and scroll.y == 0; │ │642 } │ │643 │ │686 - // moving up from the sub header returns to the page's tabs │ │644 + // moving up from the sub header returns to the page's tabs, as does moving │ │645 + // up from the top of the content where there is no sub header │ │646 pub fn atTop(self: *View) bool { │ │688 - return self.headerActive(); │ │647 + if (self.header() != null) return self.headerActive(); │ │648 + if (self.data.isEmpty(self.session)) return true; │ │649 + // the way back to the list sits above the detail while it shows │ │650 + if (self.detailActive()) if (ui.widget.listBackId(self.detailFrame())) |id| return self.detailFrame().getFocus().child_id == id;│ │651 + return self.contentAtTop(); │ │652 } │ │653 │ │654 // arriving from the page's tabs lands on the sub header, not the content │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │655 pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ │693 - root_focus.setFocus(self.headerBox().getFocus().id); │ │656 + const copyable_text = self.header() orelse return false; │ │657 + root_focus.setFocus(copyable_text.getFocus().id); │ │658 return true; │ │659 } │ │660 pub fn deinit(self: *View, allocator: std.mem.Allocator) void {│ ╰────────────────────────────────────────────────────────────────────────╯