Refactor XCUI editor module layout
This commit is contained in:
486
new_editor/src/Fields/UIEditorEnumFieldInteraction.cpp
Normal file
486
new_editor/src/Fields/UIEditorEnumFieldInteraction.cpp
Normal file
@@ -0,0 +1,486 @@
|
||||
#include <XCEditor/Fields/UIEditorEnumFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#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;
|
||||
using ::XCEngine::UI::UISize;
|
||||
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
|
||||
using ::XCEngine::UI::Widgets::UIPopupPlacement;
|
||||
using Widgets::BuildUIEditorEnumFieldLayout;
|
||||
using Widgets::BuildUIEditorMenuPopupLayout;
|
||||
using Widgets::HitTestUIEditorEnumField;
|
||||
using Widgets::HitTestUIEditorMenuPopup;
|
||||
using Widgets::MeasureUIEditorMenuPopupHeight;
|
||||
using Widgets::ResolveUIEditorMenuPopupDesiredWidth;
|
||||
using Widgets::UIEditorEnumFieldHitTarget;
|
||||
using Widgets::UIEditorEnumFieldHitTargetKind;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t ClampSelectedIndex(std::size_t selectedIndex, const UIEditorEnumFieldSpec& spec) {
|
||||
if (spec.options.empty()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
const UIEditorEnumFieldSpec& spec,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIEditorEnumFieldMetrics& metrics,
|
||||
const UIEditorMenuPopupMetrics& popupMetrics,
|
||||
const ::XCEngine::UI::UIRect& viewportRect) {
|
||||
UIEditorEnumFieldSpec resolvedSpec = spec;
|
||||
selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec);
|
||||
resolvedSpec.selectedIndex = selectedIndex;
|
||||
|
||||
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:
|
||||
eventResult.focusedChanged = !state.fieldState.focused;
|
||||
state.fieldState.focused = true;
|
||||
break;
|
||||
|
||||
case UIInputEventType::FocusLost:
|
||||
eventResult.focusedChanged = state.fieldState.focused;
|
||||
state.fieldState.focused = false;
|
||||
state.fieldState.active = false;
|
||||
state.hasPointerPosition = false;
|
||||
state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None;
|
||||
ClosePopup(state, eventResult);
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerMove:
|
||||
case UIInputEventType::PointerEnter:
|
||||
case UIInputEventType::PointerLeave:
|
||||
break;
|
||||
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
case UIInputEventType::KeyDown:
|
||||
if (!state.fieldState.focused) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.popupOpen) {
|
||||
switch (static_cast<KeyCode>(event.keyCode)) {
|
||||
case KeyCode::Up:
|
||||
MovePopupHighlight(state, resolvedSpec, -1);
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
|
||||
case KeyCode::Down:
|
||||
MovePopupHighlight(state, resolvedSpec, 1);
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
|
||||
case KeyCode::Home:
|
||||
JumpPopupHighlightToEdge(state, resolvedSpec, false);
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
|
||||
case KeyCode::End:
|
||||
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;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec);
|
||||
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;
|
||||
|
||||
if (eventResult.selectionChanged ||
|
||||
eventResult.popupOpened ||
|
||||
eventResult.popupClosed ||
|
||||
eventResult.focusedChanged ||
|
||||
eventResult.consumed ||
|
||||
eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None ||
|
||||
eventResult.popupItemIndex != UIEditorMenuPopupInvalidIndex) {
|
||||
interactionResult = std::move(eventResult);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user