╭ changed ───────────╮ │src/embed/index.html│ ╰────────────────────╯ ╭──────────────────────────────────────╮ │56 } │ │57 │ │58 #grid a { │ │59 - color: white; │ │59 + color: inherit; │ │60 + text-decoration: none;│ │61 } │ │62 │ │63 #grid .clickable { │ ╰──────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │61 _pushState(url.ptr, @intCast(url.len)); │ │62 } │ │63 │ │64 - const html = try web.generateHtml(allocator, root_ptr, &session);│ │64 + const html = try web.generateHtml(allocator, root_ptr); │ │65 defer allocator.free(html); │ │66 setHtml(html); │ │67 │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Home/AuthTab.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │23 var text_box = try wgt.TextBox(ui.Widget).init(allocator, "login", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │24 errdefer text_box.deinit(allocator); │ │25 text_box.getFocus().focusable = true; │ │26 + text_box.getFocus().kind = .{ .custom = "a:/auth" }; │ │27 return .{ │ │28 .text_box = text_box, │ │29 .data = data, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Home/Header.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │34 var tab_ids: [3]usize = undefined; │ │35 │ │36 // fixed tabs: users, repos │ │37 - const fixed_tab_names = [_][]const u8{ "users", "repos" }; │ │38 - for (fixed_tab_names, 0..) |name, i| { │ │37 + for ( │ │38 + [_][]const u8{ "users", "repos" }, │ │39 + [_][]const u8{ "a:/users", "a:/repos" }, │ │40 + 0.., │ │41 + ) |name, focus_name, i| { │ │42 var text_box = try wgt.TextBox(ui.Widget).init(allocator, name, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │43 errdefer text_box.deinit(allocator); │ │44 text_box.getFocus().focusable = true; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │45 + text_box.getFocus().kind = .{ .custom = focus_name }; │ │46 tab_ids[i] = text_box.getFocus().id; │ │47 try box.children.put(allocator, text_box.getFocus().id, .{│ │48 .widget = .{ .text_box = text_box }, │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │78 // initial selected tab follows session.data.current_page — the web │ │79 // layer decides this from the request URL, the TTY uses whatever │ │80 // the session arrives with — so we don't bake a default here. │ │77 - self.getFocus().child_id = tab_ids[switch (session.data.current_page) {│ │78 - .home_users => 0, │ │79 - .home_repos => 1, │ │80 - .home_auth => 2, │ │81 - }]; │ │81 + self.getFocus().child_id = tab_ids[ │ │82 + switch (session.data.current_page) { │ │83 + .home_users => 0, │ │84 + .home_repos => 1, │ │85 + .home_auth => 2, │ │86 + } │ │87 + ]; │ │88 return self; │ │89 } │ │90 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │311 var root = try ui.initRoot(allocator, &snapshot.page, &session);│ │312 defer root.deinit(allocator); │ │313 │ │314 - const content = try generateHtml(allocator, &root, &session); │ │314 + const content = try generateHtml(allocator, &root); │ │315 defer allocator.free(content); │ │316 const overlay = try generateOverlay(allocator, &root); │ │317 defer allocator.free(overlay); │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │367 } │ │368 │ │369 // emits the TUI grid cells (one <span> run per focusable widget) │ │370 -pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget, session: *const ui.Session) ![]const u8 { │ │371 - _ = session; │ │370 +pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget) ![]const u8 { │ │371 const grid = root.getGrid() orelse return error.MissingGrid; │ │372 const root_focus = root.getFocus(); │ │373 + │ │374 + const Tag = union(enum) { │ │375 + span, │ │376 + a: []const u8, │ │377 + │ │378 + const a_prefix = "a:"; │ │379 + │ │380 + fn init(str: []const u8) @This() { │ │381 + if (std.mem.startsWith(u8, str, a_prefix)) { │ │382 + return .{ .a = str[a_prefix.len..] }; │ │383 + } │ │384 + return .span; │ │385 + } │ │386 + │ │387 + fn writeOpenTag(self: @This(), alloc: std.mem.Allocator, out: *std.ArrayList(u8), id: usize) !void { │ │388 + switch (self) { │ │389 + .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 + }, │ │394 + .a => |href| { │ │395 + try out.appendSlice(alloc, "<a class=\"clickable\" data-focus-id=\""); │ │396 + var id_buf: [32]u8 = undefined; │ │397 + try out.appendSlice(alloc, try std.fmt.bufPrint(&id_buf, "{d}", .{id})); │ │398 + try out.appendSlice(alloc, "\" href=\""); │ │399 + try appendEscapedHtml(alloc, out, href); │ │400 + try out.appendSlice(alloc, "\">"); │ │401 + }, │ │402 + } │ │403 + } │ │404 │ │405 + fn closeTag(self: @This()) []const u8 { │ │406 + return switch (self) { │ │407 + .span => "</span>", │ │408 + .a => "</a>", │ │409 + }; │ │410 + } │ │411 + }; │ │412 + │ │413 var out: std.ArrayList(u8) = .empty; │ │414 errdefer out.deinit(allocator); │ │415 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │416 for (0..grid.size.height) |y| { │ │379 - var current_id: ?usize = null; │ │417 + var current_id_maybe: ?usize = null; │ │418 + var current_tag_maybe: ?Tag = null; │ │419 for (0..grid.size.width) |x| { │ │420 const cell_id = cellFocusId(root_focus, x, y); │ │382 - if (cell_id != current_id) { │ │383 - if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │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; │ │424 + │ │425 if (cell_id) |id| { │ │385 - var buf: [128]u8 = undefined; │ │386 - const tag = try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{d}\">", .{id});│ │387 - try out.appendSlice(allocator, tag); │ │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); │ │433 + } │ │434 } │ │389 - current_id = cell_id; │ │435 + current_id_maybe = cell_id; │ │436 } │ │437 const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; │ │438 try appendEscapedHtml(allocator, &out, rune); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │439 } │ │394 - if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │440 + if (current_tag_maybe) |current_tag| try out.appendSlice(allocator, current_tag.closeTag());│ │441 try out.append(allocator, '\n'); │ │442 } │ │443 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯