预览曲线按面板高亮 + 统计数字不再卡在“统计中…” + 中心轴面板改表格 + 默认主题 Light + 标签页 DPI 修复 + 布局边界幽灵元素
一、预览曲线悬停高亮(只开弱峰+中心轴时无反应;三个全开时总亮均值曲线)
根因:PaintLaneDerivedOverlays 里只有 MEAN 分支传 highlight,且悬停状态没有记录面板种类。
修法:新增 m_meanRowHoverKind,高亮条件 = 悬停行 == 本行 && 面板种类 == 本曲线来源;
条目栏强调色同样按种类门控;悬停种类进入面板缓存键。
二、统计数字松手后停在“统计中…”
根因:交互期间面板缓存键被冻结(为拖动跟手),冻结期满后没有重绘请求(鼠标已停、
统计也已完成),最后一次绘制仍是冻结帧。
修法:记录 m_panelKeysFrozen 并公开 PanelRefreshPending();Render 末尾把它并入
滚动动画定时器的武装条件 ⇒ 空闲后自动补一帧真实数字,随后停表。
三、「中心轴边沿计数」面板改为表格
表头固定:信号名 / 上升沿计数 / 下降沿计数;每行底色 = 该信号配色(未勾选的行普通底色),
三列与表头严格对齐,内容可滚动,单击行展开两行参数滑块。
- EdgePanelLayout 新增 tableHeader / tableColumns[3];EdgePanelLayoutFor 新增 centerTable 分支;
- 新增 EdgeCenterTableHeader() / EdgeCountCellText();EnsureEdgeCenterItem 默认 collapsed;
- PanelSlotKey 的 CENTER 分支改为按 item 判断并混合计数(表格已没有 rowStats)。
四、默认主题改为 Light(Application、ThemeFor/ThemeName 默认分支、CurrentTheme 兜底)。
五、标签页文本在 DPI 切换后被省略
根因:标签宽度恰好等于“内边距+文本+间距+关闭按钮”,无任何余量;TabItemWidget 缓存了
Configure 时的 scale 并用它反推可用文本宽度,而 TabBar::SetScale 不重建控件树
⇒ 125%→100% 后可用宽度少约 4.5px ⇒ 吃掉末字。
修法:SetScale 变化时重建控件树;并把文本可用矩形改由 ArrangeWidgets 统一下发
(SetState 增加 textRect 参数),绘制侧不再自行反推。
六、布局边界里的幽灵元素
- 新增 SignalPanel::HasStyleTarget();BuildBoxes 与 QueryCursor 在无样式目标时
不再上报/响应色块、样式、调整、线宽、透明度等控件;
- Paint 的“无网格”分支显式清空 m_legendHits / m_cursorLabelRects / m_cursorSpanRect /
m_freeMarkerHits / m_panelSlots / m_panelOrder(关闭整个分组后不再残留边界框);
- 浮动面板之上的滚动条 / 回到最上方 / 内联输入框改为按可见性上报几何。
验证(离屏探针,SW_HIDE + 合成消息,不占用鼠标键盘屏幕)
.preflight/p21.exe 9/9 .preflight/p22.exe 14/14 .preflight/p23.exe 7/7
.preflight/p20 系列(9/11/5)全部回归通过
make release / make debug 零 warning;Release 813,568 B
This commit is contained in:
1 parent
7a6fcd68e2
commit
8de8a8fb85
14 files changed
+934
-141
No files matched your search
@@ -521,7 +521,7 @@ namespace jdi
|
||||
* 默认值是 **Solarised Light**:这是项目历史默认,用户 2026-10-29 明确要求
|
||||
* 新增其它主题时"默认的主题色维持现状"。
|
||||
*/
|
||||
ThemeKind m_themeKind = ThemeKind::SOLARIZED_LIGHT;
|
||||
ThemeKind m_themeKind = ThemeKind::LIGHT;
|
||||
UiFonts m_fonts; /**< 界面字体。 */
|
||||
PopupMenu m_menu; /**< 与主题一致的自绘弹出菜单。 */
|
||||
ColorPickerPopup m_colorPicker; /**< 与主题一致的浮动拾色器。 */
|
||||
|
||||
@@ -662,8 +662,14 @@ namespace jdi
|
||||
|
||||
// 信号列表与绘图区的动画(目标信号滚动 / 分组折起展开 / 网格滚动到可视区 /
|
||||
//「回到最上方」按钮的淡入淡出):仅在动画进行中保持定时重绘。
|
||||
// ⚠️ 另外还要覆盖「面板缓存键被冻结」的情形(用户 2026-11-02 报告:拖完横向光标 /
|
||||
// 参数滑块并松开后,统计面板一直停在“统计中…”,直到鼠标移到面板上或做其它操作):
|
||||
// 交互期间面板缓存键被冻结(复用旧像素),而**鼠标停下后不会再有重绘请求** ⇒
|
||||
// 永远看不到最终数字。`PanelRefreshPending()` 为真时保持定时器,等
|
||||
// `kEdgeScanInteractiveIdleMs` 过去、某个非交互帧把面板按最新数字重画一次。
|
||||
if (m_panel.ScrollAnimationActive() || m_panel.GroupAnimationActive() ||
|
||||
m_plot.LaneScrollAnimationActive() || m_plot.BackToTopAnimationActive())
|
||||
m_plot.LaneScrollAnimationActive() || m_plot.BackToTopAnimationActive() ||
|
||||
m_plot.PanelRefreshPending())
|
||||
{
|
||||
if (!m_scrollTimerActive)
|
||||
{
|
||||
|
||||
@@ -1271,6 +1271,16 @@ namespace jdi
|
||||
/** @brief 「回到最上方」按钮是否正在淡入/淡出。 */
|
||||
bool BackToTopAnimationActive() const { return m_backToTopAnimating; }
|
||||
|
||||
/**
|
||||
* @brief 上一条绘制帧是否**冻结了实验性功能面板的缓存键**(交互帧)。
|
||||
*
|
||||
* 冻结期间面板显示的是旧像素(统计数字停在“统计中…”),而鼠标停下后
|
||||
* **不会再有重绘请求** ⇒ 面板永远停在旧数字。宿主应在为真时武装短周期定时器,
|
||||
* 直到某个非交互帧把面板按最新数字重画一次(见 `Application::Render`)。
|
||||
*/
|
||||
bool PanelRefreshPending() const { return m_panelKeysFrozen; }
|
||||
|
||||
|
||||
/**
|
||||
* @brief 取消网格滚动动画(用户自己滚动网格时调用,避免动画与手动滚动抢滚动位置)。
|
||||
*/
|
||||
@@ -1806,6 +1816,23 @@ namespace jdi
|
||||
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
|
||||
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
|
||||
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
|
||||
|
||||
/** @brief 表格的一列(左边界 + 宽度,客户区坐标)。 */
|
||||
struct TableColumn
|
||||
{
|
||||
float left = 0.0f;
|
||||
float width = 0.0f;
|
||||
};
|
||||
/**
|
||||
* @brief 「中心轴边沿计数」表格的**表头行**(固定不滚动;内容区在它下面)。
|
||||
*
|
||||
* 该面板自 2026-11-02 起改为表格:表头三列 = 信号名 / 上升沿计数 / 下降沿计数,
|
||||
* 下面每条**已勾选**的信号占一行,行底色 = 该信号的配色,三列与表头严格对齐。
|
||||
* 单击行(非复选框)展开该信号自己的两行参数滑块。
|
||||
*/
|
||||
gd::RectF tableHeader;
|
||||
/** @brief 表格三列的几何(`0` 信号名 / `1` 上升沿计数 / `2` 下降沿计数)。 */
|
||||
std::array<TableColumn, 3> tableColumns{};
|
||||
/**
|
||||
* @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。
|
||||
*
|
||||
@@ -2944,6 +2971,28 @@ namespace jdi
|
||||
*/
|
||||
int m_meanRowHoverLane = -1;
|
||||
SignalRef m_meanRowHoverRef;
|
||||
/**
|
||||
* @brief 悬停所在的**面板种类**(MEAN / CENTER / PEAK)。
|
||||
*
|
||||
* ⚠️ 必须区分:同一条信号可能在多个面板里各有一行(均值滤波、中心轴边沿计数、
|
||||
* 弱峰调整都用同一套条目栏),只比 `(lane, ref)` 会把**所有**面板的同名行一起点亮,
|
||||
* 并且无法判断“该高亮哪一条预览曲线”。
|
||||
*
|
||||
* 用户 2026-11-02 报告:只开弱峰调整 + 中心轴边沿计数时悬停**毫无反应**;
|
||||
* 一旦再开均值滤波,三个面板悬停都只高亮均值滤波曲线 ——
|
||||
* 根因就是绘制侧只给 MEAN 分支传了 `highlight`,而悬停状态没有记录面板种类。
|
||||
*/
|
||||
EdgePanelKind m_meanRowHoverKind = EdgePanelKind::MEAN;
|
||||
|
||||
/**
|
||||
* @brief 上一帧绘制面板时是否使用了「冻结的缓存键」(交互期间)。
|
||||
*
|
||||
* 用途:冻结期间面板显示的是上一帧像素(统计数字停留在“统计中…”),
|
||||
* 而**鼠标停下后不会再有重绘请求** ⇒ 面板永远停在旧数字
|
||||
*(用户 2026-11-02 报告:“松开左键后一直统计中…,动一下鼠标或悬停面板才更新”)。
|
||||
* 宿主每帧检查它:为真就武装一个短周期定时器,直到某个非交互帧重画面板为止。
|
||||
*/
|
||||
mutable bool m_panelKeysFrozen = false;
|
||||
|
||||
// ---------------------------------------------------------------- 自由标记
|
||||
|
||||
|
||||
@@ -700,6 +700,37 @@ namespace jdi
|
||||
return (!complete && rising == 0 && falling == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 表格单元格里的**纯数字**(「中心轴边沿计数」表格用)。
|
||||
*
|
||||
* 表头已经写了「上升沿计数 / 下降沿计数」,单元格里再写「上升沿 N」就重复了;
|
||||
* 未算完时在数字后追加 `…`(与旧的两列排版同一个语义)。
|
||||
*/
|
||||
inline std::wstring EdgeCountCellText(std::size_t value, bool complete)
|
||||
{
|
||||
std::wstring text = std::to_wstring(value);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/** @brief 「中心轴边沿计数」表格的表头文本(信号名 / 上升沿计数 / 下降沿计数)。 */
|
||||
inline const wchar_t* EdgeCenterTableHeader(int column)
|
||||
{
|
||||
switch (column)
|
||||
{
|
||||
case 0:
|
||||
return L"信号名";
|
||||
case 1:
|
||||
return L"上升沿计数";
|
||||
case 2:
|
||||
default:
|
||||
return L"下降沿计数";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」结果的显示文本(合并成一行,用于宽度测量与兼容旧调用)。
|
||||
*
|
||||
|
||||
@@ -1950,7 +1950,8 @@ namespace jdi
|
||||
{
|
||||
int hoverLane = -1;
|
||||
SignalRef hoverRef;
|
||||
const EdgePanelKind hoverKind = static_cast<EdgePanelKind>(hoverPanelKind);
|
||||
EdgePanelKind hoverKind =
|
||||
static_cast<EdgePanelKind>(hoverPanelKind);
|
||||
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
|
||||
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
|
||||
hoverKind == EdgePanelKind::CENTER))
|
||||
@@ -1977,13 +1978,12 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoverLane != m_meanRowHoverLane || !(hoverRef == m_meanRowHoverRef))
|
||||
if (hoverLane != m_meanRowHoverLane || !(hoverRef == m_meanRowHoverRef) ||
|
||||
(hoverLane >= 0 && hoverKind != m_meanRowHoverKind))
|
||||
{
|
||||
m_meanRowHoverLane = hoverLane;
|
||||
m_meanRowHoverRef = hoverRef;
|
||||
// 预览曲线在部分绘制路径里属于"静态场景"(整幅重绘会缓存它),
|
||||
// 因此悬停状态一变就作废场景缓存 —— 只在"进出某一行"时发生,不是每帧。
|
||||
InvalidateSceneCache();
|
||||
m_meanRowHoverKind = hoverKind;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +745,13 @@ namespace jdi
|
||||
boxes.Add(layout.edit, 3);
|
||||
boxes.Add(layout.button, 3);
|
||||
boxes.Add(layout.content, 3);
|
||||
boxes.Add(layout.tableHeader, 3);
|
||||
for (const EdgePanelLayout::TableColumn& column : layout.tableColumns)
|
||||
{
|
||||
boxes.Add(gd::RectF(column.left, layout.tableHeader.Y, column.width,
|
||||
layout.tableHeader.Height),
|
||||
4);
|
||||
}
|
||||
boxes.Add(layout.scrollTrack, 3);
|
||||
boxes.Add(layout.scrollThumb, 4);
|
||||
boxes.Add(layout.opacityTrack, 3);
|
||||
@@ -783,11 +790,24 @@ namespace jdi
|
||||
}
|
||||
// 收尾:解除面板裁剪并**复位层号**(宿主后续组件依赖它)。
|
||||
// 浮动面板之上的叠加层(滚动条 / 回到最上方 / 内联输入框)必须比**最上面板**再高一层。
|
||||
// ⚠️ **只在它们真的会被绘制时上报**(用户 2026-11-02 报告:关闭分组后仍能看到大量
|
||||
// 残留的布局边界框):这些矩形在 `Layout()` 里就无条件算好,但绘制时会按可见性 /
|
||||
// 编辑态门控。旧实现照样上报 ⇒ 空绘图区里同时出现“不存在的滚动条 / 回到最上方 /
|
||||
// 输入框”的框。
|
||||
boxes.SetClip(gd::RectF());
|
||||
boxes.SetLayer(topOverlayLayer);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
if (m_scrollBarVisible)
|
||||
{
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
}
|
||||
if (BackToTopVisible())
|
||||
{
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
}
|
||||
if (m_editTarget != PlotEditTarget::NONE)
|
||||
{
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
}
|
||||
boxes.SetLayer(baseLayer);
|
||||
}
|
||||
|
||||
@@ -855,6 +875,19 @@ namespace jdi
|
||||
if (m_lanes.empty())
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
// ⚠️⚠️ **必须清掉上一帧登记的叠加层/命中几何**(用户 2026-11-02 报告:关闭整个分组后
|
||||
// 仍能看到大量残留的布局边界框):本分支直接 return,`PaintLaneLegend` /
|
||||
// `PaintCursor` / `PaintFreeMarker` 都不会跑,于是 `m_legendHits` /
|
||||
// `m_cursorLabelRects` / `m_cursorSpanRect` / `m_freeMarkerHits` 里的**旧矩形**
|
||||
// 会一直保留(布局边界会继续把它们画出来,甚至命中测试也会命中已不存在的元素)。
|
||||
m_legendHits.clear();
|
||||
m_cursorLabelRects.clear();
|
||||
m_cursorSpanRect = gd::RectF();
|
||||
m_freeMarkerHits.clear();
|
||||
m_decorRects.clear();
|
||||
m_panelOrder.clear();
|
||||
m_panelSlots.clear();
|
||||
m_panelValid = false;
|
||||
PaintEmptyState(g);
|
||||
// ⚠️ 空状态(尚未勾选任何信号)也必须绘制提示浮层:加载失败 / 解析异常正是
|
||||
// **最需要**非模态提示的场景,若在此提前返回,宿主的错误信息将永远不可见。
|
||||
@@ -2074,7 +2107,12 @@ namespace jdi
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||
// 面板条目栏的悬停高亮(用户 2026-10-30 要求):鼠标停在哪一行,就把那条**预览曲线**
|
||||
// 画一圈光晕 + 加粗。`m_meanRowHoverLane/Ref` 由 `OnMouseMove` 维护。
|
||||
// 画一圈光晕 + 加粗。`m_meanRowHoverLane/Ref/Kind` 由 `OnMouseMove` 维护。
|
||||
//
|
||||
// ⚠️⚠️ **必须按面板种类分别判定**(用户 2026-11-02 报告):三个面板(均值滤波 /
|
||||
// 中心轴边沿计数 / 弱峰调整)都有“信号条目栏”,同一条信号可能同时在三个面板里各有一行。
|
||||
// 旧实现只给 MEAN 分支传 `highlight` ⇒ 只开弱峰 + 中心轴时悬停毫无反应;三个全开时
|
||||
// 悬停任何一个面板都只高亮均值滤波那条。现在只高亮“鼠标所在面板”对应的曲线。
|
||||
const bool hoverActive = (m_meanRowHoverLane == laneIndex) && m_meanRowHoverRef.IsValid();
|
||||
for (const SignalRef& ref : tabLane.signals)
|
||||
{
|
||||
@@ -2083,13 +2121,16 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool hovered = hoverActive && (ref == m_meanRowHoverRef);
|
||||
const bool hoveredThisRow = hoverActive && (ref == m_meanRowHoverRef);
|
||||
if (centerOn)
|
||||
{
|
||||
const Lane::EdgeCenterItem* centerItem = tabLane.FindEdgeCenterItem(ref);
|
||||
if (centerItem != nullptr)
|
||||
{
|
||||
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations);
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::CENTER);
|
||||
PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations,
|
||||
highlight);
|
||||
}
|
||||
}
|
||||
if (meanFilterOn)
|
||||
@@ -2097,7 +2138,9 @@ namespace jdi
|
||||
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
|
||||
if (item != nullptr)
|
||||
{
|
||||
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations, hovered);
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::MEAN);
|
||||
PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations, highlight);
|
||||
}
|
||||
}
|
||||
if (weakPeakOn)
|
||||
@@ -2111,7 +2154,9 @@ namespace jdi
|
||||
const int boostIterations = std::max(
|
||||
kWeakPeakIterationsMin,
|
||||
std::min(kWeakPeakIterationsMaxLimit, item->iterations));
|
||||
PaintWeakPeakCurve(g, resolved, lane, amountPercent, boostIterations, hovered);
|
||||
const bool highlight =
|
||||
hoveredThisRow && (m_meanRowHoverKind == EdgePanelKind::PEAK);
|
||||
PaintWeakPeakCurve(g, resolved, lane, amountPercent, boostIterations, highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3500,6 +3545,9 @@ namespace jdi
|
||||
// 这些叠加层都画在**面板之下**、面板本身不透明且贴回时只贴圆角形状之内,因此交互期间
|
||||
// 直接复用面板像素是安全的;鼠标停下后键自动恢复,面板重画一次即可。
|
||||
const bool interactivePanelKey = InputIsInteractive();
|
||||
// 供宿主判断“冻结过后需要补一帧”(冻结期间面板显示的是旧像素,鼠标停下后
|
||||
// 不会再有重绘请求 ⇒ 统计数字会永远停在“统计中…”)。
|
||||
m_panelKeysFrozen = interactivePanelKey;
|
||||
if (interactivePanelKey)
|
||||
{
|
||||
mix(0x9E3779B9ull);
|
||||
@@ -3534,6 +3582,9 @@ namespace jdi
|
||||
// “鼠标处于条目栏上方时对应曲线/条目需要高亮提示”)。
|
||||
mix(static_cast<std::uint64_t>(m_meanRowHoverLane + 2));
|
||||
mixRef(m_meanRowHoverRef);
|
||||
// 悬停所在的**面板种类**同样改变面板像素:同一条信号可能在多个面板里各有一行,
|
||||
// 只有“这一行属于本面板”时该面板才叠强调色(否则悬停一个面板会把其它面板的同名行一起点亮)。
|
||||
mix(static_cast<std::uint64_t>(m_meanRowHoverKind) + 2);
|
||||
|
||||
// 时间光标与统计区间:面板标题的时间后缀、以及「中心轴」行的上升/下降沿数字都依赖它们。
|
||||
// ⚠️⚠️ **交互期间必须用固定占位值**:拖动光标时时间每帧都在变,键一变就要重画全部面板。
|
||||
@@ -3665,6 +3716,12 @@ namespace jdi
|
||||
mixRect(layout.showDelta);
|
||||
mixRect(layout.showEnum);
|
||||
mixRect(layout.content);
|
||||
mixRect(layout.tableHeader);
|
||||
for (const EdgePanelLayout::TableColumn& column : layout.tableColumns)
|
||||
{
|
||||
mixFloat(column.left);
|
||||
mixFloat(column.width);
|
||||
}
|
||||
mixRect(layout.scrollTrack);
|
||||
mixRect(layout.scrollThumb);
|
||||
mixRect(layout.opacityTrack);
|
||||
@@ -3817,12 +3874,12 @@ namespace jdi
|
||||
}
|
||||
else if (kind == EdgePanelKind::CENTER && !interactiveKey)
|
||||
{
|
||||
// 「中心轴边沿计数」是**表格**:每个“已勾选统计”的行都会显示两列数字,
|
||||
// 因此 `rowRefs` 里凡是 `FindEdgeCenterItem != nullptr` 的都要把数字混进键
|
||||
//(⚠️ 表格没有 `rowStats` 了,不能再按它门控 —— 否则数字变化不会让键变化,
|
||||
// 面板会永远停在“统计中…”)。
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
if (rowIndex >= layout.rowStats.size() || layout.rowStats[rowIndex].Width <= 0.0f)
|
||||
{
|
||||
continue; // 与绘制时的条件一致:只有这一排才显示统计数字
|
||||
}
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||
@@ -4484,12 +4541,12 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// ⚠️ `itemized` 只表示**旧的逐条目排布**(现在只剩「弱峰调整」)。
|
||||
// 「均值滤波」自 2026-10-30 起、**「中心轴边沿计数」自 2026-11-02 起**都有自己的
|
||||
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
|
||||
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
|
||||
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
// ⚠️ 「均值滤波」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的两行滑块);
|
||||
// **「中心轴边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 三者都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上
|
||||
//(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
||||
const bool listStyle = (meanFilter || weakPeak);
|
||||
const bool centerTable = centerAxis;
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
{
|
||||
if (index >= count)
|
||||
@@ -4721,6 +4778,284 @@ namespace jdi
|
||||
if (!layout.collapsed)
|
||||
{
|
||||
|
||||
// 「中心轴边沿计数」= **表格**:表头(信号名 / 上升沿计数 / 下降沿计数)**固定不滚动**,
|
||||
// 下面每条可见信号一行,行底色 = 该信号的配色,三列与表头严格对齐。
|
||||
// 单击行(非复选框)展开 / 收起该信号自己的两行参数滑块。
|
||||
if (centerTable)
|
||||
{
|
||||
// 复选框自绘(在信号色叠底上用**自适应对比色**)。
|
||||
const auto paintTableCheck = [this, &g](const gd::RectF& rect, bool checked,
|
||||
const UiInk& ink,
|
||||
std::uint32_t background) {
|
||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float radius = 2.5f * m_scale;
|
||||
if (!checked)
|
||||
{
|
||||
UiDrawRoundRect(g, rect, radius, ink.text, 1.2f * m_scale);
|
||||
return;
|
||||
}
|
||||
UiFillRoundRect(g, rect, radius, ink.text);
|
||||
const float left = rect.X + rect.Width * 0.22f;
|
||||
const float middleX = rect.X + rect.Width * 0.44f;
|
||||
const float right = rect.X + rect.Width * 0.78f;
|
||||
const float highY = rect.Y + rect.Height * 0.30f;
|
||||
const float lowY = rect.Y + rect.Height * 0.72f;
|
||||
UiDrawLine(g, left, rect.Y + rect.Height * 0.52f, middleX, lowY, background,
|
||||
1.6f * m_scale);
|
||||
UiDrawLine(g, middleX, lowY, right, highY, background, 1.6f * m_scale);
|
||||
};
|
||||
|
||||
// ---- 表头(固定;不参与滚动)----
|
||||
{
|
||||
UiFillRoundRect(g, layout.tableHeader, 3.0f * m_scale, m_theme->pressed);
|
||||
UiDrawRoundRect(g, layout.tableHeader, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
for (int column = 0; column < 3; ++column)
|
||||
{
|
||||
const EdgePanelLayout::TableColumn& cell = layout.tableColumns[column];
|
||||
if (cell.width <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float textLeft =
|
||||
(column == 0) ? (cell.left + 4.0f * m_scale) : cell.left;
|
||||
UiTextClipped(g, EdgeCenterTableHeader(column), m_fonts->uiSmall,
|
||||
gd::RectF(textLeft, layout.tableHeader.Y, cell.width,
|
||||
layout.tableHeader.Height),
|
||||
m_theme->textMuted,
|
||||
(column == 0) ? TextAlign::LEFT : TextAlign::CENTER,
|
||||
layout.tableHeader);
|
||||
}
|
||||
}
|
||||
|
||||
const int tableSave = g.Save();
|
||||
// 行内容必须裁到内容区:滚动时行会越出可视范围。
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
|
||||
const float valueColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
||||
const float sliderLabelColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
||||
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
if (rowIndex >= layout.rowHeaders.size() ||
|
||||
rowIndex >= layout.rowChecks.size() ||
|
||||
rowIndex >= layout.rowTriangles.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& row = layout.rowHeaders[rowIndex];
|
||||
const Lane::EdgeCenterItem* item = tabLane.FindEdgeCenterItem(ref);
|
||||
const bool checked = (item != nullptr);
|
||||
// 整行(含展开的滑块)的纵向范围决定是否还需要绘制。
|
||||
float rowBottom = row.Y + row.Height;
|
||||
if (rowIndex < layout.rowIterTracks.size() &&
|
||||
layout.rowIterTracks[rowIndex].Height > 0.0f)
|
||||
{
|
||||
rowBottom = std::max(rowBottom, layout.rowIterTracks[rowIndex].Y +
|
||||
layout.rowIterTracks[rowIndex].Height);
|
||||
}
|
||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||
layout.rowLevelTracks[rowIndex].Height > 0.0f)
|
||||
{
|
||||
rowBottom = std::max(rowBottom, layout.rowLevelTracks[rowIndex].Y +
|
||||
layout.rowLevelTracks[rowIndex].Height);
|
||||
}
|
||||
if (rowBottom <= layout.content.Y ||
|
||||
row.Y >= layout.content.Y + layout.content.Height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
// 行底色 = 该信号的配色(用户 2026-11-02 要求);未勾选的行用普通底
|
||||
// + 弱文本,表示“尚未参与统计”。
|
||||
if (checked)
|
||||
{
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale, signalColor);
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
}
|
||||
else
|
||||
{
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, row, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
}
|
||||
if (m_meanRowHoverLane == static_cast<int>(index) &&
|
||||
m_meanRowHoverRef == ref && m_meanRowHoverKind == kind)
|
||||
{
|
||||
UiFillRoundRect(g, row, 3.0f * m_scale,
|
||||
WithAlpha(m_theme->accent, 0x2E));
|
||||
}
|
||||
|
||||
const std::uint32_t rowInk = checked ? ink.text : m_theme->text;
|
||||
|
||||
// 展开 / 收起三角(仅已勾选时存在)。
|
||||
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
|
||||
if (triangle.Width > 0.0f && checked)
|
||||
{
|
||||
UiDrawTriangleIcon(g, triangle, rowInk,
|
||||
item->collapsed ? IconDirection::RIGHT
|
||||
: IconDirection::DOWN);
|
||||
}
|
||||
|
||||
// 信号名(第一列):超出列宽时省略。
|
||||
const gd::RectF& nameRect = layout.rowNameRects[rowIndex];
|
||||
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall,
|
||||
std::max(8.0f, nameRect.Width)),
|
||||
m_fonts->uiSmall, nameRect, rowInk, TextAlign::LEFT,
|
||||
layout.content);
|
||||
|
||||
// 复选框:位于名称单元的右端。
|
||||
paintTableCheck(layout.rowChecks[rowIndex], checked, ink,
|
||||
checked ? signalColor : m_theme->cardBg);
|
||||
|
||||
// 两列计数:与表头列**同一左边界 / 同一宽度**,纵向严格对齐。
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
if (checked)
|
||||
{
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
||||
item->points, item->iterations, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
}
|
||||
for (int column = 1; column < 3; ++column)
|
||||
{
|
||||
const EdgePanelLayout::TableColumn& cell = layout.tableColumns[column];
|
||||
if (cell.width <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::wstring text;
|
||||
if (checked)
|
||||
{
|
||||
text = EdgeCountCellText((column == 1) ? rising : falling, complete);
|
||||
}
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(cell.left, row.Y, cell.width, row.Height), rowInk,
|
||||
TextAlign::CENTER, layout.content);
|
||||
}
|
||||
|
||||
// 展开后的两行参数滑块(均值点数 / 迭代次数)。
|
||||
if (!checked || item->collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const auto paintTableSlider = [&](const gd::RectF& track,
|
||||
const gd::RectF& valueRect,
|
||||
const gd::RectF& resetRect,
|
||||
const wchar_t* label, int current,
|
||||
int minimum, int maximum, bool active) {
|
||||
if (track.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float labelLeft =
|
||||
track.X - 6.0f * m_scale - sliderLabelColumn;
|
||||
UiTextClipped(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
|
||||
sliderLabelColumn, track.Height + 16.0f * m_scale),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
const double ratio =
|
||||
(maximum > minimum)
|
||||
? (static_cast<double>(current - minimum) /
|
||||
static_cast<double>(maximum - minimum))
|
||||
: 0.0;
|
||||
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
|
||||
UiTextClipped(g, std::to_wstring(current), m_fonts->monoSmall,
|
||||
gd::RectF(valueRect.X, valueRect.Y, valueColumn,
|
||||
valueRect.Height),
|
||||
m_theme->textStrong, TextAlign::LEFT, layout.content);
|
||||
const bool hot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(resetRect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
if (hot)
|
||||
{
|
||||
UiFillRoundRect(
|
||||
g, gd::RectF(resetRect.X - 1.0f * m_scale,
|
||||
resetRect.Y - 1.0f * m_scale,
|
||||
resetRect.Width + 2.0f * m_scale,
|
||||
resetRect.Height + 2.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||
}
|
||||
PaintPanelResetIcon(g, resetRect,
|
||||
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
||||
};
|
||||
|
||||
const int pointsFloor = 0; // 中心轴:0 = 不平滑(保持旧语义)
|
||||
const int pointsMax = std::max(
|
||||
pointsFloor,
|
||||
std::min(kMeanFilterPointsMaxLimit, item->maxPoints));
|
||||
const int pointsValue =
|
||||
std::max(pointsFloor, std::min(pointsMax, item->points));
|
||||
const int iterationsMax =
|
||||
std::max(kMeanFilterIterationsMin,
|
||||
std::min(kMeanFilterIterationsMaxLimit, item->maxIterations));
|
||||
const int iterationsValue =
|
||||
std::max(kMeanFilterIterationsMin,
|
||||
std::min(iterationsMax, item->iterations));
|
||||
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) &&
|
||||
(m_meanDragRef == ref);
|
||||
const bool iterationsActive =
|
||||
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
|
||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||
layout.rowLevelTracks[rowIndex].Width > 0.0f)
|
||||
{
|
||||
paintTableSlider(
|
||||
layout.rowLevelTracks[rowIndex],
|
||||
rowIndex < layout.rowLevelValues.size()
|
||||
? layout.rowLevelValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowLevelResets.size()
|
||||
? layout.rowLevelResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterPointsSliderLabel, pointsValue, pointsFloor, pointsMax,
|
||||
pointsActive);
|
||||
}
|
||||
if (rowIndex < layout.rowIterTracks.size() &&
|
||||
layout.rowIterTracks[rowIndex].Width > 0.0f)
|
||||
{
|
||||
paintTableSlider(
|
||||
layout.rowIterTracks[rowIndex],
|
||||
rowIndex < layout.rowIterValues.size()
|
||||
? layout.rowIterValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowIterResets.size()
|
||||
? layout.rowIterResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterIterationsSliderLabel, iterationsValue,
|
||||
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(tableSave);
|
||||
|
||||
// 内容滚动条(只在内容超高时出现;表头不参与滚动)。
|
||||
if (layout.scrollTrack.Width > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
|
||||
WithAlpha(m_theme->border, 0x88));
|
||||
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
|
||||
m_theme->borderStrong);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
||||
@@ -4821,14 +5156,12 @@ namespace jdi
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
|
||||
//(两者都是 `Lane::MeanFilterItem`,字段完全一致);「弱峰调整」看 `weakPeakItems`。
|
||||
// 「均值滤波」看 `meanFilterItems`、「弱峰调整」看 `weakPeakItems`
|
||||
//(中心轴边沿计数已改为独立的表格分支)。
|
||||
const Lane::WeakPeakItem* peakItem =
|
||||
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
|
||||
const Lane::MeanFilterItem* item =
|
||||
weakPeak ? nullptr
|
||||
: (meanFilter ? tabLane.FindMeanFilterItem(ref)
|
||||
: tabLane.FindEdgeCenterItem(ref));
|
||||
weakPeak ? nullptr : tabLane.FindMeanFilterItem(ref);
|
||||
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
|
||||
const bool rowCollapsed =
|
||||
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
@@ -4840,7 +5173,10 @@ namespace jdi
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
// 条目栏悬停:在信号色叠底上再压一层强调色,给出"这一行对应哪条曲线"的提示
|
||||
//(曲线本身的高亮见 `PaintLaneDerivedOverlays`)。
|
||||
if (m_meanRowHoverLane == static_cast<int>(index) && m_meanRowHoverRef == ref)
|
||||
// ⚠️ 必须同时比**面板种类**:同一条信号在其它面板里也有一行,
|
||||
// 只比 (lane, ref) 会把别的面板的同名行一起点亮。
|
||||
if (m_meanRowHoverLane == static_cast<int>(index) &&
|
||||
m_meanRowHoverRef == ref && m_meanRowHoverKind == kind)
|
||||
{
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale,
|
||||
WithAlpha(m_theme->accent, 0x2E));
|
||||
@@ -4873,21 +5209,6 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||
if (centerAxis && rowIndex < layout.rowStats.size() &&
|
||||
layout.rowStats[rowIndex].Width > 0.0f)
|
||||
{
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
|
||||
EdgeRangeMax(), rising, falling, complete);
|
||||
paintCounts(layout.rowStats[rowIndex], rising, falling, complete,
|
||||
layout.content);
|
||||
}
|
||||
|
||||
|
||||
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
||||
const auto paintMeanSlider = [&](const gd::RectF& track,
|
||||
const gd::RectF& valueRect,
|
||||
@@ -5034,7 +5355,8 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
|
||||
// 只可能是「均值滤波」:中心轴边沿计数已改为独立的表格分支。
|
||||
const int pointsFloor = kMeanFilterPointsMin;
|
||||
const int pointsMax = std::max(pointsFloor,
|
||||
std::min(kMeanFilterPointsMaxLimit,
|
||||
item->maxPoints));
|
||||
@@ -5448,9 +5770,9 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (freeMarker || listStyle)
|
||||
if (freeMarker || listStyle || centerTable)
|
||||
{
|
||||
// 这两种排布已在上面画完(各自有自己的可滚动分支)。
|
||||
// 这些排布已在上面画完(各自有自己的可滚动 / 表格分支,含裁切)。
|
||||
continue;
|
||||
}
|
||||
if (rowY + rowHeight * 2.0f <= layout.content.Y ||
|
||||
@@ -5927,4 +6249,4 @@ namespace jdi
|
||||
UiDrawLine(g, cx, cy - depth, cx + halfWidth, cy - depth * 0.15f, ink, stroke);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
} // namespace jdi
|
||||
@@ -28,12 +28,12 @@ namespace jdi
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起全部是**可滚动的分组列表**
|
||||
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
|
||||
// 「均值滤波」「弱峰调整」= 可滚动的分组列表(信号色叠底条目栏 + 右侧复选框 +
|
||||
// 展开后的两行滑块);**「中心轴边沿计数」= 表格**(信号名 / 上升沿计数 / 下降沿计数)。
|
||||
// ⚠️ 旧式「逐条目」排版(名称 + 复选框 + 参数行,无滚动)已彻底删除:
|
||||
// 上百个信号时它会算出上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去**
|
||||
// ⇒ 每一帧整块重画 ⇒ 所有交互卡顿(用户 2026-11-02 报告)。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
const bool listStyle = (meanFilter || weakPeak);
|
||||
|
||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||
// + 光标区间尾缀)。
|
||||
@@ -106,13 +106,31 @@ namespace jdi
|
||||
const float pendingWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"统计中…");
|
||||
statRisingWidth = std::max(
|
||||
statRisingWidth,
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, EdgeRisingText(rising, complete)),
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountCellText(rising, complete)),
|
||||
pendingWidth));
|
||||
statFallingWidth =
|
||||
std::max(statFallingWidth,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeFallingText(falling, complete)));
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountCellText(falling, complete)));
|
||||
}
|
||||
if (statRisingWidth > 0.0f || statFallingWidth > 0.0f)
|
||||
if (centerAxis)
|
||||
{
|
||||
// 「中心轴边沿计数」= **表格**(信号名 / 上升沿计数 / 下降沿计数):
|
||||
// 宽度 = 复选框 + 名称列 + 两列计数(列宽取「表头文字」「实际数字」的较大者)+ 滚动条。
|
||||
const float headerRising =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, EdgeCenterTableHeader(1));
|
||||
const float headerFalling =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, EdgeCenterTableHeader(2));
|
||||
const float risingColumn = std::max(headerRising, statRisingWidth) + 10.0f * m_scale;
|
||||
const float fallingColumn = std::max(headerFalling, statFallingWidth) + 10.0f * m_scale;
|
||||
const float gap = kEdgePanelStatColumnGap * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale +
|
||||
kEdgePanelDisclosureWidth * m_scale + 6.0f * m_scale +
|
||||
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale +
|
||||
std::min(nameColumnWidth, kEdgePanelNameMaxWidth * m_scale) +
|
||||
gap + risingColumn + gap + fallingColumn +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
}
|
||||
else if (statRisingWidth > 0.0f || statFallingWidth > 0.0f)
|
||||
{
|
||||
// 两列**等宽**(取两列最大值中的较大者)→ 绘制时两列起点固定,
|
||||
// 多条信号的“上升沿 / 下降沿”文字严格垂直对齐(用户明确要求)。
|
||||
@@ -254,13 +272,10 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// 「均值滤波」与「弱峰调整」共用同一套「逐条目 + 底部按钮」排布。
|
||||
// ⚠️ 「均值滤波」从 2026-10-30 起改为**可滚动的分组列表**(信号色叠底标题栏 +
|
||||
// 展开后的两行滑块),与「自由标记」同一套浏览方式 ⇒ 不再走这里的 `itemized` 分支。
|
||||
// ⚠️ 「中心轴边沿计数」从 2026-11-02 起同样改为**可滚动的分组列表**(用户明确要求
|
||||
// “改造为与均值滤波相同的风格”)⇒ 也不走 `itemized`。它没有「构建为新信号」按钮,
|
||||
// 但内容多一行「上升沿 / 下降沿」统计。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
// 「均值滤波」与「弱峰调整」共用同一套**可滚动分组列表**排布(信号色叠底条目栏 +
|
||||
// 展开后的两行滑块);「中心轴边沿计数」自 2026-11-02 起单独走**表格**分支(`centerTable`)。
|
||||
const bool listStyle = (meanFilter || weakPeak);
|
||||
const bool centerTable = centerAxis;
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool enabled = meanFilter ? lane.meanFilter
|
||||
: (weakPeak ? lane.weakPeak
|
||||
@@ -318,12 +333,46 @@ namespace jdi
|
||||
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
float meanContentHeight = 0.0f;
|
||||
float height = 0.0f;
|
||||
if (listStyle)
|
||||
float tableHeaderHeight = 0.0f;
|
||||
if (centerTable)
|
||||
{
|
||||
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整」:内容区 = 逐信号的**信号色叠底条目栏**
|
||||
//(内容总是显示,用于勾选/取消)+(勾选且未收起时)参数行。
|
||||
// 「中心轴」的参数行 = 一行「上升沿 / 下降沿」统计 + 两行滑块;
|
||||
// 「均值滤波」与「弱峰调整」= 两行滑块。
|
||||
// 「中心轴边沿计数」= **表格**:表头(信号名 / 上升沿计数 / 下降沿计数)固定不滚动,
|
||||
// 下面每条**已勾选**的信号占一行(行底色 = 信号配色),单击行展开两行参数滑块。
|
||||
// 默认只显示表格行(`EnsureEdgeCenterItem` 新建条目时 `collapsed = true`)。
|
||||
const float rowH = kEdgePanelMeanRowHeight * m_scale;
|
||||
float content = 0.0f;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||
if (item == nullptr && !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
content += rowH;
|
||||
if (item != nullptr && !item->collapsed)
|
||||
{
|
||||
content += weakPeakSliderRow * 2.0f;
|
||||
}
|
||||
}
|
||||
meanContentHeight = content;
|
||||
tableHeaderHeight = rowH;
|
||||
const float budget =
|
||||
m_plotRect.Height - (titleHeight + pad + tableHeaderHeight + bottomPad);
|
||||
const float maxContent =
|
||||
std::max(3.0f * rowH, std::min(kFreeMarkerContentMaxHeight * m_scale,
|
||||
std::max(3.0f * rowH, budget)));
|
||||
const float view = std::min(content, maxContent);
|
||||
height = titleHeight + pad + tableHeaderHeight + view + bottomPad;
|
||||
}
|
||||
else if (listStyle)
|
||||
{
|
||||
// 「均值滤波 / 弱峰调整」:内容区 = 逐信号的**信号色叠底条目栏**
|
||||
//(内容总是显示,用于勾选/取消)+(勾选且未收起时)两行参数滑块。
|
||||
// 内容超高时出现滚动条;底部「构建为新信号」按钮固定不滚动。
|
||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||
const float sliderRow = weakPeakSliderRow;
|
||||
@@ -344,17 +393,12 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||
: lane.FindEdgeCenterItem(ref);
|
||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
||||
expanded = (item != nullptr && !item->collapsed);
|
||||
}
|
||||
if (expanded)
|
||||
{
|
||||
content += sliderRow * 2.0f;
|
||||
if (centerAxis)
|
||||
{
|
||||
content += groupRow; // 统计行(上升沿 / 下降沿)
|
||||
}
|
||||
}
|
||||
}
|
||||
meanContentHeight = content;
|
||||
@@ -554,10 +598,164 @@ namespace jdi
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
if (listStyle)
|
||||
if (centerTable)
|
||||
{
|
||||
// 「均值滤波 / 中心轴边沿计数」:可滚动内容区
|
||||
//(逐信号的信号色叠底条目栏 + 展开后的统计行与两行滑块)+(均值滤波)底部按钮。
|
||||
// 「中心轴边沿计数」= **表格**:表头固定、内容区可滚动。
|
||||
// 列:① 信号名(含复选框与展开三角)② 上升沿计数 ③ 下降沿计数。
|
||||
const float rowH = kEdgePanelMeanRowHeight * m_scale;
|
||||
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
const float viewHeight =
|
||||
std::max(0.0f, height - titleHeight - pad - tableHeaderHeight - pad);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.edgeCenterScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
layout.tableHeader =
|
||||
gd::RectF(left + pad, top + titleHeight + pad, innerWidth, tableHeaderHeight);
|
||||
layout.content = gd::RectF(left + pad, layout.tableHeader.Y + tableHeaderHeight, innerWidth,
|
||||
viewHeight);
|
||||
layout.markerContentHeight = meanContentHeight;
|
||||
layout.scrollOffset = scroll;
|
||||
if (scrollable)
|
||||
{
|
||||
// 滚动条只占**内容区**(不覆盖固定的表头,表头永远干净可读)。
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / meanContentHeight));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
|
||||
// 三列:名称列吃掉剩余宽度,两列计数按内容宽度固定右对齐 —— 表头与数据行严格对齐。
|
||||
const float closeSize = kEdgePanelMeanCheckSize * m_scale;
|
||||
const float gap = kEdgePanelStatColumnGap * m_scale;
|
||||
const float contentLeft = layout.content.X;
|
||||
// 计数列宽 = 「表头文字」「当前数字」的较大者(与 `EdgePanelWidth` 同源)。
|
||||
float risingCol = UiMeasureTextWidth(m_fonts->uiSmall, EdgeCenterTableHeader(1)) + 10.0f * m_scale;
|
||||
float fallingCol = UiMeasureTextWidth(m_fonts->uiSmall, EdgeCenterTableHeader(2)) + 10.0f * m_scale;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr || resolved.style == nullptr ||
|
||||
item == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||
item->iterations, EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(),
|
||||
rising, falling, complete);
|
||||
risingCol = std::max(risingCol,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountCellText(rising, complete)) +
|
||||
10.0f * m_scale);
|
||||
fallingCol = std::max(
|
||||
fallingCol,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountCellText(falling, complete)) +
|
||||
10.0f * m_scale);
|
||||
}
|
||||
const float nameColumn =
|
||||
std::max(20.0f * m_scale, innerWidth - gap - risingCol - gap - fallingCol);
|
||||
layout.tableColumns[0].left = contentLeft;
|
||||
layout.tableColumns[0].width = nameColumn;
|
||||
layout.tableColumns[1].left = contentLeft + nameColumn + gap;
|
||||
layout.tableColumns[1].width = risingCol;
|
||||
layout.tableColumns[2].left = layout.tableColumns[1].left + risingCol + gap;
|
||||
layout.tableColumns[2].width = fallingCol;
|
||||
|
||||
// 参数行(两行滑块)的列宽:与均值滤波同一套算式。
|
||||
const float labelColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
||||
const float valueColumn = UiMeasureTextWidth(m_fonts->monoSmall, L"100000");
|
||||
const float resetSize = kWeakPeakResetButtonSize * m_scale;
|
||||
const float resetGap = kWeakPeakResetGap * m_scale;
|
||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||
const float trackLeft = contentLeft + labelColumn + 6.0f * m_scale;
|
||||
const float resetLeft = contentLeft + innerWidth - resetSize;
|
||||
const float trackWidth =
|
||||
std::max(10.0f * m_scale,
|
||||
(resetLeft - resetGap - valueColumn - 6.0f * m_scale) - trackLeft);
|
||||
|
||||
float rowY = layout.content.Y - scroll;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const Lane::EdgeCenterItem* item = lane.FindEdgeCenterItem(ref);
|
||||
if (item == nullptr && !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 未勾选(`item == nullptr`)的行也列出来:它是“启用统计”的唯一入口
|
||||
//(表格模式下没有独立的开关;点行内复选框即启用 / 取消)。
|
||||
const bool checked = (item != nullptr);
|
||||
const bool expanded = (item != nullptr && !item->collapsed);
|
||||
layout.rowRefs.push_back(ref);
|
||||
layout.rowHeaders.push_back(gd::RectF(contentLeft, rowY, innerWidth, rowH));
|
||||
layout.rowNameRects.push_back(gd::RectF(
|
||||
layout.tableColumns[0].left + kEdgePanelDisclosureWidth * m_scale + 4.0f * m_scale,
|
||||
rowY, std::max(10.0f, nameColumn - closeSize - kEdgePanelDisclosureWidth * m_scale),
|
||||
rowH));
|
||||
layout.rowTriangles.push_back(
|
||||
checked ? gd::RectF(contentLeft + 2.0f * m_scale,
|
||||
rowY + (rowH - kEdgePanelRowTriangleSize * m_scale) * 0.5f,
|
||||
kEdgePanelRowTriangleSize * m_scale,
|
||||
kEdgePanelRowTriangleSize * m_scale)
|
||||
: gd::RectF());
|
||||
layout.rowChecks.push_back(gd::RectF(
|
||||
contentLeft + nameColumn - closeSize - 2.0f * m_scale,
|
||||
rowY + (rowH - closeSize) * 0.5f, closeSize, closeSize));
|
||||
layout.rowPoints.push_back(gd::RectF());
|
||||
layout.rowIterations.push_back(gd::RectF());
|
||||
layout.rowStats.push_back(gd::RectF());
|
||||
rowY += rowH;
|
||||
|
||||
if (!expanded)
|
||||
{
|
||||
layout.rowLevelTracks.push_back(gd::RectF());
|
||||
layout.rowLevelValues.push_back(gd::RectF());
|
||||
layout.rowLevelResets.push_back(gd::RectF());
|
||||
layout.rowIterTracks.push_back(gd::RectF());
|
||||
layout.rowIterValues.push_back(gd::RectF());
|
||||
layout.rowIterResets.push_back(gd::RectF());
|
||||
continue;
|
||||
}
|
||||
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||
trackWidth, trackHeight);
|
||||
layout.rowLevelTracks.push_back(pointsTrack);
|
||||
layout.rowLevelValues.push_back(
|
||||
gd::RectF(pointsTrack.X + pointsTrack.Width + 6.0f * m_scale, rowY, valueColumn,
|
||||
sliderRow));
|
||||
layout.rowLevelResets.push_back(
|
||||
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize));
|
||||
rowY += sliderRow;
|
||||
const gd::RectF iterTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||
trackWidth, trackHeight);
|
||||
layout.rowIterTracks.push_back(iterTrack);
|
||||
layout.rowIterValues.push_back(
|
||||
gd::RectF(iterTrack.X + iterTrack.Width + 6.0f * m_scale, rowY, valueColumn,
|
||||
sliderRow));
|
||||
layout.rowIterResets.push_back(
|
||||
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize));
|
||||
rowY += sliderRow;
|
||||
}
|
||||
}
|
||||
else if (listStyle)
|
||||
{
|
||||
// 「均值滤波 / 弱峰调整」:可滚动内容区(信号色叠底条目栏 + 展开后的两行滑块)
|
||||
// +(均值滤波 / 弱峰调整)底部按钮。
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
if (hasButton)
|
||||
{
|
||||
@@ -573,8 +771,7 @@ namespace jdi
|
||||
std::max(0.0f, height - titleHeight - pad - pad - footerHeight);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float storedScroll = meanFilter ? lane.meanFilterScroll
|
||||
: (weakPeak ? lane.weakPeakScroll : lane.edgeCenterScroll);
|
||||
const float storedScroll = meanFilter ? lane.meanFilterScroll : lane.weakPeakScroll;
|
||||
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
@@ -632,8 +829,7 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||
: lane.FindEdgeCenterItem(ref);
|
||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
||||
checked = (item != nullptr);
|
||||
itemCollapsed = (item != nullptr && item->collapsed);
|
||||
}
|
||||
@@ -667,12 +863,7 @@ namespace jdi
|
||||
layout.rowIterResets.push_back(gd::RectF());
|
||||
continue;
|
||||
}
|
||||
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||
if (centerAxis && !layout.rowStats.empty())
|
||||
{
|
||||
layout.rowStats.back() = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
|
||||
rowY += groupRow;
|
||||
}
|
||||
// 「中心轴边沿计数」已改为独立表格分支,这里不再需要额外的统计行。
|
||||
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||
trackWidth, trackHeight);
|
||||
layout.rowLevelTracks.push_back(pointsTrack);
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace jdi
|
||||
}());
|
||||
|
||||
/** @brief 全局「当前主题」指针(由宿主设置,见 `SetCurrentTheme`)。 */
|
||||
const Theme* g_currentTheme = &kSolarizedLight;
|
||||
const Theme* g_currentTheme = &kLight;
|
||||
}
|
||||
|
||||
double SrgbLuminance(std::uint32_t argb)
|
||||
@@ -347,15 +347,16 @@ namespace jdi
|
||||
{
|
||||
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;
|
||||
case ThemeKind::LIGHT:
|
||||
default:
|
||||
// 默认主题 = Light(用户 2026-11-02 要求;旧默认是 Solarised Light)。
|
||||
return kLight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,26 +364,26 @@ namespace jdi
|
||||
{
|
||||
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";
|
||||
case ThemeKind::LIGHT:
|
||||
default:
|
||||
return L"Light";
|
||||
}
|
||||
}
|
||||
|
||||
const Theme& CurrentTheme()
|
||||
{
|
||||
return (g_currentTheme != nullptr) ? *g_currentTheme : kSolarizedLight;
|
||||
return (g_currentTheme != nullptr) ? *g_currentTheme : kLight;
|
||||
}
|
||||
|
||||
void SetCurrentTheme(const Theme* theme)
|
||||
{
|
||||
g_currentTheme = (theme != nullptr) ? theme : &kSolarizedLight;
|
||||
g_currentTheme = (theme != nullptr) ? theme : &kLight;
|
||||
}
|
||||
|
||||
const std::uint32_t* SignalPalette(int& count)
|
||||
|
||||
@@ -324,6 +324,19 @@ namespace jdi
|
||||
*/
|
||||
SignalStyle* StyleOf(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 当前是否存在**样式编辑目标**(选中信号且其样式可解析)。
|
||||
*
|
||||
* ⚠️ 样式区的色块 / 样式 / 调整 / 线宽 / 透明度这些控件的矩形是**无条件**由
|
||||
* `LayoutStyleControls()` 算出来的(布局与"是否有目标"无关)。若不在使用处按本判据
|
||||
* 门控,就会出现用户 2026-11-02 报告的现象:什么都没选中时,打开「显示布局边界」
|
||||
* 能看到那一排控件的框,鼠标移到「系数/偏移 | 镜像 | 绝对值」上还会变成手型。
|
||||
*
|
||||
* 使用处:`BuildBoxes`(几何上报)、`QueryCursor`(手型光标)。
|
||||
* 绘制与命中早已按 `StyleOf(SelectedRef()) != nullptr` 门控,这里只是补齐另外两处。
|
||||
*/
|
||||
bool HasStyleTarget() const;
|
||||
|
||||
/**
|
||||
* @brief 获取当前选中信号(跨标签页共享,因此切换视图后选中项保持一致)。
|
||||
* @return 信号引用。
|
||||
|
||||
@@ -100,34 +100,46 @@ namespace jdi
|
||||
boxes.Add(ScrollThumbRect(), 3);
|
||||
|
||||
// ── 样式区:五行 + 每行内部的色块/按钮/滑轨/数值 ──────────────────
|
||||
for (std::size_t index = 0; index < m_styleRowRects.size(); ++index)
|
||||
// ⚠️ **只在存在样式编辑目标时登记控件几何**(用户 2026-11-02 报告):
|
||||
// `LayoutStyleControls()` 无条件算出这些矩形,而绘制在“没有目标”时只画占位文字。
|
||||
// 旧实现照样上报 ⇒ 空面板上打开「显示布局边界」会看到一整排并不存在的控件框。
|
||||
const bool hasStyleTarget = HasStyleTarget();
|
||||
if (hasStyleTarget)
|
||||
{
|
||||
boxes.Add(m_styleRowRects[index], 2);
|
||||
for (std::size_t index = 0; index < m_styleRowRects.size(); ++index)
|
||||
{
|
||||
boxes.Add(m_styleRowRects[index], 2);
|
||||
}
|
||||
for (const gd::RectF& swatch : m_colorRects)
|
||||
{
|
||||
boxes.Add(swatch, 3);
|
||||
}
|
||||
boxes.Add(m_customChipRect, 3);
|
||||
for (const gd::RectF& mode : m_modeRects)
|
||||
{
|
||||
boxes.Add(mode, 3);
|
||||
}
|
||||
for (const gd::RectF& adjust : m_adjustRects)
|
||||
{
|
||||
boxes.Add(adjust, 3);
|
||||
}
|
||||
boxes.Add(m_widthTrackRect, 3);
|
||||
boxes.Add(m_widthValueRect, 3);
|
||||
boxes.Add(m_widthResetRect, 3);
|
||||
boxes.Add(m_opacityTrackRect, 3);
|
||||
boxes.Add(m_opacityValueRect, 3);
|
||||
boxes.Add(m_opacityResetRect, 3);
|
||||
}
|
||||
for (const gd::RectF& swatch : m_colorRects)
|
||||
{
|
||||
boxes.Add(swatch, 3);
|
||||
}
|
||||
boxes.Add(m_customChipRect, 3);
|
||||
for (const gd::RectF& mode : m_modeRects)
|
||||
{
|
||||
boxes.Add(mode, 3);
|
||||
}
|
||||
for (const gd::RectF& adjust : m_adjustRects)
|
||||
{
|
||||
boxes.Add(adjust, 3);
|
||||
}
|
||||
boxes.Add(m_widthTrackRect, 3);
|
||||
boxes.Add(m_widthValueRect, 3);
|
||||
boxes.Add(m_widthResetRect, 3);
|
||||
boxes.Add(m_opacityTrackRect, 3);
|
||||
boxes.Add(m_opacityValueRect, 3);
|
||||
boxes.Add(m_opacityResetRect, 3);
|
||||
|
||||
// ── 「目标」行(样式区顶部,展示当前选中信号) ────────────────────
|
||||
// 这一行**无论是否选中**都会绘制(未选中时显示占位文本),所以本体总是登记;
|
||||
// 但信号名 / GX 徽标只在实际有目标时存在。
|
||||
boxes.Add(m_targetRowRect, 2);
|
||||
boxes.Add(m_targetNameRect, 3);
|
||||
boxes.Add(m_targetBadgeRect, 3);
|
||||
if (hasStyleTarget)
|
||||
{
|
||||
boxes.Add(m_targetNameRect, 3);
|
||||
boxes.Add(m_targetBadgeRect, 3);
|
||||
}
|
||||
|
||||
// ── 列表里**当前可见**的行,以及每行内部的元素 ────────────────────
|
||||
// 被滚出列表的行不登记,否则边界会糊到列表区之外。
|
||||
@@ -1454,6 +1466,17 @@ namespace jdi
|
||||
return (m_dataSetWritable != nullptr) ? m_dataSetWritable->Style(ref) : nullptr;
|
||||
}
|
||||
|
||||
bool SignalPanel::HasStyleTarget() const
|
||||
{
|
||||
// 与 `PaintStyleSection` / `OnLButtonDown` 的判据**完全同源**:选中信号且样式可解析。
|
||||
if (m_tab == nullptr || m_dataSet == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalRef& selected = SelectedRef();
|
||||
return selected.IsValid() && StyleOf(selected) != nullptr;
|
||||
}
|
||||
|
||||
const SignalRef& SignalPanel::SelectedRef() const
|
||||
{
|
||||
static const SignalRef fallback;
|
||||
|
||||
@@ -55,21 +55,27 @@ namespace jdi
|
||||
// 样式区的可点击控件(「调整」行三个按钮、线宽/透明度数值框)也用手型光标。
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
// 样式区顶部「目标」行的信号名与 GX 标签:与信号列表行一致,用手型光标提示可交互。
|
||||
UpdateTargetRowGeometry();
|
||||
if ((m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy)) ||
|
||||
(m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy)))
|
||||
// ⚠️ 样式区控件只在**存在编辑目标**时才可点(用户 2026-11-02 报告:没有选中信号时,
|
||||
// 鼠标掠过「系数/偏移 | 镜像 | 绝对值」仍会变成手型,而点了什么也不会发生)。
|
||||
if (HasStyleTarget())
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||
UiPointInRect(m_opacityValueRect, fx, fy) ||
|
||||
(m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
|
||||
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
// 样式区顶部「目标」行的信号名与 GX 标签:与信号列表行一致,用手型光标提示可交互。
|
||||
UpdateTargetRowGeometry();
|
||||
if ((m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy)) ||
|
||||
(m_targetBadgeRect.Width > 0.0f && UiPointInRect(m_targetBadgeRect, fx, fy)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) ||
|
||||
UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||
UiPointInRect(m_opacityValueRect, fx, fy) ||
|
||||
(m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
|
||||
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace jdi
|
||||
m_scale = scale;
|
||||
}
|
||||
|
||||
/** @brief 更新这一格的显示状态(名称 / 激活 / 编辑态 / 关闭按钮几何)。 */
|
||||
/** @brief 更新这一格的显示状态(名称 / 激活 / 编辑态 / 关闭按钮与文本几何)。 */
|
||||
void SetState(std::wstring name, bool active, gd::RectF closeRect, bool editing,
|
||||
const TextEdit* edit, gd::RectF editRect)
|
||||
const TextEdit* edit, gd::RectF editRect, gd::RectF textRect)
|
||||
{
|
||||
const bool changed = (m_name != name) || (m_active != active) || (m_editing != editing);
|
||||
m_name = std::move(name);
|
||||
@@ -53,6 +53,7 @@ namespace jdi
|
||||
m_editing = editing;
|
||||
m_edit = edit;
|
||||
m_editRect = editRect;
|
||||
m_textRect = textRect;
|
||||
if (changed)
|
||||
{
|
||||
Invalidate(Bounds());
|
||||
@@ -111,8 +112,14 @@ namespace jdi
|
||||
1.0f * m_scale, m_theme->accent);
|
||||
}
|
||||
|
||||
const float textLeft = rect.X + kTabPadding * m_scale;
|
||||
const float textWidth = m_closeRect.X - kCloseGap * m_scale - textLeft;
|
||||
// ⚠️ 文本可用矩形由 `TabBar::ArrangeWidgets` **统一下发**(与排布同一份几何),
|
||||
// 不能用控件自己的 `m_scale` 从 `m_closeRect` 反推:标签宽度恰好等于
|
||||
// `2*pad + 文本 + gap + close`(没有任何余量),而控件缓存的 `m_scale`
|
||||
// 一旦与排布时的不一致(DPI 切换),反推出来的宽度就会少几像素 ⇒
|
||||
// `UiEllipsize` 把最后一个字符换成省略号。
|
||||
const gd::RectF textRect = m_textRect;
|
||||
const float textLeft = textRect.X;
|
||||
const float textWidth = textRect.Width;
|
||||
|
||||
if (m_editing && m_edit != nullptr)
|
||||
{
|
||||
@@ -155,8 +162,7 @@ namespace jdi
|
||||
bool m_active = false; /**< 是否激活。 */
|
||||
bool m_editing = false; /**< 是否处于重命名编辑态。 */
|
||||
const TextEdit* m_edit = nullptr; /**< 编辑模型(编辑态时非空)。 */
|
||||
gd::RectF m_editRect; /**< 编辑框矩形。 */
|
||||
gd::RectF m_closeRect; /**< 关闭按钮矩形。 */
|
||||
gd::RectF m_editRect; /**< 编辑框矩形。 */ gd::RectF m_textRect; /**< 文本可用矩形(由 `TabBar` 下发)。 */ gd::RectF m_closeRect; /**< 关闭按钮矩形。 */
|
||||
};
|
||||
|
||||
/** @brief 「+」新建按钮控件。 */
|
||||
@@ -277,7 +283,19 @@ namespace jdi
|
||||
|
||||
void TabBar::SetScale(float scale)
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
const float next = (scale > 0.1f) ? scale : 1.0f;
|
||||
if (next == m_scale)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_scale = next;
|
||||
// ⚠️⚠️ **缩放变化必须重建控件树**(用户 2026-11-02 报告:把窗口从 125% 显示器拖到
|
||||
// 100% 显示器后,唯一的标签页文本变成省略号,双击重命名再取消才恢复):
|
||||
// 每个 `TabItemWidget` 在 `Configure` 时缓存了当时的 `m_scale`,而它内部用这个缩放
|
||||
// 反推“可用文本宽度”(`closeRect.X - kCloseGap*scale - (rect.X + kTabPadding*scale)`)。
|
||||
// 标签宽度是按新缩放排的(恰好 = 2*pad + 文本 + gap + close,**没有任何余量**),
|
||||
// 而控件仍用旧缩放扣边距 ⇒ 可用宽度比实测文本宽度小几像素 ⇒ `UiEllipsize` 吃掉末字。
|
||||
RebuildWidgets();
|
||||
}
|
||||
|
||||
void TabBar::SetTabs(const std::vector<std::wstring>& names)
|
||||
@@ -448,9 +466,14 @@ namespace jdi
|
||||
TabItemWidget* widget = static_cast<TabItemWidget*>(children[index].get());
|
||||
const TabItem& tab = m_tabs[index];
|
||||
const bool editing = (static_cast<int>(index) == m_editIndex);
|
||||
// 文本可用矩形:与排布用同一份 `m_scale`,不再由控件自行反推。
|
||||
const gd::RectF textRect(tab.rect.X + kTabPadding * m_scale, tab.rect.Y,
|
||||
std::max(8.0f, tab.closeRect.X - kCloseGap * m_scale -
|
||||
(tab.rect.X + kTabPadding * m_scale)),
|
||||
tab.rect.Height);
|
||||
widget->Arrange(tab.rect);
|
||||
widget->SetState(tab.name, static_cast<int>(index) == m_activeIndex, tab.closeRect, editing,
|
||||
editing ? &m_edit : nullptr, EditRect());
|
||||
editing ? &m_edit : nullptr, EditRect(), textRect);
|
||||
}
|
||||
static_cast<TabAddWidget*>(children[m_tabs.size()].get())->Arrange(m_addRect);
|
||||
static_cast<TabOverlayWidget*>(children[m_tabs.size() + 1].get())->Arrange(m_area);
|
||||
|
||||
Reference in new issue
Block a user