╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │355 const has_status = @hasField(Event, "status"); │ │356 const supports_conflicts = Event.merge_policy == .field_conflicts; │ │357 const supports_drafts = @hasField(Self, "drafts"); │ │358 + const supports_forks = @hasField(Entry, "fork_exists"); │ │359 const FieldConflict = if (supports_conflicts) Data.FieldConflict else void;│ │360 const ViewKind = Data.ViewKind; │ │361 const thread_name = Data.thread_name; │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────╮ │374 title_id: [view_count]?usize, │ │375 description_id: [view_count]?usize,│ │376 author_id: [view_count]?usize, │ │377 + detail_only: bool, │ │378 │ │379 const header_index: usize = 0; │ │380 const stack_index: usize = 1; │ ╰────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │434 return if (supports_drafts) entry.draft else false; │ │435 } │ │436 │ │437 + fn entryHasFork(entry: Entry) bool { │ │438 + return if (supports_forks) entry.fork_exists else false;│ │439 + } │ │440 + │ │441 const StatusChange = struct { │ │442 action: []const u8, │ │443 status: Status, │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │464 return if (supports_drafts) Data.draftsRoute(identity) else null; │ │465 } │ │466 │ │467 + fn forkRoute(identity: []const u8, id: []const u8) ?ui.RoutablePage { │ │468 + return if (supports_forks) Data.forkRoute(identity, id) else null; │ │469 + } │ │470 + │ │471 fn conflictsRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage { │ │472 return if (supports_conflicts) Data.conflictsRoute(identity, selected) else null;│ │473 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │669 .title_id = @splat(null), │ │670 .description_id = @splat(null), │ │671 .author_id = @splat(null), │ │672 + .detail_only = false, │ │673 }; │ │674 + } │ │675 + │ │676 + // build the selected thread's existing detail pane without its list or │ │677 + // thread-page header, for pages that provide their own surrounding UI. │ │678 + pub fn initDetail(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !This { │ │679 + var self = try init(allocator, data, session); │ │680 + errdefer self.deinit(allocator); │ │681 + self.detail_only = true; │ │682 + self.box.getFocus().child_id = self.box.children.keys()[stack_index]; │ │683 + const index = self.selectedSplitIndex() orelse return error.NotFound; │ │684 + self.resultsBox(index).getFocus().child_id = self.resultsBox(index).children.keys()[detail_index];│ │685 + return self; │ │686 } │ │687 │ │688 // the master-detail split showing `status`'s window, or the conflicts │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1229 // beside it. the box drops the detail when the width can't hold │ │1230 // both minimums, so when it's that narrow we lift the cap and let │ │1231 // the list fill the whole width. │ │1209 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │1210 - self.resultsBox(i).children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │1232 + if (self.detail_only) { │ │1233 + self.box.children.values()[header_index].min_size = .{ .width = null, .height = 0 }; │ │1234 + self.box.children.values()[header_index].max_size = .{ .width = null, .height = 0 }; │ │1235 + self.resultsBox(i).children.values()[list_index].min_size = .{ .width = 0, .height = null }; │ │1236 + self.resultsBox(i).children.values()[list_index].max_size = .{ .width = 0, .height = null }; │ │1237 + self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = constraint.max_size.width, .height = null }; │ │1238 + } else { │ │1239 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │1240 + self.resultsBox(i).children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; │ │1241 │ │1212 - // stretch the detail pane across the rest of the width so it fills │ │1213 - // the area rather than shrinking to its content; its scroll fills │ │1214 - // the pane. │ │1215 - if (constraint.max_size.width) |w| { │ │1216 - self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null }; │ │1217 - } else { │ │1218 - self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │1242 + // stretch the detail pane across the rest of the width so it fills │ │1243 + // the area rather than shrinking to its content; its scroll fills │ │1244 + // the pane. │ │1245 + if (constraint.max_size.width) |w| { │ │1246 + self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = if (both_panes_fit) w - list_max_width else w, .height = null };│ │1247 + } else { │ │1248 + self.resultsBox(i).children.values()[detail_index].min_size = .{ .width = detail_min_width, .height = null }; │ │1249 + } │ │1250 } │ │1251 } │ │1252 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯