editor: apply pending updates

This commit is contained in:
2026-04-29 18:49:07 +08:00
parent fd6dc4ee4f
commit 626fe236bc
13 changed files with 844 additions and 872 deletions

View File

@@ -6,6 +6,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_menu_session.cpp
test_ui_editor_menu_bar.cpp
test_ui_editor_menu_popup.cpp
test_ui_editor_menu_popup_interaction.cpp
test_ui_editor_panel_content_host.cpp
test_ui_editor_panel_host_lifecycle.cpp
test_ui_editor_panel_input_filter.cpp

View File

@@ -0,0 +1,182 @@
#include <gtest/gtest.h>
#include <XCEditor/Menu/UIEditorMenuPopupInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::ResetUIEditorMenuPopupInteractionState;
using XCEngine::UI::Editor::UIEditorMenuItemKind;
using XCEngine::UI::Editor::UIEditorMenuPopupInteractionRequest;
using XCEngine::UI::Editor::UIEditorMenuPopupInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorMenuPopupInteraction;
using XCEngine::UI::Editor::Widgets::BuildUIEditorMenuPopupLayout;
using XCEngine::UI::Editor::Widgets::MeasureUIEditorMenuPopupHeight;
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupItem;
UIEditorMenuPopupItem MakeCommandItem(
std::string itemId,
std::string label,
bool enabled = true) {
UIEditorMenuPopupItem item = {};
item.itemId = std::move(itemId);
item.kind = UIEditorMenuItemKind::Command;
item.label = std::move(label);
item.enabled = enabled;
return item;
}
UIEditorMenuPopupItem MakeSeparatorItem(std::string itemId) {
UIEditorMenuPopupItem item = {};
item.itemId = std::move(itemId);
item.kind = UIEditorMenuItemKind::Separator;
item.enabled = false;
return item;
}
std::vector<UIEditorMenuPopupItem> BuildPopupItems() {
return {
MakeCommandItem("open", "Open"),
MakeSeparatorItem("separator"),
MakeCommandItem("duplicate", "Duplicate", false),
MakeCommandItem("delete", "Delete")
};
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
UIPointerButton button = UIPointerButton::Left) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.pointerButton = button;
return event;
}
UIInputEvent MakeKeyDown(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
UIInputEvent MakeFocusEvent(UIInputEventType type) {
UIInputEvent event = {};
event.type = type;
return event;
}
UIPoint RectCenter(const UIRect& rect) {
return UIPoint(
rect.x + rect.width * 0.5f,
rect.y + rect.height * 0.5f);
}
} // namespace
TEST(UIEditorMenuPopupInteractionTest, PointerHoverAndClickActivatesEnabledItem) {
const std::vector<UIEditorMenuPopupItem> items = BuildPopupItems();
const UIRect popupRect(
20.0f,
10.0f,
180.0f,
MeasureUIEditorMenuPopupHeight(items));
const auto layout = BuildUIEditorMenuPopupLayout(popupRect, items);
UIEditorMenuPopupInteractionState state = {};
const auto frame = UpdateUIEditorMenuPopupInteraction(
state,
UIEditorMenuPopupInteractionRequest{
.layout = layout,
.items = items,
.preferredHighlightedItemId = {},
.closeOnFocusLost = true,
},
{
MakePointerEvent(
UIInputEventType::PointerMove,
RectCenter(layout.itemRects[0])),
MakePointerEvent(
UIInputEventType::PointerButtonDown,
RectCenter(layout.itemRects[0])),
MakePointerEvent(
UIInputEventType::PointerButtonUp,
RectCenter(layout.itemRects[0]))
});
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.itemActivated);
EXPECT_TRUE(frame.result.closeRequested);
EXPECT_EQ(frame.result.activatedIndex, 0u);
EXPECT_EQ(frame.result.activatedItemId, "open");
EXPECT_EQ(frame.popupState.hoveredIndex, 0u);
}
TEST(UIEditorMenuPopupInteractionTest, KeyboardNavigationSkipsNonInteractiveItems) {
const std::vector<UIEditorMenuPopupItem> items = BuildPopupItems();
const UIRect popupRect(
20.0f,
10.0f,
180.0f,
MeasureUIEditorMenuPopupHeight(items));
const auto layout = BuildUIEditorMenuPopupLayout(popupRect, items);
UIEditorMenuPopupInteractionState state = {};
state.focused = true;
const auto frame = UpdateUIEditorMenuPopupInteraction(
state,
UIEditorMenuPopupInteractionRequest{
.layout = layout,
.items = items,
.preferredHighlightedItemId = {},
.closeOnFocusLost = true,
},
{
MakeKeyDown(KeyCode::Down),
MakeKeyDown(KeyCode::Enter)
});
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.itemActivated);
EXPECT_EQ(frame.result.activatedItemId, "delete");
EXPECT_EQ(frame.popupState.hoveredIndex, 3u);
}
TEST(UIEditorMenuPopupInteractionTest, FocusLostRequestsCloseAndClearsFocus) {
const std::vector<UIEditorMenuPopupItem> items = BuildPopupItems();
const UIRect popupRect(
20.0f,
10.0f,
180.0f,
MeasureUIEditorMenuPopupHeight(items));
const auto layout = BuildUIEditorMenuPopupLayout(popupRect, items);
UIEditorMenuPopupInteractionState state = {};
state.focused = true;
const auto frame = UpdateUIEditorMenuPopupInteraction(
state,
UIEditorMenuPopupInteractionRequest{
.layout = layout,
.items = items,
.preferredHighlightedItemId = {},
.closeOnFocusLost = true,
},
{
MakeFocusEvent(UIInputEventType::FocusLost)
});
EXPECT_TRUE(frame.result.closeRequested);
EXPECT_FALSE(frame.popupState.focused);
ResetUIEditorMenuPopupInteractionState(state);
EXPECT_FALSE(state.focused);
EXPECT_TRUE(state.highlightedItemId.empty());
}

View File

@@ -46,6 +46,20 @@ UIInputEvent MakePointerLeave() {
return event;
}
UIInputEvent MakePointerLeave(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerLeave;
event.position = UIPoint(x, y);
return event;
}
UIInputEvent MakePointerEnter(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerEnter;
event.position = UIPoint(x, y);
return event;
}
} // namespace
TEST(UIEditorPanelInputFilterTests, FiltersPointerByBoundsAndPreservesKeyboardAndFocusChannels) {
@@ -57,7 +71,7 @@ TEST(UIEditorPanelInputFilterTests, FiltersPointerByBoundsAndPreservesKeyboardAn
MakePointerButtonDown(140.0f, 20.0f),
MakeKeyDown(),
MakeFocusLost(),
MakePointerLeave()
MakePointerLeave(140.0f, 20.0f)
},
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = true,
@@ -121,6 +135,32 @@ TEST(UIEditorPanelInputFilterTests, ExtraOverlayBoundsAllowPointerEventsOutsideP
EXPECT_EQ(filtered[1].type, UIInputEventType::PointerButtonDown);
}
TEST(UIEditorPanelInputFilterTests, OverlayBoundsKeepPointerEnterAndSuppressSyntheticLeave) {
const std::vector<UIRect> overlayBounds = {
UIRect(120.0f, 10.0f, 80.0f, 80.0f)
};
const std::vector<UIInputEvent> filtered =
FilterUIEditorPanelInputEvents(
UIRect(0.0f, 0.0f, 100.0f, 100.0f),
{
MakePointerLeave(130.0f, 20.0f),
MakePointerEnter(130.0f, 20.0f),
MakePointerMove(130.0f, 24.0f)
},
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = true,
.allowPointerWhileCaptured = false,
.allowKeyboardInput = false,
.allowFocusEvents = false,
.includePointerLeave = true
},
&overlayBounds);
ASSERT_EQ(filtered.size(), 2u);
EXPECT_EQ(filtered[0].type, UIInputEventType::PointerEnter);
EXPECT_EQ(filtered[1].type, UIInputEventType::PointerMove);
}
TEST(UIEditorPanelInputFilterTests, SyntheticFocusTransitionsArePrependedToFilteredEvents) {
const std::vector<UIInputEvent> filtered =
BuildUIEditorPanelInputEvents(