diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 20d9aea..faf71a8 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -27,8 +27,8 @@ namespace jdi const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 默认窗口标题。 */ const float kStatusBarHeight = 26.0f; /**< 状态栏高度(逻辑像素)。 */ const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */ - const float kMinPanelWidth = 220.0f; /**< 面板最小宽度(逻辑像素)。 */ - const float kMaxPanelWidth = 560.0f; /**< 面板最大宽度(逻辑像素)。 */ + const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */ + const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */ const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */ const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */ const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */ @@ -1178,31 +1178,51 @@ namespace jdi consume = true; break; case WM_LBUTTONDOWN: + { + const int pickX = GET_X_LPARAM(lParam); + const int pickY = GET_Y_LPARAM(lParam); + // 拾色器接管输入后,取消底层组件可能仍在进行的拖拽。 m_panel.OnLButtonUp(0, 0); m_plot.OnLButtonUp(0, 0); m_activeTarget = Target::NONE; m_draggingSplitter = false; - // 点击拾色器外部时收起;若落点落在信号面板内则放行本次点击, - // 便于用户在展开状态下直接改选预设色、线宽等其它样式。 - if (m_colorPicker.ContainsPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))) + if (m_colorPicker.ContainsPoint(pickX, pickY)) { - m_colorPicker.OnLButtonDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + // 面板内部:开始拖动滑块,并吃掉本次点击,避免穿透到下方图层。 + m_colorPicker.OnLButtonDown(pickX, pickY); SetCapture(hwnd); + consume = true; } else { + // 面板外部:收起面板;若落点在信号面板内则放行本次点击, + // 便于用户在展开状态下直接改选预设色、样式或拖动滑块。 m_colorPicker.Close(); - if (!m_panel.HitTest(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))) + if (!m_panel.HitTest(pickX, pickY)) { InvalidateRect(hwnd, nullptr, FALSE); return 0; } } repaint = true; - consume = false; break; + } + case WM_SETCURSOR: + { + if (LOWORD(lParam) == HTCLIENT) + { + POINT point = {}; + GetCursorPos(&point); + ScreenToClient(hwnd, &point); + + HCURSOR cursor = m_colorPicker.QueryCursor(point.x, point.y); + SetCursor(cursor != nullptr ? cursor : LoadCursorW(nullptr, IDC_ARROW)); + return TRUE; + } + break; + } case WM_LBUTTONUP: m_colorPicker.OnLButtonUp(); ReleaseCapture(); diff --git a/Components/SignalPanel/Inc/SignalPanel.hpp b/Components/SignalPanel/Inc/SignalPanel.hpp index 29be62b..95e503e 100644 --- a/Components/SignalPanel/Inc/SignalPanel.hpp +++ b/Components/SignalPanel/Inc/SignalPanel.hpp @@ -246,12 +246,10 @@ namespace jdi NONE = 0, /**< 未命中。 */ COLOR, /**< 预设颜色色块。 */ CUSTOM_CHIP, /**< 自定义颜色入口。 */ - WIDTH_PRESET, /**< 线宽预设按钮(细/粗)。 */ - WIDTH_MINUS, /**< 线宽减号。 */ - WIDTH_PLUS, /**< 线宽加号。 */ - WIDTH_VALUE, /**< 线宽数值框。 */ MODE, /**< 绘制样式按钮。 */ - OPACITY, /**< 不透明度滑块。 */ + OPACITY_SLIDER, /**< 不透明度滑块。 */ + WIDTH_SLIDER, /**< 线宽滑块。 */ + WIDTH_VALUE, /**< 线宽数值框(可双击输入)。 */ TOGGLE_ALL /**< 全部显示/隐藏按钮。 */ }; @@ -408,13 +406,6 @@ namespace jdi */ int TotalSignalCount() const; - /** - * @brief 调整当前信号线宽。 - * @param [IN] delta 增量,正值加粗。 - * @return 取值发生变化返回 true。 - */ - bool StepLineWidth(float delta); - /** * @brief 进入线宽数值输入状态。 * @return 状态发生变化返回 true。 @@ -435,6 +426,13 @@ namespace jdi */ bool ApplyOpacityFromX(float x); + /** + * @brief 依据线宽滑块横坐标调整当前信号线宽。 + * @param [IN] x 客户区横坐标。 + * @return 取值发生变化返回 true。 + */ + bool ApplyWidthFromX(float x); + HWND m_host = nullptr; /**< 宿主窗口句柄。 */ const UiFonts* m_fonts = nullptr; /**< 字体资源。 */ const Theme* m_theme = nullptr; /**< 主题资源。 */ @@ -455,19 +453,17 @@ namespace jdi std::array m_colorRects; /**< 预设颜色色块矩形。 */ int m_colorCount = 0; /**< 实际可用的预设颜色数量。 */ gd::RectF m_customChipRect; /**< 自定义颜色入口矩形(与预设色块同尺寸)。 */ - float m_swatchSize = 18.0f; /**< 颜色色块边长(逻辑像素)。 */ - - gd::RectF m_widthThinRect; /**< 最细线宽预设矩形。 */ - gd::RectF m_widthThickRect; /**< 最粗线宽预设矩形。 */ - gd::RectF m_widthMinusRect; /**< 线宽减号矩形。 */ - gd::RectF m_widthPlusRect; /**< 线宽加号矩形。 */ - gd::RectF m_widthValueRect; /**< 线宽数值显示矩形。 */ - gd::RectF m_widthSpinnerRect; /**< 线宽数值输入框整体矩形。 */ std::array m_modeRects; /**< 绘制样式按钮矩形。 */ + // 样式区固定四行:颜色 / 样式 / 透明度 / 线宽。各行控件共用同一内容列宽度。 + std::array m_styleRowRects; /**< 四行整体矩形(含标签列)。 */ + gd::RectF m_styleContentRect; /**< 四行共用的内容列矩形。 */ + gd::RectF m_opacityTrackRect; /**< 不透明度滑块轨道矩形。 */ gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */ + gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */ + gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */ std::vector m_rows; /**< 列表行集合(惰性缓存)。 */ bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */ @@ -479,6 +475,7 @@ namespace jdi bool m_draggingScrollBar = false; /**< 是否正在拖拽滚动条。 */ int m_scrollGrabOffset = 0; /**< 拖拽时鼠标相对滑块顶部的偏移。 */ bool m_draggingOpacity = false; /**< 是否正在拖拽不透明度滑块。 */ + bool m_draggingWidth = false; /**< 是否正在拖拽线宽滑块。 */ int m_hoverRow = -1; /**< 当前悬停行。 */ RowPart m_hoverPart = RowPart::NONE; /**< 当前悬停的行内区域。 */ StyleHit m_hoverStyle = StyleHit::NONE; /**< 当前悬停的样式条目类型。 */ diff --git a/Components/SignalPanel/Src/SignalPanel.cpp b/Components/SignalPanel/Src/SignalPanel.cpp index e481184..2384633 100644 --- a/Components/SignalPanel/Src/SignalPanel.cpp +++ b/Components/SignalPanel/Src/SignalPanel.cpp @@ -25,26 +25,26 @@ namespace jdi const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */ const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */ - // 样式编辑区:固定四行网格,行高与间距恒定,保证面板宽度变化时控件不散架。 + // 样式编辑区:固定四行网格(颜色 / 样式 / 透明度 / 线宽),行高、间距与内容列宽度统一, + // 保证面板宽度变化时四行控件始终对齐,且不会被无限制拉伸。 const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */ const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */ const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */ const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */ const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */ + const float kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */ const float kStyleRowCount = 4.0f; /**< 样式区固定行数。 */ - const float kSwatchEdge = 18.0f; /**< 颜色色块边长(含自定义颜色入口)。 */ - const float kSwatchGap = 5.0f; /**< 颜色色块间距。 */ - const float kWidthPresetWidth = 46.0f; /**< 线宽预设按钮宽度。 */ - const float kWidthSpinButton = 22.0f; /**< 线宽旋钮按钮宽度。 */ - const float kWidthValueWidth = 62.0f; /**< 线宽数值框宽度。 */ - const float kOpacityValueWidth = 52.0f; /**< 不透明度数值宽度。 */ + const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */ + const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */ + const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */ + const float kSwatchGap = 4.0f; /**< 颜色色块最小间距。 */ + const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */ + const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */ + const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */ const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */ - const float kWidthMin = 0.5f; /**< 允许的最小线宽。 */ - const float kWidthMax = 6.0f; /**< 允许的最大线宽。 */ - const float kWidthStep = 0.2f; /**< 线宽增减步进。 */ - const float kWidthThinPreset = 1.0f; /**< 最细线宽预设值。 */ - const float kWidthThickPreset = 3.0f; /**< 最粗线宽预设值。 */ + const float kWidthMin = 0.5f; /**< 允许的最小线宽(px)。 */ + const float kWidthMax = 10.0f; /**< 允许的最大线宽(px)。 */ /** @brief 可选绘制样式顺序(不含阶梯方式)。 */ const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER}; @@ -154,6 +154,7 @@ namespace jdi m_rowsDirty = true; m_scrollOffset = 0.0f; m_draggingOpacity = false; + m_draggingWidth = false; m_widthEditing = false; m_widthEditText.clear(); m_pendingLaneMenu = false; @@ -435,102 +436,104 @@ namespace jdi void SignalPanel::LayoutStyleControls() { + const float padH = kStylePadH * m_scale; const float labelWidth = kStyleLabelWidth * m_scale; - const float contentLeft = m_styleRect.X + kStylePadH * m_scale + labelWidth; - const float contentWidth = std::max(60.0f, m_styleRect.Width - kStylePadH * 2.0f * m_scale - labelWidth); - + const float labelGap = kStyleLabelGap * m_scale; const float rowHeight = kStyleRowHeight * m_scale; - const float row0Top = m_styleRect.Y + kStylePadV * m_scale; + const float rowStep = (kStyleRowHeight + kStyleRowGap) * m_scale; + const float firstTop = m_styleRect.Y + kStylePadV * m_scale; - // ---- 第一行:颜色(预设色块 + 同尺寸自定义颜色入口)---- + // 内容列宽度带上下限:面板变窄时收缩,面板变宽时不再延伸,四行始终共用同一宽度。 + const float available = std::max(40.0f, m_styleRect.Width - padH * 2.0f - labelWidth - labelGap); + const float contentWidth = std::min(available, kStyleContentMax * m_scale); + + // 标签列 + 内容列作为整体在面板内居中,避免拉宽后控件偏向一侧。 + const float groupWidth = labelWidth + labelGap + contentWidth; + const float groupLeft = m_styleRect.X + std::max(padH, (m_styleRect.Width - groupWidth) * 0.5f); + const float contentLeft = groupLeft + labelWidth + labelGap; + + m_styleContentRect = gd::RectF(contentLeft, firstTop, contentWidth, + kStyleRowCount * rowHeight + (kStyleRowCount - 1.0f) * + (kStyleRowGap * m_scale)); + for (int index = 0; index < static_cast(kStyleRowCount); ++index) + { + m_styleRowRects[static_cast(index)] = + gd::RectF(groupLeft, firstTop + static_cast(index) * rowStep, groupWidth, rowHeight); + } + + const gd::RectF& colorRow = m_styleRowRects[0]; + const gd::RectF& modeRow = m_styleRowRects[1]; + const gd::RectF& opacityRow = m_styleRowRects[2]; + const gd::RectF& widthRow = m_styleRowRects[3]; + + // ---- 颜色行:预设色块 + 同尺寸自定义颜色入口(合计恰好铺满内容列宽度)---- int paletteCount = 0; SignalPalette(paletteCount); - float swatchSize = kSwatchEdge * m_scale; const float swatchGap = kSwatchGap * m_scale; + const float swatchMaxGap = kSwatchMaxGap * m_scale; + const float swatchMin = kSwatchMin * m_scale; int count = std::min(paletteCount, static_cast(m_colorRects.size())); - - // 为自定义颜色入口预留一个同尺寸格位,优先整体缩小色块以保留全部预设颜色。 - if (count > 0) + float swatchSize = kSwatchEdge * m_scale; + float gap = swatchGap; + while (count >= 1) { - const float fit = (contentWidth - swatchGap * static_cast(count)) / - static_cast(count + 1); - if (fit < swatchSize) + const float slots = static_cast(count + 1); + const float fit = (contentWidth - swatchGap * (slots - 1.0f)) / slots; + if (count == 1 || fit >= swatchMin) { - swatchSize = fit; - } - // 色块小于可用下限时再从末尾裁剪预设,避免尺寸不可用。 - const float minSwatch = 14.0f * m_scale; - while (count > 1 && swatchSize < minSwatch) - { - --count; - swatchSize = (contentWidth - swatchGap * static_cast(count)) / - static_cast(count + 1); + swatchSize = std::max(swatchMin, std::min(kSwatchEdge * m_scale, fit)); + // 用实际色块尺寸反算间距,让颜色行与透明度、线宽、样式各行同宽。 + gap = (contentWidth - swatchSize * slots) / static_cast(count); + gap = std::max(swatchGap, std::min(gap, swatchMaxGap)); + break; } + --count; } - m_colorCount = std::max(0, count); - m_swatchSize = swatchSize / m_scale; + m_colorCount = std::max(1, count); - const float swatchTop = row0Top + (rowHeight - swatchSize) * 0.5f; + // 若受色块尺寸上限约束仍有剩余空间,则整行在内容列内居中。 + const float colorRowWidth = swatchSize * static_cast(m_colorCount + 1) + + gap * static_cast(m_colorCount); + const float colorRowLeft = contentLeft + std::max(0.0f, (contentWidth - colorRowWidth) * 0.5f); + + const float swatchTop = colorRow.Y + (rowHeight - swatchSize) * 0.5f; for (int index = 0; index < m_colorCount; ++index) { - const float x = contentLeft + static_cast(index) * (swatchSize + swatchGap); - m_colorRects[static_cast(index)] = gd::RectF(x, swatchTop, swatchSize, swatchSize); + m_colorRects[static_cast(index)] = + gd::RectF(colorRowLeft + static_cast(index) * (swatchSize + gap), swatchTop, + swatchSize, swatchSize); } m_customChipRect = - gd::RectF(contentLeft + static_cast(m_colorCount) * (swatchSize + swatchGap), swatchTop, + gd::RectF(colorRowLeft + static_cast(m_colorCount) * (swatchSize + gap), swatchTop, swatchSize, swatchSize); - // ---- 第二行:线宽(最细 / 数值旋钮 / 最粗)---- - // 先按设计尺寸排布,仅在可用宽度不足时整体等比缩小,保证不溢出也不被拉伸变形。 - const float row1Top = row0Top + (kStyleRowHeight + kStyleRowGap) * m_scale; - const float naturalGap = 8.0f * m_scale; - const float naturalPreset = kWidthPresetWidth * m_scale; - const float naturalSpinner = (kWidthSpinButton * 2.0f + kWidthValueWidth) * m_scale; - - const float naturalTotal = naturalPreset * 2.0f + naturalSpinner + naturalGap * 2.0f; - const float widthFactor = - (naturalTotal > contentWidth && naturalTotal > 0.0f) ? (contentWidth / naturalTotal) : 1.0f; - - const float presetWidth = naturalPreset * widthFactor; - const float spinnerWidth = naturalSpinner * widthFactor; - const float rowGap = std::max(2.0f * m_scale, naturalGap * widthFactor); - - const float spinButtonWidth = std::min(kWidthSpinButton * m_scale, spinnerWidth * 0.26f); - const float innerValueWidth = std::max(12.0f * m_scale, spinnerWidth - spinButtonWidth * 2.0f); - - const float groupWidth = presetWidth * 2.0f + spinnerWidth + rowGap * 2.0f; - const float groupLeft = contentLeft + std::max(0.0f, (contentWidth - groupWidth) * 0.5f); - - m_widthThinRect = gd::RectF(groupLeft, row1Top, presetWidth, rowHeight); - m_widthSpinnerRect = gd::RectF(groupLeft + presetWidth + rowGap, row1Top, spinnerWidth, rowHeight); - m_widthMinusRect = gd::RectF(m_widthSpinnerRect.X, row1Top, spinButtonWidth, rowHeight); - m_widthValueRect = - gd::RectF(m_widthMinusRect.X + spinButtonWidth, row1Top, innerValueWidth, rowHeight); - m_widthPlusRect = - gd::RectF(m_widthValueRect.X + innerValueWidth, row1Top, spinButtonWidth, rowHeight); - m_widthThickRect = - gd::RectF(m_widthSpinnerRect.X + spinnerWidth + rowGap, row1Top, presetWidth, rowHeight); - - // ---- 第三行:样式(线段 / 散点 / 点线)---- - const float row2Top = row1Top + (kStyleRowHeight + kStyleRowGap) * m_scale; + // ---- 样式行:三个等宽按钮铺满内容列 ---- const float modeGap = 6.0f * m_scale; - const float modeWidth = (contentWidth - modeGap * static_cast(kModeOptionCount - 1)) / - static_cast(kModeOptionCount); + const float modeWidth = + std::max(20.0f, (contentWidth - modeGap * static_cast(kModeOptionCount - 1)) / + static_cast(kModeOptionCount)); for (int index = 0; index < kModeOptionCount; ++index) { - const float x = contentLeft + static_cast(index) * (modeWidth + modeGap); - m_modeRects[static_cast(index)] = gd::RectF(x, row2Top, modeWidth, rowHeight); + m_modeRects[static_cast(index)] = + gd::RectF(contentLeft + static_cast(index) * (modeWidth + modeGap), modeRow.Y, + modeWidth, rowHeight); } - // ---- 第四行:不透明度 ---- - const float row3Top = row2Top + (kStyleRowHeight + kStyleRowGap) * m_scale; - const float opacityValueWidth = kOpacityValueWidth * m_scale; - const float trackWidth = std::max(40.0f, contentWidth - opacityValueWidth - 8.0f * m_scale); - m_opacityTrackRect = gd::RectF(contentLeft, row3Top + (rowHeight - kTrackHeight * m_scale) * 0.5f, - trackWidth, kTrackHeight * m_scale); - m_opacityValueRect = gd::RectF(contentLeft + trackWidth + 8.0f * m_scale, row3Top, - opacityValueWidth - 8.0f * m_scale, rowHeight); + // ---- 透明度行与线宽行:几何完全一致,保证纵向宽度对齐 ---- + const float valueWidth = std::min(kSliderValueWidth * m_scale, contentWidth * 0.4f); + const float trackWidth = std::max(24.0f, contentWidth - valueWidth - kSliderValueGap * m_scale); + const float trackHeight = kTrackHeight * m_scale; + + m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f, + trackWidth, trackHeight); + m_opacityValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, opacityRow.Y, + valueWidth, rowHeight); + + m_widthTrackRect = + gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight); + m_widthValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, widthRow.Y, + valueWidth, rowHeight); } void SignalPanel::Layout(const gd::RectF& area) @@ -835,27 +838,35 @@ namespace jdi return; } - const float labelX = m_styleRect.X + kStylePadH * m_scale; const float labelWidth = kStyleLabelWidth * m_scale; const float rowHeight = kStyleRowHeight * m_scale; - // ---- 第一行:颜色 ---- + // 行标签统一以整行为基准垂直居中,确保与同行控件纵向对齐。 + auto paintRowLabel = [&](int rowIndex, const wchar_t* text) { - UiText(g, L"颜色", m_fonts->uiSmall, gd::RectF(labelX, m_customChipRect.Y, labelWidth, rowHeight), + const gd::RectF& row = m_styleRowRects[static_cast(rowIndex)]; + UiText(g, text, m_fonts->uiSmall, gd::RectF(row.X, row.Y, labelWidth, row.Height), m_theme->textMuted, TextAlign::LEFT); + }; + + // ---- 第一行:颜色(预设色块 + 自定义颜色入口)---- + { + paintRowLabel(0, L"颜色"); int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); - bool presetActive = false; + + // 当自定义颜色与某个预设色恰好相同时,优先视为自定义颜色,避免两处同时高亮。 + const bool customActive = (style->color == style->customColor); for (int index = 0; index < m_colorCount; ++index) { const gd::RectF& rect = m_colorRects[static_cast(index)]; const std::uint32_t color = palette[index % paletteCount]; const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index); - const bool active = (style->color == color); - presetActive = presetActive || active; + const bool active = !customActive && (style->color == color); - UiFillRoundRect(g, rect, kCorner * m_scale, color); + // 预设色块同样叠加不透明度,直观反映实际显示效果。 + UiFillRoundRect(g, rect, kCorner * m_scale, ApplyOpacity(color, style->opacity)); if (active || hovered) { const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale, @@ -867,125 +878,32 @@ namespace jdi } // 自定义颜色入口:与预设色块完全同尺寸、同外框样式的小方格,仅多一个加号图标。 - // 当前颜色不在预设中时,方格直接填充该颜色,与预设色块的表现保持一致。 + // 方格始终显示记忆的自定义颜色,因此从任意预设色再次点击它都能立即回到上次的自定义色。 const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP); - - std::uint32_t chipColor = m_theme->cardBg; - std::uint32_t iconColor = chipHot ? m_theme->accent : m_theme->text; - if (!presetActive) - { - chipColor = ApplyOpacity(style->color, style->opacity); - iconColor = IsDarkColor(chipColor) ? 0xFFFDF6E3u : 0xFF073642u; - } + const std::uint32_t chipColor = ApplyOpacity(style->customColor, style->opacity); + const std::uint32_t iconColor = IsDarkColor(chipColor) ? 0xFFFDF6E3u : 0xFF073642u; UiFillRoundRect(g, m_customChipRect, kCorner * m_scale, chipColor); UiDrawRoundRect(g, m_customChipRect, kCorner * m_scale, - chipHot ? m_theme->accent : m_theme->borderStrong, 1.0f); + customActive ? m_theme->textStrong : (chipHot ? m_theme->accent : m_theme->borderStrong), + customActive ? 1.6f : 1.0f); UiDrawPlusIcon(g, m_customChipRect, iconColor, 1.6f * m_scale); - // 选中外框与预设色块完全一致(当前正在使用自定义颜色或被悬停时显示)。 - if (!presetActive || chipHot) + // 当前正在使用自定义颜色、或鼠标悬停时,显示与预设色块一致的外框。 + if (customActive || chipHot) { const gd::RectF& chip = m_customChipRect; const gd::RectF ring(chip.X - 2.0f * m_scale, chip.Y - 2.0f * m_scale, chip.Width + 4.0f * m_scale, chip.Height + 4.0f * m_scale); UiDrawRoundRect(g, ring, 5.0f * m_scale, - !presetActive ? m_theme->textStrong : m_theme->borderStrong, - !presetActive ? 1.6f : 1.0f); + customActive ? m_theme->textStrong : m_theme->borderStrong, + customActive ? 1.6f : 1.0f); } } - // ---- 第二行:线宽 ---- + // ---- 第二行:样式(线段 / 散点 / 点线)---- { - UiText(g, L"线宽", m_fonts->uiSmall, - gd::RectF(labelX, m_widthThinRect.Y, labelWidth, rowHeight), m_theme->textMuted, - TextAlign::LEFT); - - const gd::RectF presetRects[2] = {m_widthThinRect, m_widthThickRect}; - const float presetWidths[2] = {kWidthThinPreset, kWidthThickPreset}; - for (int index = 0; index < 2; ++index) - { - const gd::RectF& rect = presetRects[index]; - const bool active = std::fabs(style->lineWidth - presetWidths[index]) < 0.01f; - const bool hovered = - (m_hoverStyle == StyleHit::WIDTH_PRESET && m_hoverStyleIndex == index); - - UiFillRoundRect(g, rect, kCorner * m_scale, - active ? m_theme->accentSoft : (hovered ? m_theme->hover : m_theme->cardBg)); - UiDrawRoundRect(g, rect, kCorner * m_scale, active ? m_theme->accent : m_theme->border, 1.0f); - - const float centerY = rect.Y + rect.Height * 0.5f; - UiDrawLine(g, rect.X + 9.0f * m_scale, centerY, rect.X + rect.Width - 9.0f * m_scale, - centerY, active ? m_theme->accent : m_theme->text, - presetWidths[index] * m_scale); - } - - // 数值旋钮(线宽输入框)。 - const bool valueHot = (m_hoverStyle == StyleHit::WIDTH_VALUE); - UiFillRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, m_theme->cardBg); - UiDrawRoundRect(g, m_widthSpinnerRect, kCorner * m_scale, - m_widthEditing ? m_theme->accent - : (valueHot ? m_theme->borderStrong : m_theme->border), - m_widthEditing ? 1.6f : 1.0f); - - if (m_widthEditing) - { - const std::wstring editText = - m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText; - // 面板极窄时省略单位,保证数值本身完整可读。 - const std::wstring fullText = editText + L" px"; - const std::wstring shown = - (UiTextWidth(g, fullText, m_fonts->monoSmall) > m_widthValueRect.Width - 6.0f * m_scale) - ? editText - : fullText; - const float textWidth = UiTextWidth(g, shown, m_fonts->monoSmall); - const float caretX = - m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth; - UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, - TextAlign::RIGHT); - if (caretX > m_widthValueRect.X) - { - const float caretY = m_widthValueRect.Y + 5.0f * m_scale; - UiDrawLine(g, caretX, caretY, caretX, m_widthValueRect.Y + rowHeight - 5.0f * m_scale, - m_theme->accent, 1.4f * m_scale); - } - } - else - { - const std::wstring numberText = FormatNumber(style->lineWidth, 1); - const std::wstring fullText = numberText + L" px"; - const std::wstring shown = - (UiTextWidth(g, fullText, m_fonts->monoSmall) > m_widthValueRect.Width - 6.0f * m_scale) - ? numberText - : fullText; - UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, - TextAlign::CENTER); - } - - const bool minusHot = (m_hoverStyle == StyleHit::WIDTH_MINUS); - const bool plusHot = (m_hoverStyle == StyleHit::WIDTH_PLUS); - const bool minusEnabled = style->lineWidth > (kWidthMin + 1.0e-4f); - const bool plusEnabled = style->lineWidth < (kWidthMax - 1.0e-4f); - - if (minusHot && minusEnabled) - { - UiFillRoundRect(g, m_widthMinusRect, kCorner * m_scale, m_theme->hover); - } - if (plusHot && plusEnabled) - { - UiFillRoundRect(g, m_widthPlusRect, kCorner * m_scale, m_theme->hover); - } - - UiDrawArrowIcon(g, m_widthMinusRect, minusEnabled ? m_theme->text : m_theme->textMuted, - 1.6f * m_scale, IconDirection::LEFT); - UiDrawArrowIcon(g, m_widthPlusRect, plusEnabled ? m_theme->text : m_theme->textMuted, - 1.6f * m_scale, IconDirection::RIGHT); - } - - // ---- 第三行:样式 ---- - { - UiText(g, L"样式", m_fonts->uiSmall, gd::RectF(labelX, m_modeRects[0].Y, labelWidth, rowHeight), - m_theme->textMuted, TextAlign::LEFT); + paintRowLabel(1, L"样式"); for (int index = 0; index < kModeOptionCount; ++index) { @@ -1001,13 +919,11 @@ namespace jdi } } - // ---- 第四行:不透明度 ---- + // ---- 第三行:透明度 ---- { - UiText(g, L"透明度", m_fonts->uiSmall, - gd::RectF(labelX, m_opacityValueRect.Y, labelWidth, rowHeight), m_theme->textMuted, - TextAlign::LEFT); + paintRowLabel(2, L"透明度"); - const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY); + const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER); DrawSlider(g, m_opacityTrackRect, static_cast(style->opacity), m_theme->accent, active, m_scale); @@ -1015,6 +931,47 @@ namespace jdi UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect, m_theme->textStrong, TextAlign::RIGHT); } + + // ---- 第四行:线宽(与透明度行几何一致)---- + { + paintRowLabel(3, L"线宽"); + + const double ratio = + (static_cast(style->lineWidth) - kWidthMin) / (kWidthMax - kWidthMin); + const bool active = m_draggingWidth || (m_hoverStyle == StyleHit::WIDTH_SLIDER); + DrawSlider(g, m_widthTrackRect, ratio, m_theme->accent, active, m_scale); + + if (m_widthEditing) + { + // 输入状态:右对齐并显示插入符,便于用户看到键入内容。 + const std::wstring editText = m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText; + UiFillRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->cardBg); + UiDrawRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->accent, 1.6f); + + const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall); + UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, + TextAlign::RIGHT); + + const float caretX = m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth; + if (caretX > m_widthValueRect.X) + { + UiDrawLine(g, caretX, m_widthValueRect.Y + 5.0f * m_scale, caretX, + m_widthValueRect.Y + rowHeight - 5.0f * m_scale, m_theme->accent, + 1.4f * m_scale); + } + } + else + { + const std::wstring numberText = FormatNumber(style->lineWidth, 1); + const std::wstring fullText = numberText + L" px"; + // 空间不足时省略单位,保证数值本身完整可读。 + const std::wstring shown = + (UiTextWidth(g, fullText, m_fonts->monoSmall) > m_widthValueRect.Width - 6.0f * m_scale) + ? numberText + : fullText; + UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT); + } + } } bool SignalPanel::AreAllSignalsVisible() const @@ -1026,7 +983,7 @@ namespace jdi return m_tab->VisibleCount() == TotalSignalCount(); } - bool SignalPanel::StepLineWidth(float delta) + bool SignalPanel::ApplyWidthFromX(float x) { if (m_tab == nullptr) { @@ -1038,8 +995,10 @@ namespace jdi return false; } - float value = ClampFloat(style->lineWidth + delta, kWidthMin, kWidthMax); + const double ratio = UiSliderRatioFromX(m_widthTrackRect, x); + float value = static_cast(kWidthMin + ratio * (kWidthMax - kWidthMin)); value = std::round(value * 10.0f) / 10.0f; + value = ClampFloat(value, kWidthMin, kWidthMax); if (std::fabs(value - style->lineWidth) < 0.001f) { return false; @@ -1191,12 +1150,12 @@ namespace jdi { return false; } - if (style->color == color) - { - return false; - } + + // 同时记住最后一次自定义取值,便于切换到预设色后一键回到该颜色。 + const bool changed = (style->color != color) || (style->customColor != color); style->color = color; - return true; + style->customColor = color; + return changed; } bool SignalPanel::ApplyOpacityFromX(float x) @@ -1228,7 +1187,8 @@ namespace jdi } const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area; - m_colorPicker->Open(bounds, m_customChipRect, style.color); + // 以记忆的自定义颜色作为初始取值,实现“再次打开即恢复上次取色”。 + m_colorPicker->Open(bounds, m_customChipRect, style.customColor); return true; } @@ -1254,6 +1214,10 @@ namespace jdi { return ApplyOpacityFromX(static_cast(x)); } + if (m_draggingWidth) + { + return ApplyWidthFromX(static_cast(x)); + } if (m_draggingScrollBar) { const gd::RectF thumb = ScrollThumbRect(); @@ -1296,28 +1260,6 @@ namespace jdi { hoverStyle = StyleHit::CUSTOM_CHIP; } - if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThinRect, fx, fy)) - { - hoverStyle = StyleHit::WIDTH_PRESET; - hoverStyleIndex = 0; - } - if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthThickRect, fx, fy)) - { - hoverStyle = StyleHit::WIDTH_PRESET; - hoverStyleIndex = 1; - } - if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthMinusRect, fx, fy)) - { - hoverStyle = StyleHit::WIDTH_MINUS; - } - if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthPlusRect, fx, fy)) - { - hoverStyle = StyleHit::WIDTH_PLUS; - } - if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy)) - { - hoverStyle = StyleHit::WIDTH_VALUE; - } if (hoverStyle == StyleHit::NONE) { for (int index = 0; index < kModeOptionCount; ++index) @@ -1333,7 +1275,16 @@ namespace jdi if (hoverStyle == StyleHit::NONE && UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy)) { - hoverStyle = StyleHit::OPACITY; + hoverStyle = StyleHit::OPACITY_SLIDER; + } + if (hoverStyle == StyleHit::NONE && + UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy)) + { + hoverStyle = StyleHit::WIDTH_SLIDER; + } + if (hoverStyle == StyleHit::NONE && UiPointInRect(m_widthValueRect, fx, fy)) + { + hoverStyle = StyleHit::WIDTH_VALUE; } } @@ -1416,33 +1367,11 @@ namespace jdi } if (UiPointInRect(m_customChipRect, fx, fy)) { - // 在左键抬起时展开,避免按下即被后续消息立即收起。 + // 先恢复到记忆的自定义颜色(即便随后收起浮窗也会生效),再在抬起时展开浮窗。 + style->color = style->customColor; m_pendingColorPicker = true; return true; } - if (UiPointInRect(m_widthThinRect, fx, fy)) - { - style->lineWidth = kWidthThinPreset; - return true; - } - if (UiPointInRect(m_widthThickRect, fx, fy)) - { - style->lineWidth = kWidthThickPreset; - return true; - } - if (UiPointInRect(m_widthMinusRect, fx, fy)) - { - return StepLineWidth(-kWidthStep); - } - if (UiPointInRect(m_widthPlusRect, fx, fy)) - { - return StepLineWidth(kWidthStep); - } - if (UiPointInRect(m_widthValueRect, fx, fy)) - { - // 单击即进入输入状态,双击亦等效。 - return BeginWidthEdit(); - } for (int index = 0; index < kModeOptionCount; ++index) { if (UiPointInRect(m_modeRects[static_cast(index)], fx, fy)) @@ -1454,7 +1383,19 @@ namespace jdi if (UiPointInRect(SliderHitRect(m_opacityTrackRect, m_scale), fx, fy)) { m_draggingOpacity = true; - return ApplyOpacityFromX(fx) || true; + ApplyOpacityFromX(fx); + return true; + } + if (UiPointInRect(SliderHitRect(m_widthTrackRect, m_scale), fx, fy)) + { + m_draggingWidth = true; + ApplyWidthFromX(fx); + return true; + } + if (UiPointInRect(m_widthValueRect, fx, fy)) + { + // 单击即进入输入状态,双击亦等效。 + return BeginWidthEdit(); } } @@ -1518,6 +1459,11 @@ namespace jdi m_draggingOpacity = false; changed = true; } + if (m_draggingWidth) + { + m_draggingWidth = false; + changed = true; + } if (m_draggingScrollBar) { m_draggingScrollBar = false; diff --git a/Utilities/Inc/ColorPickerPopup.hpp b/Utilities/Inc/ColorPickerPopup.hpp index 948f486..42d3f3a 100644 --- a/Utilities/Inc/ColorPickerPopup.hpp +++ b/Utilities/Inc/ColorPickerPopup.hpp @@ -113,6 +113,14 @@ namespace jdi */ bool ContainsPoint(int x, int y) const; + /** + * @brief 查询该坐标应使用的系统光标。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 系统光标句柄;面板未打开时返回空指针。 + */ + HCURSOR QueryCursor(int x, int y) const; + private: /** * @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index b706e84..ed92b99 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -195,6 +195,7 @@ namespace jdi { bool visible = false; /**< 是否参与绘制。默认关闭,由用户自行勾选。 */ std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */ + std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */ float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */ float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */ DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */ diff --git a/Utilities/Src/ColorPickerPopup.cpp b/Utilities/Src/ColorPickerPopup.cpp index 9273a1c..cb231e1 100644 --- a/Utilities/Src/ColorPickerPopup.cpp +++ b/Utilities/Src/ColorPickerPopup.cpp @@ -193,6 +193,21 @@ namespace jdi return UiPointInRect(m_rect, static_cast(x), static_cast(y)); } + HCURSOR ColorPickerPopup::QueryCursor(int x, int y) const + { + if (!m_open) + { + return nullptr; + } + + // 拖拽过程中始终保持横向调整光标,避免指针移出轨道后光标跳变。 + if (m_draggingSlider >= 0 || SliderAtPoint(x, y) >= 0) + { + return LoadCursorW(nullptr, IDC_SIZEWE); + } + return LoadCursorW(nullptr, IDC_ARROW); + } + int ColorPickerPopup::SliderAtPoint(int x, int y) const { if (!m_open)