#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 Widgets::BuildUIEditorTabStripLayout; using Widgets::HitTestUIEditorTabStrip; using Widgets::ResolveUIEditorTabStripSelectedIndex; using Widgets::UIEditorTabStripHitTarget; using Widgets::UIEditorTabStripHitTargetKind; using Widgets::UIEditorTabStripInvalidIndex; 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; } } bool HasNavigationModifiers(const ::XCEngine::UI::UIInputModifiers& modifiers) { return modifiers.shift || modifiers.control || modifiers.alt || modifiers.super; } bool IsPointInside(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } bool AreEquivalentTargets( const UIEditorTabStripHitTarget& lhs, const UIEditorTabStripHitTarget& rhs) { return lhs.kind == rhs.kind && lhs.index == rhs.index; } void ClearHoverState(UIEditorTabStripInteractionState& state) { state.tabStripState.hoveredIndex = UIEditorTabStripInvalidIndex; state.tabStripState.closeHoveredIndex = UIEditorTabStripInvalidIndex; } void SyncHoverTarget( UIEditorTabStripInteractionState& state, const Widgets::UIEditorTabStripLayout& layout) { ClearHoverState(state); if (!state.hasPointerPosition) { return; } const UIEditorTabStripHitTarget hitTarget = HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition); if (hitTarget.index == UIEditorTabStripInvalidIndex) { return; } switch (hitTarget.kind) { case UIEditorTabStripHitTargetKind::CloseButton: state.tabStripState.hoveredIndex = hitTarget.index; state.tabStripState.closeHoveredIndex = hitTarget.index; break; case UIEditorTabStripHitTargetKind::Tab: state.tabStripState.hoveredIndex = hitTarget.index; break; default: break; } } void SyncSelectionState( UIEditorTabStripInteractionState& state, std::string_view selectedTabId, const std::vector& items) { state.navigationModel.SetItemCount(items.size()); const std::size_t fallbackIndex = state.navigationModel.HasSelection() ? state.navigationModel.GetSelectedIndex() : UIEditorTabStripInvalidIndex; const std::size_t resolvedSelectedIndex = ResolveUIEditorTabStripSelectedIndex(items, selectedTabId, fallbackIndex); if (resolvedSelectedIndex != UIEditorTabStripInvalidIndex) { state.navigationModel.SetSelectedIndex(resolvedSelectedIndex); } state.tabStripState.selectedIndex = state.navigationModel.HasSelection() ? state.navigationModel.GetSelectedIndex() : UIEditorTabStripInvalidIndex; } bool SelectTab( UIEditorTabStripInteractionState& state, std::string& selectedTabId, const std::vector& items, std::size_t selectedIndex, UIEditorTabStripInteractionResult& result, bool keyboardNavigated) { if (selectedIndex >= items.size()) { return false; } state.navigationModel.SetSelectedIndex(selectedIndex); state.tabStripState.selectedIndex = selectedIndex; const std::string& tabId = items[selectedIndex].tabId; result.selectionChanged = selectedTabId != tabId; selectedTabId = tabId; result.keyboardNavigated = keyboardNavigated; result.selectedTabId = tabId; result.selectedIndex = selectedIndex; result.consumed = true; return true; } bool ApplyKeyboardNavigation( UIEditorTabStripInteractionState& state, std::int32_t keyCode) { switch (static_cast(keyCode)) { case KeyCode::Left: return state.navigationModel.SelectPrevious(); case KeyCode::Right: return state.navigationModel.SelectNext(); case KeyCode::Home: return state.navigationModel.SelectFirst(); case KeyCode::End: return state.navigationModel.SelectLast(); default: return false; } } } // namespace UIEditorTabStripInteractionFrame UpdateUIEditorTabStripInteraction( UIEditorTabStripInteractionState& state, std::string& selectedTabId, const ::XCEngine::UI::UIRect& bounds, const std::vector& items, const std::vector& inputEvents, const Widgets::UIEditorTabStripMetrics& metrics) { SyncSelectionState(state, selectedTabId, items); Widgets::UIEditorTabStripLayout layout = BuildUIEditorTabStripLayout(bounds, items, state.tabStripState, metrics); SyncHoverTarget(state, layout); UIEditorTabStripInteractionResult 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; } UIEditorTabStripInteractionResult eventResult = {}; eventResult.hitTarget = state.hasPointerPosition ? HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition) : UIEditorTabStripHitTarget {}; switch (event.type) { case UIInputEventType::FocusGained: state.tabStripState.focused = true; break; case UIInputEventType::FocusLost: state.tabStripState.focused = false; state.hasPointerPosition = false; state.pressedTarget = {}; ClearHoverState(state); break; case UIInputEventType::PointerMove: case UIInputEventType::PointerEnter: case UIInputEventType::PointerLeave: break; case UIInputEventType::PointerButtonDown: { if (event.pointerButton != UIPointerButton::Left) { break; } state.pressedTarget = eventResult.hitTarget; if (eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None || (state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition))) { state.tabStripState.focused = true; eventResult.consumed = true; } else { state.tabStripState.focused = false; state.pressedTarget = {}; } break; } case UIInputEventType::PointerButtonUp: { if (event.pointerButton != UIPointerButton::Left) { break; } const bool insideStrip = state.hasPointerPosition && IsPointInside(layout.bounds, state.pointerPosition); const bool matchedPressedTarget = AreEquivalentTargets(state.pressedTarget, eventResult.hitTarget); if (matchedPressedTarget) { switch (eventResult.hitTarget.kind) { case UIEditorTabStripHitTargetKind::CloseButton: if (eventResult.hitTarget.index < items.size() && items[eventResult.hitTarget.index].closable) { eventResult.closeRequested = true; eventResult.closedTabId = items[eventResult.hitTarget.index].tabId; eventResult.closedIndex = eventResult.hitTarget.index; eventResult.consumed = true; state.tabStripState.focused = true; } else if (insideStrip) { state.tabStripState.focused = true; eventResult.consumed = true; } break; case UIEditorTabStripHitTargetKind::Tab: SelectTab( state, selectedTabId, items, eventResult.hitTarget.index, eventResult, false); state.tabStripState.focused = true; break; case UIEditorTabStripHitTargetKind::HeaderBackground: case UIEditorTabStripHitTargetKind::Content: state.tabStripState.focused = true; eventResult.consumed = true; break; case UIEditorTabStripHitTargetKind::None: default: if (!insideStrip) { state.tabStripState.focused = false; } break; } } else if (!insideStrip) { state.tabStripState.focused = false; } else if (eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::HeaderBackground || eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::Content) { state.tabStripState.focused = true; eventResult.consumed = true; } state.pressedTarget = {}; break; } case UIInputEventType::KeyDown: if (state.tabStripState.focused && !HasNavigationModifiers(event.modifiers) && ApplyKeyboardNavigation(state, event.keyCode) && state.navigationModel.HasSelection()) { const std::size_t selectedIndex = state.navigationModel.GetSelectedIndex(); SelectTab( state, selectedTabId, items, selectedIndex, eventResult, true); } break; default: break; } SyncSelectionState(state, selectedTabId, items); layout = BuildUIEditorTabStripLayout(bounds, items, state.tabStripState, metrics); SyncHoverTarget(state, layout); if (eventResult.hitTarget.kind == UIEditorTabStripHitTargetKind::None && state.hasPointerPosition) { eventResult.hitTarget = HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition); } if (eventResult.consumed || eventResult.selectionChanged || eventResult.closeRequested || eventResult.keyboardNavigated || eventResult.hitTarget.kind != UIEditorTabStripHitTargetKind::None || !eventResult.selectedTabId.empty() || !eventResult.closedTabId.empty()) { interactionResult = std::move(eventResult); } } SyncSelectionState(state, selectedTabId, items); layout = BuildUIEditorTabStripLayout(bounds, items, state.tabStripState, metrics); SyncHoverTarget(state, layout); if (interactionResult.hitTarget.kind == UIEditorTabStripHitTargetKind::None && state.hasPointerPosition) { interactionResult.hitTarget = HitTestUIEditorTabStrip(layout, state.tabStripState, state.pointerPosition); } return { std::move(layout), std::move(interactionResult), state.tabStripState.focused }; } } // namespace XCEngine::UI::Editor