#include #include #include #include #include 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 BuildPopupItems( const UIEditorEnumFieldSpec& spec, std::size_t selectedIndex) { std::vector 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& 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& popupItems) { if (!state.popupOpen || !state.hasPointerPosition) { return {}; } return HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition); } void SyncPopupHover( UIEditorEnumFieldInteractionState& state, const UIEditorMenuPopupLayout& popupLayout, const std::vector& 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& 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 popupItems = state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector {}; 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(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(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 {}; 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 {}; 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