From fe86fb3a4d64d055b1837b5a809d241d40f573da Mon Sep 17 00:00:00 2001 From: yetone Date: Fri, 2 Oct 2026 06:10:49 +0800 Subject: [PATCH] =?UTF-8?q?gui:=20the=20page=20is=20first=20drawn=20in=20i?= =?UTF-8?q?ts=20saved=20theme,=20not=20the=20system's=20(seg-contrast=20fa?= =?UTF-8?q?iled=20in=20Chromium=20on=20the=20release=20branch:=20in=20dark?= =?UTF-8?q?=20the=20view=20shown=20read=20at=203.96:1,=20the=20other=20vie?= =?UTF-8?q?ws=20at=204.69:1)=20=E2=80=94=20the=20saved=20theme=20was=20put?= =?UTF-8?q?=20on=20the=20page=20by=20app.js,=20at=20the=20foot=20of=20inde?= =?UTF-8?q?x.html,=20so=20a=20window=20kept=20dark=20under=20a=20light=20s?= =?UTF-8?q?ystem=20had=20its=20header=20parsed=20and=20styled=20light=20fi?= =?UTF-8?q?rst,=20and=20its=20buttons'=20colour=20transitions=20(the=20swi?= =?UTF-8?q?tch's=20views,=20Sync,=20Settings)=20then=20faded=20them=20to?= =?UTF-8?q?=20dark=20as=20the=20page=20loaded;=20the=20test=20read=20them?= =?UTF-8?q?=20mid-way,=20the=20more=20so=20as=20#471=20made=20the=20page?= =?UTF-8?q?=20longer=20to=20parse.=20theme.js,=20run=20in=20the=20head=20a?= =?UTF-8?q?fter=20boot.js=20and=20before=20the=20styles,=20sets=20it=20fro?= =?UTF-8?q?m=20bootPrefs=20(or=20=3Ftheme=3D)=20for=20the=20first=20paint;?= =?UTF-8?q?=20app.js=20still=20sets=20it=20as=20before.=20seg-contrast.tes?= =?UTF-8?q?t.cjs=20now=20holds=20app.js=20back=20and=20checks=20the=20page?= =?UTF-8?q?=20was=20already=20drawn=20in=20its=20theme=20(fails=20without?= =?UTF-8?q?=20theme.js,=20Chromium=20and=20WebKit,=20en=20and=20zh,=20dark?= =?UTF-8?q?),=20and=20reads=20colours=20once=20the=20page's=20transitions?= =?UTF-8?q?=20are=20over.=20old-webkit=20lets=20theme.js=20(ES5,=20URLSear?= =?UTF-8?q?chParams)=20run=20before=20compat.js;=20api-key-theme,=20from?= =?UTF-8?q?=20the=20release=20branch,=20expects=20a=20text=20button=20in?= =?UTF-8?q?=20--ctl-fg=20as=20#477=20set.=20seg-contrast,=20settings-secti?= =?UTF-8?q?ons,=20header-zoom-fit,=20header-centre,=20click-scroll,=20old-?= =?UTF-8?q?webkit,=20api-key-theme,=20omarchy,=20agent-grip,=20web-page,?= =?UTF-8?q?=20gateway-fold,=20sessions-toolbar=20pass;=20go=20vet,=20go=20?= =?UTF-8?q?build=20-tags=20nogui,=20go=20test=20./internal/gui=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gui/assets/index.html | 1 + internal/gui/assets/theme.js | 11 +++++++++++ internal/gui/tests/api-key-theme.test.cjs | 4 ++-- internal/gui/tests/old-webkit.test.cjs | 8 +++++--- internal/gui/tests/seg-contrast.test.cjs | 23 +++++++++++++++++++---- 5 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 internal/gui/assets/theme.js diff --git a/internal/gui/assets/index.html b/internal/gui/assets/index.html index a9310813..23e920e1 100644 --- a/internal/gui/assets/index.html +++ b/internal/gui/assets/index.html @@ -6,6 +6,7 @@ magpie + diff --git a/internal/gui/assets/theme.js b/internal/gui/assets/theme.js new file mode 100644 index 00000000..d3e2b782 --- /dev/null +++ b/internal/gui/assets/theme.js @@ -0,0 +1,11 @@ +// The saved theme (boot.js), or one the page's address asks for, on the page +// before its styles first apply: set only once app.js ran, at the foot of the +// page, a window kept dark under a light system was first drawn light, and +// the header's buttons faded from light to dark as it loaded. app.js sets it +// again; this one is for the first paint. +(function () { + var asked = new URLSearchParams(location.search).get("theme"); + var saved = window.bootPrefs && window.bootPrefs.theme; + var theme = asked || (saved && saved !== "system" ? saved : ""); + if (theme) document.documentElement.dataset.theme = theme; +})(); diff --git a/internal/gui/tests/api-key-theme.test.cjs b/internal/gui/tests/api-key-theme.test.cjs index 554758d6..59a9cde8 100644 --- a/internal/gui/tests/api-key-theme.test.cjs +++ b/internal/gui/tests/api-key-theme.test.cjs @@ -12,7 +12,7 @@ async function palette(page, selectors) { const hex = root.getPropertyValue(name).trim(); return `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`; }; - const colors = Object.fromEntries(["bg", "card", "card-2", "fg", "fg-2", "muted", "pill", "line", "accent", "accent-fg"] + const colors = Object.fromEntries(["bg", "card", "card-2", "fg", "fg-2", "muted", "ctl-fg", "pill", "line", "accent", "accent-fg"] .map((name) => [name, rgb("--" + name)])); const elements = Object.fromEntries(Object.entries(selectors).map(([name, selector]) => { const s = getComputedStyle(document.querySelector(selector)); @@ -60,7 +60,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", assert.equal(input.bg, c.card, "key inputs must not use the browser's default background"); assert.equal(input.color, c.fg); } - assert.equal(e.rotate.color, c.muted); + assert.equal(e.rotate.color, c["ctl-fg"]); // a text button, in a control's quiet text (#477) assert.equal(e.keyPick.bg, "rgba(0, 0, 0, 0)"); assert.equal(e.create.bg, c.accent); assert.equal(e.create.color, c["accent-fg"]); diff --git a/internal/gui/tests/old-webkit.test.cjs b/internal/gui/tests/old-webkit.test.cjs index 8cabee9f..56ea56cf 100644 --- a/internal/gui/tests/old-webkit.test.cjs +++ b/internal/gui/tests/old-webkit.test.cjs @@ -132,10 +132,12 @@ test("no built-in newer than Safari 15.0 unless compat.js fills it in", async () test("compat.js is the first of the page's own scripts", async () => { const html = await read("index.html"); - // boot.js and omarchy.js run in the head, before the first paint: - // omarchy.js does nothing off Omarchy, whose WebKitGTK is a current one + // boot.js, theme.js and omarchy.js run in the head, before the first + // paint: theme.js is ES5 with nothing newer than Safari 10.1's + // URLSearchParams, and omarchy.js does nothing off Omarchy, whose + // WebKitGTK is a current one const all = [...html.matchAll(/ m[1]); - const srcs = all.filter((s) => s !== "boot.js" && s !== "omarchy.js"); + const srcs = all.filter((s) => s !== "boot.js" && s !== "theme.js" && s !== "omarchy.js"); assert.equal(srcs[0], "compat.js", "loaded first: " + srcs.join(", ")); for (const f of await scripts()) assert(all.includes(f), f + " is not loaded by index.html"); }); diff --git a/internal/gui/tests/seg-contrast.test.cjs b/internal/gui/tests/seg-contrast.test.cjs index d209900f..203fb0e3 100644 --- a/internal/gui/tests/seg-contrast.test.cjs +++ b/internal/gui/tests/seg-contrast.test.cjs @@ -8,7 +8,11 @@ // browser computed them, each background laid over the ones under it: the // other views and a text button on the page, a card and the inset editor // read at 4.5:1 with some margin to spare (4.7), the view shown on its thumb -// too, and the thumb is told from the track by more than it was. Chromium +// too, and the thumb is told from the track by more than it was. They are +// read once the page's transitions are over; and the page is first drawn in +// its theme, before app.js runs: a window kept dark under a light system was +// first drawn light, its header's buttons fading to dark as they were read, +// so they measured mid-way (3.96:1 for the view shown). Chromium // and WebKit, English and Chinese; no backend, the API is faked here. const assert = require("node:assert/strict"); const fs = require("node:fs/promises"); @@ -18,9 +22,10 @@ const { chromium, webkit } = require("playwright"); const assets = path.resolve(__dirname, "../assets"); -function server(lang, theme) { +function server(lang, theme, gate) { return async (route) => { const url = new URL(route.request().url()); + if (url.pathname === "/app.js") await gate; if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"${theme}",web:false};` }); if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" }); if (url.pathname === "/api/state") return route.fulfill({ json: { agents: [], profiles: [], settings: { lang, theme } } }); @@ -101,10 +106,20 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", const ctx = await browser.newContext({ viewport: { width: 1000, height: 640 }, colorScheme: scheme }); const page = await ctx.newPage(); page.on("pageerror", (e) => errors.push(e.message)); - await page.route("**/*", server(lang, theme)); - await page.goto("http://magpie.test/"); + let release; + const gate = new Promise((r) => { release = r; }); + await page.route("**/*", server(lang, theme, gate)); + // the page as first drawn, app.js held back: already in its theme + const loaded = page.goto("http://magpie.test/"); + await page.waitForSelector("#nav", { state: "attached" }); + const first = await page.evaluate(() => getComputedStyle(document.body).backgroundColor); + release(); + await loaded; await page.waitForSelector("#nav button.on"); await page.waitForFunction(() => document.querySelector("#nav .thumb")?.getBoundingClientRect().width > 0); + assert.equal(first, await page.evaluate(() => getComputedStyle(document.body).backgroundColor), "the page was first drawn in another theme"); + // what is drawn once it has settled, not a colour on its way + await page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.transitionProperty).map((a) => a.finished.catch(() => {})))); const r = await ratios(page), at = JSON.stringify(r); for (const [what, v] of Object.entries(r)) { if (what === "thumb against the track") continue;