yggdrasil/scripts/vt-theme-harness.html
xfy bbb77954e9 fix(theme): sync CodeMirror/xterm theme in VT callback for circular-expand animation
主题切换的圆形展开 VT 动画对可运行代码块不生效:圆形扫过 CodeMirror/xterm
区域时看不到颜色变化,动画结束后才瞬切。

根因(逐帧像素验证确认):VT 回调只 toggle .dark class,但 CodeMirror 背景
由 catppuccin Extension 注入(.cm-editor 的 EditorView.theme 规则),xterm
背景由 .xterm-scrollable-element 的 inline background-color 注入——两者都
不随 .dark 翻转。set_theme 由 Dioxus use_effect 在 theme.set() 之后异步触发,
晚于 NEW 快照捕获,导致 OLD/NEW 快照在编辑器区域同色,圆形展开无可揭示的变化。

修复:在 VT 回调内(NEW 快照捕获前)同步 dispatch 'yggdrasil:theme-change'
CustomEvent,CodeMirror/xterm 各自的 index.ts 在模块加载时订阅并遍历
_instances 同步调 setTheme。事件先于 applyDarkClass dispatch,确保编辑器换肤
+ class 翻转被同一个 getComputedStyle reflow 捕获进 NEW 快照。

与现有 Dioxus use_effect 幂等共存:setTheme 对相同主题是 no-op(CodeMirror
Compartment.reconfigure + xterm options.theme = 均幂等),use_effect 作兜底
覆盖初始挂载 / 非 VT 场景。

验证:
- yggdrasil-core 测试扩展:断言 VT 回调 / 降级路径 / applyResolvedTheme 都
  dispatch 事件,且事件先于 dark class 翻转
- scripts/vt-theme-sampler.mjs(Playwright 逐帧像素采样):修复前 xterm 比
  等距的 cssvar 探针晚 100ms 变色(bug);修复后同帧变色(lag=0ms)

改动文件:
- libs/yggdrasil-core/src/theme-transition.ts: THEME_CHANGE_EVENT 常量 +
  notifyThemeChange 辅助函数,VT 回调 / 降级路径 / applyResolvedTheme 三处 dispatch
- libs/codemirror-editor/src/index.ts: 订阅事件,forEach _instances.setTheme
- libs/xterm-terminal/src/index.ts: 同样模式
- libs/yggdrasil-core/src/theme-transition.test.ts: 3 个新用例
- public/xterm/terminal.js: 重建产物(含事件订阅)
- scripts/vt-theme-{harness,sampler}: 逐帧像素验证工具(throwaway,可作回归)
2026-07-13 11:20:08 +08:00

148 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<!--
VT 主题切换动画验证 harness —— 逐帧像素采样。
目的:验证「暗/亮切换的圆形展开 VT 动画」是否对可运行代码块的
xterm 终端区域生效。预期 bug:VT 动画期间(0.4s),xterm canvas 背景仍是
旧色(因为 set_theme 在 VT 回调之后才由 Dioxus use_effect 触发,而 NEW 快照
在回调里就拍好了),圆形扫过时终端区域「不动」,动画结束后才瞬切。
本 harness 加载项目真实的构建产物(不做任何 JS 改写),用真实的
yggdrasil-core __startThemeTransition + 真实的 XtermTerminal.create +
真实的 terminal.css。额外放三个采样探针 div,由 Playwright 按坐标读色:
- body-probe : 背景 = var(--color-paper-theme) (随 .dark 翻转)
- cssvar-probe : 背景 = var(--color-paper-code-block) (随 .dark 翻转)
- xterm-mount : XtermTerminal.create 挂载点,canvas 渲染 (命令式 set_theme)
关键:模拟真实时序——
1. __startThemeTransition(x, y) ← 同步:截 OLD → 回调翻 .dark → 截 NEW
2. 之后(用 setTimeout 模拟 Dioxus use_effect 的延迟)调 set_theme('dark')
这样 VT 的 NEW 快照里 xterm 仍是旧色,而 body/cssvar(随 CSS 变量)已是新色。
圆形展开用 body/cssvar 的 NEW 覆盖 OLD;但 xterm 的 NEW 快照=旧色,故圆形
扫过 xterm 区域时看不到变化——直到 set_theme 跑完才瞬切。
探针布局:toggle 原点在左侧,三个探针排成一行在右侧,使圆形从左向右扫过,
便于观察时序差异(理想:三探针同时变;bug 态:xterm 滞后)。
-->
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>VT theme harness</title>
<link rel="stylesheet" href="/yggdrasil-core/yggdrasil-core.css" />
<link rel="stylesheet" href="/xterm/terminal.css" />
<style>
/* ===== 复刻 input.css 里与本次验证相关的变量定义 =====
只抄验证需要的两对变量(theme / code-block),不引整个 input.css。 */
:root {
--color-paper-theme: #eff1f5; /* input.css light, Latte Base */
--color-paper-code-block: #dce0e8; /* input.css light, Latte Surface0 */
}
.dark {
--color-paper-theme: #1e1e2e; /* input.css dark, Mocha Base */
--color-paper-code-block: #313244;/* input.css dark, Mocha Surface0 */
}
html, body { margin: 0; padding: 0; }
body {
background: var(--color-paper-theme);
color: #4c4f69;
font-family: ui-monospace, monospace;
transition: none;
}
/* 三个探针:固定大小 60×60,采样中心像素。
关键布局:三个探针放在同一垂直列(top 相同),水平紧邻——使它们到 toggle
原点的距离几乎相同,圆形 clip-path 同时覆盖三者。这样任何「xterm 比 body 晚变色」
的 lag 都只能来自快照不同步(真 bug),而非几何距离差(假阳性)。 */
.probe {
position: fixed;
top: 120px;
width: 60px;
height: 60px;
border: 1px solid rgba(0,0,0,0.15);
}
#body-probe {
left: 200px;
background: var(--color-paper-theme);
}
#cssvar-probe {
left: 270px;
background: var(--color-paper-code-block);
}
#xterm-mount {
left: 340px;
/* 容器底色与生产一致(runner.rs 输出区 div 用 var(--color-paper-code-block)),
xterm canvas 画在其上。采样点取 canvas 中心,读的是 xterm 画的背景色。 */
background: var(--color-paper-code-block);
}
#xterm-mount .xterm { height: 100%; }
#xterm-mount .xterm-viewport { height: 100% !important; }
.label {
position: fixed; top: 10px; font-size: 10px;
color: #888; pointer-events: none; white-space: nowrap;
}
#toggle-btn {
position: fixed; left: 40px; top: 40px;
padding: 12px 20px; font-size: 14px; cursor: pointer;
background: #fff; border: 1px solid #999;
}
#status {
position: fixed; left: 40px; top: 90px;
font-size: 12px; color: #555;
}
</style>
</head>
<body class="">
<button id="toggle-btn" type="button">toggle theme</button>
<div id="status">ready</div>
<div class="label" style="left:200px">body-probe</div>
<div class="label" style="left:270px">cssvar-probe</div>
<div class="label" style="left:340px">xterm-mount</div>
<div id="body-probe" class="probe"></div>
<div id="cssvar-probe" class="probe"></div>
<div id="xterm-mount" class="probe"></div>
<script src="/yggdrasil-core/yggdrasil-core.js"></script>
<script src="/xterm/terminal.js"></script>
<script>
// 挂载真实 xterm 实例,写几行字让 canvas 有可见像素。
// XtermOptions 构建产物里是空 class;直接赋值 JS 属性(theme/fontSize/onReady)。
// create 直接返回 TerminalInstance;onReady 在构造函数末尾同步触发(create 返回前),
// 故 onReady 只置标志,返回后再写内容。
window.__harnessReady = false;
(function () {
var opts = new window.XtermOptions();
opts.theme = 'light';
opts.fontSize = 13;
opts.onReady = function () { window.__xtermConstructed = true; };
var inst = window.XtermTerminal.create('xterm-mount', opts);
window.__harnessTerm = inst;
if (inst) {
inst.writeAll('hello\nline2\nline3', '');
window.__harnessReady = true;
document.getElementById('status').textContent = 'xterm mounted';
}
})();
// toggle:模拟真实 ThemeToggle::onclick 时序。
// __startThemeTransition(x, y) ← VT:截OLD → 回调(翻.dark + dispatch 事件) → 截NEW
//
// 修复后,CodeMirror/xterm 的 set_theme 由 VT 回调内的 'yggdrasil:theme-change'
// 事件同步触发(在 NEW 快照捕获前),不再需要外部 setTimeout 模拟 Dioxus use_effect。
// 真实 Dioxus use_effect 仍会跑(兜底),但本 harness 不模拟它——只验证 VT 事件路径
// 是否让 xterm 在快照前换肤(这才是修复的核心)。
//
// 原点放在按钮中心(左上),圆形向右展开扫过三个探针。
document.getElementById('toggle-btn').addEventListener('click', function (e) {
var fn = window.__startThemeTransition;
if (typeof fn === 'function') {
fn(e.clientX, e.clientY);
} else {
document.documentElement.classList.toggle('dark');
}
});
</script>
</body>
</html>