╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────╮ │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 {│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1264 - self.box.deinit(allocator); │ │1334 + self.scroll.deinit(allocator); │ │1335 } │ │1336 │ │1337 pub fn build(self: *SearchHeader, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1338 self.clearGrid(); │ │1269 - try self.box.build(allocator, constraint, root_focus); │ │1339 + try self.scroll.build(allocator, constraint, root_focus); │ │1340 } │ │1341 │ │1342 pub fn input(self: *SearchHeader, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │1273 - const current = self.box.getFocus().child_id orelse return; │ │1343 + const current = self.scroll.row().getFocus().child_id orelse return;│ │1344 if (self.searchBox()) |search_box| { │ │1345 if (current == search_box.getFocus().id) { │ │1346 // tab, and right-arrow past the last character, step to the│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │1368 // focus. the caller owns the returned text. │ │1369 pub fn submittedText(self: *SearchHeader, allocator: std.mem.Allocator) !?[]u8 { │ │1370 const search_box = self.searchBox() orelse return null; │ │1301 - if (self.box.getFocus().child_id != search_box.getFocus().id) return null; │ │1371 + if (self.scroll.row().getFocus().child_id != search_box.getFocus().id) return null;│ │1372 return try search_box.text(allocator); │ │1373 } │ │1374 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │1388 } │ │1389 │ │1390 fn cloneUrl(self: *SearchHeader) ?*CopyableText { │ │1321 - for (self.box.children.values()) |*child| switch (child.widget) { │ │1391 + for (self.scroll.row().children.values()) |*child| switch (child.widget) {│ │1392 .copyable_text => |*copyable_text| return copyable_text, │ │1393 else => {}, │ │1394 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯