焦点栈 Focus Stack
Modal 弹窗、下拉菜单、右键菜单、蓝图内联编辑器……这些覆盖层如果同时出现,鼠标点一下外面,到底该关哪个?HiEasyX 用焦点栈来回答这个问题:只有栈顶的作用域有权消费“外部点击”,底层的作用域乖乖等着。
核心设计
焦点栈是一个后进先出的栈结构。每打开一个覆盖层,就 Push 一个作用域;关闭时 Pop。栈顶永远是最新的、最上层的覆盖层。
[ Bottom ] Window (普通窗口)
Modal (模态对话框)
PopupMenu (右键菜单)
Dropdown (下拉框)
[ Top ] InlineEditor (蓝图内联编辑)
只有栈顶(InlineEditor)可以调用 ConsumeMessagesOutsideRect。如果它决定不消费,消息会继续留给下面的 Dropdown,依此类推。
API 详解
PushFocusScope
void PushFocusScope(void *profile, const HXString &scopeId);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
profile | void * | 指向该作用域配置对象的指针,用作唯一标识。通常传 &Profile 或 this。 |
scopeId | HXString | 可读的作用域名称,用于调试和日志。 |
HiEasyX 内置的 Modal、PopupMenu、Dropdown 已经自动 Push/Pop。只有你自己写新的覆盖层(如自定义浮动面板、蓝图内联编辑器)时,才需要手动管理。
PopFocusScope
void PopFocusScope();
弹出栈顶作用域。必须与对应的 PushFocusScope 成对出现,否则栈会错乱。
PopFocusScopeUntil
void PopFocusScopeUntil(void *profile);
连续弹出,直到栈顶是指定的 profile。用于强制关闭到某一层。例如关闭 Modal 时,连带关闭它上面挂着的所有 PopupMenu:
HX::PopFocusScopeUntil(&modalProfile);
IsFocusScopeTop
bool IsFocusScopeTop(void *profile);
返回值:当前栈顶是否是该 profile。
如果你想实现“只有顶层菜单才绘制阴影/高亮边框”,可以每帧检查:
if (HX::IsFocusScopeTop(&myMenuProfile)) {
DrawExtraShadow();
}
IsWithinFocusScope
bool IsWithinFocusScope(void *profile);
返回值:该 profile 是否还在栈中(不一定是顶)。用于判断“我是否还在活跃的作用域链里”。
GetTopFocusScopeId
HXString GetTopFocusScopeId();
返回值:栈顶作用域的 scopeId 字符串。用于调试打印。
点击外部检测
ConsumeMessagesOutsideRect
bool ConsumeMessagesOutsideRect(const HXRect &rect, bool detectLeftPressOutside);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
rect | HXRect | 该作用域的“安全区”矩形。消息落在这个矩形内部不会被消费。 |
detectLeftPressOutside | bool | 是否只检测左键按下(true),还是任何鼠标消息(false)。 |
返回值
bool — 如果当前帧有落在 rect 外且未被更上层消费的消息,返回 true。
如果当前作用域不是栈顶,ConsumeMessagesOutsideRect 会直接返回 false,消息原封不动。这保证了“点击关闭最上层菜单”的直觉行为。
完整示例:自定义浮动面板
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
void DemoCustomOverlay() {
static bool open = false;
static HX::PanelProfile overlayProfile;
static HXString overlayId = HXStr("my_overlay");
HX::WindowProfile wp;
wp.Title = HXStr("Focus Stack Demo");
HX::Window(HXStr("main"), wp);
HX::ButtonProfile btn;
if (HX::Button(HXStr("Open Overlay"), btn)) {
open = true;
}
if (open) {
// 推入焦点栈
HX::PushFocusScope(&overlayProfile, overlayId);
// 绘制浮动面板
HX::PanelProfile panel;
panel.Position = {200, 150};
panel.Size = {300, 200};
HX::BeginPanel(HXStr("overlay_panel"), panel);
HX::Text(HXStr("I am a floating overlay!"));
HX::Text(HXStr("Click outside to close."));
HX::EndPanel(panel);
// 只有栈顶才能消费外部点击
if (HX::IsFocusScopeTop(&overlayProfile)) {
HXRect safeRect = {200, 150, 200 + 300, 150 + 200};
bool clickedOutside = HX::ConsumeMessagesOutsideRect(safeRect, true);
if (clickedOutside) {
open = false; // 点击外部,关闭面板
}
}
// 推出焦点栈
HX::PopFocusScope();
}
HX::End();
}
与内置覆盖层的协作
HX::OpenModal(HXStr("confirm"));
if (HX::BeginModal(HXStr("confirm"), mp)) {
// Modal 内部已经 Push/Pop 了焦点栈
// 并且自动消费外部消息
HX::EndModal();
}
HX::PopupMenuProfile menu;
// ...
HX::OpenPopupMenu(menu, {mouseX, mouseY});
HXGInt clicked = HX::PopupMenu(menu);
// PopupMenu 内部自动 Push/Pop 焦点栈
常见错误
if (open) {
HX::PushFocusScope(&p, HXStr("x"));
// ...
if (clickedOutside) open = false;
// ❌ 忘记 PopFocusScope() 就 return 了!
}
务必保证每条代码路径都 Pop。用 RAII 封装是个好习惯:
struct FocusScopeGuard {
FocusScopeGuard(void *p, const HXString &id) { HX::PushFocusScope(p, id); }
~FocusScopeGuard() { HX::PopFocusScope(); }
};
总结
| 需求 | API |
|---|---|
| 打开覆盖层 | PushFocusScope(profile, id) |
| 关闭覆盖层 | PopFocusScope() |
| 检测是否栈顶 | IsFocusScopeTop(profile) |
| 检测点击外部 | ConsumeMessagesOutsideRect(rect, true) |
| 强制关闭到某层 | PopFocusScopeUntil(profile) |
| 调试查看栈顶 | GetTopFocusScopeId() |
焦点栈是覆盖层的交通规则。没有它,Modal、Popup、Dropdown 就会像没有红绿灯的十字路口——乱成一团 🚦