╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 {│ ╰────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1280 pub fn build(self: *HeaderScroll, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │1281 self.clearGrid(); │ │1282 const row_box = self.row(); │ │1283 - // bring the focused child into view where the last build put it │ │1284 - if (!self.scroll.options.web_native) if (row_box.getFocus().child_id) |id| if (row_box.children.get(id)) |child| if (child.rect) |rect| self.scroll.scrollToRect(rect);│ │1283 + // bring the focused child into view where the last build put it. one │ │1284 + // wider than the window shows its start, since chasing both its edges │ │1285 + // would scroll back and forth on every build. │ │1286 + if (!self.scroll.options.web_native) if (row_box.getFocus().child_id) |id| if (row_box.children.get(id)) |child| if (child.rect) |rect| { │ │1287 + var target = rect; │ │1288 + if (self.scroll.viewport) |viewport| target.size.width = @min(rect.size.width, viewport.width); │ │1289 + self.scroll.scrollToRect(target); │ │1290 + }; │ │1291 │ │1292 // the window's width, or the children's minimums when they need more. │ │1293 // only a max, since a min would stretch the row's last laid out child. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 switch (host) { │ │159 .server => |server| { │ │160 // "repo/new" and "user/new" come before "new", which would claim them │ │161 - const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, addkey, removekey, settings, grant, revoke, promote, demote, protect, unprotect, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, subscribe, mute, undo, clear };│ │161 + const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, addkey, removekey, settings, grant, revoke, promote, demote, protect, unprotect, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, subscribe, mute, undo }; │ │162 inline for (@typeInfo(PostRoute).@"enum".field_names) |name| { │ │163 const suffix = "/" ++ name; │ │164 if (std.mem.endsWith(u8, path, suffix)) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 .publish => handlePatchPublish(io, request, allocator, base, host), │ │188 .merge => handlePatchMerge(io, request, allocator, base, host, .source), │ │189 .squash => handlePatchMerge(io, request, allocator, base, host, .squash),│ │190 - .undo => handleUndo(io, request, allocator, base, host, .undo), │ │191 - .clear => handleUndo(io, request, allocator, base, host, .clear), │ │190 + .undo => handleUndo(io, request, allocator, base, host), │ │191 .attach => handleAttach(io, request, allocator, base, host), │ │192 .assign => handleAssign(io, request, allocator, base, host), │ │193 .unassign => handleUnassign(io, request, allocator, base, host), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯