╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │495 } │ │496 │ │497 fn searchBox(self: *ReposView) *ui.widget.SearchBox { │ │497 - return &self.box.children.values()[header_index].widget.box.children.values()[0].widget.search_box; │ │498 + return &self.box.children.values()[header_index].widget.header_scroll.row().children.values()[0].widget.search_box;│ │499 } │ │500 │ │501 fn listScroll(self: *ReposView) *ui.widget.FlowBox.Scroll { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────────╮ │src/ui/UserNotifications.zig│ ╰────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 const settings_route = user_route.withUserTab(.{ .view = .settings }); │ │106 const keys_route = user_route.withUserTab(.{ .view = .keys }); │ │107 { │ │108 - var header = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │109 - errdefer header.deinit(allocator); │ │108 + var header_scroll = try ui.widget.HeaderScroll.init(allocator, session); │ │109 + errdefer header_scroll.deinit(allocator); │ │110 + const header = header_scroll.row(); │ │111 const on_tab = std.meta.activeTag(session.data.current_page) == std.meta.activeTag(user_route); │ │112 const routes = [_]ui.RoutablePage{ user_route.withUserTab(.{}), settings_route, keys_route }; │ │113 for ([_]ui.RoutablePage.UserView{ .notifications, .settings, .keys }, [_][]const u8{ "notifications", "user settings", "ssh keys" }, routes, 0..) |view, label, route, index| {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 } │ │145 header.getFocus().child_id = control_ids[@backingInt(data.view)]; │ │146 // a row taller than the button, leaving a blank line beneath it │ │146 - try box.children.put(allocator, header.getFocus().id, .{ .widget = .{ .box = header }, .rect = null, .min_size = .{ .width = null, .height = 4 } }); │ │147 + try box.children.put(allocator, header_scroll.getFocus().id, .{ .widget = .{ .header_scroll = header_scroll }, .rect = null, .min_size = .{ .width = null, .height = 4 } });│ │148 } │ │149 │ │150 { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │205 } │ │206 │ │207 fn headerBox(self: *View) *wgt.Box(ui.Widget) { │ │207 - return &self.box.children.values()[header_index].widget.box; │ │208 + return self.box.children.values()[header_index].widget.header_scroll.row();│ │209 } │ │210 │ │211 fn contentStack(self: *View) *wgt.Stack(ui.Widget) { │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │213 } │ │214 │ │215 fn headerActive(self: *View) bool { │ │215 - return self.box.getFocus().child_id == self.headerBox().getFocus().id; │ │216 + return self.box.getFocus().child_id == self.box.children.keys()[header_index]; │ │217 } │ │218 │ │219 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────╮ │44 search_box: SearchBox, │ │45 radio: Radio, │ │46 search_header: SearchHeader, │ │47 + header_scroll: HeaderScroll, │ │48 repo_issues_header: ui.Repo.Issues.Header, │ │49 repo_patches_header: ui.Repo.Patches.Header, │ │50 repo_discussions_header: ui.Repo.Discussions.Header,│ ╰────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1211 │ │1212 pub fn getFocus(self: *Radio) *Focus { │ │1213 return self.box.getFocus(); │ │1214 + } │ │1215 +}; │ │1216 + │ │1217 +// a sub-header's row, which scrolls sideways once its children's minimum widths │ │1218 +// outgrow the window, like the page header: natively on the web, following the │ │1219 +// focused child in the terminal │ │1220 +pub const HeaderScroll = struct { │ │1221 + scroll: wgt.Scroll(Widget), │ │1222 + │ │1223 + // an empty row, which the caller fills through `row` │ │1224 + pub fn init(allocator: std.mem.Allocator, session: *const ui.Session) !HeaderScroll { │ │1225 + // the outer box hands the row a max width, which the scroll alone can't │ │1226 + var outer = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │1227 + errdefer outer.deinit(allocator); │ │1228 + { │ │1229 + var row_box = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .horiz }); │ │1230 + errdefer row_box.deinit(allocator); │ │1231 + try outer.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null }); │ │1232 + outer.getFocus().child_id = row_box.getFocus().id; │ │1233 + } │ │1234 + return .{ .scroll = try wgt.Scroll(Widget).init(allocator, .{ .box = outer }, .{ .direction = .horiz, .show_bar = false, .web_native = !session.is_terminal }) }; │ │1235 + } │ │1236 + │ │1237 + pub fn deinit(self: *HeaderScroll, allocator: std.mem.Allocator) void { │ │1238 + self.scroll.deinit(allocator); │ │1239 + } │ │1240 + │ │1241 + fn rowChild(self: *const HeaderScroll) *wgt.Box(Widget).Child { │ │1242 + return &self.scroll.child.box.children.values()[0]; │ │1243 + } │ │1244 + │ │1245 + pub fn row(self: *const HeaderScroll) *wgt.Box(Widget) { │ │1246 + return &self.rowChild().widget.box; │ │1247 + } │ │1248 + │ │1249 + pub fn build(self: *HeaderScroll, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │1250 + self.clearGrid(); │ │1251 + const row_box = self.row(); │ │1252 + // bring the focused child into view where the last build put it │ │1253 + 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);│ │1254 + │ │1255 + // the window's width, or the children's minimums when they need more. │ │1256 + // only a max, since a min would stretch the row's last laid out child. │ │1257 + const width: ?usize = if (constraint.max_size.width) |viewport| blk: { │ │1258 + var needed: usize = 0; │ │1259 + for (row_box.children.values()) |child| { │ │1260 + if (child.min_size) |min_size| needed += min_size.width orelse 0; │ │1261 + } │ │1262 + break :blk @max(viewport, needed); │ │1263 + } else null; │ │1264 + self.rowChild().max_size = .{ .width = width, .height = null }; │ │1265 + try self.scroll.build(allocator, constraint, root_focus); │ │1266 + } │ │1267 + │ │1268 + pub fn input(self: *HeaderScroll, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │1269 + try self.scroll.input(allocator, key, root_focus); │ │1270 + } │ │1271 + │ │1272 + pub fn clearGrid(self: *HeaderScroll) void { │ │1273 + self.scroll.clearGrid(); │ │1274 + } │ │1275 + │ │1276 + pub fn getGrid(self: HeaderScroll) ?Grid { │ │1277 + return self.scroll.getGrid(); │ │1278 + } │ │1279 + │ │1280 + pub fn getFocus(self: *HeaderScroll) *Focus { │ │1281 + return self.scroll.getFocus(); │ │1282 } │ │1283 }; │ │1284 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1286 // the clone url on the right. the owning view turns a submitted query into a│ │1287 // route, so route building stays with the tab that knows its own url. │ │1288 pub const SearchHeader = struct { │ │1220 - box: wgt.Box(Widget), │ │1289 + scroll: HeaderScroll, │ │1290 session: *ui.Session, │ │1291 │ │1292 pub fn init( │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1298 text: ?[]const u8, │ │1299 show_input: bool, │ │1300 ) !SearchHeader { │ │1232 - var box = try wgt.Box(Widget).init(allocator, .{ .border = null, .direction = .horiz });│ │1233 - errdefer box.deinit(allocator); │ │1301 + var scroll = try HeaderScroll.init(allocator, session); │ │1302 + errdefer scroll.deinit(allocator); │ │1303 + const box = scroll.row(); │ │1304 │ │1305 // the search box only appears where an index can serve it. │ │1306 if (show_input) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1327 } │ │1328 } │ │1329 │ │1260 - return .{ .box = box, .session = session }; │ │1330 + return .{ .scroll = scroll, .session = session }; │ │1331 } │ │1332 │ │1333 pub fn deinit(self: *SearchHeader, allocator: std.mem.Allocator) void {│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯