Files
XCEngine/new_editor/src/Fields/UIEditorEnumFieldInteraction.cpp

487 lines
18 KiB
C++
Raw Normal View History

2026-04-10 00:41:28 +08:00
#include <XCEditor/Fields/UIEditorEnumFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
2026-04-08 02:52:28 +08:00
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
#include <algorithm>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
using ::XCEngine::Input::KeyCode;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPointerButton;
2026-04-08 02:52:28 +08:00
using ::XCEngine::UI::UISize;
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
using ::XCEngine::UI::Widgets::UIPopupPlacement;
using Widgets::BuildUIEditorEnumFieldLayout;
2026-04-08 02:52:28 +08:00
using Widgets::BuildUIEditorMenuPopupLayout;
using Widgets::HitTestUIEditorEnumField;
2026-04-08 02:52:28 +08:00
using Widgets::HitTestUIEditorMenuPopup;
using Widgets::MeasureUIEditorMenuPopupHeight;
using Widgets::ResolveUIEditorMenuPopupDesiredWidth;
using Widgets::UIEditorEnumFieldHitTarget;
using Widgets::UIEditorEnumFieldHitTargetKind;
2026-04-08 02:52:28 +08:00
using Widgets::UIEditorEnumFieldLayout;
using Widgets::UIEditorEnumFieldMetrics;
using Widgets::UIEditorEnumFieldSpec;
using Widgets::UIEditorMenuPopupHitTarget;
using Widgets::UIEditorMenuPopupHitTargetKind;
using Widgets::UIEditorMenuPopupInvalidIndex;
using Widgets::UIEditorMenuPopupItem;
using Widgets::UIEditorMenuPopupLayout;
using Widgets::UIEditorMenuPopupMetrics;
using Widgets::UIEditorMenuPopupState;
using ::XCEngine::UI::Editor::UIEditorMenuItemKind;
bool ShouldUsePointerPosition(const UIInputEvent& event) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
return true;
default:
return false;
}
}
2026-04-08 02:52:28 +08:00
std::size_t ClampSelectedIndex(std::size_t selectedIndex, const UIEditorEnumFieldSpec& spec) {
if (spec.options.empty()) {
2026-04-08 02:52:28 +08:00
return 0u;
}
return (std::min)(selectedIndex, spec.options.size() - 1u);
}
::XCEngine::UI::UIRect ResolveViewportRect(
const ::XCEngine::UI::UIRect& bounds,
const ::XCEngine::UI::UIRect& viewportRect) {
if (viewportRect.width > 0.0f && viewportRect.height > 0.0f) {
return viewportRect;
}
return ::XCEngine::UI::UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f);
}
std::vector<UIEditorMenuPopupItem> BuildPopupItems(
const UIEditorEnumFieldSpec& spec,
std::size_t selectedIndex) {
std::vector<UIEditorMenuPopupItem> items = {};
items.reserve(spec.options.size());
const std::size_t clampedIndex = ClampSelectedIndex(selectedIndex, spec);
for (std::size_t index = 0u; index < spec.options.size(); ++index) {
UIEditorMenuPopupItem item = {};
item.itemId = spec.fieldId + "." + std::to_string(index);
item.kind = UIEditorMenuItemKind::Command;
item.label = spec.options[index];
item.enabled = !spec.readOnly;
item.checked = index == clampedIndex;
items.push_back(std::move(item));
}
return items;
}
UIEditorMenuPopupLayout BuildPopupLayout(
const UIEditorEnumFieldLayout& fieldLayout,
const std::vector<UIEditorMenuPopupItem>& popupItems,
const UIEditorMenuPopupMetrics& popupMetrics,
const ::XCEngine::UI::UIRect& viewportRect) {
if (popupItems.empty()) {
return {};
}
const float popupWidth = (std::max)(
fieldLayout.valueRect.width,
ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics));
const float popupHeight = MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics);
const auto placement = ResolvePopupPlacementRect(
fieldLayout.valueRect,
UISize(popupWidth, popupHeight),
viewportRect,
UIPopupPlacement::BottomStart);
return BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics);
}
void SyncControlHover(
UIEditorEnumFieldInteractionState& state,
const UIEditorEnumFieldLayout& layout) {
if (!state.hasPointerPosition) {
state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None;
return;
}
state.fieldState.hoveredTarget = HitTestUIEditorEnumField(layout, state.pointerPosition).kind;
}
UIEditorMenuPopupHitTarget ResolvePopupHit(
const UIEditorEnumFieldInteractionState& state,
const UIEditorMenuPopupLayout& popupLayout,
const std::vector<UIEditorMenuPopupItem>& popupItems) {
if (!state.popupOpen || !state.hasPointerPosition) {
return {};
}
return HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition);
}
void SyncPopupHover(
UIEditorEnumFieldInteractionState& state,
const UIEditorMenuPopupLayout& popupLayout,
const std::vector<UIEditorMenuPopupItem>& popupItems,
std::size_t selectedIndex) {
if (!state.popupOpen) {
state.highlightedIndex = UIEditorMenuPopupInvalidIndex;
return;
}
if (popupItems.empty()) {
state.popupOpen = false;
state.highlightedIndex = UIEditorMenuPopupInvalidIndex;
return;
}
const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems);
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) {
state.highlightedIndex = popupHit.index;
return;
}
if (state.highlightedIndex == UIEditorMenuPopupInvalidIndex ||
state.highlightedIndex >= popupItems.size()) {
state.highlightedIndex = (std::min)(selectedIndex, popupItems.size() - 1u);
}
}
void OpenPopup(
UIEditorEnumFieldInteractionState& state,
const UIEditorEnumFieldSpec& spec,
std::size_t selectedIndex,
UIEditorEnumFieldInteractionResult& result) {
if (spec.readOnly || spec.options.empty()) {
return;
}
if (!state.popupOpen) {
state.popupOpen = true;
state.highlightedIndex = ClampSelectedIndex(selectedIndex, spec);
result.popupOpened = true;
result.consumed = true;
}
}
void ClosePopup(
UIEditorEnumFieldInteractionState& state,
UIEditorEnumFieldInteractionResult& result) {
if (!state.popupOpen) {
return;
}
state.popupOpen = false;
state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex;
state.highlightedIndex = UIEditorMenuPopupInvalidIndex;
result.popupClosed = true;
result.consumed = true;
}
void MovePopupHighlight(
UIEditorEnumFieldInteractionState& state,
const UIEditorEnumFieldSpec& spec,
int delta) {
if (spec.options.empty()) {
state.highlightedIndex = UIEditorMenuPopupInvalidIndex;
return;
}
2026-04-08 02:52:28 +08:00
if (state.highlightedIndex == UIEditorMenuPopupInvalidIndex ||
state.highlightedIndex >= spec.options.size()) {
state.highlightedIndex = ClampSelectedIndex(0u, spec);
}
const std::size_t currentIndex = state.highlightedIndex;
if (delta < 0) {
state.highlightedIndex = currentIndex == 0u ? 0u : currentIndex - 1u;
} else {
2026-04-08 02:52:28 +08:00
state.highlightedIndex =
currentIndex + 1u >= spec.options.size() ? spec.options.size() - 1u : currentIndex + 1u;
}
}
void JumpPopupHighlightToEdge(
UIEditorEnumFieldInteractionState& state,
const UIEditorEnumFieldSpec& spec,
bool toEnd) {
if (spec.options.empty()) {
state.highlightedIndex = UIEditorMenuPopupInvalidIndex;
return;
}
2026-04-08 02:52:28 +08:00
state.highlightedIndex = toEnd ? spec.options.size() - 1u : 0u;
}
bool TrySelectHighlighted(
UIEditorEnumFieldInteractionState& state,
std::size_t& selectedIndex,
const UIEditorEnumFieldSpec& spec,
UIEditorEnumFieldInteractionResult& result) {
if (!state.popupOpen ||
spec.readOnly ||
spec.options.empty() ||
state.highlightedIndex == UIEditorMenuPopupInvalidIndex ||
state.highlightedIndex >= spec.options.size()) {
return false;
}
2026-04-08 02:52:28 +08:00
selectedIndex = state.highlightedIndex;
result.selectionChanged = true;
result.selectedIndex = selectedIndex;
result.popupItemIndex = state.highlightedIndex;
ClosePopup(state, result);
return true;
}
} // namespace
UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction(
UIEditorEnumFieldInteractionState& state,
std::size_t& selectedIndex,
const ::XCEngine::UI::UIRect& bounds,
2026-04-08 02:52:28 +08:00
const UIEditorEnumFieldSpec& spec,
const std::vector<UIInputEvent>& inputEvents,
2026-04-08 02:52:28 +08:00
const UIEditorEnumFieldMetrics& metrics,
const UIEditorMenuPopupMetrics& popupMetrics,
const ::XCEngine::UI::UIRect& viewportRect) {
UIEditorEnumFieldSpec resolvedSpec = spec;
selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec);
resolvedSpec.selectedIndex = selectedIndex;
2026-04-08 02:52:28 +08:00
UIEditorEnumFieldLayout layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics);
const ::XCEngine::UI::UIRect resolvedViewport = ResolveViewportRect(bounds, viewportRect);
std::vector<UIEditorMenuPopupItem> popupItems =
state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector<UIEditorMenuPopupItem> {};
UIEditorMenuPopupLayout popupLayout =
state.popupOpen ? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport) : UIEditorMenuPopupLayout {};
SyncControlHover(state, layout);
SyncPopupHover(state, popupLayout, popupItems, selectedIndex);
state.fieldState.popupOpen = state.popupOpen;
UIEditorEnumFieldInteractionResult interactionResult = {};
for (const UIInputEvent& event : inputEvents) {
if (ShouldUsePointerPosition(event)) {
state.pointerPosition = event.position;
state.hasPointerPosition = true;
} else if (event.type == UIInputEventType::PointerLeave) {
state.hasPointerPosition = false;
}
UIEditorEnumFieldInteractionResult eventResult = {};
switch (event.type) {
case UIInputEventType::FocusGained:
2026-04-08 02:52:28 +08:00
eventResult.focusedChanged = !state.fieldState.focused;
state.fieldState.focused = true;
break;
case UIInputEventType::FocusLost:
2026-04-08 02:52:28 +08:00
eventResult.focusedChanged = state.fieldState.focused;
state.fieldState.focused = false;
state.fieldState.active = false;
state.hasPointerPosition = false;
state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None;
2026-04-08 02:52:28 +08:00
ClosePopup(state, eventResult);
break;
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerLeave:
break;
2026-04-08 02:52:28 +08:00
case UIInputEventType::PointerButtonDown: {
if (event.pointerButton != UIPointerButton::Left) {
break;
}
const UIEditorEnumFieldHitTarget fieldHit =
state.hasPointerPosition ? HitTestUIEditorEnumField(layout, state.pointerPosition) : UIEditorEnumFieldHitTarget {};
const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems);
eventResult.hitTarget = fieldHit;
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) {
state.fieldState.focused = true;
state.pressedPopupIndex = popupHit.index;
state.highlightedIndex = popupHit.index;
eventResult.popupItemIndex = popupHit.index;
eventResult.consumed = true;
} else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) {
state.fieldState.focused = true;
state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex;
eventResult.consumed = true;
} else if (
fieldHit.kind == UIEditorEnumFieldHitTargetKind::ValueBox ||
fieldHit.kind == UIEditorEnumFieldHitTargetKind::DropdownArrow) {
eventResult.focusedChanged = !state.fieldState.focused;
state.fieldState.focused = true;
state.fieldState.active = true;
eventResult.consumed = true;
2026-04-08 02:52:28 +08:00
} else {
if (state.popupOpen) {
ClosePopup(state, eventResult);
}
if (state.fieldState.focused) {
eventResult.focusedChanged = true;
state.fieldState.focused = false;
}
state.fieldState.active = false;
state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex;
}
break;
2026-04-08 02:52:28 +08:00
}
2026-04-08 02:52:28 +08:00
case UIInputEventType::PointerButtonUp: {
if (event.pointerButton != UIPointerButton::Left) {
break;
}
const UIEditorEnumFieldHitTarget fieldHit =
state.hasPointerPosition ? HitTestUIEditorEnumField(layout, state.pointerPosition) : UIEditorEnumFieldHitTarget {};
const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems);
eventResult.hitTarget = fieldHit;
if (state.pressedPopupIndex != UIEditorMenuPopupInvalidIndex) {
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item &&
popupHit.index == state.pressedPopupIndex &&
popupHit.index < resolvedSpec.options.size()) {
state.highlightedIndex = popupHit.index;
selectedIndex = popupHit.index;
eventResult.selectionChanged = true;
eventResult.selectedIndex = selectedIndex;
2026-04-08 02:52:28 +08:00
eventResult.popupItemIndex = popupHit.index;
ClosePopup(state, eventResult);
}
state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex;
eventResult.consumed = true;
} else if (state.fieldState.active) {
state.fieldState.active = false;
if (fieldHit.kind == UIEditorEnumFieldHitTargetKind::ValueBox ||
fieldHit.kind == UIEditorEnumFieldHitTargetKind::DropdownArrow) {
if (state.popupOpen) {
ClosePopup(state, eventResult);
} else {
OpenPopup(state, resolvedSpec, selectedIndex, eventResult);
}
}
}
break;
2026-04-08 02:52:28 +08:00
}
case UIInputEventType::KeyDown:
2026-04-08 02:52:28 +08:00
if (!state.fieldState.focused) {
break;
}
if (state.popupOpen) {
switch (static_cast<KeyCode>(event.keyCode)) {
2026-04-08 02:52:28 +08:00
case KeyCode::Up:
MovePopupHighlight(state, resolvedSpec, -1);
eventResult.consumed = true;
break;
2026-04-08 02:52:28 +08:00
case KeyCode::Down:
MovePopupHighlight(state, resolvedSpec, 1);
eventResult.consumed = true;
break;
2026-04-08 02:52:28 +08:00
case KeyCode::Home:
2026-04-08 02:52:28 +08:00
JumpPopupHighlightToEdge(state, resolvedSpec, false);
eventResult.consumed = true;
break;
2026-04-08 02:52:28 +08:00
case KeyCode::End:
2026-04-08 02:52:28 +08:00
JumpPopupHighlightToEdge(state, resolvedSpec, true);
eventResult.consumed = true;
break;
case KeyCode::Enter:
case KeyCode::Space:
TrySelectHighlighted(state, selectedIndex, resolvedSpec, eventResult);
break;
case KeyCode::Escape:
ClosePopup(state, eventResult);
break;
2026-04-08 02:52:28 +08:00
default:
break;
}
2026-04-08 02:52:28 +08:00
} else {
switch (static_cast<KeyCode>(event.keyCode)) {
case KeyCode::Enter:
case KeyCode::Space:
case KeyCode::Down:
case KeyCode::Up:
OpenPopup(state, resolvedSpec, selectedIndex, eventResult);
break;
default:
break;
}
}
break;
default:
break;
}
2026-04-08 02:52:28 +08:00
selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec);
resolvedSpec.selectedIndex = selectedIndex;
layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics);
2026-04-08 02:52:28 +08:00
popupItems = state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector<UIEditorMenuPopupItem> {};
popupLayout = state.popupOpen
? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport)
: UIEditorMenuPopupLayout {};
SyncControlHover(state, layout);
SyncPopupHover(state, popupLayout, popupItems, selectedIndex);
state.fieldState.popupOpen = state.popupOpen;
2026-04-08 02:52:28 +08:00
if (eventResult.selectionChanged ||
eventResult.popupOpened ||
eventResult.popupClosed ||
eventResult.focusedChanged ||
eventResult.consumed ||
eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None ||
eventResult.popupItemIndex != UIEditorMenuPopupInvalidIndex) {
interactionResult = std::move(eventResult);
}
}
2026-04-08 02:52:28 +08:00
resolvedSpec.selectedIndex = selectedIndex;
layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics);
popupItems = state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector<UIEditorMenuPopupItem> {};
popupLayout = state.popupOpen
? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport)
: UIEditorMenuPopupLayout {};
SyncControlHover(state, layout);
SyncPopupHover(state, popupLayout, popupItems, selectedIndex);
state.fieldState.popupOpen = state.popupOpen;
UIEditorMenuPopupState popupState = {};
popupState.focused = state.fieldState.focused || state.popupOpen;
popupState.hoveredIndex = state.popupOpen ? state.highlightedIndex : UIEditorMenuPopupInvalidIndex;
return {
std::move(layout),
std::move(popupLayout),
std::move(popupState),
std::move(popupItems),
state.popupOpen,
std::move(interactionResult)
};
}
} // namespace XCEngine::UI::Editor