① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
`MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
`ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
(旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
(单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
(点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
`UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
`MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。
验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
374 lines
13 KiB
C++
374 lines
13 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 把绘图区内部的"结构性"颜色按**底色**自动校正到足够对比(见 `EnsureContrast`)。
|
||
*
|
||
* 阈值(对比度)取"Solarised Light 现有配色刚好达标"的水位:
|
||
* 该主题的历史配色是人工调过的,达标时 `EnsureContrast` 逐位返回原色 ⇒ 默认主题
|
||
* **一个像素都不会变**;其它主题(尤其深色)则自动把网格线/时间轴/分界线提亮。
|
||
*
|
||
* | 字段 | 阈值 | Solarised Light 现状 |
|
||
* | --- | ---: | ---: |
|
||
* | `gridWeak`(网格主刻线 + 五等分线基色) | 1.30 | 约 1.38 |
|
||
* | `gridStrong`(零线/加重线) | 1.65 | 约 1.80 |
|
||
* | `axisLine`(时间轴、网格分界线、绘图区外框) | 2.10 | 约 2.43 |
|
||
* | `borderStrong`(网格间分界线) | 1.70 | 约 2.43 |
|
||
*/
|
||
Theme NormalizePlotContrast(Theme theme)
|
||
{
|
||
const std::uint32_t plot = theme.plotBg | 0xFF000000u;
|
||
theme.gridWeak = EnsureContrast(plot, theme.gridWeak | 0xFF000000u, 1.30);
|
||
theme.gridStrong = EnsureContrast(plot, theme.gridStrong | 0xFF000000u, 1.65);
|
||
theme.axisLine = EnsureContrast(plot, theme.axisLine | 0xFF000000u, 2.10);
|
||
theme.borderStrong = EnsureContrast(plot, theme.borderStrong | 0xFF000000u, 1.70);
|
||
return theme;
|
||
}
|
||
|
||
/** @brief 全局唯一主题实例(Solarised Light;历史默认主题)。 */
|
||
const Theme kSolarizedLight = NormalizePlotContrast([]
|
||
{
|
||
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)上清晰可辨,同时又要与网格边界
|
||
// (axisLine/borderStrong 的蓝灰)明显区分——因此采用偏暖的加深米色。
|
||
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 = NormalizePlotContrast([]
|
||
{
|
||
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(0xCECECEu);
|
||
|
||
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);
|
||
|
||
// 白底上网格线必须够淡(否则抢波形),但主刻线要能与次级线区分。
|
||
// ⚠️ 这三个颜色会被 `NormalizePlotContrast` 按白底自动校正到可见水位。
|
||
theme.gridWeak = MakeArgb(0xEDEDEDu);
|
||
theme.gridStrong = MakeArgb(0xD6D6D6u);
|
||
theme.axisLine = MakeArgb(0xCECECEu);
|
||
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 = NormalizePlotContrast([]
|
||
{
|
||
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(0x4A4A4Au);
|
||
|
||
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);
|
||
|
||
// ⚠️ 深色底上的原始取值几乎与底色同亮度(差 2~3 个色阶)⇒ 交给
|
||
// `NormalizePlotContrast` 按 `plotBg` 自动提亮到可见水位。
|
||
theme.gridWeak = MakeArgb(0x2B2B2Bu);
|
||
theme.gridStrong = MakeArgb(0x3A3A3Au);
|
||
theme.axisLine = MakeArgb(0x4A4A4Au);
|
||
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 = NormalizePlotContrast([]
|
||
{
|
||
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(kBase01);
|
||
|
||
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);
|
||
|
||
theme.gridWeak = MakeArgb(0x0B3A46u);
|
||
theme.gridStrong = MakeArgb(0x14495Au);
|
||
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)
|
||
{
|
||
// 网格底色必须与绘图区底色**可辨**(否则几十条网格看起来是一整片)。
|
||
// 用统一的对比度阈值 1.03:本就有差异的主题(Solarised Light / Light / Dark)
|
||
// 逐位返回 `cardBg`;两侧完全同色的主题(Solarised Dark 的 base03)会被轻轻推开。
|
||
return EnsureContrast(theme.plotBg | 0xFF000000u, 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
|