Refactor layout pipeline, add KEY5 regression, and fix tooltip hide

This commit is contained in:
Codex
2026-04-10 23:26:25 +08:00
parent 241a564095
commit b7ad960518
13 changed files with 719 additions and 290 deletions
+232
View File
@@ -2,6 +2,138 @@
#include "SxLog.h"
#include<assert.h>
#include "Window.h"
#include <algorithm>
namespace
{
// 单轴解算后的临时结果:只描述“在父局部坐标系中的位置和尺寸”。
struct LayoutAxisResult
{
int pos = 0;
int size = 0;
};
int ClampNonNegative(int value)
{
return value < 0 ? 0 : value;
}
int RoundDiv(long long numerator, long long denominator)
{
if (denominator == 0)
return 0;
if (numerator >= 0)
return static_cast<int>((numerator + denominator / 2) / denominator);
return -static_cast<int>(((-numerator) + denominator / 2) / denominator);
}
// 旧接口兼容层:
// 将 setAnchor(a1, a2) 的双锚点输入映射为单轴 LayoutSpec。
// 这里只负责“从旧输入翻译成新模型”,不直接参与后续求解。
StellarX::AxisLayoutSpec BuildLegacyAxisSpec(StellarX::Anchor anchor1, StellarX::Anchor anchor2,
StellarX::Anchor startAnchor, StellarX::Anchor endAnchor)
{
StellarX::AxisLayoutSpec spec{};
spec.anchorStart = (anchor1 == startAnchor || anchor2 == startAnchor);
spec.anchorEnd = (anchor1 == endAnchor || anchor2 == endAnchor);
if (spec.anchorStart && spec.anchorEnd)
{
spec.sizePolicy = StellarX::AxisSizePolicy::Stretch;
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
}
else if (spec.anchorEnd)
{
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
spec.alignPolicy = StellarX::AxisAlignPolicy::End;
}
else
{
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
}
return spec;
}
// 单轴统一解算器:
// 输入设计态父尺寸、当前父尺寸、子控件设计态位置/尺寸,以及当前轴策略,
// 输出该轴在“父局部坐标系”下的运行态位置与尺寸。
LayoutAxisResult ResolveAxis(int parentDesignSize, int parentCurrentSize, int childDesignPos, int childDesignSize,
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
{
LayoutAxisResult result{};
const int startMargin = childDesignPos;
const int endMargin = parentDesignSize - (childDesignPos + childDesignSize);
const bool wantsStretch = spec.sizePolicy == StellarX::AxisSizePolicy::Stretch;
const bool canStretch = allowStretch && spec.anchorStart && spec.anchorEnd;
if (wantsStretch && canStretch)
{
// 双边锚定 + Stretch
// 保持设计态中的起边距和终边距,让尺寸随父容器变化。
result.pos = startMargin;
result.size = ClampNonNegative(parentCurrentSize - startMargin - endMargin);
return result;
}
// 其余情况一律按固定尺寸处理;即便调用方请求 Stretch,
// 只要锚点条件或控件能力边界不满足,也会在这里自然降级为 FixedSize。
result.size = ClampNonNegative(childDesignSize);
if (spec.anchorStart && !spec.anchorEnd)
{
// 仅锚定起边:保持起边距,尺寸不变。
result.pos = startMargin;
return result;
}
if (!spec.anchorStart && spec.anchorEnd)
{
// 仅锚定终边:保持终边距,尺寸不变。
result.pos = parentCurrentSize - endMargin - result.size;
return result;
}
if (spec.anchorStart && spec.anchorEnd)
{
// 双边锚定但尺寸固定:
// 位置由 alignPolicy 决定,用于表达“只位移、不拉伸”的场景。
switch (spec.alignPolicy)
{
case StellarX::AxisAlignPolicy::End:
result.pos = parentCurrentSize - endMargin - result.size;
break;
case StellarX::AxisAlignPolicy::Center:
// 保持相对父容器中心的偏移关系。
result.pos = childDesignPos + (parentCurrentSize - parentDesignSize) / 2;
break;
case StellarX::AxisAlignPolicy::Proportional:
{
// 保持设计态中的相对位置比例。
// 注意:这里只调整位置,不改变尺寸。
const int designTravel = parentDesignSize - result.size;
const int currentTravel = parentCurrentSize - result.size;
if (designTravel <= 0 || currentTravel <= 0)
result.pos = startMargin;
else
result.pos = RoundDiv(static_cast<long long>(startMargin) * currentTravel, designTravel);
break;
}
case StellarX::AxisAlignPolicy::Start:
default:
result.pos = startMargin;
break;
}
return result;
}
// 无锚点:退回设计态位置,尺寸保持设计值。
result.pos = childDesignPos;
return result;
}
}
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
{
@@ -86,6 +218,11 @@ void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2)
{
this->anchor_1 = anchor_1;
this->anchor_2 = anchor_2;
// 旧 API 只作为兼容输入层存在:
// 这里把历史上的 anchor_1 / anchor_2 映射为新的水平/垂直轴布局规格,
// 后续统一解算全部以 layoutSpec 为准。
this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right);
this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom);
}
StellarX::Anchor Control::getAnchor_1() const
{
@@ -99,6 +236,101 @@ StellarX::LayoutMode Control::getLayoutMode() const
{
return this->layoutMode;
}
const StellarX::LayoutSpec& Control::getLayoutSpec() const
{
return layoutSpec;
}
const StellarX::LayoutCapability& Control::getLayoutCapability() const
{
return layoutCapability;
}
void Control::setX(int x)
{
this->x = x;
dirty = true;
}
void Control::setY(int y)
{
this->y = y;
dirty = true;
}
void Control::setWidth(int width)
{
this->width = width;
dirty = true;
}
void Control::setHeight(int height)
{
this->height = height;
dirty = true;
}
void Control::commitCurrentGeometryAsDesignRect()
{
// 该接口是“显式提交新的设计基线”的唯一入口之一。
// 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local*
// 否则会导致设计基线漂移,后续解算越来越不稳定。
localx = parent ? (x - parent->getX()) : x;
localy = parent ? (y - parent->getY()) : y;
localWidth = width;
localHeight = height;
}
StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH,
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const
{
StellarX::ResolvedLayoutRect rect{};
if (layoutMode != StellarX::LayoutMode::AnchorToEdges)
{
// 非锚点布局模式:直接使用设计态矩形,再映射到当前世界坐标。
rect.localX = localx;
rect.localY = localy;
rect.width = localWidth;
rect.height = localHeight;
rect.worldX = parentWorldX + rect.localX;
rect.worldY = parentWorldY + rect.localY;
return rect;
}
// 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。
const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth,
layoutSpec.horizontal, layoutCapability.allowStretchX);
const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight,
layoutSpec.vertical, layoutCapability.allowStretchY);
// 第 2 层:把父局部矩形映射到世界坐标。
rect.localX = horizontal.pos;
rect.localY = vertical.pos;
rect.width = horizontal.size;
rect.height = vertical.size;
rect.worldX = parentWorldX + rect.localX;
rect.worldY = parentWorldY + rect.localY;
return rect;
}
void Control::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// 默认实现只应用运行态世界矩形。
// 若某个控件还需要在应用后继续刷新内部布局,可重写此函数。
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
}
void Control::applyRuntimeRectDirect(int worldX, int worldY, int width, int height)
{
// 最底层运行态赋值入口:
// 只修改当前世界坐标和运行态尺寸,不触碰设计基线 local*。
this->x = worldX;
this->y = worldY;
this->width = width;
this->height = height;
dirty = true;
}
// 保存当前的绘图状态(字体、颜色、线型等)
// 在控件绘制前调用,确保不会影响全局绘图状态
void Control::saveStyle()