╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │ 6 const decoder = new TextDecoder(); │ │ 7 const encoder = new TextEncoder(); │ │ 8 let currentHtml = ""; │ │ 9 +// each scroll div's wasm-side offset (in cells) as of the last render, keyed by│ │10 +// scroll id. when a new render's offset differs, the wasm moved the scroll on │ │11 +// purpose and its offset is applied; otherwise the browser position (the user's│ │12 +// scrolling) is preserved. │ │13 +const wasmOffsets = {}; │ │14 // seeded from the server-rendered overlay so the first wasm tick can no-op │ │15 // when its layout happens to match the server's. otherwise the first tick │ │16 // always rebuilds and any pre-load focus state is lost. │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │73 } │ │74 grid.innerHTML = html; │ │75 for (const el of grid.querySelectorAll(".scroll[data-scroll-id]")) { │ │71 - const p = positions[el.dataset.scrollId]; │ │72 - if (p) { el.scrollTop = p.top; el.scrollLeft = p.left; } │ │76 + const id = el.dataset.scrollId; │ │77 + const offset = { top: Number(el.dataset.scrollY || 0), left: Number(el.dataset.scrollX || 0) };│ │78 + const prev = wasmOffsets[id]; │ │79 + const p = positions[id]; │ │80 + if (p && prev && prev.top === offset.top && prev.left === offset.left) { │ │81 + el.scrollTop = p.top; │ │82 + el.scrollLeft = p.left; │ │83 + } else { │ │84 + // the wasm's offset changed (or this div is new — a fresh │ │85 + // page load or a bumped content version): apply it, │ │86 + // converted from cells to pixels. │ │87 + if (!cellWidth || !cellHeight) measureCell(); │ │88 + el.scrollTop = offset.top * cellHeight; │ │89 + el.scrollLeft = offset.left * cellWidth; │ │90 + } │ │91 + wasmOffsets[id] = offset; │ │92 } │ │93 }, │ │94 _replaceState: function (ptr, len) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │609 .both => "overflow:auto", │ │610 }; │ │611 // the id carries a content version so JS preserves the native scroll │ │612 - // position across re-renders of the same content but resets it (to the │ │613 - // top) when the content is replaced — e.g. selecting a different commit. │ │614 - var buf: [160]u8 = undefined; │ │615 - 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 })); │ │612 + // position across re-renders of the same content but resets it when the │ │613 + // content is replaced — e.g. selecting a different commit. the widget's │ │614 + // scroll offset (in cells) rides along so JS can apply it when it has no │ │615 + // preserved position, letting a wasm-side scrollToRect (the files list │ │616 + // scrolling to its selected row on page load) reach the browser. │ │617 + var buf: [224]u8 = undefined; │ │618 + try output.appendSlice(allocator, try std.fmt.bufPrint(&buf, "<div class=\"scroll\" data-scroll-id=\"{d}-{d}\" data-scroll-x=\"{d}\" data-scroll-y=\"{d}\" style=\"left:{d}ch;top:{d}em;width:{d}ch;height:{d}em;{s}\">", .{ id, child.focus.version, info.offset_x, info.offset_y, r.x, r.y, r.size.width, r.size.height, overflow }));│ │619 try renderPanel(allocator, output, child.focus, info.content); │ │620 try output.appendSlice(allocator, "</div>"); │ │621 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯