## 为什么
用户 2026-10-29 报告/要求五件事:
1. 派生信号的「时间轴偏移保留在样式上、其它调整烘值」维持现状(确认无需改动);
2. 信号名右键菜单里很多功能已被下方样式自定义区覆盖 ⇒ 只保留「仅显示该信号」,
并在它**上方**新增「克隆该信号」;
3. 设置菜单新增「主题配色」二级菜单(带 √ 状态),顺序 Light / Solarised Light /
Dark / Solarised Dark,默认主题维持现状;
4. 布局边界:中键平移视界时浮动面板上的边界**闪烁**;光标取值胶囊的**文字**框会**穿透**
到浮动面板上;并请核对网格内固定构件(图例 / Y 轴标尺 / GX 标签 / 回到最上方 /
浮动面板)的层级关系;
5. 单数据点信号在拖动光标时的**吸附**功能消失。
## 根因
### 布局边界层级(两个都很隐蔽)
- **文本层号写死**:文本是在绘制过程中由 `UiTextRectSink` 收集的,而宿主把所有文本都记在
固定层 2;组件几何层却是"组件表下标"(`PlotView` = 0)。面板层 = PlotView 层 + 1 = 1,
**低于**文本层 2 ⇒ 面板的遮挡矩形挖不到文本 ⇒ 胶囊文字画在面板表面。
- **面板像素缓存**:面板"先画进图层、命中缓存就贴图",贴图那一帧不画字 ⇒ 文本框凭空消失
⇒ 面板上的边界闪烁。
- 顺带发现 `PlotView::BuildBoxes` 用 `PushLayer()` 却**从不复位**,会把宿主后续所有组件的
层号整体推高。
### 单点信号吸附
`MinorCellSparse` 在"把判定范围收窄到数据实际时间跨度"的分支里有
`if (!(dataMax > dataMin)) return false;`。单个有效采样点时 `dataMax == dataMin`,
于是被判定成"不稀疏" ⇒ 不吸附。
## 改了什么
### 1. 布局边界层级
- `UiTextRectSink` 新增 `OnTextLayer(int)` / `TextLayer()`;新增 `UiSetTextLayer` /
`UiGetTextLayer`;`UiBoxTable` 实现为改 `m_layer`,并暴露 `Layer()`。
- `Application::Render` 在绘制每个组件前下发 `UiSetTextLayer(ComponentLayer(&component))`,
浮层用 `OverlayLayer(1/2)`;新增 `ComponentLayer()` / `OverlayLayer()` /
`kLayersPerComponent = 3`,与 `PaintLayoutBounds` 的层号**完全同源**。
- `PaintLayoutBounds` 改为**显式** `SetLayer(下标 × 3)`(不再依赖 `PushLayer` 累加)。
- `PlotView`:`BuildBoxes` 用 `baseLayer + 1` 放浮动面板(并登记遮挡)、
`baseLayer + 2` 放绘制在面板之上的滚动条 / 回到最上方 / 输入框,结束**复位**层号;
`paintOverlays` 同步分两次抬高文本层。
- 面板像素图层新增 `PanelSlot::textRects` + 转发型收集器 `PanelTextCollector`:
渲染时存文本框,命中缓存贴图时回放 ⇒ 几何与缓存命中无关(消除闪烁)。
### 2. 信号右键菜单 + 克隆
- 菜单只剩:标题、**克隆该信号**、**仅显示该信号**;删除显示/隐藏、显示全部/隐藏全部、
预设色块行、样式三项,以及对应的 `CMD_SIGNAL_TOGGLE/SHOW_ALL/HIDE_ALL`、
`CMD_COLOR_BASE`、`CMD_MODE_BASE` 及其分支(死代码)。
- 新增 `CMD_SIGNAL_CLONE`(post `WM_APP + 5`)→ `Application::CloneSignal`:
把「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘进采样值、**时间轴偏移保留在样式上**,
名称前置 `*`,插入到源信号**正下方**,并**保留原外观**(颜色/透明度/线宽/绘制样式)。
- 抽出 `Application::InsertSignalBelowSource(...)` 供派生与克隆共用。
### 3. 主题配色
- `ThemeKind{LIGHT, SOLARIZED_LIGHT, DARK, SOLARIZED_DARK}` + `ThemeFor()` / `ThemeName()`;
Light 取 VS Code 2026 默认浅色、Dark 取 Dark+、Solarized Dark 取 VS Code solarized dark。
**默认仍是 Solarised Light**。
- 新增 `CurrentTheme()` / `SetCurrentTheme()`:宿主交出自己的 `m_theme` 地址,
切换主题只改内容;`UiControls` 里没有 `Theme` 形参的原语改用它(原来硬编码
`SolarizedLight()`,换主题不生效)。
- `Application::ApplyTheme()`:改配色 → `PlotView::OnThemeChanged()`(作废场景/宽基底/
装饰/光晕/面板图层)+ `SignalPanel::OnThemeChanged()`(作废行位图)→ 整窗重绘。
- 菜单用 `SUBMENU` + 4 个 `keepOpen` 子项;新增 `PopupMenu::UpdateSubmenuItemLabel()`
回写勾选,并让子菜单分支尊重 `keepOpen`(原为无条件关闭)。
### 4. 单点信号吸附
`MinorCellSparse` 改为只在 `dataMax < dataMin` 时放弃;相等时按"那一个小格"判定
(`need = 5 > finiteCount = 1` ⇒ 稀疏 ⇒ 吸附恢复)。
### 5. 审计
`Makefile` 的 `LAYOUT_BOUNDS` / `JDI_LAYOUT_BOUNDS` **从未被任何 `#if` 引用**
(`LAYOUT_BOUNDS=0` 产物字节数完全相同),属名实不符的死开关 ⇒ 删除并注明原因。
## 怎么验证
新增两支离屏探针(不占鼠标 / 键盘 / 屏幕):
```
.preflight/round15check 28 passed, 0 failed
单点吸附 7 条(含"远处不吸附")、面板层级 5 条、面板文本框跨帧与中键平移稳定 2 条、
克隆 14 条(烘值逐点比对 / 名称前缀 / 偏移与外观保留 / 调整复位)
.preflight/themecheck 17 passed, 0 failed
4 套主题可读性与可区分、默认 = Solarised Light、逐主题换色且无旧底色残留、
切回默认逐像素还原(diff = 0)、子菜单 4 项 / 顺序 / 唯一勾选 / 命令切换 / 菜单保持打开
```
回归全绿:
```
uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4
lbcovr 7/7 lbpanel 10/10 derivecheck 16/16
rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
```
`make release` / `make debug` 零 error / 零 warning;产物 **791,552 B**
(单一 exe,未拆 DLL,未改 `-Oz`;离 1MB 预警线约 250 KB)。
404 lines
14 KiB
C++
404 lines
14 KiB
C++
// 通用 UI 控件:按钮图标 / 复选框 / 滑块 / 内联输入框 / 颜色样块 / 取值胶囊配色。
|
|
//
|
|
// 本文件属于 Components/Shared —— 这些控件被信号面板、绘图区、工具栏、浮动面板等
|
|
// 多个功能组件复用,因此集中放在共享组件里,而不是散落在各自的实现文件中。
|
|
// 依赖方向:Components/Shared -> Utilities(只用低层绘制原语,不反向依赖)。
|
|
#include "UiControls.hpp"
|
|
|
|
#include <cmath>
|
|
|
|
namespace jdi
|
|
{
|
|
|
|
namespace
|
|
{
|
|
/** Swatch outline opacity threshold (shared with UiSwatchNeedsOutline). */
|
|
const float kSwatchOutlineOpacity = 0.45f;
|
|
}
|
|
|
|
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
|
|
{
|
|
const float cx = rect.X + rect.Width * 0.5f;
|
|
const float cy = rect.Y + rect.Height * 0.5f;
|
|
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
|
|
UiDrawLine(g, cx - arm, cy, cx + arm, cy, argb, strokeWidth);
|
|
UiDrawLine(g, cx, cy - arm, cx, cy + arm, argb, strokeWidth);
|
|
}
|
|
|
|
void UiDrawCrossIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
|
|
{
|
|
const float cx = rect.X + rect.Width * 0.5f;
|
|
const float cy = rect.Y + rect.Height * 0.5f;
|
|
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
|
|
UiDrawLine(g, cx - arm, cy - arm, cx + arm, cy + arm, argb, strokeWidth);
|
|
UiDrawLine(g, cx + arm, cy - arm, cx - arm, cy + arm, argb, strokeWidth);
|
|
}
|
|
|
|
void UiDrawTriangleIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, IconDirection direction)
|
|
{
|
|
const float cx = rect.X + rect.Width * 0.5f;
|
|
const float cy = rect.Y + rect.Height * 0.5f;
|
|
const float arm = std::min(rect.Width, rect.Height) * 0.28f;
|
|
|
|
gd::PointF points[3];
|
|
switch (direction)
|
|
{
|
|
case IconDirection::LEFT:
|
|
points[0] = gd::PointF(cx + arm * 0.7f, cy - arm);
|
|
points[1] = gd::PointF(cx - arm * 0.7f, cy);
|
|
points[2] = gd::PointF(cx + arm * 0.7f, cy + arm);
|
|
break;
|
|
case IconDirection::RIGHT:
|
|
points[0] = gd::PointF(cx - arm * 0.7f, cy - arm);
|
|
points[1] = gd::PointF(cx + arm * 0.7f, cy);
|
|
points[2] = gd::PointF(cx - arm * 0.7f, cy + arm);
|
|
break;
|
|
case IconDirection::UP:
|
|
points[0] = gd::PointF(cx - arm, cy + arm * 0.7f);
|
|
points[1] = gd::PointF(cx, cy - arm * 0.7f);
|
|
points[2] = gd::PointF(cx + arm, cy + arm * 0.7f);
|
|
break;
|
|
case IconDirection::DOWN:
|
|
default:
|
|
points[0] = gd::PointF(cx - arm, cy - arm * 0.7f);
|
|
points[1] = gd::PointF(cx, cy + arm * 0.7f);
|
|
points[2] = gd::PointF(cx + arm, cy - arm * 0.7f);
|
|
break;
|
|
}
|
|
|
|
gd::SolidBrush brush(ToGdiColor(argb));
|
|
g.FillPolygon(&brush, points, 3);
|
|
}
|
|
|
|
double UiSliderRatioFromX(const gd::RectF& trackRect, float x)
|
|
{
|
|
if (trackRect.Width <= 0.0f)
|
|
{
|
|
return 0.0;
|
|
}
|
|
const double ratio = static_cast<double>(x - trackRect.X) / static_cast<double>(trackRect.Width);
|
|
if (ratio < 0.0)
|
|
{
|
|
return 0.0;
|
|
}
|
|
if (ratio > 1.0)
|
|
{
|
|
return 1.0;
|
|
}
|
|
return ratio;
|
|
}
|
|
double UiColorLuminance(std::uint32_t color)
|
|
{
|
|
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
|
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
|
const double b = static_cast<double>(color & 0xFFu);
|
|
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
|
|
}
|
|
|
|
UiInk UiContrastInk(std::uint32_t background)
|
|
{
|
|
// 极值文字/描边色(Solarized base3 / base02 / base03),亮度差很大,
|
|
// 因此任意底色下文字与底色之间都有充足对比。
|
|
const std::uint32_t kInkLight = 0xFFFDF6E3u;
|
|
const std::uint32_t kInkDark = 0xFF073642u;
|
|
const std::uint32_t kOutlineLight = 0xFFFDF6E3u;
|
|
const std::uint32_t kOutlineDark = 0xFF002B36u;
|
|
/** 向底色互补色混入的比例:线性响应信号配色,同时把对比度损失限制在 15% 以内。 */
|
|
const double kTint = 0.15;
|
|
|
|
const double luminance = UiColorLuminance(background);
|
|
// 底色越亮 → 文字越暗(取最大对比的一侧)。
|
|
const bool darkText = (luminance >= 0.5);
|
|
// 底色的互补色:相对亮度恰为 1 - 底色亮度。
|
|
const std::uint32_t complement = 0xFF000000u | ((~background) & 0x00FFFFFFu);
|
|
|
|
UiInk ink;
|
|
ink.text = MixColor(darkText ? kInkDark : kInkLight, complement, kTint);
|
|
ink.text = WithAlpha(ink.text, 0xFFu);
|
|
ink.outline = MixColor(darkText ? kOutlineLight : kOutlineDark, complement, kTint);
|
|
ink.outline = WithAlpha(ink.outline, 0x99u);
|
|
return ink;
|
|
}
|
|
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius)
|
|
{
|
|
const Theme& theme = CurrentTheme();
|
|
const float centerY = rect.Y + rect.Height * 0.5f;
|
|
|
|
if (state == 1)
|
|
{
|
|
UiFillRoundRect(g, rect, radius, theme.accent);
|
|
gd::Pen pen(ToGdiColor(theme.textOnAccent), std::max(1.4f, rect.Height * 0.11f));
|
|
pen.SetStartCap(gd::LineCapRound);
|
|
pen.SetEndCap(gd::LineCapRound);
|
|
g.DrawLine(&pen, rect.X + rect.Width * 0.22f, centerY, rect.X + rect.Width * 0.42f,
|
|
rect.Y + rect.Height * 0.72f);
|
|
g.DrawLine(&pen, rect.X + rect.Width * 0.42f, rect.Y + rect.Height * 0.72f,
|
|
rect.X + rect.Width * 0.78f, rect.Y + rect.Height * 0.28f);
|
|
}
|
|
else if (state == 2)
|
|
{
|
|
UiFillRoundRect(g, rect, radius, theme.accentSoft);
|
|
UiDrawRoundRect(g, rect, radius, theme.accent, 1.0f);
|
|
UiFillRect(g, gd::RectF(rect.X + rect.Width * 0.22f, centerY - rect.Height * 0.08f,
|
|
rect.Width * 0.56f, rect.Height * 0.16f),
|
|
theme.accent);
|
|
}
|
|
else
|
|
{
|
|
UiFillRoundRect(g, rect, radius, theme.cardBg);
|
|
UiDrawRoundRect(g, rect, radius, theme.borderStrong, 1.0f);
|
|
}
|
|
}
|
|
|
|
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
|
bool active, float scale)
|
|
{
|
|
if (track.Width <= 0.0f || track.Height <= 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ratio < 0.0)
|
|
{
|
|
ratio = 0.0;
|
|
}
|
|
if (ratio > 1.0)
|
|
{
|
|
ratio = 1.0;
|
|
}
|
|
|
|
const float radius = track.Height * 0.5f;
|
|
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
|
|
|
// 已填充部分严格止于圆钮中心,最小值时不会留下超出圆钮的填充残留。
|
|
const float fillWidth = static_cast<float>(ratio) * track.Width;
|
|
if (fillWidth > 0.5f)
|
|
{
|
|
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
|
}
|
|
|
|
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
|
|
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
|
|
|
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
|
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
|
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
knobRadius * 2.0f);
|
|
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
|
knobRadius * 2.0f);
|
|
}
|
|
|
|
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
|
|
float& innerWidth)
|
|
{
|
|
innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
|
|
const float textLeft = rect.X + 6.0f * scale;
|
|
|
|
const std::wstring& text = edit.Text();
|
|
if (text.empty())
|
|
{
|
|
return textLeft;
|
|
}
|
|
|
|
const float textWidth = UiMeasureTextWidth(font, text);
|
|
if (textWidth <= innerWidth)
|
|
{
|
|
return textLeft;
|
|
}
|
|
|
|
// 保证插入符可见:插入符超出右边界时向左滚动,并夹到总可滚动范围内。
|
|
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
|
float scroll = 0.0f;
|
|
if (caretX > innerWidth)
|
|
{
|
|
scroll = caretX - innerWidth;
|
|
}
|
|
const float maximum = textWidth - innerWidth;
|
|
scroll = std::max(0.0f, std::min(scroll, maximum));
|
|
return textLeft - scroll;
|
|
}
|
|
|
|
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX)
|
|
{
|
|
if (text.empty() || font == nullptr)
|
|
{
|
|
return 0;
|
|
}
|
|
if (localX <= 0.0f)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// 逐字符累加宽度,找到距离最近的字符边界(文本通常很短,线性扫描足够)。
|
|
float previous = 0.0f;
|
|
for (std::size_t index = 0; index < text.size(); ++index)
|
|
{
|
|
const float current = UiMeasureTextWidth(font, text.substr(0, index + 1));
|
|
if (localX < current)
|
|
{
|
|
// 落在本字符的左半 → 取左边界,右半 → 取右边界。
|
|
return (localX - previous <= current - localX) ? index : (index + 1);
|
|
}
|
|
previous = current;
|
|
}
|
|
return text.size();
|
|
}
|
|
|
|
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale)
|
|
{
|
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const Theme& theme = CurrentTheme();
|
|
const float corner = 3.0f * scale;
|
|
|
|
UiFillRoundRect(g, rect, corner, theme.cardBg);
|
|
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
|
|
|
|
UiDrawEditFieldContent(g, rect, edit, font, scale);
|
|
}
|
|
|
|
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
|
float scale)
|
|
{
|
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const Theme& theme = CurrentTheme();
|
|
const std::wstring& text = edit.Text();
|
|
const float innerLeft = rect.X + 6.0f * scale;
|
|
|
|
float innerWidth = 0.0f;
|
|
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
|
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
|
|
|
|
if (text.empty())
|
|
{
|
|
// 空内容仍绘制插入符,保持焦点可见。
|
|
UiDrawLine(g, textLeft, rect.Y + 4.0f * scale, textLeft, rect.Y + rect.Height - 4.0f * scale,
|
|
theme.accent, 1.4f * scale);
|
|
return;
|
|
}
|
|
|
|
// 光标与选区端点对应的像素位置(以文本左端为原点)。
|
|
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
|
const float selBeginX =
|
|
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionBegin())) : 0.0f;
|
|
const float selEndX =
|
|
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionEnd())) : 0.0f;
|
|
|
|
const bool fullSelection =
|
|
edit.HasSelection() && edit.SelectionBegin() == 0 && edit.SelectionEnd() == text.size();
|
|
|
|
const int saveState = g.Save();
|
|
g.SetClip(clipRect, gd::CombineModeIntersect);
|
|
|
|
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感。
|
|
// ⚠️ 这里**必须自己把矩形夹到内容区**:`UiFillRect` 对面积足够大的不透明矩形会走
|
|
// GDI `FillRect` 快路径,而 GDI 绘制**不受 GDI+ 裁剪区约束**,长文本的全选高亮
|
|
// (宽度等于整段文本宽度)因此会溢出到输入框之外。夹取之后矩形始终落在框内,
|
|
// 既保留快路径又不越界。
|
|
if (edit.HasSelection() && selEndX > selBeginX)
|
|
{
|
|
gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale, selEndX - selBeginX,
|
|
rect.Height - 6.0f * scale);
|
|
const float left = std::max(highlight.X, clipRect.X);
|
|
const float right = std::min(highlight.X + highlight.Width, clipRect.X + clipRect.Width);
|
|
const float top = std::max(highlight.Y, clipRect.Y);
|
|
const float bottom = std::min(highlight.Y + highlight.Height, clipRect.Y + clipRect.Height);
|
|
highlight = gd::RectF(left, top, right - left, bottom - top);
|
|
UiFillRect(g, highlight, theme.accent);
|
|
}
|
|
|
|
// 长文本用 ETO_CLIPPING 交给 GDI 裁剪,避免字符画到输入框之外。
|
|
UiTextAtClipped(g, text, font, textLeft, rect,
|
|
fullSelection ? theme.textOnAccent : theme.textStrong, clipRect);
|
|
|
|
if (!fullSelection && edit.HasSelection() && selEndX > selBeginX)
|
|
{
|
|
// 局部选区:在反白底衬上重绘被选中的片段,其余文字保持常规颜色。
|
|
const std::wstring selected =
|
|
text.substr(edit.SelectionBegin(), edit.SelectionEnd() - edit.SelectionBegin());
|
|
UiTextAtClipped(g, selected, font, textLeft + selBeginX, rect, theme.textOnAccent, clipRect);
|
|
}
|
|
|
|
// 插入符。
|
|
const float caretDrawX = textLeft + caretX;
|
|
UiDrawLine(g, caretDrawX, rect.Y + 4.0f * scale, caretDrawX, rect.Y + rect.Height - 4.0f * scale,
|
|
theme.textStrong, 1.4f * scale);
|
|
|
|
g.Restore(saveState);
|
|
}
|
|
|
|
bool UiEditFieldMouseDown(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x, int y)
|
|
{
|
|
if (!edit.Active() || font == nullptr || rect.Width <= 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
if (!UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 用与绘制完全相同的布局函数换算文本起点,保证点哪儿光标就落在哪儿。
|
|
float innerWidth = 0.0f;
|
|
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
|
edit.BeginDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
|
|
return true;
|
|
}
|
|
|
|
bool UiEditFieldMouseMove(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x)
|
|
{
|
|
if (!edit.Active() || !edit.Dragging() || font == nullptr || rect.Width <= 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float innerWidth = 0.0f;
|
|
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
|
return edit.UpdateDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
|
|
}
|
|
|
|
bool UiSwatchNeedsOutline(float opacity)
|
|
{
|
|
return !(opacity >= kSwatchOutlineOpacity);
|
|
}
|
|
|
|
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
|
|
float radius, float scale)
|
|
{
|
|
double alpha = static_cast<double>(opacity);
|
|
if (!(alpha > 0.0))
|
|
{
|
|
alpha = 0.0;
|
|
}
|
|
if (alpha > 1.0)
|
|
{
|
|
alpha = 1.0;
|
|
}
|
|
|
|
const std::uint32_t filled = WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
|
|
if (rect.Width > 0.0f && rect.Height > 0.0f)
|
|
{
|
|
UiFillRoundRect(g, rect, radius, filled);
|
|
|
|
// 透明度过低时色块本身难以辨识,补充一圈描边以保持可读性;可见时不绘制描边。
|
|
if (UiSwatchNeedsOutline(opacity))
|
|
{
|
|
UiDrawRoundRect(g, rect, radius, CurrentTheme().borderStrong,
|
|
std::max(1.0f, scale));
|
|
}
|
|
}
|
|
return filled;
|
|
}
|
|
|
|
|
|
}
|
|
// namespace jdi
|
|
|