1. 「元素超出浮动面板」真根因(用户 2026-10-30 报告的截图)
- 绘制侧 PaintEdgeCursor 的 itemized 仍是 (centerAxis || meanFilter || weakPeak),
而布局侧 EdgePanelLayoutFor 早已是 (centerAxis || weakPeak)。
于是均值滤波面板被旧的「逐条目」分支**再画一遍**(每行名称 + 复选框,且没有任何裁切),
滚动后那些行的坐标是负值/越界 ⇒ 直接画到标题栏上。
- 修法:绘制侧与布局侧统一;旧分支的兜底循环跳过条件补 meanFilter。
底部「构建为新信号」由共享的 if (hasButton) 绘制,不受影响。
- 顺带删除「只画完全落在内容区里的行」的 fullyInside 门控:真因修掉后
g.SetClip(layout.content) 已足够,半截行正常显示,滚动观感连续。
2. 条目栏交互(与「自由标记」分组栏一致)
- 整行可点:复选框区 = 切换是否预览;已启用 = 展开/收起参数行;未启用 = 直接开始预览。
命中只要求点落在 layout.content 内(不再要求整行在内容区内),因此半截行也能操作。
- 悬停行 → 对应预览曲线高亮:新增 m_meanRowHoverLane / m_meanRowHoverRef,
由 OnMouseMove 维护、OnMouseLeave 与 InvalidateDerivedCaches 清除;
PaintDerivedCurve/PaintMeanFilterCurve/PaintWeakPeakCurve 新增 bool highlight,
先画 +5px 半透明同色光晕再把线加粗,行头叠一层 accent;悬停态变化会 InvalidateSceneCache。
3. 均值点数下限 1(用户要求:不可以为 0)
- kMeanFilterPointsMin 0 → 1;输入提交用 centerTarget ? 0 : kMeanFilterPointsMin
(中心轴边沿计数沿用旧语义 0 = 不平滑);预览与构建请求都再夹一次下限,
即使条目被外部写成 0,渲染/构建结果与 1 点逐位一致。
4. 网格线颜色成为主题配色的一部分
- struct Theme 新增 laneBg(网格叠底色);NormalizePlotContrast → FinalizeTheme,
先解 laneBg = EnsureContrast(plotBg, cardBg, 1.03),再让 gridWeak/gridStrong/
axisLine/borderStrong 统一对 laneBg 归一(1.30 / 1.70 / 2.10 / 1.70)。
- 四个主题各自给出显式取值:Dark 的网格线对叠底色原本只有 1.071(几乎不可见)⇒ 现 ≥1.30;
其它三个主题同步微调,网格可见性一致。ThemeLaneBackground 返回 laneBg。
5. 行三角尺寸统一
- 新增 kEdgePanelRowTriangleSize = 12 逻辑像素,均值滤波行与自由标记分组栏共用;
标题栏的 kEdgePanelDisclosureWidth = 20 保持不变。
6. 均值面板滚动条与文本
- 滚动条必须在 g.Restore(meanSave) 之后绘制(它在内容裁剪区之外,裁剪内画会被整条裁掉)。
- 滑块标签/数值改用 UiTextClipped(..., layout.content)(GDI 文本不受 GDI+ 裁剪约束)。
验证(全部离屏)
uicheck 69/69、lbcheck 21/21、lbresid 5/5、lbzorder 4/4、lbcovr 7/7、lbpanel 10/10、
derivecheck 16/16、round15check 28/28、themecheck 17/17、.preflight/meancheck 17/17;
.preflight/meanpix:滚到顶/中/底时标题栏与面板外均为 0 像素差异,内容区正常滚动;
rendercheck:A–H 八个场景哈希与基线完全一致;
make release 零 warning(805,376 B);make debug 零 warning。
同步文档:Documents/agent_mem.md 追加「2026-10-30 第十七轮」,
并把第十六轮里「均值点数(0…上限)」「命中要求行完全落在内容区里」两条改成现行做法。
395 lines
14 KiB
C++
395 lines
14 KiB
C++
#include "Theme.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
// Solarised Light 基础色板。
|
||
const std::uint32_t kBase03 = 0x002B36u; // #002B36
|
||
const std::uint32_t kBase02 = 0x073642u; // #073642
|
||
const std::uint32_t kBase01 = 0x586E75u; // #586E75
|
||
const std::uint32_t kBase00 = 0x657B83u; // #657B83
|
||
const std::uint32_t kBase0 = 0x839496u; // #839496
|
||
const std::uint32_t kBase1 = 0x93A1A1u; // #93A1A1
|
||
const std::uint32_t kBase2 = 0xEEE8D5u; // #EEE8D5
|
||
const std::uint32_t kBase3 = 0xFDF6E3u; // #FDF6E3
|
||
|
||
// Solarised 强调色。
|
||
const std::uint32_t kYellow = 0xB58900u; // #B58900
|
||
const std::uint32_t kOrange = 0xCB4B16u; // #CB4B16
|
||
const std::uint32_t kRed = 0xDC322Fu; // #DC322F
|
||
const std::uint32_t kMagenta = 0xD33682u; // #D33682
|
||
const std::uint32_t kViolet = 0x6C71C4u; // #6C71C4
|
||
const std::uint32_t kBlue = 0x268BD2u; // #268BD2
|
||
const std::uint32_t kCyan = 0x2AA198u; // #2AA198
|
||
const std::uint32_t kGreen = 0x859900u; // #859900
|
||
|
||
/** @brief 波形默认配色,按视觉对比度排序。 */
|
||
const std::uint32_t kSignalPalette[] = {
|
||
MakeArgb(kBlue), MakeArgb(kGreen), MakeArgb(kRed), MakeArgb(kViolet),
|
||
MakeArgb(kYellow), MakeArgb(kCyan), MakeArgb(kMagenta), MakeArgb(kOrange),
|
||
MakeArgb(kBase01), MakeArgb(kBase0),
|
||
};
|
||
|
||
/**
|
||
* @brief 主题**最终化**:确定网格叠底色,并保证结构色在**它**之上可辨。
|
||
*
|
||
* ⚠️⚠️ **参照底色必须是 `laneBg`(网格叠底),不是 `plotBg`**:
|
||
* 网格线画在每条网格内部的叠底上。深色主题里 `plotBg`(#1E1E1E)与 `laneBg`
|
||
* (#2D2D30)亮度差得远,只按 `plotBg` 校正会让网格线落进叠底色的亮度范围
|
||
* ⇒ 实测对比度只有 **1.07**(几乎看不见,用户 2026-10-30 报告)。
|
||
*
|
||
* 阈值取"Solarised Light 现有配色刚好达标"的水位(针对 `laneBg` 实测:
|
||
* 1.335 / 1.771 / 2.395 / 2.395),因此默认主题**逐位不变**;其它主题若不足才被校正。
|
||
*
|
||
* | 字段 | 阈值(相对 `laneBg`) | Solarised Light 现状 |
|
||
* | --- | ---: | ---: |
|
||
* | `gridWeak`(网格主刻线 + 五等分线基色) | 1.30 | 1.335 |
|
||
* | `gridStrong`(零线/加重线) | 1.70 | 1.771 |
|
||
* | `axisLine`(时间轴、网格分界线、绘图区外框) | 2.10 | 2.395 |
|
||
* | `borderStrong`(网格间分界线) | 1.70 | 2.395 |
|
||
*/
|
||
Theme FinalizeTheme(Theme theme)
|
||
{
|
||
const std::uint32_t plot = theme.plotBg | 0xFF000000u;
|
||
// 网格叠底:未显式配置时回退到 `cardBg`,并保证与绘图区底色可辨(1.03)。
|
||
const std::uint32_t laneSource =
|
||
(theme.laneBg != 0u) ? (theme.laneBg | 0xFF000000u) : (theme.cardBg | 0xFF000000u);
|
||
theme.laneBg = EnsureContrast(plot, laneSource, 1.03);
|
||
|
||
theme.gridWeak = EnsureContrast(theme.laneBg, theme.gridWeak | 0xFF000000u, 1.30);
|
||
theme.gridStrong = EnsureContrast(theme.laneBg, theme.gridStrong | 0xFF000000u, 1.70);
|
||
theme.axisLine = EnsureContrast(theme.laneBg, theme.axisLine | 0xFF000000u, 2.10);
|
||
theme.borderStrong = EnsureContrast(theme.laneBg, theme.borderStrong | 0xFF000000u, 1.70);
|
||
return theme;
|
||
}
|
||
|
||
/** @brief 全局唯一主题实例(Solarised Light;历史默认主题)。 */
|
||
const Theme kSolarizedLight = FinalizeTheme([]
|
||
{
|
||
Theme theme;
|
||
theme.windowBg = MakeArgb(kBase3);
|
||
theme.toolbarBg = MakeArgb(kBase2);
|
||
theme.panelBg = MakeArgb(kBase2);
|
||
theme.cardBg = MakeArgb(kBase3);
|
||
theme.plotBg = MakeArgb(kBase3);
|
||
theme.border = MakeArgb(0xD9D1BCu);
|
||
theme.borderStrong = MakeArgb(kBase1);
|
||
|
||
theme.text = MakeArgb(kBase00);
|
||
theme.textStrong = MakeArgb(kBase02);
|
||
theme.textMuted = MakeArgb(kBase1);
|
||
theme.textOnAccent = MakeArgb(kBase3);
|
||
|
||
theme.accent = MakeArgb(kBlue);
|
||
theme.accentSoft = MakeArgb(0xCFE4F3u);
|
||
theme.hover = MakeArgb(0xE4DCC6u);
|
||
theme.pressed = MakeArgb(0xD8CFB6u);
|
||
theme.selection = MakeArgb(0xDCEAF6u);
|
||
|
||
// 网格线需要在**网格叠底**(kBase3 推暗 1.03 ≈ #F9F2E0)上清晰可辨,同时又要与
|
||
// 网格边界(axisLine/borderStrong 的蓝灰)明显区分——因此采用偏暖的加深米色。
|
||
// 这三个值相对叠底色的对比度分别是 1.335 / 1.771 / 2.395,已达标 ⇒ 最终化不改动。
|
||
theme.laneBg = MakeArgb(0xF9F2E0u);
|
||
theme.gridWeak = MakeArgb(0xDDD3B8u);
|
||
theme.gridStrong = MakeArgb(0xC3B896u);
|
||
theme.axisLine = MakeArgb(kBase1);
|
||
theme.cursor = MakeArgb(kOrange);
|
||
theme.readoutBg = MakeArgb(0xF6EFDCu);
|
||
theme.danger = MakeArgb(kRed);
|
||
theme.warning = MakeArgb(kYellow);
|
||
return theme;
|
||
}());
|
||
|
||
/**
|
||
* @brief 浅色主题(参考 VS Code 2026 的默认浅色配色)。
|
||
*
|
||
* 与 Solarised Light 的区别:底色是**中性白/浅灰**而不是暖米色,
|
||
* 强调色是 VS Code 的系统蓝 `#0078D4`,描边是中性灰。
|
||
*/
|
||
const Theme kLight = FinalizeTheme([]
|
||
{
|
||
Theme theme;
|
||
theme.windowBg = MakeArgb(0xFFFFFFu); // editor.background
|
||
theme.toolbarBg = MakeArgb(0xF8F8F8u); // titleBar/sideBar
|
||
theme.panelBg = MakeArgb(0xF8F8F8u);
|
||
theme.cardBg = MakeArgb(0xFFFFFFu);
|
||
theme.plotBg = MakeArgb(0xFFFFFFu);
|
||
theme.border = MakeArgb(0xE5E5E5u); // panel.border
|
||
theme.borderStrong = MakeArgb(0xC3C3C3u);
|
||
|
||
theme.text = MakeArgb(0x3B3B3Bu); // foreground
|
||
theme.textStrong = MakeArgb(0x1F1F1Fu);
|
||
theme.textMuted = MakeArgb(0x8B8B8Bu); // descriptionForeground
|
||
theme.textOnAccent = MakeArgb(0xFFFFFFu);
|
||
|
||
theme.accent = MakeArgb(0x0078D4u); // button.background
|
||
theme.accentSoft = MakeArgb(0xE5F1FBu);
|
||
theme.hover = MakeArgb(0xF2F2F2u); // list.hoverBackground
|
||
theme.pressed = MakeArgb(0xE8E8E8u);
|
||
theme.selection = MakeArgb(0xE8E8E8u);
|
||
|
||
// 网格叠底比绘图区白底略暗一档,便于分辨相邻网格;
|
||
// 结构色按**叠底色**选中(对比度:1.30 / 1.70 / 2.10),不要再用原白底的值。
|
||
theme.laneBg = MakeArgb(0xFBFBFBu);
|
||
theme.gridWeak = MakeArgb(0xDEDEDEu);
|
||
theme.gridStrong = MakeArgb(0xC3C3C3u);
|
||
theme.axisLine = MakeArgb(0xAFAFAFu);
|
||
theme.cursor = MakeArgb(0xD18616u); // VS Code charts.orange
|
||
theme.readoutBg = MakeArgb(0xF3F3F3u);
|
||
theme.danger = MakeArgb(0xE51400u); // errorForeground
|
||
theme.warning = MakeArgb(0xBF8803u);
|
||
return theme;
|
||
}());
|
||
|
||
/**
|
||
* @brief 深色主题(参考 VS Code Dark+)。
|
||
*/
|
||
const Theme kDark = FinalizeTheme([]
|
||
{
|
||
Theme theme;
|
||
theme.windowBg = MakeArgb(0x1E1E1Eu); // editor.background
|
||
theme.toolbarBg = MakeArgb(0x252526u); // sideBar.background
|
||
theme.panelBg = MakeArgb(0x252526u);
|
||
theme.cardBg = MakeArgb(0x2D2D30u);
|
||
theme.plotBg = MakeArgb(0x1E1E1Eu);
|
||
theme.border = MakeArgb(0x3C3C3Cu);
|
||
theme.borderStrong = MakeArgb(0x505050u);
|
||
|
||
theme.text = MakeArgb(0xCCCCCCu);
|
||
theme.textStrong = MakeArgb(0xFFFFFFu);
|
||
theme.textMuted = MakeArgb(0x8C8C8Cu);
|
||
theme.textOnAccent = MakeArgb(0xFFFFFFu);
|
||
|
||
theme.accent = MakeArgb(0x007ACCu); // focusBorder / statusBar
|
||
theme.accentSoft = MakeArgb(0x094771u); // list.activeSelectionBackground
|
||
theme.hover = MakeArgb(0x2A2D2Eu); // list.hoverBackground
|
||
theme.pressed = MakeArgb(0x37373Du);
|
||
theme.selection = MakeArgb(0x04395Eu);
|
||
|
||
// ⚠️ 深色主题的结构色**必须比旧值亮得多**:它们画在网格叠底(#2D2D30)上,
|
||
// 旧值(#2B2B2B / #3A3A3A)与叠底几乎同亮度(实测对比度 1.07)⇒ 网格线看不见。
|
||
// 这里的值按叠底色选:1.30 / 1.70 / 2.10。
|
||
theme.laneBg = MakeArgb(0x2D2D30u);
|
||
theme.gridWeak = MakeArgb(0x3F3F3Fu);
|
||
theme.gridStrong = MakeArgb(0x505050u);
|
||
theme.axisLine = MakeArgb(0x5E5E5Eu);
|
||
theme.cursor = MakeArgb(0xD18616u);
|
||
theme.readoutBg = MakeArgb(0x2D2D30u);
|
||
theme.danger = MakeArgb(0xF44747u);
|
||
theme.warning = MakeArgb(0xCCA700u);
|
||
return theme;
|
||
}());
|
||
|
||
/**
|
||
* @brief Solarised Dark(参考 VS Code 的 Solarized Dark)。
|
||
*
|
||
* 底色用 base03、面板用 base02,强调色仍是 Solarised 那一套 —— 因此波形配色表
|
||
* (`SignalPalette`)与 Solarised Light 完全一致。
|
||
*/
|
||
const Theme kSolarizedDark = FinalizeTheme([]
|
||
{
|
||
Theme theme;
|
||
theme.windowBg = MakeArgb(kBase03);
|
||
theme.toolbarBg = MakeArgb(kBase02);
|
||
theme.panelBg = MakeArgb(kBase02);
|
||
theme.cardBg = MakeArgb(kBase03);
|
||
theme.plotBg = MakeArgb(kBase03);
|
||
theme.border = MakeArgb(0x18505Fu);
|
||
theme.borderStrong = MakeArgb(0x205363u);
|
||
|
||
theme.text = MakeArgb(kBase0);
|
||
theme.textStrong = MakeArgb(kBase1);
|
||
theme.textMuted = MakeArgb(kBase01);
|
||
theme.textOnAccent = MakeArgb(kBase03);
|
||
|
||
theme.accent = MakeArgb(kBlue);
|
||
theme.accentSoft = MakeArgb(0x12414Fu);
|
||
theme.hover = MakeArgb(0x10505Fu);
|
||
theme.pressed = MakeArgb(0x0A3F4Cu);
|
||
theme.selection = MakeArgb(0x12414Fu);
|
||
|
||
// 网格叠底与绘图区底色同为 base03 时看不出网格层次 ⇒ 显式给出略亮一档的叠底,
|
||
// 结构色按**它**选(对比度 1.30 / 1.70 / 2.10)。
|
||
theme.laneBg = MakeArgb(0x032E38u);
|
||
theme.gridWeak = MakeArgb(0x13414Cu);
|
||
theme.gridStrong = MakeArgb(0x205363u);
|
||
theme.axisLine = MakeArgb(kBase01);
|
||
theme.cursor = MakeArgb(kOrange);
|
||
theme.readoutBg = MakeArgb(kBase02);
|
||
theme.danger = MakeArgb(kRed);
|
||
theme.warning = MakeArgb(kYellow);
|
||
return theme;
|
||
}());
|
||
|
||
/** @brief 全局「当前主题」指针(由宿主设置,见 `SetCurrentTheme`)。 */
|
||
const Theme* g_currentTheme = &kSolarizedLight;
|
||
}
|
||
|
||
double SrgbLuminance(std::uint32_t argb)
|
||
{
|
||
// sRGB → 线性光:> 0.03928 用 2.4 次幂,否则用 12.92 线性段(WCAG 2.x)。
|
||
const auto channel = [](std::uint32_t value) -> double
|
||
{
|
||
const double c = static_cast<double>(value & 0xFFu) / 255.0;
|
||
return (c <= 0.03928) ? (c / 12.92) : std::pow((c + 0.055) / 1.055, 2.4);
|
||
};
|
||
const double r = channel((argb >> 16) & 0xFFu);
|
||
const double g = channel((argb >> 8) & 0xFFu);
|
||
const double b = channel(argb & 0xFFu);
|
||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||
}
|
||
|
||
double ContrastRatio(std::uint32_t firstColor, std::uint32_t secondColor)
|
||
{
|
||
const double first = SrgbLuminance(firstColor);
|
||
const double second = SrgbLuminance(secondColor);
|
||
const double high = std::max(first, second);
|
||
const double low = std::min(first, second);
|
||
return (high + 0.05) / (low + 0.05);
|
||
}
|
||
|
||
std::uint32_t EnsureContrast(std::uint32_t background, std::uint32_t foreground, double minRatio)
|
||
{
|
||
if (!(minRatio > 1.0) || ContrastRatio(background, foreground) >= minRatio)
|
||
{
|
||
return foreground; // 已达标(或不限制):**逐位返回原色**
|
||
}
|
||
// 远离底色:深底就提亮、浅底就加深。二分求解"刚好达标"的混色量,
|
||
// 使输出随底色**连续**变化(不会在阈值附近两个极端色之间跳)。
|
||
const bool lighten = SrgbLuminance(background) < 0.5;
|
||
const std::uint32_t extreme = lighten ? 0xFFFFFFFFu : 0xFF000000u;
|
||
double low = 0.0;
|
||
double high = 1.0;
|
||
for (int iteration = 0; iteration < 18; ++iteration)
|
||
{
|
||
const double middle = (low + high) * 0.5;
|
||
if (ContrastRatio(background, MixColor(foreground, extreme, middle)) < minRatio)
|
||
{
|
||
low = middle;
|
||
}
|
||
else
|
||
{
|
||
high = middle;
|
||
}
|
||
}
|
||
return MixColor(foreground | 0xFF000000u, extreme, high);
|
||
}
|
||
|
||
std::uint32_t MixToLuminance(std::uint32_t color, double targetLuminance)
|
||
{
|
||
const std::uint32_t base = color | 0xFF000000u;
|
||
const double current = SrgbLuminance(base);
|
||
if (!(targetLuminance > 0.0))
|
||
{
|
||
targetLuminance = 0.0;
|
||
}
|
||
if (targetLuminance > 1.0)
|
||
{
|
||
targetLuminance = 1.0;
|
||
}
|
||
if (std::fabs(targetLuminance - current) < 1.0e-4)
|
||
{
|
||
return base;
|
||
}
|
||
// 亮度沿"混向白 / 混向黑"单调 ⇒ 二分解出混色量;
|
||
// 目标亮度等于当前亮度时混色量为 0(等价返回原色),因此两侧在该点**连续衔接**。
|
||
const std::uint32_t extreme = (targetLuminance > current) ? 0xFFFFFFFFu : 0xFF000000u;
|
||
double low = 0.0;
|
||
double high = 1.0;
|
||
for (int iteration = 0; iteration < 20; ++iteration)
|
||
{
|
||
const double middle = (low + high) * 0.5;
|
||
const double luminance = SrgbLuminance(MixColor(base, extreme, middle));
|
||
if (targetLuminance > current)
|
||
{
|
||
if (luminance < targetLuminance)
|
||
{
|
||
low = middle;
|
||
}
|
||
else
|
||
{
|
||
high = middle;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (luminance > targetLuminance)
|
||
{
|
||
low = middle;
|
||
}
|
||
else
|
||
{
|
||
high = middle;
|
||
}
|
||
}
|
||
}
|
||
return MixColor(base, extreme, high);
|
||
}
|
||
|
||
std::uint32_t ThemeLaneBackground(const Theme& theme)
|
||
{
|
||
// 网格叠底色是**主题配色的一部分**(`Theme::laneBg`),由 `FinalizeTheme` 在构建主题时
|
||
// 保证与绘图区底色可辨;这里只做"未配置"的兜底。
|
||
if (theme.laneBg != 0u)
|
||
{
|
||
return theme.laneBg | 0xFF000000u;
|
||
}
|
||
const std::uint32_t plot = theme.plotBg | 0xFF000000u;
|
||
return EnsureContrast(plot, theme.cardBg | 0xFF000000u, 1.03);
|
||
}
|
||
|
||
const Theme& ThemeFor(ThemeKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case ThemeKind::LIGHT:
|
||
return kLight;
|
||
case ThemeKind::DARK:
|
||
return kDark;
|
||
case ThemeKind::SOLARIZED_DARK:
|
||
return kSolarizedDark;
|
||
case ThemeKind::SOLARIZED_LIGHT:
|
||
default:
|
||
return kSolarizedLight;
|
||
}
|
||
}
|
||
|
||
const wchar_t* ThemeName(ThemeKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case ThemeKind::LIGHT:
|
||
return L"Light";
|
||
case ThemeKind::DARK:
|
||
return L"Dark";
|
||
case ThemeKind::SOLARIZED_DARK:
|
||
return L"Solarised Dark";
|
||
case ThemeKind::SOLARIZED_LIGHT:
|
||
default:
|
||
return L"Solarised Light";
|
||
}
|
||
}
|
||
|
||
const Theme& CurrentTheme()
|
||
{
|
||
return (g_currentTheme != nullptr) ? *g_currentTheme : kSolarizedLight;
|
||
}
|
||
|
||
void SetCurrentTheme(const Theme* theme)
|
||
{
|
||
g_currentTheme = (theme != nullptr) ? theme : &kSolarizedLight;
|
||
}
|
||
|
||
const std::uint32_t* SignalPalette(int& count)
|
||
{
|
||
count = static_cast<int>(sizeof(kSignalPalette) / sizeof(kSignalPalette[0]));
|
||
return kSignalPalette;
|
||
}
|
||
|
||
} // namespace jdi
|