╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1452 return &self.scroll.child.flow_box; │ │1453 } │ │1454 }; │ │1455 +}; │ │1456 + │ │1457 +// a left-to-right flow of variable-width focusable items that wraps at the │ │1458 +// available width, like words in a paragraph. unlike FlowBox there is no cell │ │1459 +// grid: each item is as wide as its text. selection movement is driven by the │ │1460 +// owning view via indexOfFocusId/verticalNeighbor. │ │1461 +pub const TagFlow = struct { │ │1462 + focus: *Focus, │ │1463 + grid: ?Grid, │ │1464 + text_boxes: std.ArrayList(wgt.TextBox(Widget)), │ │1465 + // last build's item rects (content space), for vertical navigation and │ │1466 + // scroll-into-view. │ │1467 + rects: std.ArrayList(layout.IRect), │ │1468 + // backs the per-item strings the text boxes borrow. │ │1469 + arena: std.heap.ArenaAllocator, │ │1470 + │ │1471 + pub const Item = struct { text: []const u8, link: []const u8 = "" }; │ │1472 + │ │1473 + pub fn init(allocator: std.mem.Allocator) !TagFlow { │ │1474 + return .{ │ │1475 + .focus = try Focus.create(allocator, .container), │ │1476 + .grid = null, │ │1477 + .text_boxes = .empty, │ │1478 + .rects = .empty, │ │1479 + .arena = std.heap.ArenaAllocator.init(allocator), │ │1480 + }; │ │1481 + } │ │1482 + │ │1483 + pub fn deinit(self: *TagFlow, allocator: std.mem.Allocator) void { │ │1484 + self.focus.destroy(allocator); │ │1485 + if (self.grid) |*grid| { │ │1486 + grid.deinit(); │ │1487 + self.grid = null; │ │1488 + } │ │1489 + for (self.text_boxes.items) |*tb| tb.deinit(allocator); │ │1490 + self.text_boxes.deinit(allocator); │ │1491 + self.rects.deinit(allocator); │ │1492 + self.arena.deinit(); │ │1493 + } │ │1494 + │ │1495 + // both the item text and the link are copied into the arena, so the caller's │ │1496 + // slices needn't outlive this call. │ │1497 + pub fn setItems(self: *TagFlow, allocator: std.mem.Allocator, items: []const Item) !void { │ │1498 + for (self.text_boxes.items) |*tb| tb.deinit(allocator); │ │1499 + self.text_boxes.clearAndFree(allocator); │ │1500 + │ │1501 + _ = self.arena.reset(.retain_capacity); │ │1502 + const aa = self.arena.allocator(); │ │1503 + │ │1504 + self.focus.clear(); │ │1505 + self.focus.child_id = null; │ │1506 + │ │1507 + for (items) |item| { │ │1508 + const line = try aa.dupe(u8, item.text); │ │1509 + │ │1510 + var text_box = try wgt.TextBox(Widget).init(allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none });│ │1511 + errdefer text_box.deinit(allocator); │ │1512 + text_box.getFocus().focusable = true; │ │1513 + │ │1514 + if (item.link.len > 0) { │ │1515 + text_box.getFocus().kind = .{ .custom = try aa.dupe(u8, item.link) }; │ │1516 + } │ │1517 + │ │1518 + try self.text_boxes.append(allocator, text_box); │ │1519 + } │ │1520 + │ │1521 + if (self.text_boxes.items.len > 0) { │ │1522 + self.focus.child_id = self.text_boxes.items[0].getFocus().id; │ │1523 + } │ │1524 + } │ │1525 + │ │1526 + pub fn build(self: *TagFlow, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │1527 + self.clearGrid(); │ │1528 + self.focus.clear(); │ │1529 + │ │1530 + if (self.text_boxes.items.len == 0) return; │ │1531 + const max_width = constraint.max_size.width orelse 80; │ │1532 + if (max_width == 0) return; │ │1533 + │ │1534 + // lay the items out left to right, wrapping to a new row when the next │ │1535 + // one wouldn't fit. │ │1536 + self.rects.clearRetainingCapacity(); │ │1537 + var x: usize = 0; │ │1538 + var y: usize = 0; │ │1539 + var row_height: usize = 0; │ │1540 + var total_width: usize = 0; │ │1541 + for (self.text_boxes.items) |*tb| { │ │1542 + tb.options.border_style = if (self.focus.child_id == tb.getFocus().id) .single else .hidden; │ │1543 + try tb.build(allocator, .{ │ │1544 + .min_size = .{ .width = null, .height = null }, │ │1545 + .max_size = .{ .width = max_width, .height = null }, │ │1546 + }, root_focus); │ │1547 + const tb_grid_maybe = tb.getGrid(); │ │1548 + const w: usize = if (tb_grid_maybe) |g| g.size.width else 0; │ │1549 + const h: usize = if (tb_grid_maybe) |g| g.size.height else 0; │ │1550 + if (x > 0 and x + w > max_width) { │ │1551 + x = 0; │ │1552 + y += row_height; │ │1553 + row_height = 0; │ │1554 + } │ │1555 + try self.rects.append(allocator, .{ .x = @intCast(x), .y = @intCast(y), .size = .{ .width = w, .height = h } }); │ │1556 + if (h > row_height) row_height = h; │ │1557 + x += w; │ │1558 + if (x > total_width) total_width = x; │ │1559 + } │ │1560 + │ │1561 + const total_height = y + row_height; │ │1562 + if (total_width == 0 or total_height == 0) return; │ │1563 + │ │1564 + var grid = try Grid.init(allocator, .{ .width = total_width, .height = total_height }); │ │1565 + errdefer grid.deinit(); │ │1566 + │ │1567 + for (self.text_boxes.items, self.rects.items) |*tb, rect| { │ │1568 + const tb_grid = tb.getGrid() orelse continue; │ │1569 + try self.focus.addChild(allocator, tb.getFocus(), tb_grid.size, @intCast(rect.x), @intCast(rect.y)); │ │1570 + try grid.drawGrid(tb_grid, @intCast(rect.x), @intCast(rect.y)); │ │1571 + } │ │1572 + │ │1573 + self.grid = grid; │ │1574 + } │ │1575 + │ │1576 + pub fn input(self: *TagFlow, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │1577 + _ = self; │ │1578 + _ = allocator; │ │1579 + _ = key; │ │1580 + _ = root_focus; │ │1581 + } │ │1582 + │ │1583 + pub fn clearGrid(self: *TagFlow) void { │ │1584 + if (self.grid) |*grid| { │ │1585 + grid.deinit(); │ │1586 + self.grid = null; │ │1587 + } │ │1588 + for (self.text_boxes.items) |*tb| tb.clearGrid(); │ │1589 + } │ │1590 + │ │1591 + pub fn getGrid(self: TagFlow) ?Grid { │ │1592 + return self.grid; │ │1593 + } │ │1594 + │ │1595 + pub fn getFocus(self: *TagFlow) *Focus { │ │1596 + return self.focus; │ │1597 + } │ │1598 + │ │1599 + pub fn indexOfFocusId(self: TagFlow, focus_id: usize) ?usize { │ │1600 + for (self.text_boxes.items, 0..) |tb, i| { │ │1601 + if (tb.box.focus.id == focus_id) return i; │ │1602 + } │ │1603 + return null; │ │1604 + } │ │1605 + │ │1606 + // the first item of the row below (when `downward`) or above, or null at │ │1607 + // the flow's edge. items are laid out in order, so y is nondecreasing. │ │1608 + pub fn rowStep(self: TagFlow, index: usize, downward: bool) ?usize { │ │1609 + const rects = self.rects.items; │ │1610 + if (index >= rects.len) return null; │ │1611 + const cur_y = rects[index].y; │ │1612 + if (downward) { │ │1613 + for (rects[index + 1 ..], index + 1..) |r, i| { │ │1614 + if (r.y > cur_y) return i; │ │1615 + } │ │1616 + return null; │ │1617 + } │ │1618 + // walk the earlier rows; the last one visited is the row above. │ │1619 + var prev_first: ?usize = null; │ │1620 + var i: usize = 0; │ │1621 + while (i < rects.len and rects[i].y < cur_y) { │ │1622 + prev_first = i; │ │1623 + const y = rects[i].y; │ │1624 + while (i < rects.len and rects[i].y == y) i += 1; │ │1625 + } │ │1626 + return prev_first; │ │1627 + } │ │1628 }; │ │1629 │ │1630 // an invisible widget that fills the horizontal space granted by its parent. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯