master
John Lauer v2.0.558 (insiders): Extension-lane taskbar identity: late rebind covers unbound, dead-hwnd and unstamped ext windows;… a2714a9 1mo ago
// Adom Pup thread tile: the ONE renderer for the taskbar name tile (the thread's name on the teal
// plate, every ICO entry drawn at its own size). Two hosts run this same file:
//   - the extension service worker (action "tile"): OffscreenCanvas, no page involved, so a page's
//     Content-Security-Policy cannot block it (claude.ai and the wiki did, 2026-09-14, and the tile
//     silently fell back to the per-window icon);
//   - a page evaluation on the CDP lane: the Rust side embeds this file verbatim (include_str!).
// Returns { lines, size, tiles: { <px>: base64 RGBA } }. Keep the layout identical in both hosts.
self.pupRenderTile = async function (nm, fontUrl, sizes, R) {
  const mk = (w, h) => (typeof document !== 'undefined')
    ? Object.assign(document.createElement('canvas'), { width: w, height: h })
    : new OffscreenCanvas(w, h);
  try { const f = new FontFace('FGtile', 'url(' + fontUrl + ')'); await f.load(); (self.fonts || document.fonts).add(f); } catch (e) {}
const box = 256;
    const cv = mk(box, box); const ctx = cv.getContext('2d');
    const setF = (s) => { ctx.font = '700 ' + s + "px FGtile,'Segoe UI',sans-serif"; };
    const wid = (t, s) => { setF(s); return ctx.measureText(t).width; };
    const FLOOR = box * 0.36;
    const wFor = (i, total) => (total === 2 && i === 0) ? box * 0.78 : box * 0.90;   // text starts at 7.5% in; 0.90 keeps the right edge inside the plate (Node: 0.92 from 5.5%)
    const fitSize = (lines) => { let s = box * 0.9; for (let i = 0; i < 90; i++) { let ok = true; for (let j = 0; j < lines.length; j++) { if (wid(lines[j], s) > wFor(j, lines.length)) { ok = false; break; } } if (ok && lines.length * s * 0.95 <= box * 0.88) break; s *= 0.95; } return s; };
    let best = null, bf = 0;
    // whole words first (split on spaces, after hyphens and slashes), two lines, only if it fits at or above the floor
    const toks = nm.split(/\s+/).flatMap(t => t.split(/(?<=[-\/])/)).filter(Boolean);
    if (toks.length > 1) {
      for (let k = 1; k < toks.length; k++) {
        const glue = (t) => t.reduce((a, x) => a + ((a && !/[-\/]$/.test(a)) ? ' ' : '') + x, '');
        const c = [glue(toks.slice(0, k)), glue(toks.slice(k))]; const f = fitSize(c);
        if (f >= FLOOR && f > bf) { bf = f; best = c; }
      }
      const one = fitSize([nm]); if (one >= FLOOR && one > bf) { bf = one; best = [nm]; }
    }
    if (!best) {
      const cands = [[nm]]; const sp = nm.lastIndexOf(' '), hy = nm.indexOf('-');
      if (sp > 0) cands.push([nm.slice(0, sp), nm.slice(sp + 1)]); else if (hy > 0) cands.push([nm.slice(0, hy + 1), nm.slice(hy + 1)]);
      if (sp < 0 && hy < 0 && nm.length >= 7) { const m = Math.ceil(nm.length / 2); cands.push([nm.slice(0, m), nm.slice(m)]); }
      best = [nm]; bf = 0;
      for (const c of cands) { const f = fitSize(c); if (f > bf) { bf = f; best = c; } }
      if (bf < FLOOR) {
        bf = FLOOR;
        const take = (str, maxw) => { let n = 0; for (let i = 1; i <= str.length; i++) { if (wid(str.slice(0, i), FLOOR) <= maxw) n = i; else break; } return n; };
        let rest = String(nm).trim(); const out = [];
        for (let ln = 0; ln < 2 && rest; ln++) {
          let n = take(rest, wFor(ln, 2));
          if (n >= rest.length) { out.push(rest); rest = ''; break; }
          if (n < 1) n = 1;
          const seg = rest.slice(0, n + 1);
          const bp = Math.max(seg.lastIndexOf(' '), seg.lastIndexOf('-'));
          const cut = (bp >= Math.ceil(n * 0.5)) ? bp + (seg.charAt(bp) === '-' ? 1 : 0) : n;
          out.push(rest.slice(0, cut).trim());
          rest = rest.slice(cut).replace(/^[ -]+/, '');
        }
        best = out.filter(Boolean);
      }
    }
    // t10: draw EVERY entry at its own size (plate rx = s/5.5, name bottom-left in the on-accent colour), so the
    // glyphs are rasterised by Skia for that exact pixel grid; the 256 layout only decides lines and font size.
    const tiles = {};
    for (const s of sizes) {
      const big = mk(s, s); const c = big.getContext('2d');
      const k = s / box, r = s / 5.5;
      c.beginPath(); c.moveTo(r, 0); c.arcTo(s, 0, s, s, r); c.arcTo(s, s, 0, s, r); c.arcTo(0, s, 0, 0, r); c.arcTo(0, 0, s, 0, r); c.closePath(); c.fillStyle = '#00b8b1'; c.fill();
      c.font = '700 ' + (bf * k) + "px FGtile,'Segoe UI',sans-serif"; c.fillStyle = '#05221f'; c.textAlign = 'left'; c.textBaseline = 'alphabetic';
      const lh = bf * k * 0.95, mx = s * 0.075, baseY = s - s * 0.075 - bf * k * 0.14;
      best.forEach((ln, i) => c.fillText(ln, mx, baseY - (best.length - 1 - i) * lh));
      const d = c.getImageData(0, 0, s, s).data; let bin = ''; const CH = 8192; for (let i = 0; i < d.length; i += CH) bin += String.fromCharCode.apply(null, d.subarray(i, Math.min(i + CH, d.length)));
      tiles[s] = btoa(bin);
    }
    return { lines: best, size: Math.round(bf), tiles };
};