╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Home/Header.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │32 const auth_tab_min_width: usize = @as(usize, "logout".len) + 2; │ │33 │ │34 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │35 - var box = wgt.Box(ui.Widget).init(.{ .border_style = null, .rounded_corners = true, .direction = .horiz }); │ │35 + var box = wgt.Box(ui.Widget).init(.{ .border_style = .hidden, .rounded_corners = true, .direction = .horiz });│ │36 errdefer box.deinit(allocator); │ │37 │ │38 var tab_ids: [3]usize = undefined; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │39 │ │40 - // spacer │ │41 - { │ │42 - var text = wgt.Text(ui.Widget).init(" "); │ │43 - errdefer text.deinit(allocator); │ │44 - try box.children.put(allocator, text.getFocus().id, .{ │ │45 - .widget = .{ .text = text }, │ │46 - .rect = null, │ │47 - .min_size = .{ .width = 1, .height = null }, │ │48 - }); │ │49 - } │ │50 - │ │40 // title sits to the left of the tabs │ │41 { │ │42 var title_view = try ui.Title.View.init(allocator, &data.title);│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │ 7 const xitui = xit.xitui; │ │ 8 const wgt = xitui.widget; │ │ 9 const Focus = xitui.focus.Focus; │ │10 +const Grid = xitui.grid.Grid; │ │11 │ │12 const cookie_name = "haxy_user"; │ │13 // flash cookie for surfacing the outcome of the most recent /login POST.│ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │367 err.flush() catch {}; │ │368 } │ │369 │ │369 -// emits the TUI grid cells (one <span> run per focusable widget) │ │370 +// emits the TUI grid cells as static HTML │ │371 pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget) ![]const u8 {│ │372 const grid = root.getGrid() orelse return error.MissingGrid; │ │373 const root_focus = root.getFocus(); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │374 │ │375 const Tag = union(enum) { │ │375 - span, │ │376 - a: []const u8, │ │376 + span, // clickable focusable cell │ │377 + a: []const u8, // clickable focusable link│ │378 + plain, // non-focusable, colored │ │379 │ │380 const a_prefix = "a:"; │ │381 │ ╰───────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 return .span; │ │387 } │ │388 │ │387 - fn writeOpenTag(self: @This(), alloc: std.mem.Allocator, out: *std.ArrayList(u8), id: usize) !void { │ │389 + fn writeOpenTag(self: @This(), alloc: std.mem.Allocator, out: *std.ArrayList(u8), id: usize, style_attr: []const u8) !void {│ │390 + var id_buf: [32]u8 = undefined; │ │391 switch (self) { │ │392 .span => { │ │390 - var buf: [128]u8 = undefined; │ │391 - const open = try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\">", .{id}); │ │392 - try out.appendSlice(alloc, open); │ │393 + try out.appendSlice(alloc, "<span class=\"clickable\" data-focus-id=\""); │ │394 + try out.appendSlice(alloc, try std.fmt.bufPrint(&id_buf, "{d}", .{id})); │ │395 + try out.appendSlice(alloc, "\""); │ │396 + try out.appendSlice(alloc, style_attr); │ │397 + try out.appendSlice(alloc, ">"); │ │398 }, │ │399 .a => |href| { │ │400 try out.appendSlice(alloc, "<a class=\"clickable\" data-focus-id=\""); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │396 - var id_buf: [32]u8 = undefined; │ │401 try out.appendSlice(alloc, try std.fmt.bufPrint(&id_buf, "{d}", .{id}));│ │402 try out.appendSlice(alloc, "\" href=\""); │ │403 try appendEscapedHtml(alloc, out, href); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────╮ │400 - try out.appendSlice(alloc, "\">"); │ │404 + try out.appendSlice(alloc, "\""); │ │405 + try out.appendSlice(alloc, style_attr);│ │406 + try out.appendSlice(alloc, ">"); │ │407 + }, │ │408 + .plain => { │ │409 + try out.appendSlice(alloc, "<span"); │ │410 + try out.appendSlice(alloc, style_attr);│ │411 + try out.appendSlice(alloc, ">"); │ │412 }, │ │413 } │ │414 } │ ╰────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │415 │ │416 fn closeTag(self: @This()) []const u8 {│ │417 return switch (self) { │ │407 - .span => "</span>", │ │418 + .span, .plain => "</span>", │ │419 .a => "</a>", │ │420 }; │ │421 } │ ╰────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │425 errdefer out.deinit(allocator); │ │426 │ │427 for (0..grid.size.height) |y| { │ │417 - var current_id_maybe: ?usize = null; │ │418 - var current_tag_maybe: ?Tag = null; │ │428 + var cur_id: ?usize = null; │ │429 + var cur_fg: ?Grid.Color = null; │ │430 + var cur_bg: ?Grid.Color = null; │ │431 + var open_tag: ?Tag = null; │ │432 + var first = true; │ │433 for (0..grid.size.width) |x| { │ │434 + const cell = grid.cells.items[try grid.cells.at(.{ y, x })]; │ │435 const cell_id = cellFocusId(root_focus, x, y); │ │421 - if (cell_id != current_id_maybe) { │ │422 - if (current_tag_maybe) |current_tag| try out.appendSlice(allocator, current_tag.closeTag());│ │423 - current_tag_maybe = null; │ │436 + const fg = cell.style.fg; │ │437 + const bg = cell.style.bg; │ │438 + │ │439 + if (first or cell_id != cur_id or !colorEql(fg, cur_fg) or !colorEql(bg, cur_bg)) { │ │440 + if (open_tag) |t| try out.appendSlice(allocator, t.closeTag()); │ │441 │ │442 + var new_tag: ?Tag = null; │ │443 if (cell_id) |id| { │ │426 - current_tag_maybe = if (root_focus.children.get(id)) |child| switch (child.focus.kind) {│ │427 - .custom => |custom| .init(custom), │ │428 - else => .span, │ │429 - } else null; │ │430 - │ │431 - if (current_tag_maybe) |current_tag| { │ │432 - try current_tag.writeOpenTag(allocator, &out, id); │ │444 + if (root_focus.children.get(id)) |child| { │ │445 + new_tag = switch (child.focus.kind) { │ │446 + .custom => |custom| Tag.init(custom), │ │447 + else => .span, │ │448 + }; │ │449 } │ │450 } │ │435 - current_id_maybe = cell_id; │ │451 + if (new_tag == null and (fg != null or bg != null)) new_tag = .plain; │ │452 + │ │453 + if (new_tag) |t| { │ │454 + var style_buf: [64]u8 = undefined; │ │455 + try t.writeOpenTag(allocator, &out, cell_id orelse 0, styleAttr(&style_buf, fg, bg)); │ │456 + } │ │457 + │ │458 + open_tag = new_tag; │ │459 + cur_id = cell_id; │ │460 + cur_fg = fg; │ │461 + cur_bg = bg; │ │462 + first = false; │ │463 } │ │437 - const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; │ │438 - try appendEscapedHtml(allocator, &out, rune); │ │464 + │ │465 + try appendEscapedHtml(allocator, &out, cell.rune orelse " "); │ │466 } │ │440 - if (current_tag_maybe) |current_tag| try out.appendSlice(allocator, current_tag.closeTag()); │ │467 + if (open_tag) |t| try out.appendSlice(allocator, t.closeTag()); │ │468 try out.append(allocator, '\n'); │ │469 } │ │470 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯