Finalize layout stage 2 fixes and refresh regression scenes

This commit is contained in:
Codex
2026-04-16 11:40:39 +08:00
parent b7ad960518
commit 738cf035bb
20 changed files with 1470 additions and 308 deletions
+219 -64
View File
@@ -1,6 +1,25 @@
#include "TabControl.h"
#include "SxLog.h"
#include "Window.h"
namespace
{
bool SxTabRectsIntersect(const RECT& a, const RECT& b)
{
return a.left < b.right && a.right > b.left &&
a.top < b.bottom && a.bottom > b.top;
}
RECT SxTabUnionRect(const RECT& a, const RECT& b)
{
RECT out{};
out.left = (std::min)(a.left, b.left);
out.top = (std::min)(a.top, b.top);
out.right = (std::max)(a.right, b.right);
out.bottom = (std::max)(a.bottom, b.bottom);
return out;
}
}
inline void TabControl::initTabBar()
{
if (controls.empty())return;
@@ -65,9 +84,9 @@ inline void TabControl::initTabPage()
// TabControl 内部页签页仍然保留专用布局:
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
// 不把这部分细节下放到通用布局解算器里。
//子控件坐标原点
int nX = 0;
int nY = 0;
// 但页面内容子树的世界坐标映射职责,继续归页面 Canvas 自己管理。
// 因此这里不再手工遍历页内子控件做 setX/setY,
// 只安置每个页面 Canvas 的矩形,让 Canvas 通过自身 relayoutManagedChildren() 收口。
switch (this->tabPlacement)
{
case StellarX::TabPlacement::Top:
@@ -78,16 +97,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight);
}
nX = this->x;
nY = this->y + tabBarHeight;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break;
case StellarX::TabPlacement::Bottom:
for (auto& c : controls)
@@ -97,16 +106,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight);
}
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break;
case StellarX::TabPlacement::Left:
for (auto& c : controls)
@@ -116,16 +115,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height);
}
nX = this->x + tabBarHeight;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break;
case StellarX::TabPlacement::Right:
for (auto& c : controls)
@@ -135,16 +124,6 @@ inline void TabControl::initTabPage()
c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height);
}
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break;
default:
break;
@@ -163,12 +142,18 @@ void TabControl::refreshRuntimeLayout()
TabControl::TabControl() :Canvas()
{
this->id = "TabControl";
// TabControl 作为外层容器,当前阶段显式允许双轴 Stretch;
// 内部页签栏和页面区仍由自己的专用布局逻辑管理。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
TabControl::TabControl(int x, int y, int width, int height)
: Canvas(x, y, width, height)
{
this->id = "TabControl";
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
TabControl::~TabControl()
@@ -218,13 +203,13 @@ void TabControl::draw()
Canvas::draw();
for (auto& c : controls)
{
c.first->setDirty(true);
c.first->draw();
c.second->setDirty(true);
c.second->draw();
}
for (auto& c : controls)
{
c.second->setDirty(true);
c.second->draw();
c.first->setDirty(true);
c.first->draw();
}
// 首次绘制时处理默认激活页签
@@ -242,28 +227,90 @@ void TabControl::draw()
bool TabControl::handleEvent(const ExMessage& msg)
{
if (!show)return false;
resetEventVisualChanged();
bool consume = false;
for (auto& c : controls)
if (c.first->handleEvent(msg))
{
consume = true;
break;
}
if (!consume)
bool anyVisualChanged = false;
// TabControl 的同层页签按钮/页面区事件分发顺序,
// 与 Window / Canvas 保持一致:按倒序处理,后加入者视为更靠上层。
if (msg.message == WM_MOUSEMOVE)
{
for (auto& c : controls)
if (c.second->IsVisible())
if (c.second->handleEvent(msg))
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (!consume)
{
if (it->first->handleEvent(msg))
{
consume = true;
break;
}
if (it->first->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->first->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (!it->second->IsVisible())
continue;
if (!consume)
{
if (it->second->handleEvent(msg))
{
consume = true;
}
if (it->second->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->second->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->first->handleEvent(msg))
{
consume = true;
break;
}
if (!consume)
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->second->IsVisible())
if (it->second->handleEvent(msg))
{
consume = true;
break;
}
}
}
if (dirty)
requestRepaint(parent);
markEventVisualChanged(anyVisualChanged || dirty);
return consume;
}
bool TabControl::clearTransientMouseState()
{
bool changed = false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (it->first->IsVisible() && it->first->clearTransientMouseState())
changed = true;
if (it->second->IsVisible() && it->second->clearTransientMouseState())
changed = true;
}
return changed;
}
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
{
controls.push_back(std::move(control));
@@ -399,8 +446,15 @@ void TabControl::setActiveIndex(int idx)
defaultActivation = idx;
else
{
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true)
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
if (idx >= 0 && idx < controls.size())
{
if (getActiveIndex() == idx)
return;
controls[idx].first->setButtonClick(true);
}
}
}
@@ -444,17 +498,118 @@ void TabControl::requestRepaint(Control* parent)
if (this == parent)
{
RECT coverage{};
bool hasCoverage = false;
auto commitTabUnit = [&](Control* unit, bool forceOverlayRedraw)
{
if (!unit || !unit->IsVisible())
return;
const bool directDirty = unit->isDirty();
const bool subtreeDirty = unit->hasManagedDirtySubtree();
if (forceOverlayRedraw)
{
// 下层单元已经写过像素,上层页签/页面作为 overlay 补画时,
// 必须先丢掉旧快照,重新抓取当前背景后再画,否则会把旧背景再贴回来。
unit->invalidateBackgroundSnapshot();
unit->setDirty(true);
unit->draw();
}
else if (directDirty)
{
unit->draw();
}
else if (subtreeDirty)
{
unit->commitManagedRepaint();
}
else
{
return;
}
const RECT rc = unit->getManagedRepaintCoverageRect();
if (!hasCoverage)
{
coverage = rc;
hasCoverage = true;
}
else
{
coverage = SxTabUnionRect(coverage, rc);
}
};
// 局部重绘必须和 draw() 维持同一套顺序:
// 先页面,再页签按钮。
// 否则页签按钮上的 Tooltip 会被后画的页面盖掉,
// 表现为“有页面打开时 Tooltip 看不到,所有页关闭时才正常”。
for (auto& control : controls)
{
if (control.first->isDirty() && control.first->IsVisible())
control.first->draw();
if (control.second->isDirty() && control.second->IsVisible())
control.second->draw();
Control* page = control.second.get();
if (!page->IsVisible())
continue;
if (page->hasManagedDirtySubtree())
{
commitTabUnit(page, false);
}
else if (hasCoverage && SxTabRectsIntersect(page->getManagedRepaintCoverageRect(), coverage))
{
commitTabUnit(page, true);
}
}
for (auto& control : controls)
{
Control* button = control.first.get();
if (!button->IsVisible())
continue;
if (button->hasManagedDirtySubtree())
{
commitTabUnit(button, false);
}
else if (hasCoverage && SxTabRectsIntersect(button->getManagedRepaintCoverageRect(), coverage))
{
commitTabUnit(button, true);
}
}
return;
}
else
onRequestRepaintAsRoot();
onRequestRepaintAsRoot();
}
bool TabControl::hasManagedDirtySubtree() const
{
if (dirty)
return true;
for (const auto& control : controls)
{
if (control.first->IsVisible() && control.first->hasManagedDirtySubtree())
return true;
if (control.second->IsVisible() && control.second->hasManagedDirtySubtree())
return true;
}
return false;
}
RECT TabControl::getManagedRepaintCoverageRect() const
{
// TabControl 的 draw() 会写自身背景、全部页签按钮以及全部页面。
// 因此 coverage 也必须按“页签按钮 + 页面”递归并集,避免内部 Tooltip 等附加绘制区域被漏算。
RECT coverage = getBoundsRect();
for (const auto& control : controls)
{
if (control.first->IsVisible())
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintCoverageRect());
if (control.second->IsVisible())
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintCoverageRect());
}
return coverage;
}
bool TabControl::canCommitManagedPartialRepaint() const