Files
Just-DataInspector/Components/Shared/Src/Theme.cpp
T
DeepSeek V4.1 Flash 2d83b494a3 均值面板越界根因 + 网格线进入主题配色 + 条目栏悬停/整行点击 + 均值点数下限 1
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…上限)」「命中要求行完全落在内容区里」两条改成现行做法。
2026-10-30 10:51:39 +08:00

395 lines
14 KiB
C++
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.
#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