╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │610 first = false; │ │611 } │ │612 │ │544 - try appendEscapedHtml(allocator, &out, cell.rune orelse " "); │ │613 + try appendEscapedHtml(allocator, output, cell.rune orelse " "); │ │614 } │ │546 - if (open_tag) |t| try out.appendSlice(allocator, t.closeTag()); │ │547 - try out.append(allocator, '\n'); │ │615 + if (open_tag) |t| try output.appendSlice(allocator, t.closeTag()); │ │616 + try output.append(allocator, '\n'); │ │617 } │ │618 │ │550 - return try out.toOwnedSlice(allocator); │ │619 + // each child scroll becomes a natively-scrollable div positioned over its │ │620 + // viewport, holding its full content rendered recursively. │ │621 + for (direct.items) |id| { │ │622 + const child = focus.children.get(id) orelse continue; │ │623 + const info = child.focus.scroll orelse continue; │ │624 + const r = child.rect; │ │625 + // overflow only on the axes the widget scrolls; `auto` shows a bar only │ │626 + // when that axis actually overflows. the other axis is clipped (hidden), │ │627 + // matching how the terminal Scroll handles its non-scrolling axis. │ │628 + const overflow = switch (info.direction) { │ │629 + .vert => "overflow-x:hidden;overflow-y:auto", │ │630 + .horiz => "overflow-x:auto;overflow-y:hidden", │ │631 + .both => "overflow:auto", │ │632 + }; │ │633 + // the id carries a content version so JS preserves the native scroll │ │634 + // position across re-renders of the same content but resets it (to the │ │635 + // top) when the content is replaced — e.g. selecting a different commit. │ │636 + var buf: [160]u8 = undefined; │ │637 + try output.appendSlice(allocator, try std.fmt.bufPrint(&buf, "<div class=\"scroll\" data-scroll-id=\"{d}-{d}\" style=\"left:{d}ch;top:{d}em;width:{d}ch;height:{d}em;{s}\">", .{ id, child.focus.version, r.x, r.y, r.size.width, r.size.height, overflow }));│ │638 + try renderPanel(allocator, output, child.focus, info.content); │ │639 + try output.appendSlice(allocator, "</div>"); │ │640 + } │ │641 } │ │642 │ │643 // emits the form overlay — one <form> per "form:<url>" focus subtree in the │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │757 return buf[0..i]; │ │758 } │ │759 │ │670 -fn cellFocusId(focus: *Focus, x: usize, y: usize) ?usize { │ │671 - var iter = focus.children.iterator(); │ │672 - while (iter.next()) |entry| { │ │673 - const child = entry.value_ptr.*; │ │674 - if (!child.focus.focusable) continue; │ │675 - const r = child.rect; │ │676 - if (x >= r.x and y >= r.y and x < r.x + r.size.width and y < r.y + r.size.height) { │ │677 - return entry.key_ptr.*; │ │678 - } │ │679 - } │ │680 - return null; │ │681 -} │ │682 - │ │760 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void {│ │761 for (input) |ch| { │ │762 switch (ch) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯