Build XCEditor menu and status shell widgets

This commit is contained in:
2026-04-07 03:51:26 +08:00
parent 5f9f3386ab
commit 8eeb7af56e
25 changed files with 3708 additions and 106 deletions

View File

@@ -6,6 +6,8 @@
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorMenuSession.h>
#include <XCEditor/Core/UIEditorShortcutManager.h>
#include <XCEditor/Widgets/UIEditorMenuBar.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -77,6 +79,20 @@ using XCEngine::UI::UIShortcutScope;
using XCEngine::UI::Widgets::ResolvePopupPlacementRect;
using XCEngine::UI::Widgets::UIPopupOverlayEntry;
using XCEngine::UI::Widgets::UIPopupPlacement;
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuBarBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuBarForeground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopupBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopupForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorMenuBarLayout;
using XCEngine::UI::Editor::Widgets::BuildUIEditorMenuPopupLayout;
using XCEngine::UI::Editor::Widgets::MeasureUIEditorMenuPopupHeight;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorMenuPopupDesiredWidth;
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem;
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarState;
using UIEditorMenuPopupWidgetItem = XCEngine::UI::Editor::Widgets::UIEditorMenuPopupItem;
using UIEditorMenuPopupWidgetState = XCEngine::UI::Editor::Widgets::UIEditorMenuPopupState;
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
@@ -159,8 +175,11 @@ std::string JoinClosedPopupIds(const UIEditorMenuSessionMutationResult& result);
const UIEditorResolvedMenuDescriptor* FindResolvedMenu(const UIEditorResolvedMenuModel& model, std::string_view menuId);
const UIEditorResolvedMenuItem* FindResolvedMenuItemRecursive(const std::vector<UIEditorResolvedMenuItem>& items, std::string_view itemId);
const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(const UIEditorResolvedMenuModel& model, const UIEditorMenuPopupState& popupState);
std::vector<UIEditorMenuBarItem> BuildMenuBarWidgetItems(const UIEditorResolvedMenuModel& model);
std::vector<UIEditorMenuPopupWidgetItem> BuildMenuPopupWidgetItems(const std::vector<UIEditorResolvedMenuItem>& items);
std::size_t FindMenuBarWidgetIndex(const std::vector<UIEditorMenuBarItem>& items, std::string_view menuId);
std::size_t FindMenuPopupWidgetIndex(const std::vector<UIEditorMenuPopupWidgetItem>& items, std::string_view itemId);
std::uint64_t HashText(std::string_view text);
float MeasureMenuPopupHeight(const std::vector<UIEditorResolvedMenuItem>& items);
class ScenarioApp {
public:
@@ -602,13 +621,61 @@ const std::vector<UIEditorResolvedMenuItem>* ResolvePopupItems(
return &item->children;
}
float MeasureMenuPopupHeight(const std::vector<UIEditorResolvedMenuItem>& items) {
float contentHeight = 10.0f;
std::vector<UIEditorMenuBarItem> BuildMenuBarWidgetItems(
const UIEditorResolvedMenuModel& model) {
std::vector<UIEditorMenuBarItem> items = {};
items.reserve(model.menus.size());
for (const UIEditorResolvedMenuDescriptor& menu : model.menus) {
UIEditorMenuBarItem item = {};
item.menuId = menu.menuId;
item.label = menu.label;
item.enabled = true;
items.push_back(std::move(item));
}
return items;
}
std::vector<UIEditorMenuPopupWidgetItem> BuildMenuPopupWidgetItems(
const std::vector<UIEditorResolvedMenuItem>& items) {
std::vector<UIEditorMenuPopupWidgetItem> popupItems = {};
popupItems.reserve(items.size());
for (const UIEditorResolvedMenuItem& item : items) {
contentHeight += item.kind == UIEditorMenuItemKind::Separator ? 12.0f : 34.0f;
UIEditorMenuPopupWidgetItem popupItem = {};
popupItem.itemId = item.itemId;
popupItem.kind = item.kind;
popupItem.label = item.label;
popupItem.shortcutText = item.shortcutText;
popupItem.enabled = item.enabled;
popupItem.checked = item.checked;
popupItem.hasSubmenu =
item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
popupItems.push_back(std::move(popupItem));
}
return popupItems;
}
std::size_t FindMenuBarWidgetIndex(
const std::vector<UIEditorMenuBarItem>& items,
std::string_view menuId) {
for (std::size_t index = 0; index < items.size(); ++index) {
if (items[index].menuId == menuId) {
return index;
}
}
return contentHeight + 8.0f;
return UIEditorMenuBarInvalidIndex;
}
std::size_t FindMenuPopupWidgetIndex(
const std::vector<UIEditorMenuPopupWidgetItem>& items,
std::string_view itemId) {
for (std::size_t index = 0; index < items.size(); ++index) {
if (items[index].itemId == itemId) {
return index;
}
}
return UIEditorMenuPopupInvalidIndex;
}
int ScenarioApp::Run(HINSTANCE hInstance, int nCmdShow) {
@@ -867,7 +934,8 @@ const MenuPopupLayout* ScenarioApp::HitTestMenuPopup(float x, float y) const {
const MenuItemLayout* ScenarioApp::HitTestMenuItem(float x, float y) const {
for (auto it = m_menuItems.rbegin(); it != m_menuItems.rend(); ++it) {
if (ContainsPoint(it->rect, x, y)) {
if (it->kind != UIEditorMenuItemKind::Separator &&
ContainsPoint(it->rect, x, y)) {
return &(*it);
}
}
@@ -1234,31 +1302,25 @@ void ScenarioApp::DrawMenuBar(
UIDrawList& drawList,
const UIRect& rect,
const UIEditorResolvedMenuModel& resolvedModel) {
drawList.AddFilledRect(rect, kMenuBarBg, 8.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 8.0f);
const auto barItems = BuildMenuBarWidgetItems(resolvedModel);
UIEditorMenuBarState barState = {};
barState.openIndex = FindMenuBarWidgetIndex(barItems, m_menuSession.GetOpenRootMenuId());
barState.hoveredIndex = FindMenuBarWidgetIndex(barItems, m_hoveredMenuId);
barState.focused = m_menuSession.HasOpenMenu();
float buttonX = rect.x + 12.0f;
for (const UIEditorResolvedMenuDescriptor& menu : resolvedModel.menus) {
const bool open = m_menuSession.IsMenuOpen(menu.menuId);
const bool hovered = m_hoveredMenuId == menu.menuId;
const float buttonWidth = 104.0f;
const UIRect buttonRect(buttonX, rect.y + 6.0f, buttonWidth, rect.height - 12.0f);
drawList.AddFilledRect(
buttonRect,
open ? kMenuButtonOpen : (hovered ? kMenuButtonHover : kMenuButtonBg),
6.0f);
drawList.AddRectOutline(buttonRect, kCardBorder, 1.0f, 6.0f);
drawList.AddText(
UIPoint(buttonRect.x + 14.0f, buttonRect.y + 10.0f),
menu.label,
kTextPrimary,
14.0f);
const auto barLayout = BuildUIEditorMenuBarLayout(rect, barItems);
AppendUIEditorMenuBarBackground(drawList, barLayout, barItems, barState);
AppendUIEditorMenuBarForeground(drawList, barLayout, barItems, barState);
for (std::size_t index = 0; index < barItems.size() && index < barLayout.buttonRects.size(); ++index) {
m_menuButtons.push_back(
{ menu.menuId, menu.label, BuildRootPopupId(menu.menuId), buttonRect, BuildMenuButtonPath(menu.menuId) });
buttonX += buttonWidth + 10.0f;
{
barItems[index].menuId,
barItems[index].label,
BuildRootPopupId(barItems[index].menuId),
barLayout.buttonRects[index],
BuildMenuButtonPath(barItems[index].menuId)
});
}
}
@@ -1269,75 +1331,45 @@ void ScenarioApp::DrawPopup(
const std::vector<UIEditorResolvedMenuItem>& items,
const UIPopupOverlayEntry& popupEntry,
const UIRect& viewportRect) {
const auto popupItems = BuildMenuPopupWidgetItems(items);
const float popupWidth = (std::max)(
kMenuPopupWidth,
ResolveUIEditorMenuPopupDesiredWidth(popupItems));
const auto placementResult =
ResolvePopupPlacementRect(
popupEntry.anchorRect,
XCEngine::UI::UISize(kMenuPopupWidth, MeasureMenuPopupHeight(items)),
XCEngine::UI::UISize(popupWidth, MeasureUIEditorMenuPopupHeight(popupItems)),
viewportRect,
popupEntry.placement);
const UIRect popupRect = placementResult.rect;
drawList.AddFilledRect(popupRect, kMenuDropBg, 8.0f);
drawList.AddRectOutline(popupRect, kCardBorder, 1.0f, 8.0f);
UIEditorMenuPopupWidgetState popupState = {};
popupState.hoveredIndex = FindMenuPopupWidgetIndex(popupItems, m_hoveredItemId);
popupState.focused = true;
for (std::size_t index = 0; index < popupItems.size(); ++index) {
if (!popupItems[index].hasSubmenu) {
continue;
}
if (m_menuSession.IsPopupOpen(BuildSubmenuPopupId(popupItems[index].itemId))) {
popupState.submenuOpenIndex = index;
break;
}
}
const auto popupLayout = BuildUIEditorMenuPopupLayout(popupRect, popupItems);
AppendUIEditorMenuPopupBackground(drawList, popupLayout, popupItems, popupState);
AppendUIEditorMenuPopupForeground(drawList, popupLayout, popupItems, popupState);
m_menuPopups.push_back(
{ std::string(popupId), std::string(menuId), popupEntry.parentPopupId, popupRect, popupEntry.surfacePath });
float itemY = popupRect.y + 8.0f;
for (const UIEditorResolvedMenuItem& item : items) {
if (item.kind == UIEditorMenuItemKind::Separator) {
drawList.AddFilledRect(
UIRect(popupRect.x + 12.0f, itemY + 4.0f, popupRect.width - 24.0f, 1.0f),
kMenuDivider);
itemY += 12.0f;
continue;
}
for (std::size_t index = 0; index < items.size() && index < popupItems.size() && index < popupLayout.itemRects.size(); ++index) {
const UIEditorResolvedMenuItem& item = items[index];
const bool hasSubmenu =
item.kind == UIEditorMenuItemKind::Submenu && !item.children.empty();
const std::string childPopupId =
hasSubmenu ? BuildSubmenuPopupId(item.itemId) : std::string();
const bool submenuOpen =
hasSubmenu && m_menuSession.IsPopupOpen(childPopupId);
const bool hovered = m_hoveredItemId == item.itemId;
const UIRect itemRect(popupRect.x + 8.0f, itemY, popupRect.width - 16.0f, 30.0f);
if (hovered || submenuOpen) {
drawList.AddFilledRect(itemRect, kMenuItemHover, 6.0f);
}
drawList.AddRectOutline(
UIRect(itemRect.x + 10.0f, itemRect.y + 8.0f, 10.0f, 10.0f),
item.checked ? kAccent : kMenuDivider,
item.checked ? 2.0f : 1.0f,
3.0f);
if (item.checked) {
drawList.AddFilledRect(
UIRect(itemRect.x + 12.0f, itemRect.y + 10.0f, 6.0f, 6.0f),
kAccent,
2.0f);
}
drawList.AddText(
UIPoint(itemRect.x + 30.0f, itemRect.y + 7.0f),
item.label,
item.enabled ? kTextPrimary : kTextDisabled,
13.0f);
if (!item.shortcutText.empty()) {
drawList.AddText(
UIPoint(itemRect.x + itemRect.width - 92.0f, itemRect.y + 7.0f),
item.shortcutText,
item.enabled ? kTextMuted : kTextDisabled,
12.0f);
}
if (hasSubmenu) {
drawList.AddText(
UIPoint(itemRect.x + itemRect.width - 24.0f, itemRect.y + 7.0f),
">",
kTextMuted,
13.0f);
}
m_menuItems.push_back(
{
std::string(popupId),
@@ -1348,13 +1380,12 @@ void ScenarioApp::DrawPopup(
item.commandId,
item.shortcutText,
childPopupId,
itemRect,
popupLayout.itemRects[index],
BuildMenuItemPath(popupId, item.itemId),
item.enabled,
item.checked,
hasSubmenu
});
itemY += 34.0f;
}
}
@@ -1410,7 +1441,7 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
shellRect.y,
width - shellRect.width - margin * 2.0f - 16.0f,
height - 312.0f);
const UIRect footerRect(margin, height - 100.0f, width - margin * 2.0f, 80.0f);
const UIRect footerRect(margin, height - 124.0f, width - margin * 2.0f, 104.0f);
DrawCard(
drawList,
@@ -1461,8 +1492,8 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 482.0f), "menu model validation", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 502.0f), menuValidation.IsValid() ? "OK" : menuValidation.message, menuValidation.IsValid() ? kSuccess : kDanger, 12.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 28.0f), "Last interaction: " + m_lastActionName + " | Result: " + m_lastStatusLabel, m_lastStatusColor, 13.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 48.0f), m_lastMessage, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 56.0f), "Last interaction: " + m_lastActionName + " | Result: " + m_lastStatusLabel, m_lastStatusColor, 13.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 74.0f), m_lastMessage, kTextPrimary, 12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
@@ -1470,7 +1501,7 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/integration/shell/menu_bar_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 66.0f), captureSummary, kTextMuted, 12.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 92.0f), captureSummary, kTextMuted, 12.0f);
DrawOpenPopups(drawList, resolvedModel, viewportRect);
}