╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/User/Header.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │165 text_box.getFocus().kind = .{ .custom = settings_link }; │ │166 try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │167 if (std.mem.eql(u8, settings_link, selected_link)) selected_tab = text_box.getFocus().id;│ │156 - try box.children.put(allocator, text_box.getFocus().id, .{ │ │168 + try tabs_box.children.put(allocator, text_box.getFocus().id, .{ │ │169 .widget = .{ .text_box = text_box }, │ │170 .rect = null, │ │171 .min_size = .{ .width = settings_tab_label.len + 2, .height = null }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │180 auth_tab.text_box.getFocus().kind = .{ .custom = auth_link }; │ │181 try tab_ids.put(allocator, auth_tab.getFocus().id, {}); │ │182 if (std.mem.eql(u8, auth_link, selected_link)) selected_tab = auth_tab.getFocus().id;│ │171 - try box.children.put(allocator, auth_tab.getFocus().id, .{ │ │183 + try tabs_box.children.put(allocator, auth_tab.getFocus().id, .{ │ │184 .widget = .{ .auth_tab = auth_tab }, │ │185 .rect = null, │ │186 .min_size = .{ .width = auth_tab.minWidth(), .height = null }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │194 text_box.getFocus().mode = .all; │ │195 text_box.getFocus().kind = .{ .custom = ui.Quit.tab_kind }; │ │196 try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │185 - try box.children.put(allocator, text_box.getFocus().id, .{ │ │197 + try tabs_box.children.put(allocator, text_box.getFocus().id, .{│ │198 .widget = .{ .text_box = text_box }, │ │199 .rect = null, │ │200 // the label is a single column; +2 for the border │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │202 }); │ │203 } │ │204 │ │193 - var self = View{ │ │205 + tabs_box.getFocus().child_id = selected_tab orelse tab_ids.keys()[0]; │ │206 + const title_id = title_box.getFocus().id; │ │207 + const tabs_id = tabs_box.getFocus().id; │ │208 + try box.children.put(allocator, title_id, .{ .widget = .{ .box = title_box }, .rect = null, .min_size = null }); │ │209 + title_box_owned = true; │ │210 + // a blank row between the title and the tabs, shown only when they wrap │ │211 + const gap_id = blk: { │ │212 + var gap = try wgt.Text.init(allocator, " "); │ │213 + errdefer gap.deinit(allocator); │ │214 + const id = gap.getFocus().id; │ │215 + try box.children.put(allocator, id, .{ .widget = .{ .text = gap }, .rect = null, .min_size = null, .hidden = true }); │ │216 + break :blk id; │ │217 + }; │ │218 + try box.children.put(allocator, tabs_id, .{ .widget = .{ .box = tabs_box }, .rect = null, .min_size = null }); │ │219 + tabs_box_owned = true; │ │220 + box.getFocus().child_id = tabs_id; │ │221 + │ │222 + return .{ │ │223 .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = box }, .{ .direction = .horiz, .show_bar = false, .web_native = !session.is_terminal }),│ │224 .tab_ids = tab_ids, │ │225 + .tabs_id = tabs_id, │ │226 + .gap_id = gap_id, │ │227 + .first_group_width = first_group_width, │ │228 .session = session, │ │229 }; │ │198 - self.getFocus().child_id = selected_tab orelse self.tab_ids.keys()[0]; │ │199 - return self; │ │230 } │ │231 │ │232 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │236 │ │237 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ │238 self.clearGrid(); │ │209 - // only the selected tab shows its border │ │239 const box = &self.scroll.child.box; │ │211 - ui.widget.setBackButtonVisible(box, self.session.back == .available); │ │212 - for (box.children.keys(), box.children.values()) |id, *child| { │ │240 + const back_visible = self.session.back == .available; │ │241 + ui.widget.setBackButtonVisible(box, back_visible); │ │242 + const tabs_child = self.tabsChild(); │ │243 + const tabs_box = &tabs_child.widget.box; │ │244 + │ │245 + // only the selected tab shows its border │ │246 + const selected_tab = if (box.getFocus().child_id == self.tabs_id) tabs_box.getFocus().child_id else null; │ │247 + var tabs_width: usize = 0; │ │248 + for (tabs_box.children.keys(), tabs_box.children.values()) |id, *child| { │ │249 const tb: ?*wgt.TextBox = switch (child.widget) { │ │250 .text_box => |*x| x, │ │251 .auth_tab => |*at| blk: { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │255 }, │ │256 else => null, │ │257 }; │ │222 - if (tb) |t| ui.widget.markSelected(t, self.getFocus().child_id == id); │ │258 + if (tb) |t| ui.widget.markSelected(t, selected_tab == id); │ │259 + tabs_width += if (child.min_size) |min_size| min_size.width orelse 0 else 0; │ │260 } │ │261 + │ │262 + // the outer box's hidden border occupies two columns. keep the title │ │263 + // and tabs together if they fit; otherwise tab strip on its own row. │ │264 + const viewport_width = constraint.max_size.width orelse constraint.min_size.width; │ │265 + const content_width = if (viewport_width) |width| width -| 2 else null; │ │266 + const back_width: usize = if (back_visible) ui.widget.back_button_width else 0; │ │267 + const wrap = if (content_width) |width| self.first_group_width + back_width + tabs_width > width else false;│ │268 + box.options.direction = if (wrap) .vert else .horiz; │ │269 + tabs_child.min_size = if (wrap) .{ .width = content_width, .height = null } else null; │ │270 + (box.children.getPtr(self.gap_id) orelse unreachable).hidden = !wrap; │ │271 + │ │272 var scroll_constraint = constraint; │ │225 - scroll_constraint.min_size.width = constraint.max_size.width orelse constraint.min_size.width; │ │273 + scroll_constraint.min_size.width = viewport_width; │ │274 try self.scroll.build(allocator, scroll_constraint, root_focus); │ │275 } │ │276 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │280 if (inp.moveTab(key, current_tab, self.tab_ids.count())) |new_tab| { │ │281 const tab_id = self.tab_ids.keys()[new_tab]; │ │282 root_focus.setFocus(tab_id); │ │235 - const tab = self.scroll.child.box.children.get(tab_id) orelse return;│ │236 - if (tab.rect) |rect| self.scroll.scrollToRect(rect); │ │283 + const tabs_child = self.tabsChild(); │ │284 + const tabs_rect = tabs_child.rect orelse return; │ │285 + const tab = tabs_child.widget.box.children.get(tab_id) orelse return;│ │286 + var rect = tab.rect orelse return; │ │287 + rect.x += tabs_rect.x; │ │288 + rect.y += tabs_rect.y; │ │289 + self.scroll.scrollToRect(rect); │ │290 } │ │291 } │ │292 │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │307 } │ │308 │ │309 fn currentTabIndex(self: View) ?usize { │ │257 - const child_id = self.scroll.child.box.focus.child_id orelse return null; │ │310 + if (self.scroll.child.box.focus.child_id != self.tabs_id) return null; │ │311 + const child_id = self.tabsChild().widget.box.focus.child_id orelse return null;│ │312 return self.tab_ids.getIndex(child_id); │ │313 + } │ │314 + │ │315 + fn tabsChild(self: *const View) *wgt.Box(ui.Widget).Child { │ │316 + return self.scroll.child.box.children.getPtr(self.tabs_id) orelse unreachable; │ │317 } │ │318 }; │ │319 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │482 else => {}, │ │483 } │ │484 } │ │485 - try self.box.build(allocator, constraint, root_focus); │ │485 + // a row taller than the tabs, leaving a blank line beneath them. the │ │486 + // parent sets no min, which would make it lay the focused content out first. │ │487 + var header_constraint = constraint; │ │488 + header_constraint.min_size.height = @max(constraint.min_size.height orelse 0, 4); │ │489 + try self.box.build(allocator, header_constraint, root_focus); │ │490 } │ │491 │ │492 pub fn input(self: *Header, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯