Spaces:
Running
Sandbox: fix line overflow when seq block measured before font load
Browse filesThe bases-per-line calc was attaching a probe to document.body with
hand-mirrored font CSS, then dividing the seq block width by the
measured glyph width. That drifted from the real rendering context in
two ways:
1. If JetBrains Mono hadn't finished loading at measure-time, the
fallback monospace was narrower → blockW underestimated → bpl too
large → rendered lines overflowed the container.
2. If the sandbox panel was still display:none when first measured
(landing on #intro and switching tabs later), clientWidth was 0
and the probe-based metrics never got refreshed against the real
layout once the panel became visible.
Three coordinated fixes:
- Move the probe inside the seq block itself so it inherits the real
font / letter-spacing / feature settings.
- tabs.js emits a tab:changed CustomEvent on every tab switch;
sandbox listens and drops charMetrics + re-renders on the next
frame once its panel is actually laid out.
- After each render, compare the first line's scrollWidth to the
available width and proportionally shrink bpl if it overshoots,
then back-solve charMetrics from the rendered line so the next
call converges without retriggering the loop.
- CSS overflow-x: auto on .sb-seq-block as a defense-in-depth net
if all of the above ever misses an edge case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- assets/js/sections/sandbox.js +58 -4
- assets/js/tabs.js +7 -0
- assets/styles/sandbox.css +5 -1
|
@@ -158,14 +158,21 @@
|
|
| 158 |
return DARK_RGB_S;
|
| 159 |
}
|
| 160 |
function measureSeqChars() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
const probe = document.createElement("div");
|
| 162 |
-
probe.style.cssText = "position:absolute;visibility:hidden;top:-9999px;
|
| 163 |
probe.textContent = " 1 ";
|
| 164 |
-
|
| 165 |
const prefixW = probe.getBoundingClientRect().width;
|
| 166 |
probe.textContent = "AAAAAAAAAA ";
|
| 167 |
const blockW = probe.getBoundingClientRect().width;
|
| 168 |
-
|
| 169 |
charMetrics = { prefixW, blockW };
|
| 170 |
}
|
| 171 |
function basesPerLineSb() {
|
|
@@ -264,7 +271,7 @@
|
|
| 264 |
if (colorMode === "logprob") recomputeLpRange();
|
| 265 |
const total = promptBases + genText;
|
| 266 |
els.copy.disabled = total.length === 0;
|
| 267 |
-
|
| 268 |
const totalLines = total ? Math.ceil(total.length / bpl) : 0;
|
| 269 |
const renderedLines = els.seq.children.length;
|
| 270 |
const needFull =
|
|
@@ -275,6 +282,43 @@
|
|
| 275 |
(colorMode === "logprob" && lpRangeShifted(lastRenderedLpRange, lpRange));
|
| 276 |
if (needFull) fullRender(bpl);
|
| 277 |
else incrementalRender(bpl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
let renderQueued = false;
|
| 280 |
function scheduleRender() {
|
|
@@ -466,5 +510,15 @@
|
|
| 466 |
if (document.fonts && document.fonts.ready) {
|
| 467 |
document.fonts.ready.then(() => { charMetrics = null; renderSequence(); });
|
| 468 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
})();
|
| 470 |
|
|
|
|
| 158 |
return DARK_RGB_S;
|
| 159 |
}
|
| 160 |
function measureSeqChars() {
|
| 161 |
+
// Measure inside the actual seq block so the probe inherits the same
|
| 162 |
+
// font, size, weight, letter-spacing, font-feature-settings and any
|
| 163 |
+
// ancestor-driven context. Earlier this lived on document.body with
|
| 164 |
+
// hand-mirrored font styles, but that drifted from the real rendering
|
| 165 |
+
// context (font fallback while the web font was still loading, or any
|
| 166 |
+
// future style change on .sb-seq-block) and the resulting blockW was
|
| 167 |
+
// narrower than reality → bpl overshot → lines overflowed.
|
| 168 |
const probe = document.createElement("div");
|
| 169 |
+
probe.style.cssText = "position:absolute;visibility:hidden;top:-9999px;left:-9999px;white-space:pre;pointer-events:none";
|
| 170 |
probe.textContent = " 1 ";
|
| 171 |
+
els.seq.appendChild(probe);
|
| 172 |
const prefixW = probe.getBoundingClientRect().width;
|
| 173 |
probe.textContent = "AAAAAAAAAA ";
|
| 174 |
const blockW = probe.getBoundingClientRect().width;
|
| 175 |
+
els.seq.removeChild(probe);
|
| 176 |
charMetrics = { prefixW, blockW };
|
| 177 |
}
|
| 178 |
function basesPerLineSb() {
|
|
|
|
| 271 |
if (colorMode === "logprob") recomputeLpRange();
|
| 272 |
const total = promptBases + genText;
|
| 273 |
els.copy.disabled = total.length === 0;
|
| 274 |
+
let bpl = basesPerLineSb();
|
| 275 |
const totalLines = total ? Math.ceil(total.length / bpl) : 0;
|
| 276 |
const renderedLines = els.seq.children.length;
|
| 277 |
const needFull =
|
|
|
|
| 282 |
(colorMode === "logprob" && lpRangeShifted(lastRenderedLpRange, lpRange));
|
| 283 |
if (needFull) fullRender(bpl);
|
| 284 |
else incrementalRender(bpl);
|
| 285 |
+
// Self-correct: the probe-based bpl can overshoot when the web font
|
| 286 |
+
// wasn't loaded yet at measure-time (the fallback monospace is
|
| 287 |
+
// narrower than JetBrains Mono → blockW too small → bpl too large).
|
| 288 |
+
// Measure the actual rendered first line, scale bpl down proportionally
|
| 289 |
+
// until it fits, then back-solve charMetrics so future renders converge
|
| 290 |
+
// on the right value without the loop.
|
| 291 |
+
if (total) {
|
| 292 |
+
const cs = getComputedStyle(els.seq);
|
| 293 |
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
| 294 |
+
const padR = parseFloat(cs.paddingRight) || 0;
|
| 295 |
+
const fit = els.seq.clientWidth - padL - padR;
|
| 296 |
+
let safety = 3;
|
| 297 |
+
while (safety-- > 0 && fit > 0) {
|
| 298 |
+
const first = els.seq.firstElementChild;
|
| 299 |
+
if (!first || first.scrollWidth <= fit + 2) break;
|
| 300 |
+
if (bpl <= 10) break;
|
| 301 |
+
// Proportional shrink so we converge in 1-2 iterations even when
|
| 302 |
+
// the probe was off by 2x (font fallback case).
|
| 303 |
+
const ratio = fit / first.scrollWidth;
|
| 304 |
+
const next = Math.max(10, Math.floor((bpl * ratio) / 10) * 10);
|
| 305 |
+
if (next >= bpl) break;
|
| 306 |
+
bpl = next;
|
| 307 |
+
fullRender(bpl);
|
| 308 |
+
}
|
| 309 |
+
const first = els.seq.firstElementChild;
|
| 310 |
+
if (first && fit > 0 && bpl >= 10) {
|
| 311 |
+
// Back-solve charMetrics from the rendered first line so the next
|
| 312 |
+
// basesPerLineSb call lands at this bpl without retriggering the loop.
|
| 313 |
+
const renderedW = first.getBoundingClientRect().width;
|
| 314 |
+
const usedBlocks = bpl / 10;
|
| 315 |
+
const assumedPrefix = (charMetrics && charMetrics.prefixW) || 65;
|
| 316 |
+
const recoveredBlockW = (renderedW - assumedPrefix) / usedBlocks;
|
| 317 |
+
if (recoveredBlockW > 0 && isFinite(recoveredBlockW)) {
|
| 318 |
+
charMetrics = { prefixW: assumedPrefix, blockW: recoveredBlockW };
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
}
|
| 323 |
let renderQueued = false;
|
| 324 |
function scheduleRender() {
|
|
|
|
| 510 |
if (document.fonts && document.fonts.ready) {
|
| 511 |
document.fonts.ready.then(() => { charMetrics = null; renderSequence(); });
|
| 512 |
}
|
| 513 |
+
// First time the sandbox panel is shown (or any time we come back to
|
| 514 |
+
// it), the seq block may have been laid out at display:none, leaving
|
| 515 |
+
// basesPerLineSb with a stale measurement (clientWidth = 0 → bpl
|
| 516 |
+
// fallback, or font-not-yet-loaded probe → blockW too small → bpl too
|
| 517 |
+
// large → overflow). Drop charMetrics and re-render once the panel is
|
| 518 |
+
// actually visible, after the browser has had a frame to paint it.
|
| 519 |
+
window.addEventListener("tab:changed", e => {
|
| 520 |
+
if (e.detail?.name !== "sandbox") return;
|
| 521 |
+
requestAnimationFrame(() => { charMetrics = null; renderSequence(); });
|
| 522 |
+
});
|
| 523 |
})();
|
| 524 |
|
|
@@ -23,6 +23,13 @@
|
|
| 23 |
if (opts.anchor) location.hash = opts.anchor;
|
| 24 |
else if (location.hash.replace("#", "") !== name) location.hash = name;
|
| 25 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
// Exposed so the §0 intro-guide cards (in sections/intro.js) can jump tabs.
|
| 28 |
window.setTab = setTab;
|
|
|
|
| 23 |
if (opts.anchor) location.hash = opts.anchor;
|
| 24 |
else if (location.hash.replace("#", "") !== name) location.hash = name;
|
| 25 |
}
|
| 26 |
+
// Inform tab modules that own layout-measured DOM (e.g. sandbox's
|
| 27 |
+
// bases-per-line calc, which depends on the seq block's clientWidth)
|
| 28 |
+
// that they should re-measure on the next frame, after the new panel
|
| 29 |
+
// has actually been laid out at display:block. Without this, a panel
|
| 30 |
+
// that was first measured while hidden (clientWidth = 0, or before
|
| 31 |
+
// its web font loaded) keeps stale metrics until the user nudges it.
|
| 32 |
+
window.dispatchEvent(new CustomEvent("tab:changed", { detail: { name } }));
|
| 33 |
}
|
| 34 |
// Exposed so the §0 intro-guide cards (in sections/intro.js) can jump tabs.
|
| 35 |
window.setTab = setTab;
|
|
@@ -223,12 +223,16 @@
|
|
| 223 |
|
| 224 |
#panel-sandbox .sb-seq-block {
|
| 225 |
font-family: "JetBrains Mono", monospace;
|
| 226 |
-
background: #
|
| 227 |
padding: 16px 20px;
|
| 228 |
white-space: pre; font-size: 12px; font-weight: 400;
|
| 229 |
line-height: 1.85; letter-spacing: 1.5px;
|
| 230 |
min-height: 80px;
|
| 231 |
flex: 1 0 auto;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
#panel-sandbox .sb-seq-block.empty {
|
| 234 |
color: #cfcfcf;
|
|
|
|
| 223 |
|
| 224 |
#panel-sandbox .sb-seq-block {
|
| 225 |
font-family: "JetBrains Mono", monospace;
|
| 226 |
+
background: #fff; border: 1px solid #ddd;
|
| 227 |
padding: 16px 20px;
|
| 228 |
white-space: pre; font-size: 12px; font-weight: 400;
|
| 229 |
line-height: 1.85; letter-spacing: 1.5px;
|
| 230 |
min-height: 80px;
|
| 231 |
flex: 1 0 auto;
|
| 232 |
+
/* Defense-in-depth: if the bases-per-line calculation ever overshoots
|
| 233 |
+
(e.g. before the web font finishes loading), keep the overflow inside
|
| 234 |
+
the block as a horizontal scroll instead of bleeding into the page. */
|
| 235 |
+
overflow-x: auto;
|
| 236 |
}
|
| 237 |
#panel-sandbox .sb-seq-block.empty {
|
| 238 |
color: #cfcfcf;
|