crates/gunnar-ui-server/src/passkeys_at_gunnar/first_touch/readme_selection.rs
//! ★ **SHADOW-SEL (owner 2026-10-26, gunnar.rs/repo/nordisk/gunnar): selecting the README with
//! the mouse showed a SHADOW copy of the words.** The default grade is docbook's typeset SVG: the
//! glyphs are paths, and over them lies an invisible, selectable text layer (`fill:
//! transparent`). A selection must paint only its highlight over that layer, never the layer's
//! words, or they show beside the glyphs as a second, offset copy.
//!
//! A README typeset by the console's own renderer, in the console's own stylesheet, in headless
//! Chromium, in each theme: everything is selected and a screenshot taken with the glyph paths
//! hidden. What is left on the screen is the highlight alone: no dark pixel (the words drawn a
//! second time). The text layer's own `::selection` style is read too. Needs `CHROME`.
use serde_json::json;
use super::{chrome, Chromium};
const CSS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../gunnar-ui-web/styles/console.css"));
fn page(svg: &str, theme: &str) -> String {
format!(
"<!doctype html><html data-theme=\"{theme}\"><head><meta charset=\"utf-8\"><style>{CSS}</style></head>\
<body><main class=\"page\" style=\"width:900px\"><div class=\"readme-page\">{svg}</div></main></body></html>"
)
}
#[test]
fn a_selected_readme_shows_its_words_once_with_no_shadow_copy() {
let Some(chrome) = chrome() else { return };
let md = "# The heading\n\nGunnar keeps every repository, and this line is prose to select.\n\nA second paragraph, with more words in it.\n";
let svg = crate::readme::render(md, "nordisk/gunnar README.md", &crate::readme::Pictures::new(), crate::readme::Shape::Svg).expect("the README typesets");
assert!(svg.contains("class=\"text-layer\""), "the typeset page carries its text layer");
let c = Chromium::launch(&chrome);
for theme in ["light", "dark", "surprise"] {
c.open("about:blank");
c.until("a blank page", "document.readyState === 'complete'");
let frame = c.on_page("Page.getFrameTree", json!({}))["frameTree"]["frame"]["id"].as_str().expect("the frame").to_owned();
c.on_page("Page.setDocumentContent", json!({ "frameId": frame, "html": page(&svg, theme) }));
c.until("the typeset README", "document.querySelectorAll('.readme-page svg .text-layer text').length > 0");
// The text layer under ::selection paints nothing of its own, in every engine's word for
// it: Chromium paints a selected SVG text with `fill`, Firefox and Safari with the
// selection's `color` (a transparent fill alone left the words drawn there, offset
// from the glyphs: the owner's shadow copy).
let sel = c.js("(() => { const t = document.querySelector('.readme-page svg .text-layer text'); const s = getComputedStyle(t, '::selection'); return { fill: s.fill, color: s.color, textFill: s.webkitTextFillColor, shadow: s.textShadow }; })()");
let clear = |v: &serde_json::Value| matches!(v.as_str().unwrap_or(""), "transparent" | "rgba(0, 0, 0, 0)" | "none");
assert!(clear(&sel["fill"]) && clear(&sel["color"]) && clear(&sel["textFill"]), "{theme}: the selected text layer still paints its words: {sel}");
assert_eq!(sel["shadow"], json!("none"), "{theme}: a selection draws no shadow: {sel}");
// Everything selected, the glyph paths hidden: what is left is the highlight alone.
c.js("(() => { document.querySelectorAll('.readme-page svg > g[aria-hidden=\"true\"], .readme-page svg g[aria-hidden=\"true\"]').forEach(g => g.style.visibility = 'hidden'); const r = document.createRange(); r.selectNodeContents(document.querySelector('.readme-page svg .text-layer')); const s = getSelection(); s.removeAllRanges(); s.addRange(r); return s.toString().length; })()");
assert!(c.js("getSelection().toString()").as_str().unwrap_or("").contains("prose to select"), "{theme}: the words are selected");
let shot = c.on_page("Page.captureScreenshot", json!({ "format": "png", "clip": { "x": 0, "y": 0, "width": 900, "height": 400, "scale": 1 } }));
let png = base64_decode(shot["data"].as_str().expect("a screenshot"));
let drawn = stray_pixels(&png);
// The panel's rounded corners are a few dozen blended pixels; words are thousands.
assert!(drawn < 200, "{theme}: with the glyphs hidden, the selection still draws {drawn} pixels of words: a shadow copy");
}
}
/// The pixels in a PNG that are none of its four commonest colours (the page, the panel, its
/// border and the highlight, each one solid colour): words drawn are antialiased into many.
fn stray_pixels(png: &[u8]) -> usize {
let img = image::load_from_memory(png).expect("a PNG").to_rgb8();
let mut count: std::collections::HashMap<[u8; 3], usize> = std::collections::HashMap::new();
for p in img.pixels() {
*count.entry(p.0).or_default() += 1;
}
let mut by: Vec<usize> = count.values().copied().collect();
by.sort_unstable_by(|a, b| b.cmp(a));
by.iter().skip(4).sum()
}
fn base64_decode(s: &str) -> Vec<u8> {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = Vec::with_capacity(s.len() * 3 / 4);
let (mut acc, mut bits) = (0u32, 0u32);
for b in s.bytes() {
let Some(v) = T.iter().position(|&t| t == b) else { continue };
acc = (acc << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
out
}