330 lines
12 KiB
C++
330 lines
12 KiB
C++
|
|
#include <XCEditor/Collections/UIEditorTabStripInteraction.h>
|
||
|
|
|
||
|
|
#include <XCEngine/Input/InputTypes.h>
|
||
|
|
|
||
|
|
#include <utility>
|
||
|
|
|
||
|
|
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<Widgets::UIEditorTabStripItem>& 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<Widgets::UIEditorTabStripItem>& 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>(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<Widgets::UIEditorTabStripItem>& items,
|
||
|
|
const std::vector<UIInputEvent>& 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
|