╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────╮ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 const ui = @import("./ui.zig"); │ │5 -const pg = @import("./page.zig");│ │5 +const xitui = xit.xitui; │ │6 +const Focus = xitui.focus.Focus; │ │7 │ │8 const Embed = struct { │ │9 path: []const u8, │ ╰────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │88 var repo_or_err = Repo.open(io, allocator, .{ .path = admin_repo_path }); │ │89 var page_arena = std.heap.ArenaAllocator.init(allocator); │ │90 defer page_arena.deinit(); │ │90 - const page: pg.Page = if (repo_or_err) |*repo| blk: { │ │91 + const page: ui.Page = if (repo_or_err) |*repo| blk: { │ │92 defer repo.deinit(io, allocator); │ │92 - break :blk .{ .user_repo = try .init(repo_opts, &page_arena, repo) }; │ │93 - } else |_| .{ .user_repo = .empty() }; │ │93 + break :blk .{ .users_and_repos = try .init(repo_opts, &page_arena, repo) };│ │94 + } else |_| .{ .users_and_repos = .empty() }; │ │95 │ │96 var root = try ui.initRoot(allocator, &page); │ │97 defer root.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │ 98 │ │ 98 - const content = try ui.generateHtml(allocator, &root); │ │ 99 + const content = try generateHtml(allocator, &root); │ │100 defer allocator.free(content); │ │101 │ │102 // serialize the page so the wasm side can parse it back without making│ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │256 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { │ │257 err.print(fmt, args) catch return; │ │258 err.flush() catch {}; │ │259 +} │ │260 + │ │261 +pub fn generateHtml(allocator: std.mem.Allocator, root: *ui.Widget) ![]const u8 { │ │262 + const grid = root.getGrid() orelse return error.MissingGrid; │ │263 + const root_focus = root.getFocus(); │ │264 + │ │265 + var out: std.ArrayList(u8) = .empty; │ │266 + errdefer out.deinit(allocator); │ │267 + │ │268 + for (0..grid.size.height) |y| { │ │269 + // wrap runs of cells belonging to a focusable widget in a span tagged │ │270 + // with that widget's focus id. CSS paints them with a pointer cursor; │ │271 + // JS reads the id on click and dispatches focus directly. │ │272 + var current_id: ?usize = null; │ │273 + for (0..grid.size.width) |x| { │ │274 + const cell_id = cellFocusId(root_focus, x, y); │ │275 + if (cell_id != current_id) { │ │276 + if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │277 + if (cell_id) |id| { │ │278 + var buf: [64]u8 = undefined; │ │279 + const tag = try std.fmt.bufPrint(&buf, "<span class=\"clickable\" data-focus-id=\"{}\">", .{id});│ │280 + try out.appendSlice(allocator, tag); │ │281 + } │ │282 + current_id = cell_id; │ │283 + } │ │284 + const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; │ │285 + try appendEscapedHtml(allocator, &out, rune); │ │286 + } │ │287 + if (current_id != null) try out.appendSlice(allocator, "</span>"); │ │288 + try out.append(allocator, '\n'); │ │289 + } │ │290 + │ │291 + return try out.toOwnedSlice(allocator); │ │292 } │ │293 │ │294 +fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { │ │295 + var iter = focus.children.iterator(); │ │296 + while (iter.next()) |entry| { │ │297 + const child = entry.value_ptr.*; │ │298 + if (!child.focus.focusable) continue; │ │299 + const r = child.rect; │ │300 + if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { │ │301 + return entry.key_ptr.*; │ │302 + } │ │303 + } │ │304 + return null; │ │305 +} │ │306 + │ │307 +fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { │ │308 + for (input) |ch| { │ │309 + switch (ch) { │ │310 + '&' => try out.appendSlice(allocator, "&amp;"), │ │311 + '<' => try out.appendSlice(allocator, "&lt;"), │ │312 + '>' => try out.appendSlice(allocator, "&gt;"), │ │313 + '"' => try out.appendSlice(allocator, "&quot;"), │ │314 + '\'' => try out.appendSlice(allocator, "&#39;"), │ │315 + else => try out.append(allocator, ch), │ │316 + } │ │317 + } │ │318 +} │ │319 + │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯