╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1289 const row = self.toolRow(index); │ │1290 const pa = self.session.page_arena.allocator(); │ │1291 │ │1292 + if (entryHasFork(entry) and self.session.data.current_page.parent() != .fork) { │ │1293 + const route = forkRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1294 + try addToolButton(allocator, row, "view fork", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}));│ │1295 + } │ │1296 + │ │1297 { │ │1298 var spacer = try Spacer.init(allocator); │ │1299 errdefer spacer.deinit(allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1312 const remove_route = ui.RoutablePage.repoThreadRemoveRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong;│ │1313 try addToolButton(allocator, row, "✕", try std.fmt.allocPrint(pa, "a:{s}", .{try remove_route.toUrl(self.session.page_arena)})); │ │1314 } │ │1279 - if (row.children.count() > first_in_row_index) │ │1280 - row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1315 } else { │ │1316 // a terminal has no file picker, so attaching is web only. the │ │1317 // renderer covers this button with a file input, so clicking it │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1354 const route = ui.RoutablePage.repoThreadRemoveRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong;│ │1355 try addToolButton(allocator, row, "✕", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │1356 } │ │1357 + } │ │1358 │ │1324 - // the leftmost button: the attachment button when the session has │ │1325 - // one, else the resolve button on a conflicted thread, the │ │1326 - // open/close button when present, the edit button otherwise │ │1327 - row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1359 + for (row.children.keys(), row.children.values()) |id, *child| { │ │1360 + if (!child.widget.getFocus().focusable) continue; │ │1361 + row.getFocus().child_id = id; │ │1362 + break; │ │1363 } │ │1364 } │ │1365 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1691 else => false, │ │1692 }; │ │1693 switch (key) { │ │1659 - .arrow_left => if (cur > first_in_row_index) root_focus.setFocus(row.children.keys()[cur - 1]) else self.focusList(index, root_focus),│ │1660 - .arrow_right => if (cur + 1 < row.children.count()) root_focus.setFocus(row.children.keys()[cur + 1]), │ │1694 + .arrow_left => { │ │1695 + var previous = cur; │ │1696 + while (previous > 0) { │ │1697 + previous -= 1; │ │1698 + if (!row.children.values()[previous].widget.getFocus().focusable) continue; │ │1699 + root_focus.setFocus(row.children.keys()[previous]); │ │1700 + return; │ │1701 + } │ │1702 + self.focusList(index, root_focus); │ │1703 + }, │ │1704 + .arrow_right => { │ │1705 + var next = cur + 1; │ │1706 + while (next < row.children.count()) : (next += 1) { │ │1707 + if (!row.children.values()[next].widget.getFocus().focusable) continue; │ │1708 + root_focus.setFocus(row.children.keys()[next]); │ │1709 + return; │ │1710 + } │ │1711 + }, │ │1712 .arrow_up => self.focusHeader(root_focus), │ │1713 .arrow_down => _ = self.moveDetailVertical(index, root_focus, true), │ │1714 .enter => if (on_primary) try self.primaryAction(allocator, index), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │2474 else => {}, │ │2475 } │ │2476 } │ │2426 - │ │2427 - // the tool row's first child after the spacer that pushes it right │ │2428 - const first_in_row_index: usize = 1; │ │2477 │ │2478 fn toolRowFocused(self: *This, index: usize, root_focus: *Focus) bool { │ │2479 return self.focusedDetailChild(index, root_focus) == tool_row_index;│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │2510 │ │2511 // return to the list. │ │2512 fn focusList(self: *This, index: usize, root_focus: *Focus) void {│ │2513 + if (self.detail_only) return; │ │2514 root_focus.setFocus(self.listScroll(index).getFocus().id); │ │2515 } │ │2516 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │2517 // cross to the header tabs above the stack. │ │2518 fn focusHeader(self: *This, root_focus: *Focus) void { │ │2519 + if (self.detail_only) return; │ │2520 root_focus.setFocus(self.box.children.keys()[header_index]);│ │2521 } │ │2522 │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │2530 │ │2531 pub fn getFocus(self: *This) *Focus { │ │2532 return self.box.getFocus(); │ │2533 + } │ │2534 + │ │2535 + pub fn atTop(self: *This, root_focus: *Focus) bool { │ │2536 + if (!self.detail_only) return self.getSelectedIndex() == 0; │ │2537 + const index = self.selectedSplitIndex() orelse return false; │ │2538 + const focused = self.focusedDetailChild(index, root_focus) orelse return false;│ │2539 + for (0..self.detailInner(index).children.count()) |child_index| { │ │2540 + if (!self.detailChildFocusable(index, child_index)) continue; │ │2541 + return focused == child_index; │ │2542 + } │ │2543 + return false; │ │2544 } │ │2545 │ │2546 // for the parent's "scroll up at the top jumps to the header" check: at the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ────────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭ added ───────────────╮ │src/ui/Fork/Header.zig│ ╰──────────────────────╯