╭ changed ───────────╮ │src/ui/Repo/Refs.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │237 { │ │238 var items: std.ArrayList(ui.widget.TagFlow.Item) = .empty; │ │239 defer items.deinit(allocator); │ │189 - if (data.prev) |p| try items.append(allocator, .{ .text = "← previous", .link = try windowLink(session.page_arena, identity, kind, p) }); │ │240 + if (data.prev) |p| try items.append(allocator, .{ .text = "← previous", .link = try windowLink(session.page_arena, identity, kind, p, search) });│ │241 for (data.names) |name| try items.append(allocator, .{ .text = name, .link = try refLink(session.page_arena, identity, kind, name) }); │ │191 - if (data.next) |n| try items.append(allocator, .{ .text = "next →", .link = try windowLink(session.page_arena, identity, kind, n) }); │ │242 + if (data.next) |n| try items.append(allocator, .{ .text = "next →", .link = try windowLink(session.page_arena, identity, kind, n, search) }); │ │243 │ │244 var scroll = blk: { │ │245 var flow = try ui.widget.TagFlow.init(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │268 // known; otherwise let them size to their content. │ │269 if (constraint.max_size.width) |w| { │ │270 const half = w / 2; │ │220 - for (self.box.children.values()) |*child| { │ │271 + for (self.columns().children.values()) |*child| { │ │272 child.min_size = .{ .width = half, .height = null };│ │273 child.max_size = .{ .width = half, .height = null };│ │274 } │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │275 } else { │ │225 - for (self.box.children.values()) |*child| { │ │276 + for (self.columns().children.values()) |*child| {│ │277 child.min_size = null; │ │278 child.max_size = null; │ │279 } │ ╰──────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │288 } │ │289 │ │290 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │240 - _ = allocator; │ │291 + if (self.headerActive()) { │ │292 + const search_box = self.searchBox(); │ │293 + if (inp.vertDirection(key) == .down) return root_focus.setFocus(self.columns().getFocus().id);│ │294 + if (key == .enter) { │ │295 + const text = try search_box.text(allocator); │ │296 + defer allocator.free(text); │ │297 + return self.submit(text); │ │298 + } │ │299 + return search_box.input(allocator, key, root_focus); │ │300 + } │ │301 // arrows and the scroll wheel move the selection within the active │ │302 // flow. left from a row's first name and right from its last cross to │ │243 - // the other column's same row. the parent (Repo) takes up from the first │ │244 - // row to the header. │ │303 + // the other column's same row. up from a first row reaches the search │ │304 + // box. │ │305 const index = self.activeIndex() orelse return; │ │306 const flow = self.flowAt(index); │ │307 const cur = flow.indexOfFocusId(flow.focus.child_id orelse return) orelse return; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │310 const count = flow.text_boxes.items.len; │ │311 const cur_y = flow.rects.items[cur].y; │ │312 switch (inp.vertDirection(key)) { │ │253 - .up => if (flow.rowStep(cur, false)) |i| return self.focusName(index, i, root_focus), │ │254 - .down => if (flow.rowStep(cur, true)) |i| return self.focusName(index, i, root_focus), │ │313 + .up => { │ │314 + if (flow.rowStep(cur, false)) |i| self.focusName(index, i, root_focus) else _ = self.focusHeader(root_focus);│ │315 + return; │ │316 + }, │ │317 + .down => { │ │318 + if (flow.rowStep(cur, true)) |i| self.focusName(index, i, root_focus); │ │319 + return; │ │320 + }, │ │321 .none => {}, │ │322 } │ │323 switch (key) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │331 .end => self.focusName(index, count - 1, root_focus), │ │332 else => {}, │ │333 } │ │334 + } │ │335 + │ │336 + // both lists narrowed to `text`, each from its first window. an empty box │ │337 + // on an unsearched page has nothing to clear. │ │338 + fn submit(self: *View, text: []const u8) !void { │ │339 + if (text.len == 0 and self.data.search == null) return; │ │340 + const route = ui.RoutablePage.repoRefsRoute(self.data.identity, .branch, "") orelse return; │ │341 + try self.session.navigate(route.withSearch(text) orelse return); │ │342 } │ │343 │ │344 + fn searchBox(self: *View) *ui.widget.SearchBox { │ │345 + return &self.box.children.values()[header_index].widget.box.children.values()[0].widget.search_box;│ │346 + } │ │347 + │ │348 + fn headerActive(self: *View) bool { │ │349 + return self.box.getFocus().child_id == self.box.children.keys()[header_index]; │ │350 + } │ │351 + │ │352 + fn columns(self: *View) *wgt.Box(ui.Widget) { │ │353 + return &self.box.children.values()[columns_index].widget.box; │ │354 + } │ │355 + │ │356 fn activeIndex(self: *View) ?usize { │ │271 - const id = self.box.getFocus().child_id orelse return null; │ │272 - return self.box.children.getIndex(id); │ │357 + const cols = self.columns(); │ │358 + const id = cols.getFocus().child_id orelse return null; │ │359 + return cols.children.getIndex(id); │ │360 } │ │361 │ │362 // [0] = the fixed label, [1] = the scrolling flow │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │363 fn scrollAt(self: *View, index: usize) *wgt.Scroll(ui.Widget) { │ │277 - return &self.box.children.values()[index].widget.box.children.values()[1].widget.scroll; │ │364 + return &self.columns().children.values()[index].widget.box.children.values()[1].widget.scroll;│ │365 } │ │366 │ │367 fn flowAt(self: *View, index: usize) *ui.widget.TagFlow { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │413 return self.box.getFocus(); │ │414 } │ │415 │ │329 - // on the active flow's first row, where up leaves the tab │ │416 + // up leaves the tab from the search box │ │417 pub fn atTop(self: *View) bool { │ │331 - const index = self.activeIndex() orelse return false; │ │332 - const flow = self.flowAt(index); │ │333 - const cur = flow.indexOfFocusId(flow.focus.child_id orelse return false) orelse return false;│ │334 - return flow.rowStep(cur, false) == null; │ │418 + return self.headerActive(); │ │419 + } │ │420 + │ │421 + pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ │422 + root_focus.setFocus(self.searchBox().getFocus().id); │ │423 + return true; │ │424 } │ │425 }; │ │426 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │427 // the "a:" link to `kind`'s column windowed from ref `name` (raw; "" = the │ │428 // first window) within `identity` ("owner/name"). the other column resets to │ │429 // its first window. │ │341 -fn windowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, name: []const u8) ![]const u8 { │ │430 +fn windowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, name: []const u8, search: []const u8) ![]const u8 {│ │431 const encoded = try ui.urlEncodeRef(page_arena.allocator(), name); │ │343 - const route = ui.RoutablePage.repoRefsRoute(identity, kind, encoded) orelse return error.RouteTooLong; │ │432 + const window = ui.RoutablePage.repoRefsRoute(identity, kind, encoded) orelse return error.RouteTooLong; │ │433 + const route = window.withSearch(search) orelse return error.RouteTooLong; │ │434 const url = try route.toUrl(page_arena); │ │435 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │436 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯