「调整」行:系数/偏移 按钮 + 偏移调整(Y 轴/时间轴)二级面板;结算顺序加入偏移;派生信号继承调整变换
按钮与面板: - 「系数」按钮改名为「系数/偏移」;点击展开的二级面板在原「系数调整」输入框下方新增「偏移调整」: 与「系数调整」同一套绘制(标题 + 标题栏右侧的还原图标),下面两行分别是「Y轴」「时间轴」, 每行左侧说明文字、右侧内联输入框(整行数值框)。 - 「Y轴」默认 0.00,非编辑态**始终带正负号**(+0.00 / -12.50),小数不足两位时补两位、更高精度保留更多位。 - 「时间轴」默认 0.00,非编辑态显示与双光标差值同一套复合单位文本(`Δt:500ms`、`Δt:1h 1min 2s`); 进入编辑态时显示以**秒**为单位的数值,并在输入框最右侧提示单位 `s`。 - 「偏移调整」的还原图标把两个偏移恢复为默认值 0;「系数调整」的还原图标仍只重置系数。 - 面板/输入框的编辑初值与显示文本分离(`MenuItem::editValue` / `editSuffix`), 非编辑态(`Δt:…`)与编辑态(`3662`)不一致时也能正确回显。 语义与结算顺序: - 结算顺序变为 **系数 → 绝对值 → 镜像 → 偏移**(前三步不变)。 - Y 偏移:给绘制值加常数(波形上下移动);时间偏移:采样绘制在 `t + offsetTime` 处 (例如 t=1s、偏移 -0.5s → 画在 0.5s)。 - ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量, 使绘制等价于「先加上偏移再镜像」(开关来回切换可逆)。 - 绘制链路全部跟随偏移:`BuildPolyline`(逐点描边 / 像素列包络 / 跨列边界插值 / 粗粒度分组层, 保序的 Y 偏移直接加在分组极值上)、派生预览曲线、光标取值读数(含 Δt)、 可见区间 Y 轴自适应、整段自适应的区间映射;折线缓存键、场景指纹、样式快照与图层指纹都纳入两个偏移。 派生信号(实验性功能「构建为新信号」): - 修复「预览是调整后的波形、构建出来却按原始状态绘制」:新信号**继承源信号的调整变换** (系数/绝对值/两个镜像/两个偏移)——网格预览绘制的是 `变换(滤波结果)`, 而构建出的信号只存了滤波结果,因此必须把变换一并继承; 时间轴偏移**只能**靠继承(派生信号与源信号共用同一文件时间轴,无法把时间平移烘进数据)。 离屏验证(不干扰用户): - ⚠️ harness 现在在每次注入鼠标消息后**立即释放本窗口的鼠标捕获** (宿主 WM_LBUTTONDOWN 会 SetCapture,隐藏窗口同样会把用户的真实鼠标输入吸走); 后续消息本来就是直接投递,无需捕获。 - 新增 [AP]:按钮标签不裁切、面板 5 行顺序与「偏移调整」位于系数输入框下方、 Y轴/时间轴行的标签与输入框同行且默认值/单位提示正确、时间轴编辑态显示秒值并在最右侧提示 `s`、 Y 偏移提交/显示正负号/波形整体平移量、时间偏移提交/Δt 复合单位(含 3662 → Δt:1h 1min 2s)/ 编辑态为秒、**按"偏移若干个采样点"构造的精确几何验证**(采样被画到移位后的像素位置、且不再画在原处)、 读数跟随、两个还原图标、垂直镜像对 Y 偏移取反(可逆)、 以及派生信号继承变换;[AL] 补充「系数→绝对值→镜像→偏移」的顺序与区间映射(含 Y 偏移)。 - 合计 645/645 通过;make clean && make all 零警告。
This commit is contained in:
1 parent
2b02518f85
commit
fc77b0262f
11 files changed
+567
-252
No files matched your search
+46
-18
@@ -107,35 +107,51 @@ namespace jdi
|
||||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||||
|
||||
/**
|
||||
* 绘制前的数值变换(依次作用,见 `ApplyDrawTransform`):
|
||||
* 绘制前的变换(依次作用,见 `ApplyDrawTransform`):
|
||||
* 1. `gain` 系数:采样值乘以该系数(默认 1,即不改变);
|
||||
* 2. `absolute` 绝对值:|value|;
|
||||
* 3. 镜像:`mirrorVertical` 按 Y = 0 上下镜像(value → −value),
|
||||
* `mirrorHorizontal` 为**数据反序**(把采样序列头尾对调后绘制,即水平翻转)。
|
||||
* `mirrorHorizontal` 为**数据反序**(把采样序列头尾对调后绘制,即水平翻转);
|
||||
* 4. 偏移:`offsetY` 给绘制值加上一个常数(波形整体上下移动),
|
||||
* `offsetTime` 把采样沿时间轴平移(绘制在 `t + offsetTime` 处)。
|
||||
*
|
||||
* ⚠️ 结算顺序固定为「系数 → 绝对值 → 镜像」(用户明确要求),
|
||||
* 其中镜像的两个方向互相正交:垂直改**值**、水平改**取值下标**。
|
||||
* ⚠️ 结算顺序固定为「系数 → 绝对值 → 镜像 → 偏移」(用户明确要求)。
|
||||
* 其中镜像的两个方向互相正交(垂直改**值**、水平改**取值下标**),偏移同理
|
||||
*(`offsetY` 改值、`offsetTime` 改**绘制时刻**)。
|
||||
* ⚠️ `offsetY` 以**未镜像空间**为准:启用/取消垂直镜像时会同步取反(见
|
||||
* `SignalStyle::offsetY` 的使用处),使绘制等价于「先加上偏移再镜像」。
|
||||
*/
|
||||
float gain = 1.0f; /**< 系数,默认 1.0(不改变)。 */
|
||||
bool absolute = false; /**< 是否对绘制值取绝对值,默认关闭。 */
|
||||
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像(值取反),默认关闭。 */
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序绘制),默认关闭。 */
|
||||
float offsetY = 0.0f; /**< Y 轴偏移(叠加到绘制值上),默认 0。 */
|
||||
double offsetTime = 0.0; /**< 时间轴偏移(秒,绘制在 t + offsetTime 处),默认 0。 */
|
||||
|
||||
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
|
||||
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
|
||||
|
||||
/**
|
||||
* @brief 是否存在任何**偏移**(决定「系数/偏移」按钮的激活态之一)。
|
||||
* @return Y 或时间偏移不为 0 返回 true。
|
||||
*/
|
||||
bool Offset() const
|
||||
{
|
||||
return std::fabs(offsetY) > 1.0e-9f || std::fabs(offsetTime) > 1.0e-15;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 把原始采样值变换为**绘制用值**。
|
||||
*
|
||||
* 顺序固定为「系数 → 绝对值 → 镜像(垂直分量)」,例如系数 2、原始值 1.5 →
|
||||
* 2×1.5 = 3;再取绝对值仍为 3;若开启垂直镜像则为 −3。
|
||||
* NaN 经乘/取绝对值/取反后仍为 NaN(保持断线语义)。
|
||||
* 顺序固定为「系数 → 绝对值 → 垂直镜像 → Y 偏移」,例如系数 2、原始值 1.5 →
|
||||
* 2×1.5 = 3;取绝对值仍为 3;垂直镜像则 −3;Y 偏移 +1 得 −2。
|
||||
* NaN 经乘/取绝对值/取反/加偏移后仍为 NaN(保持断线语义)。
|
||||
*
|
||||
* ⚠️ 水平镜像**不在这里处理**:它是"取值下标反序"(把采样序列头尾对调),
|
||||
* 必须在遍历采样时按 `SourceIndexFor(style, count, index)` 取源下标。
|
||||
* ⚠️ 水平镜像与时间偏移**不在这里处理**:它们分别改变“取值下标”与“绘制时刻”,
|
||||
* 必须在遍历采样时处理(见 `SourceIndexFor` 与 `offsetTime` 的使用处)。
|
||||
*
|
||||
* @param [IN] style 信号外观(提供系数与两个数值开关)。
|
||||
* @param [IN] style 信号外观。
|
||||
* @param [IN] value 原始采样值。
|
||||
* @return 绘制用值。
|
||||
*/
|
||||
@@ -153,6 +169,10 @@ namespace jdi
|
||||
{
|
||||
value = -value;
|
||||
}
|
||||
if (style.offsetY != 0.0f)
|
||||
{
|
||||
value += static_cast<double>(style.offsetY);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -193,9 +213,9 @@ namespace jdi
|
||||
/**
|
||||
* @brief 把**数值区间**按绘制变换映射为变换后的区间(用于整段 Y 轴自适应)。
|
||||
*
|
||||
* 三个变换作用在区间端点上是精确的(系数为线性、绝对值为 |x|、镜像为线性),
|
||||
* 各步作用在区间端点上是精确的(系数为线性、绝对值为 |x|、镜像与偏移为线性),
|
||||
* 因此 `[minValue, maxValue]` 映射后就是真实的数据范围,无需扫描采样点。
|
||||
* ⚠️ 水平镜像不改变值的范围,因此不影响本函数。
|
||||
* ⚠️ 水平镜像与时间偏移都不改变值的范围,因此不影响本函数。
|
||||
*
|
||||
* @param [IN] style 信号外观。
|
||||
* @param [IN,OUT] low 区间下限,返回变换后的下限。
|
||||
@@ -219,7 +239,7 @@ namespace jdi
|
||||
}
|
||||
if (style.absolute)
|
||||
{
|
||||
// |x| 在 [low, high] 上的取值范围:跨 0 时从 0 起,否则从较近的一端起。
|
||||
// |x| 在 [low, high] 上的取值范围:跳 0 时从 0 起,否则从较近的一端起。
|
||||
const double first = std::fabs(low);
|
||||
const double second = std::fabs(high);
|
||||
const bool spansZero = (low <= 0.0) && (high >= 0.0);
|
||||
@@ -232,6 +252,12 @@ namespace jdi
|
||||
low = -high;
|
||||
high = flipped;
|
||||
}
|
||||
if (style.offsetY != 0.0f)
|
||||
{
|
||||
const double offset = static_cast<double>(style.offsetY);
|
||||
low += offset;
|
||||
high += offset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -696,12 +722,14 @@ namespace jdi
|
||||
double& value);
|
||||
|
||||
/**
|
||||
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序")。
|
||||
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序"与时间偏移)。
|
||||
*
|
||||
* 与 `SampleSignalAt` 的唯一区别:水平镜像启用时用 `SourceIndexFor` 把下标映射到源采样
|
||||
*(头尾对调),因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、
|
||||
* 数据点标注都必须用它,否则读数会与看到的波形不一致。
|
||||
* ⚠️ 系数 / 绝对值 / 垂直镜像不在这里处理,由 `ApplyDrawTransform` 负责。
|
||||
* 与 `SampleSignalAt` 的区别:
|
||||
* * 水平镜像时用 `SourceIndexFor` 把下标映射到源采样(头尾对调);
|
||||
* * 时间偏移时先按 `time - style.offsetTime` 求出源采样时刻(因为绘制在 `t + offsetTime`)。
|
||||
* 因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、数据点标注都必须用它,
|
||||
* 否则读数会与看到的波形不一致。
|
||||
* ⚠️ 系数 / 绝对值 / 垂直镜像 / `offsetY` 不在这里处理,由 `ApplyDrawTransform` 负责。
|
||||
*
|
||||
* @param [IN] file 信号所属的已加载数据分组(提供时间轴)。
|
||||
* @param [IN] signal 目标信号。
|
||||
|
||||
@@ -55,6 +55,19 @@ namespace jdi
|
||||
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
|
||||
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
||||
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
||||
/**
|
||||
* EDIT 项**进入编辑时的初始文本**;为空则使用 `value`。
|
||||
*
|
||||
* 用于「非编辑态的显示文本」与「编辑态的数值」不一致的场合,例如时间轴偏移:
|
||||
* 平时显示 `Δt:5s 60ms`,进入编辑后应显示以秒为单位的数值 `5.06`。
|
||||
*/
|
||||
std::wstring editValue;
|
||||
/**
|
||||
* EDIT 项**编辑态**在输入框最右侧提示的单位文本(如 `s`);为空则不提示。
|
||||
*
|
||||
* 非编辑态不显示(那时显示的是 `value` 的完整文案)。
|
||||
*/
|
||||
std::wstring editSuffix;
|
||||
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
||||
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
||||
/**
|
||||
@@ -231,10 +244,14 @@ namespace jdi
|
||||
/**
|
||||
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
|
||||
* @param [IN] id 命令标识。
|
||||
* @param [IN] value 新的值文本。
|
||||
* @param [IN] value 新的值文本(非编辑态显示)。
|
||||
* @param [IN] editValue 新的**编辑初值**(为空时保持原值)。
|
||||
* ⚠️ 非编辑态文本与编辑态文本不一致的行(如时间轴偏移:`Δt:1h 1min 2s` ↔ `3662`)
|
||||
* 必须一并更新,否则下次进入编辑会拿到陈旧的初值。
|
||||
* @return 找到对应项并更新返回 true。
|
||||
*/
|
||||
bool UpdateItemValue(int id, const std::wstring& value);
|
||||
bool UpdateItemValue(int id, const std::wstring& value,
|
||||
const std::wstring& editValue = std::wstring());
|
||||
|
||||
/**
|
||||
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
|
||||
@@ -291,6 +308,17 @@ namespace jdi
|
||||
*/
|
||||
gd::RectF EditFieldRect(int index) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 EDIT 项**编辑态单位后缀**所占的宽度(0 表示无后缀)。
|
||||
*
|
||||
* 排版、命中测试与绘制共用:输入框正文区会向右让出这么多宽度,
|
||||
* 后缀文本绘制在它右侧(靠右对齐)。
|
||||
*
|
||||
* @param [IN] item 菜单项。
|
||||
* @return 后缀宽度(像素)。
|
||||
*/
|
||||
float EditSuffixWidth(const MenuItem& item) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
|
||||
*
|
||||
|
||||
@@ -97,6 +97,30 @@ namespace jdi
|
||||
*/
|
||||
std::wstring FormatCount(unsigned long long value);
|
||||
|
||||
/**
|
||||
* @brief 把时间间隔格式化为「Δt 语义」的复合单位文本(不含 "Δt:" 前缀)。
|
||||
*
|
||||
* 规则(用户明确要求,与双光标差值的显示完全一致):
|
||||
* - 顶级单位高于 s(即 h / min)时**一路整合到 s**,例如 3662s → "1h 1min 2s";
|
||||
* - 顶级单位为 s 及以下时,若不是该单位的整数,则**向下一级递进**,
|
||||
* 例如 5.06s → "5s 60ms";整秒数不加下级单位,例如 5s → "5s";
|
||||
* - 顶级单位为 ps 时不再递进次级单位,例如 5.123ps → "5.123 ps"。
|
||||
*
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 形如 "1h 1min 2s" / "5s 60ms" 的间隔文本。
|
||||
*/
|
||||
std::wstring FormatDurationCompound(double seconds);
|
||||
|
||||
/**
|
||||
* @brief 把时间间隔格式化为「秒」数值文本(不含单位),用于内联编辑。
|
||||
*
|
||||
* 编辑时的量级**始终是秒**:即使显示为 "1h 1min 2s",进入编辑后也是 3602。
|
||||
*
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 形如 "3602" 的文本。
|
||||
*/
|
||||
std::wstring FormatDurationSeconds(double seconds);
|
||||
|
||||
/**
|
||||
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
||||
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
||||
|
||||
@@ -1586,8 +1586,9 @@ namespace jdi
|
||||
bool SampleDisplayedSignalAt(const DataFile& file, const SignalData& signal,
|
||||
const SignalStyle& style, double time, bool interpolate, double& value)
|
||||
{
|
||||
// 未启用水平镜像时与 SampleSignalAt 完全等价(避免两套实现产生细微差异)。
|
||||
if (!style.mirrorHorizontal)
|
||||
// 未启用水平镜像 / 时间偏移时与 SampleSignalAt 完全等价
|
||||
//(避免两套实现产生细微差异)。
|
||||
if (!style.mirrorHorizontal && style.offsetTime == 0.0)
|
||||
{
|
||||
return SampleSignalAt(file, signal, time, interpolate, value);
|
||||
}
|
||||
@@ -1597,7 +1598,9 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (time < file.minTime || time > file.maxTime)
|
||||
// 绘制位置 = 源时刻 + offsetTime,因此查询源数据要用「绘制时刻 − offsetTime」。
|
||||
const double query = time - style.offsetTime;
|
||||
if (query < file.minTime || query > file.maxTime)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1616,7 +1619,7 @@ namespace jdi
|
||||
|
||||
if (!file.timeMonotonic)
|
||||
{
|
||||
const int nearest = FindNearestSample(file, time);
|
||||
const int nearest = FindNearestSample(file, query);
|
||||
if (nearest < 0 || static_cast<std::size_t>(nearest) >= count)
|
||||
{
|
||||
return false;
|
||||
@@ -1626,7 +1629,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
const std::vector<double>::const_iterator lower =
|
||||
std::lower_bound(file.time.begin(), file.time.end(), time);
|
||||
std::lower_bound(file.time.begin(), file.time.end(), query);
|
||||
const std::size_t rightIndex = static_cast<std::size_t>(lower - file.time.begin());
|
||||
if (rightIndex == 0)
|
||||
{
|
||||
@@ -1647,13 +1650,13 @@ namespace jdi
|
||||
|
||||
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
|
||||
{
|
||||
const double leftDistance = std::fabs(time - t0);
|
||||
const double rightDistance = std::fabs(t1 - time);
|
||||
const double leftDistance = std::fabs(query - t0);
|
||||
const double rightDistance = std::fabs(t1 - query);
|
||||
value = (leftDistance <= rightDistance) ? v0 : v1;
|
||||
return std::isfinite(value);
|
||||
}
|
||||
|
||||
const double ratio = (time - t0) / (t1 - t0);
|
||||
const double ratio = (query - t0) / (t1 - t0);
|
||||
value = v0 + (v1 - v0) * ratio;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -215,10 +215,13 @@ namespace jdi
|
||||
|
||||
float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const
|
||||
{
|
||||
const std::wstring& widest =
|
||||
(item.editValue.size() > item.value.size()) ? item.editValue : item.value;
|
||||
const float natural = std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, widest) + 20.0f * m_scale);
|
||||
const float withSuffix = natural + EditSuffixWidth(item);
|
||||
// 无说明文字 = 整行输入框(如「系数调整」面板):给一个更宽的默认值,视觉上独占整行。
|
||||
return item.label.empty() ? std::max(natural, kRowFieldWidth * m_scale) : natural;
|
||||
return item.label.empty() ? std::max(withSuffix, kRowFieldWidth * m_scale) : withSuffix;
|
||||
}
|
||||
|
||||
void PopupMenu::CloseSubmenu()
|
||||
@@ -346,6 +349,15 @@ namespace jdi
|
||||
return -1;
|
||||
}
|
||||
|
||||
float PopupMenu::EditSuffixWidth(const MenuItem& item) const
|
||||
{
|
||||
if (item.kind != MenuItemKind::EDIT || item.editSuffix.empty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return UiMeasureTextWidth(m_fonts->uiSmall, item.editSuffix) + 8.0f * m_scale;
|
||||
}
|
||||
|
||||
gd::RectF PopupMenu::EditFieldRect(int index) const
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||
@@ -420,8 +432,8 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 输入框初值取当前显示值,整段选中,便于直接键入新数值。
|
||||
m_edit.Begin(item.value, true, item.filter, 0u);
|
||||
// 输入框初值取「编辑初值」(未设置时用显示值),整段选中,便于直接键入新数值。
|
||||
m_edit.Begin(item.editValue.empty() ? item.value : item.editValue, true, item.filter, 0u);
|
||||
m_editIndex = index;
|
||||
m_hoverIndex = index;
|
||||
return true;
|
||||
@@ -445,8 +457,10 @@ namespace jdi
|
||||
}
|
||||
|
||||
MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值)。
|
||||
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值);
|
||||
// 键入文本同时记为**编辑初值**(`value` 与 `editValue` 不一致的行也能正确回显上次输入)。
|
||||
item.value = text;
|
||||
item.editValue = text;
|
||||
|
||||
result.valid = true;
|
||||
result.scope = m_scope;
|
||||
@@ -471,17 +485,23 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
|
||||
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value, const std::wstring& editValue)
|
||||
{
|
||||
for (MenuItem& item : m_items)
|
||||
{
|
||||
if (item.id == id && item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
if (item.value == value)
|
||||
const bool valueSame = (item.value == value);
|
||||
const bool editSame = editValue.empty() || (item.editValue == editValue);
|
||||
if (valueSame && editSame)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
item.value = value;
|
||||
if (!editValue.empty())
|
||||
{
|
||||
item.editValue = editValue;
|
||||
}
|
||||
RebuildLayout();
|
||||
return true;
|
||||
}
|
||||
@@ -1212,9 +1232,31 @@ namespace jdi
|
||||
UiText(g, UiEllipsize(item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
||||
|
||||
const float suffix = EditSuffixWidth(item);
|
||||
if (static_cast<int>(index) == m_editIndex)
|
||||
{
|
||||
UiDrawEditField(g, fieldRect, m_edit, m_fonts->uiSmall, m_scale);
|
||||
// 编辑态:输入框底板覆盖整行,正文区向右让出后缀宽度,
|
||||
// 后缀(单位提示)靠右对齐画在它右侧。
|
||||
// ⚠️ 必须用 `UiDrawEditFieldContent`(只画内容)——`UiDrawEditField`
|
||||
// 会把底板/描边按传入矩形绘制,那样带后缀时外框会短一截。
|
||||
const gd::RectF textRect =
|
||||
(suffix > 0.0f)
|
||||
? gd::RectF(fieldRect.X, fieldRect.Y, std::max(12.0f, fieldRect.Width - suffix),
|
||||
fieldRect.Height)
|
||||
: fieldRect;
|
||||
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->accent, 1.6f);
|
||||
UiDrawEditFieldContent(g, textRect, m_edit, m_fonts->uiSmall, m_scale);
|
||||
if (suffix > 0.0f)
|
||||
{
|
||||
const float suffixLeft = textRect.X + textRect.Width;
|
||||
UiText(g, item.editSuffix, m_fonts->uiSmall,
|
||||
gd::RectF(suffixLeft, fieldRect.Y,
|
||||
std::max(4.0f, fieldRect.X + fieldRect.Width - suffixLeft -
|
||||
4.0f * m_scale),
|
||||
fieldRect.Height),
|
||||
m_theme->textMuted, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+133
-2
@@ -24,6 +24,13 @@ namespace jdi
|
||||
{
|
||||
return c >= L'A' && c <= L'Z';
|
||||
}
|
||||
|
||||
/** @brief 时间间隔的显示单位。 */
|
||||
struct DurationUnit
|
||||
{
|
||||
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
|
||||
const wchar_t* name = L"s"; /**< 单位名称。 */
|
||||
};
|
||||
}
|
||||
|
||||
std::wstring Utf8ToWide(const char* text, int length)
|
||||
@@ -339,9 +346,133 @@ namespace jdi
|
||||
return std::wstring(buffer);
|
||||
}
|
||||
|
||||
std::wstring FormatByteSize(unsigned long long bytes)
|
||||
std::wstring FormatDurationCompound(double seconds)
|
||||
{
|
||||
// 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||||
if (!std::isfinite(seconds))
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
const double value = std::fabs(seconds);
|
||||
if (value <= 0.0)
|
||||
{
|
||||
return L"0s";
|
||||
}
|
||||
|
||||
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
|
||||
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
|
||||
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
|
||||
if (value >= 60.0)
|
||||
{
|
||||
const long long totalSeconds = static_cast<long long>(std::llround(value));
|
||||
long long hours = 0;
|
||||
long long minutes = 0;
|
||||
long long secs = totalSeconds;
|
||||
if (totalSeconds >= 3600)
|
||||
{
|
||||
hours = totalSeconds / 3600;
|
||||
secs = totalSeconds % 3600;
|
||||
}
|
||||
if (secs >= 60)
|
||||
{
|
||||
minutes = secs / 60;
|
||||
secs = secs % 60;
|
||||
}
|
||||
|
||||
std::wstring text;
|
||||
const auto append = [&text](long long amount, const wchar_t* unit)
|
||||
{
|
||||
if (amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!text.empty())
|
||||
{
|
||||
text += L" ";
|
||||
}
|
||||
text += std::to_wstring(amount);
|
||||
text += unit;
|
||||
};
|
||||
append(hours, L"h");
|
||||
append(minutes, L"min");
|
||||
append(secs, L"s");
|
||||
if (!text.empty())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
|
||||
if (value >= 1.0)
|
||||
{
|
||||
const double whole = std::floor(value);
|
||||
const double fraction = value - whole;
|
||||
std::wstring text = FormatValue(whole) + L"s";
|
||||
const double remainderMs = fraction * 1000.0;
|
||||
if (remainderMs >= 1.0e-6)
|
||||
{
|
||||
text += L" ";
|
||||
text += FormatValue(remainderMs);
|
||||
text += L"ms";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// ---- 顶级单位低于 s:ms / us / ns / ps ----
|
||||
const DurationUnit subUnits[] = {
|
||||
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
|
||||
};
|
||||
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
|
||||
std::size_t index = subCount - 1;
|
||||
for (std::size_t i = 0; i < subCount; ++i)
|
||||
{
|
||||
if (value >= subUnits[i].scale)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶级单位为 ps:不再递进次级单位(保留小数)。
|
||||
if (index + 1 >= subCount)
|
||||
{
|
||||
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
|
||||
}
|
||||
|
||||
const double scaled = value / subUnits[index].scale;
|
||||
double whole = std::floor(scaled);
|
||||
double fraction = scaled - whole;
|
||||
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
|
||||
if (fraction >= 1.0 - 1.0e-9)
|
||||
{
|
||||
whole += 1.0;
|
||||
fraction = 0.0;
|
||||
}
|
||||
|
||||
std::wstring text = FormatValue(whole);
|
||||
text += subUnits[index].name;
|
||||
// 仍是该单位的整数 → 不显示下级单位。
|
||||
const double remainder = fraction * subUnits[index].scale;
|
||||
const double inNext = remainder / subUnits[index + 1].scale;
|
||||
if (inNext >= 1.0e-6)
|
||||
{
|
||||
text += L" ";
|
||||
text += FormatValue(inNext);
|
||||
text += subUnits[index + 1].name;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::wstring FormatDurationSeconds(double seconds)
|
||||
{
|
||||
if (!std::isfinite(seconds))
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
return FormatValue(std::fabs(seconds));
|
||||
}
|
||||
|
||||
std::wstring FormatByteSize(unsigned long long bytes)
|
||||
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||||
constexpr double kKilo = 1024.0;
|
||||
constexpr double kMega = kKilo * 1024.0;
|
||||
constexpr double kGiga = kMega * 1024.0;
|
||||
|
||||
Reference in new issue
Block a user