Skip to content
48 changes: 48 additions & 0 deletions docs/Widget_Performance_Plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Widget 渲染性能改进方案(仅方案,未实施)

> 目标:梳理当前 Widget 体系的绘制链路,找出影响性能的热点,并给出可操作的优化方向。本文仅提供方案,未改动任何代码。

## 现状梳理
- `Panel::draw` 每帧都会清空图层、重绘圆角背景,并在循环中为全部子控件调用 `draw`,即便状态未变化仍整板重绘(`src/Widget.cpp`)。
- `Panel::draw` 每次绘制都会调用 `layout->apply`,布局无变更也重复计算。
- `Button`、`InputBox`、`Slider` 等控件有局部缓存(`needRedraw` 等),但在 `Panel` 上层的全量重绘使缓存收益打折。
- `InputBox::draw` 在聚焦时每帧多次 `measuretext` 计算光标/IME 宽度,即便内容未变化也有重复测量。
- 各控件的绘制区域总是整块 `putimage`,未利用脏矩形或可视区域裁剪。

## 优化方向
1. **脏矩形 / 重绘标记**
- 为 `Widget`/`Panel` 增加脏标记,只有自身或子控件状态变动时才重绘缓存图层;聚合子控件的脏区域,在最终 `putimage` 时只复制变动区域。
- 动画控件(Ripple/进度条等)在动画结束后清理脏标记,避免持续刷新。

2. **分层缓存与静态背景复用**
- 将静态背景(圆角面板底色、按钮底板)与动态前景(文本、动画)分层缓存:静态层仅在尺寸/颜色/圆角/缩放变化时重绘,动态层按需叠加。
- 对 `Panel` 支持“冻结”模式:内容静态时直接复用上次合成结果,不再遍历子控件。
- 依据现有 `Button` 的懒标记思路,为 `Panel` 增加懒标记并复用已遮罩好的缓存层,复用时优先使用 `putimage_withalpha`(直接混合已有 alpha,不再逐像素做遮罩过滤,开销低于 `putimage_alphafilter`)。
- 仅在尺寸 / 圆角 / 缩放变化导致遮罩失效时,再重新生成遮罩并走 `putimage_alphafilter`。

3. **布局与尺寸缓存**
- 为 `Layout` 维护 `layoutDirty` 标记,在子控件增删、尺寸/缩放变化时才重算 offsets;`Panel::draw` 不再每帧调用 `apply`。
- 当 `Panel` 缩放时批量更新子控件 offset 后缓存结果,避免每帧重复 `setPosition`/`setScale`。

4. **文本测量与字体缓存**
- `InputBox` / `Text`:将 `measuretext` 结果按 `(content, cursorPos, scale, font)` 组合缓存,仅在对应字段变化时重新测量;IME 组合字符串也走相同缓存。
- 预创建字体对象(LOGFONT)并按 `scale` 分级缓存,减少 `ege_setfont` 调用。

5. **可见性与裁剪**
- 为控件增加可见性/裁剪检查,超出父面板或窗口区域时跳过绘制或仅绘制交集区域,降低 `putimage` 面积。
- 对隐藏或折叠(如 Sidebar 收起)状态,直接跳过子树绘制与事件命中。

6. **动画驱动与刷新节流**
- 将 Ripple / 过渡动画驱动放入统一的 ticker,只有动画存活时触发脏标记;无动画时不进入重绘分支。
- 鼠标悬停/光标闪烁使用计时器节流,降低高频 `draw` 调用。

7. **资源与内存管理**
- 为 `newimage` 创建的缓存层加入池化或生命周期复用,避免频繁分配释放(特别是 `setScale` 后的重建)。
- 对大尺寸面板支持惰性分块缓存(tiles),局部脏区域只更新对应 tile。

## 验证与落地建议
- 增加简单的绘制耗时采样(宏封装 `std::chrono` 或平台计时)输出到调试日志,先定位重绘热点,再按优先级落地。
- 先从「脏标记 + 静态背景缓存」与「布局缓存」入手,对现有接口侵入小、收益高;其余方案可分阶段逐步实现。
- 优先在典型场景(含多 Panel/大量文本输入/频繁动画)做 A/B 对比,验证 FPS 与 CPU 占用变化。

以上方案可单独或组合实施,可按优先级逐步引入,确保每次改动可测、可回退。
10 changes: 8 additions & 2 deletions include/Widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class Panel : public Widget {
* @param offsetY 相对于面板中心的 y 偏移
*/
void addChild(Widget* child, double offsetX, double offsetY);
void markDirty() { needRedraw = true; activeRedrawFrames = kDefaultActiveFrames; }

/**
* @brief 绘制 Panel 到指定图层
Expand Down Expand Up @@ -189,7 +190,7 @@ class Panel : public Widget {
* @brief 设置布局管理器
* @param l 布局对象
*/
void setLayout(std::shared_ptr<Layout> l) { layout = std::move(l); }
void setLayout(std::shared_ptr<Layout> l) { layout = std::move(l); markDirty(); }

/**
* @brief 获取布局管理器
Expand All @@ -204,7 +205,12 @@ class Panel : public Widget {
double alpha = 255;
PIMAGE layer = nullptr;
PIMAGE maskLayer = nullptr;
PIMAGE cachedLayer = nullptr;
bool scaleChanged = true;
bool needRedraw = true;
int activeRedrawFrames = 0;
// Keep redraws alive for ~80 frames after interactions (covers ripple/transition ~1s @60fps)
static constexpr int kDefaultActiveFrames = 80;

std::vector<Widget*> children;
std::vector<Position> childOffsets; ///< 每个子控件的相对偏移(以面板中心为参考)
Expand Down Expand Up @@ -1643,4 +1649,4 @@ extern std::map<std::wstring,Widget*> IdToWidget; ///< ID到控件的映射(Wi
*/
Widget* getWidgetById(const std::wstring& identifier);

void assignOrder(std::vector<Widget*> widgetWithOrder);
void assignOrder(std::vector<Widget*> widgetWithOrder);
87 changes: 59 additions & 28 deletions src/Widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ Panel::Panel(double cx, double cy, double w, double h, double r, color_t bg) {
origin_height = height = h;
origin_radius = radius = r;
layer = newimage(w,h);
cachedLayer = newimage(w,h);
maskLayer = newimage(w,h);
ege_enable_aa(true,layer);
ege_enable_aa(true,maskLayer);
ege_enable_aa(true,cachedLayer);

// 遮罩使用不透明颜色:黑色背景(隐藏)和白色填充(显示)
// 这在PRGB32模式下更可靠,因为它依赖RGB值而非alpha值进行遮罩
Expand All @@ -58,48 +60,61 @@ void Panel::addChild(Widget* child, double offsetX, double offsetY) {
children.push_back(child);
childOffsets.push_back(Position{ offsetX, offsetY });
child->is_global = false;
markDirty();
}

void Panel::draw() {
draw(nullptr,cx,cy);
}

void Panel::draw(PIMAGE dst, double x, double y) {
if (layout) layout->apply(*this); // 自动计算子控件位置
// activeRedrawFrames keeps a short redraw window after interactions/animations
bool shouldRedraw = needRedraw || scaleChanged || PanelScaleChanged || activeRedrawFrames > 0;
if (layout && shouldRedraw) layout->apply(*this); // 自动计算子控件位置

double left = x - width / 2;
double top = y - height / 2;

// 总是清空并重绘(子控件可能有动态内容)
// 注意:子控件(如Button, InputBox)内部有自己的缓存机制来避免不必要的工作
// 使用真正的透明色(PRGB32模式下alpha=0时RGB也应为0)
setbkcolor_f(EGEARGB(0, 0, 0, 0), layer);
cleardevice(layer);

// 绘制自身背景(圆角矩形)
setfillcolor(bgColor, layer);
ege_fillroundrect(0, 0, width, height, radius, radius, radius, radius, layer);

// 绘制子控件
if(scaleChanged) PanelScaleChanged = true;
for (size_t i = 0; i < children.size(); ++i) {
double childX = width / 2 + childOffsets[i].x * scale;
double childY = height / 2 + childOffsets[i].y * scale;
absolutPosDeltaX = left;
absolutPosDeltaY = top;
children[i]->setPosition(cx + childOffsets[i].x * scale,cy + childOffsets[i].y * scale);
children[i]->draw(layer, childX, childY);
absolutPosDeltaX = 0;
absolutPosDeltaY = 0;
if (shouldRedraw) {
// 总是清空并重绘(子控件可能有动态内容)
// 注意:子控件(如Button, InputBox)内部有自己的缓存机制来避免不必要的工作
// 使用真正的透明色(PRGB32模式下alpha=0时RGB也应为0)
setbkcolor_f(EGEARGB(0, 0, 0, 0), layer);
cleardevice(layer);

// 绘制自身背景(圆角矩形)
setfillcolor(bgColor, layer);
ege_fillroundrect(0, 0, width, height, radius, radius, radius, radius, layer);

// 绘制子控件
if(scaleChanged) PanelScaleChanged = true;
for (size_t i = 0; i < children.size(); ++i) {
double childX = width / 2 + childOffsets[i].x * scale;
double childY = height / 2 + childOffsets[i].y * scale;
absolutPosDeltaX = left;
absolutPosDeltaY = top;
children[i]->setPosition(cx + childOffsets[i].x * scale,cy + childOffsets[i].y * scale);
children[i]->draw(layer, childX, childY);
absolutPosDeltaX = 0;
absolutPosDeltaY = 0;
}
PanelScaleChanged = false;
scaleChanged = false;
if (activeRedrawFrames > 0) activeRedrawFrames--;
needRedraw = activeRedrawFrames > 0;

// 生成一次遮罩后的缓存层,后续直接用 putimage_withalpha
setbkcolor_f(EGEARGB(0, 0, 0, 0), cachedLayer);
cleardevice(cachedLayer);
putimage_alphafilter(cachedLayer, layer, 0, 0, maskLayer, 0, 0, -1, -1);
}
PanelScaleChanged = false;
scaleChanged = false;
// 粘贴到主窗口
putimage_alphafilter(dst, layer, left, top, maskLayer, 0, 0, -1, -1);
// 粘贴到主窗口,优先使用 withalpha(相对 alphafilter 开销更低)
putimage_withalpha(dst, cachedLayer, left, top);
}

Panel::~Panel(){
if (layer) delimage(layer);
if (cachedLayer) delimage(cachedLayer);
if (maskLayer) delimage(maskLayer);
}

Expand All @@ -115,6 +130,7 @@ Position Panel::getPosition(){
void Panel::setScale(double s){
if(sgn(s - scale) == 0) return;
scaleChanged = true;
markDirty();
width = origin_width * s;
height = origin_height * s;
radius = origin_radius * s;
Expand All @@ -128,8 +144,11 @@ void Panel::setScale(double s){
maskLayer = newimage(width,height);
if(layer) delimage(layer);
layer = newimage(width,height);
if(cachedLayer) delimage(cachedLayer);
cachedLayer = newimage(width,height);
ege_enable_aa(true,layer);
ege_enable_aa(true,maskLayer);
ege_enable_aa(true,cachedLayer);

// 遮罩使用不透明颜色:黑色背景(隐藏)和白色填充(显示)
setbkcolor_f(EGEARGB(255, 0, 0, 0), maskLayer);
Expand Down Expand Up @@ -179,16 +198,21 @@ bool Panel::handleEvent(const mouse_msg& msg){
}
for(Widget* w : children){
bool state = w->handleEvent(msg);
if(state) return true;
if(state) {
markDirty();
return true;
}
}
if(msg.is_left() && msg.is_down()){
mouseOwningFlag = this;
markDirty();
return true;
}
else if(msg.is_left() && msg.is_up()){
if(mouseOwningFlag == this){
mouseOwningFlag = nullptr;
}
markDirty();
return false;
}
return false;
Expand All @@ -197,12 +221,16 @@ bool Panel::handleEvent(const mouse_msg& msg){
void Panel::setSize(double w,double h){
origin_width = width = w;
origin_height = height = h;
markDirty();
if(layer) delimage(layer);
if(maskLayer) delimage(maskLayer);
if(cachedLayer) delimage(cachedLayer);
layer = newimage(width,height);
maskLayer = newimage(width,height);
cachedLayer = newimage(width,height);
ege_enable_aa(true,layer);
ege_enable_aa(true,maskLayer);
ege_enable_aa(true,cachedLayer);

// 遮罩使用不透明颜色:黑色背景(隐藏)和白色填充(显示)
setbkcolor_f(EGEARGB(255, 0, 0, 0), maskLayer);
Expand All @@ -214,10 +242,12 @@ void Panel::setSize(double w,double h){
void Panel::clearChildren(){
children.clear();
childOffsets.clear();
markDirty();
}

void Panel::setAlpha(double a) {
alpha = clamp(a, 0, 255);
markDirty();
// 遮罩使用不透明颜色:黑色背景(隐藏)和白色填充(显示)
setbkcolor_f(EGEARGB(255, 0, 0, 0), maskLayer);
cleardevice(maskLayer);
Expand All @@ -232,6 +262,7 @@ std::vector<Widget*>& Panel::getChildren() {
void Panel::setChildrenOffset(int index,Position pos){
if (index >= 0 && index < static_cast<int>(childOffsets.size())) {
childOffsets[index] = pos;
markDirty();
}
}

Expand Down Expand Up @@ -2978,4 +3009,4 @@ Widget* getWidgetById(const std::wstring& identifier){

void assignOrder(std::vector<Widget*> widgetWithOrder){
swap(widgetWithOrder,widgets);
}
}