跳到主要内容

焦点栈 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);

参数

参数类型说明
profilevoid *指向该作用域配置对象的指针,用作唯一标识。通常传 &Profilethis
scopeIdHXString可读的作用域名称,用于调试和日志。
谁负责 Push/Pop?

HiEasyX 内置的 ModalPopupMenuDropdown 已经自动 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);

参数

参数类型说明
rectHXRect该作用域的“安全区”矩形。消息落在这个矩形内部不会被消费。
detectLeftPressOutsidebool是否只检测左键按下(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();
}

与内置覆盖层的协作

Modal 自动管理
HX::OpenModal(HXStr("confirm"));
if (HX::BeginModal(HXStr("confirm"), mp)) {
// Modal 内部已经 Push/Pop 了焦点栈
// 并且自动消费外部消息
HX::EndModal();
}
PopupMenu 自动管理
HX::PopupMenuProfile menu;
// ...
HX::OpenPopupMenu(menu, {mouseX, mouseY});
HXGInt clicked = HX::PopupMenu(menu);
// PopupMenu 内部自动 Push/Pop 焦点栈

常见错误

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 就会像没有红绿灯的十字路口——乱成一团 🚦