╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │1737 // indices within a split (the horizontal box inside the stack). │ │1738 const list_index: usize = 0; │ │1739 const detail_index: usize = 1; │ │1740 + // indices within the detail frame (the way back above the detail view).│ │1741 + const detail_back_index: usize = 0; │ │1742 + const detail_view_index: usize = 1; │ │1743 const list_max_width: usize = 35; │ │1744 const detail_min_width: usize = 40; │ │1745 │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2068 try box.children.put(allocator, list_scroll.getFocus().id, .{ .widget = .{ .scroll = list_scroll }, .rect = null, .min_size = .{ .width = list_max_width, .height = null }, .max_size = .{ .width = list_max_width, .height = null } });│ │2069 } │ │2070 │ │2068 - // frame the detail pane inside the master-detail split │ │2071 + // frame the detail pane inside the master-detail split, below the │ │2072 + // way back to the list, shown only while the list is hidden │ │2073 { │ │2074 var frame = try wgt.Box(Widget).init(allocator, .{ .border = .hidden, .direction = .vert }); │ │2075 errdefer frame.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2076 frame.getFocus().mode = .mouse; │ │2077 + { │ │2078 + const label = try session.page_arena.allocator().print("← back to {s} list", .{thread_name}); │ │2079 + var back = try wgt.TextBox.init(allocator, label, .{ .border = .single, .round_corners = true, .wrap_kind = .none }); │ │2080 + errdefer back.deinit(allocator); │ │2081 + back.getFocus().mode = .all; │ │2082 + try frame.children.put(allocator, back.getFocus().id, .{ .widget = .{ .text_box = back }, .rect = null, .min_size = null, .hidden = true });│ │2083 + } │ │2084 var detail_view = try DetailType.init(allocator, data, session, null, .{}); │ │2085 errdefer detail_view.deinit(allocator); │ │2086 frame.getFocus().child_id = detail_view.getFocus().id; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2516 return &self.listScroll(index).child.box; │ │2517 } │ │2518 │ │2519 + fn detailFrame(self: *This, index: usize) *wgt.Box(Widget) { │ │2520 + return &self.resultsBox(index).children.values()[detail_index].widget.box; │ │2521 + } │ │2522 + │ │2523 fn detail(self: *This, index: usize) *DetailType { │ │2509 - const frame = &self.resultsBox(index).children.values()[detail_index].widget.box; │ │2510 - return &@field(frame.children.values()[0].widget, Data.detail_widget_name); │ │2524 + return &@field(self.detailFrame(index).children.values()[detail_view_index].widget, Data.detail_widget_name); │ │2525 + } │ │2526 + │ │2527 + // the detail's way back to a list that isn't shown beside it │ │2528 + fn backId(self: *This, index: usize) ?usize { │ │2529 + const frame = self.detailFrame(index); │ │2530 + return if (frame.children.values()[detail_back_index].hidden) null else frame.children.keys()[detail_back_index];│ │2531 } │ │2532 │ │2533 fn window(self: *This, index: usize) *const Window { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2587 const selected = self.selectedThreadIndex(index) orelse return; │ │2588 const entry = self.window(index).items[selected]; │ │2589 const changed = if (self.detail(index).entry) |current| !std.mem.eql(u8, current.id, entry.id) else true; │ │2570 - if (changed) try self.detail(index).setEntry(allocator, entry); │ │2590 + if (!changed) return; │ │2591 + try self.detail(index).setEntry(allocator, entry); │ │2592 + // the way back links to the shown thread's own row, so both hosts │ │2593 + // hand its press and enter to input instead of navigating │ │2594 + const back = &self.detailFrame(index).children.values()[detail_back_index].widget.text_box; │ │2595 + back.getFocus().kind = .{ .custom = try rowLink(self.session.page_arena, self.data, entry.id) }; │ │2596 + } │ │2597 + │ │2598 + // show the way back to the list only while the list is hidden │ │2599 + fn showBack(self: *This, index: usize, shown: bool) void { │ │2600 + const frame = self.detailFrame(index); │ │2601 + frame.children.values()[detail_back_index].hidden = !shown; │ │2602 + if (!shown and frame.getFocus().child_id == frame.children.keys()[detail_back_index]) │ │2603 + frame.getFocus().child_id = frame.children.keys()[detail_view_index]; │ │2604 } │ │2605 │ │2606 pub fn build(self: *This, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2613 } │ │2614 │ │2615 if (self.selectedSplitIndex()) |i| { │ │2616 + // whether the detail pane fits beside the list, which decides │ │2617 + // the list cap below and whether the detail carries a way back │ │2618 + const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true;│ │2619 try self.refreshDetail(allocator, i); │ │2620 + self.showBack(i, !both_panes_fit); │ │2621 │ │2622 // the selected list row shows a border (the focused TextBox │ │2623 // upgrades it to a double border itself); the rest stay borderless. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2633 // beside it. the box drops the detail when the width can't hold │ │2634 // both minimums, so when it's that narrow we lift the cap and let │ │2635 // the list fill the whole width. │ │2599 - const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + detail_min_width else true; │ │2636 self.resultsBox(i).children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null;│ │2637 │ │2638 // stretch the detail pane across the rest of the width so it fills │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2724 } │ │2725 const i = self.selectedSplitIndex() orelse return; │ │2726 if (self.detailActive(i)) { │ │2727 + // the way back to the list, by enter or a click │ │2728 + if (self.backId(i)) |id| { │ │2729 + if (inp.activated(root_focus, id, key)) return self.focusList(i, root_focus);│ │2730 + if (root_focus.grandchild_id == id) return switch (key) { │ │2731 + .arrow_left => self.focusList(i, root_focus), │ │2732 + .arrow_up => self.focusHeader(root_focus), │ │2733 + .arrow_down => _ = self.detail(i).focusFirst(root_focus), │ │2734 + else => {}, │ │2735 + }; │ │2736 + } │ │2737 switch (try self.detail(i).inputWithExit(allocator, key, root_focus)) { │ │2738 .none => {}, │ │2739 .list => self.focusList(i, root_focus), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2694 - .header => self.focusHeader(root_focus), │ │2740 + .header => if (self.backId(i)) |id| root_focus.setFocus(id) else self.focusHeader(root_focus),│ │2741 } │ │2742 } else { │ │2743 try self.listInput(allocator, i, key, root_focus); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │2800 return; │ │2801 } │ │2802 switch (key) { │ │2757 - .enter, .arrow_right => _ = self.detail(index).focusFirst(root_focus),│ │2803 + .enter, .arrow_right => self.focusDetail(index, root_focus), │ │2804 // when the window is too narrow to lay out the detail pane beside │ │2805 // the list, a click on a row opens it like enter. the row was just │ │2806 // selected, so the detail is swapped to it ahead of the build. │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯