#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::Text::HandleKeyDown; using ::XCEngine::UI::Text::InsertCharacter; using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout; using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field; using ::XCEngine::UI::Editor::Widgets::IsUIEditorVector2FieldPointInside; using ::XCEngine::UI::Editor::Widgets::NormalizeUIEditorVector2FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::TryParseUIEditorVector2FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; bool ShouldUsePointerPosition(const UIInputEvent& event) { switch (event.type) { case UIInputEventType::PointerMove: case UIInputEventType::PointerEnter: case UIInputEventType::PointerButtonDown: case UIInputEventType::PointerButtonUp: case UIInputEventType::PointerWheel: return true; default: return false; } } std::size_t ResolveFallbackSelectedComponentIndex( const UIEditorVector2FieldInteractionState& state) { return state.vector2FieldState.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex ? 0u : state.vector2FieldState.selectedComponentIndex; } std::string BuildComponentEditFieldId( const UIEditorVector2FieldSpec& spec, std::size_t componentIndex) { return spec.fieldId + "." + std::to_string(componentIndex); } bool IsPermittedCharacter( const UIEditorVector2FieldSpec& spec, std::uint32_t character) { if (character >= static_cast('0') && character <= static_cast('9')) { return true; } if (character == static_cast('-') || character == static_cast('+')) { return true; } return !spec.integerMode && character == static_cast('.'); } void SyncDisplayTexts( UIEditorVector2FieldInteractionState& state, const UIEditorVector2FieldSpec& spec) { for (std::size_t componentIndex = 0u; componentIndex < state.vector2FieldState.displayTexts.size(); ++componentIndex) { if (state.vector2FieldState.editing && state.vector2FieldState.selectedComponentIndex == componentIndex) { continue; } state.vector2FieldState.displayTexts[componentIndex] = FormatUIEditorVector2FieldComponentValue(spec, componentIndex); } } void SyncHoverTarget( UIEditorVector2FieldInteractionState& state, const UIEditorVector2FieldLayout& layout) { if (!state.hasPointerPosition) { state.vector2FieldState.hoveredTarget = UIEditorVector2FieldHitTargetKind::None; state.vector2FieldState.hoveredComponentIndex = UIEditorVector2FieldInvalidComponentIndex; return; } const UIEditorVector2FieldHitTarget hitTarget = HitTestUIEditorVector2Field(layout, state.pointerPosition); state.vector2FieldState.hoveredTarget = hitTarget.kind; state.vector2FieldState.hoveredComponentIndex = hitTarget.componentIndex; } bool MoveSelection( UIEditorVector2FieldInteractionState& state, int direction, UIEditorVector2FieldInteractionResult& result) { const std::size_t before = ResolveFallbackSelectedComponentIndex(state); const std::size_t after = direction < 0 ? (before == 0u ? 0u : before - 1u) : (before >= 1u ? 1u : before + 1u); state.vector2FieldState.selectedComponentIndex = after; result.selectionChanged = before != after; result.selectedComponentIndex = after; result.consumed = true; return true; } bool SelectComponent( UIEditorVector2FieldInteractionState& state, std::size_t componentIndex, UIEditorVector2FieldInteractionResult& result) { if (componentIndex >= 2u) { return false; } const std::size_t before = ResolveFallbackSelectedComponentIndex(state); state.vector2FieldState.selectedComponentIndex = componentIndex; result.selectionChanged = before != componentIndex; result.selectedComponentIndex = componentIndex; return true; } bool BeginEdit( UIEditorVector2FieldInteractionState& state, const UIEditorVector2FieldSpec& spec, std::size_t componentIndex, bool clearText) { if (spec.readOnly || componentIndex >= spec.values.size()) { return false; } const std::string baseline = FormatUIEditorVector2FieldComponentValue(spec, componentIndex); const std::string editFieldId = BuildComponentEditFieldId(spec, componentIndex); const bool changed = state.editModel.BeginEdit(editFieldId, baseline); if (!changed && state.editModel.HasActiveEdit() && state.editModel.GetActiveFieldId() != editFieldId) { return false; } if (!changed && state.vector2FieldState.editing && state.vector2FieldState.selectedComponentIndex == componentIndex) { return false; } state.vector2FieldState.selectedComponentIndex = componentIndex; state.vector2FieldState.editing = true; state.textInputState.value = clearText ? std::string() : baseline; state.textInputState.caret = state.textInputState.value.size(); state.editModel.UpdateStagedValue(state.textInputState.value); state.vector2FieldState.displayTexts[componentIndex] = state.textInputState.value; return true; } bool CommitEdit( UIEditorVector2FieldInteractionState& state, UIEditorVector2FieldSpec& spec, UIEditorVector2FieldInteractionResult& result) { if (!state.vector2FieldState.editing || !state.editModel.HasActiveEdit() || state.vector2FieldState.selectedComponentIndex >= spec.values.size()) { return false; } const std::size_t componentIndex = state.vector2FieldState.selectedComponentIndex; double parsedValue = spec.values[componentIndex]; if (!TryParseUIEditorVector2FieldComponentValue( spec, state.textInputState.value, parsedValue)) { result.consumed = true; result.editCommitRejected = true; return false; } result.valuesBefore = spec.values; spec.values[componentIndex] = NormalizeUIEditorVector2FieldComponentValue(spec, parsedValue); result.valuesAfter = spec.values; result.valueChanged = result.valuesBefore != result.valuesAfter; result.editCommitted = true; result.consumed = true; result.changedComponentIndex = componentIndex; result.selectedComponentIndex = componentIndex; result.committedText = FormatUIEditorVector2FieldComponentValue(spec, componentIndex); state.editModel.CommitEdit(); state.textInputState = {}; state.vector2FieldState.editing = false; state.vector2FieldState.displayTexts[componentIndex] = result.committedText; return true; } bool CancelEdit( UIEditorVector2FieldInteractionState& state, const UIEditorVector2FieldSpec& spec, UIEditorVector2FieldInteractionResult& result) { if (!state.vector2FieldState.editing || !state.editModel.HasActiveEdit() || state.vector2FieldState.selectedComponentIndex >= spec.values.size()) { return false; } const std::size_t componentIndex = state.vector2FieldState.selectedComponentIndex; state.editModel.CancelEdit(); state.textInputState = {}; state.vector2FieldState.editing = false; state.vector2FieldState.displayTexts[componentIndex] = FormatUIEditorVector2FieldComponentValue(spec, componentIndex); result.consumed = true; result.editCanceled = true; result.valuesBefore = spec.values; result.valuesAfter = spec.values; result.selectedComponentIndex = componentIndex; return true; } bool ApplyStep( UIEditorVector2FieldInteractionState& state, UIEditorVector2FieldSpec& spec, double direction, bool snapToEdge, UIEditorVector2FieldInteractionResult& result) { if (spec.readOnly) { return false; } const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); state.vector2FieldState.selectedComponentIndex = componentIndex; if (state.vector2FieldState.editing && !CommitEdit(state, spec, result)) { return result.editCommitRejected; } result.valuesBefore = spec.values; if (snapToEdge) { spec.values[componentIndex] = direction < 0.0 ? NormalizeUIEditorVector2FieldComponentValue(spec, spec.minValue) : NormalizeUIEditorVector2FieldComponentValue(spec, spec.maxValue); } else { const double step = spec.step == 0.0 ? 1.0 : spec.step; spec.values[componentIndex] = NormalizeUIEditorVector2FieldComponentValue( spec, spec.values[componentIndex] + step * direction); result.stepDelta = step * direction; } result.valuesAfter = spec.values; result.stepApplied = true; result.valueChanged = result.valuesBefore != result.valuesAfter || result.valueChanged; result.changedComponentIndex = componentIndex; result.selectedComponentIndex = componentIndex; result.consumed = true; state.vector2FieldState.displayTexts[componentIndex] = FormatUIEditorVector2FieldComponentValue(spec, componentIndex); return true; } } // namespace UIEditorVector2FieldInteractionFrame UpdateUIEditorVector2FieldInteraction( UIEditorVector2FieldInteractionState& state, UIEditorVector2FieldSpec& spec, const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorVector2FieldMetrics& metrics) { UIEditorVector2FieldLayout layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); UIEditorVector2FieldInteractionResult interactionResult = {}; interactionResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; for (const UIInputEvent& event : inputEvents) { if (ShouldUsePointerPosition(event)) { state.pointerPosition = event.position; state.hasPointerPosition = true; } else if (event.type == UIInputEventType::PointerLeave) { state.hasPointerPosition = false; } UIEditorVector2FieldInteractionResult eventResult = {}; eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; switch (event.type) { case UIInputEventType::FocusGained: eventResult.focusChanged = !state.vector2FieldState.focused; state.vector2FieldState.focused = true; if (state.vector2FieldState.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { state.vector2FieldState.selectedComponentIndex = 0u; } eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; break; case UIInputEventType::FocusLost: eventResult.focusChanged = state.vector2FieldState.focused; state.vector2FieldState.focused = false; state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; state.hasPointerPosition = false; if (state.vector2FieldState.editing) { CommitEdit(state, spec, eventResult); if (eventResult.editCommitRejected) { CancelEdit(state, spec, eventResult); } } break; case UIInputEventType::PointerMove: case UIInputEventType::PointerEnter: case UIInputEventType::PointerLeave: break; case UIInputEventType::PointerButtonDown: { const UIEditorVector2FieldHitTarget hitTarget = state.hasPointerPosition ? HitTestUIEditorVector2Field(layout, state.pointerPosition) : UIEditorVector2FieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton != UIPointerButton::Left) { break; } const bool insideField = state.hasPointerPosition && IsUIEditorVector2FieldPointInside(layout.bounds, state.pointerPosition); if (insideField) { eventResult.focusChanged = !state.vector2FieldState.focused; state.vector2FieldState.focused = true; state.vector2FieldState.activeTarget = hitTarget.kind == UIEditorVector2FieldHitTargetKind::None ? UIEditorVector2FieldHitTargetKind::Row : hitTarget.kind; state.vector2FieldState.activeComponentIndex = hitTarget.componentIndex; if (hitTarget.kind == UIEditorVector2FieldHitTargetKind::Component) { SelectComponent(state, hitTarget.componentIndex, eventResult); } else if (state.vector2FieldState.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { state.vector2FieldState.selectedComponentIndex = 0u; eventResult.selectedComponentIndex = 0u; } eventResult.consumed = true; } else { if (state.vector2FieldState.editing) { CommitEdit(state, spec, eventResult); if (!eventResult.editCommitRejected) { eventResult.focusChanged = state.vector2FieldState.focused; state.vector2FieldState.focused = false; } } else if (state.vector2FieldState.focused) { eventResult.focusChanged = true; state.vector2FieldState.focused = false; } state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; } break; } case UIInputEventType::PointerButtonUp: { const UIEditorVector2FieldHitTarget hitTarget = state.hasPointerPosition ? HitTestUIEditorVector2Field(layout, state.pointerPosition) : UIEditorVector2FieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton == UIPointerButton::Left) { const UIEditorVector2FieldHitTargetKind activeTarget = state.vector2FieldState.activeTarget; const std::size_t activeComponentIndex = state.vector2FieldState.activeComponentIndex; state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; if (activeTarget == UIEditorVector2FieldHitTargetKind::Component && hitTarget.kind == UIEditorVector2FieldHitTargetKind::Component && activeComponentIndex == hitTarget.componentIndex) { SelectComponent(state, hitTarget.componentIndex, eventResult); if (!state.vector2FieldState.editing) { eventResult.editStarted = BeginEdit(state, spec, hitTarget.componentIndex, false); } eventResult.consumed = true; } else if (hitTarget.kind == UIEditorVector2FieldHitTargetKind::Row) { eventResult.consumed = true; } } break; } case UIInputEventType::KeyDown: if (!state.vector2FieldState.focused) { break; } if (state.vector2FieldState.editing) { if (event.keyCode == static_cast(KeyCode::Escape)) { CancelEdit(state, spec, eventResult); break; } if (event.keyCode == static_cast(KeyCode::Tab)) { if (CommitEdit(state, spec, eventResult)) { MoveSelection( state, event.modifiers.shift ? -1 : 1, eventResult); } eventResult.consumed = true; break; } const auto textResult = HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); if (textResult.handled) { state.editModel.UpdateStagedValue(state.textInputState.value); state.vector2FieldState.displayTexts[state.vector2FieldState.selectedComponentIndex] = state.textInputState.value; eventResult.consumed = true; eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; if (textResult.submitRequested) { CommitEdit(state, spec, eventResult); } break; } } else { switch (static_cast(event.keyCode)) { case KeyCode::Left: MoveSelection(state, -1, eventResult); break; case KeyCode::Right: case KeyCode::Tab: MoveSelection( state, static_cast(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1, eventResult); break; case KeyCode::Up: ApplyStep(state, spec, 1.0, false, eventResult); break; case KeyCode::Down: ApplyStep(state, spec, -1.0, false, eventResult); break; case KeyCode::Home: ApplyStep(state, spec, -1.0, true, eventResult); break; case KeyCode::End: ApplyStep(state, spec, 1.0, true, eventResult); break; case KeyCode::Enter: eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state); eventResult.editStarted = BeginEdit( state, spec, eventResult.selectedComponentIndex, false); eventResult.consumed = eventResult.editStarted; break; default: break; } } break; case UIInputEventType::Character: if (!state.vector2FieldState.focused || spec.readOnly || event.modifiers.control || event.modifiers.alt || event.modifiers.super || !IsPermittedCharacter(spec, event.character)) { break; } if (state.vector2FieldState.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { state.vector2FieldState.selectedComponentIndex = 0u; } eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; if (!state.vector2FieldState.editing) { eventResult.editStarted = BeginEdit( state, spec, state.vector2FieldState.selectedComponentIndex, true); } if (InsertCharacter(state.textInputState, event.character)) { state.editModel.UpdateStagedValue(state.textInputState.value); state.vector2FieldState.displayTexts[state.vector2FieldState.selectedComponentIndex] = state.textInputState.value; eventResult.consumed = true; } break; default: break; } layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); if (eventResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && state.hasPointerPosition) { eventResult.hitTarget = HitTestUIEditorVector2Field(layout, state.pointerPosition); } if (eventResult.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; } if (eventResult.consumed || eventResult.focusChanged || eventResult.valueChanged || eventResult.stepApplied || eventResult.selectionChanged || eventResult.editStarted || eventResult.editCommitted || eventResult.editCommitRejected || eventResult.editCanceled || eventResult.hitTarget.kind != UIEditorVector2FieldHitTargetKind::None) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); if (interactionResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && state.hasPointerPosition) { interactionResult.hitTarget = HitTestUIEditorVector2Field(layout, state.pointerPosition); } if (interactionResult.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { interactionResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; } return { std::move(layout), std::move(interactionResult) }; } } // namespace XCEngine::UI::Editor