#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::BuildUIEditorVector4FieldLayout; using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field; using ::XCEngine::UI::Editor::Widgets::IsUIEditorVector4FieldPointInside; using ::XCEngine::UI::Editor::Widgets::NormalizeUIEditorVector4FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::TryParseUIEditorVector4FieldComponentValue; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTarget; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldInvalidComponentIndex; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldLayout; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldMetrics; using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; 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 UIEditorVector4FieldInteractionState& state) { return state.vector4FieldState.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex ? 0u : state.vector4FieldState.selectedComponentIndex; } std::string BuildComponentEditFieldId( const UIEditorVector4FieldSpec& spec, std::size_t componentIndex) { return spec.fieldId + "." + std::to_string(componentIndex); } bool IsPermittedCharacter( const UIEditorVector4FieldSpec& 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( UIEditorVector4FieldInteractionState& state, const UIEditorVector4FieldSpec& spec) { for (std::size_t componentIndex = 0u; componentIndex < state.vector4FieldState.displayTexts.size(); ++componentIndex) { if (state.vector4FieldState.editing && state.vector4FieldState.selectedComponentIndex == componentIndex) { continue; } state.vector4FieldState.displayTexts[componentIndex] = FormatUIEditorVector4FieldComponentValue(spec, componentIndex); } } void SyncHoverTarget( UIEditorVector4FieldInteractionState& state, const UIEditorVector4FieldLayout& layout) { if (!state.hasPointerPosition) { state.vector4FieldState.hoveredTarget = UIEditorVector4FieldHitTargetKind::None; state.vector4FieldState.hoveredComponentIndex = UIEditorVector4FieldInvalidComponentIndex; return; } const UIEditorVector4FieldHitTarget hitTarget = HitTestUIEditorVector4Field(layout, state.pointerPosition); state.vector4FieldState.hoveredTarget = hitTarget.kind; state.vector4FieldState.hoveredComponentIndex = hitTarget.componentIndex; } bool MoveSelection( UIEditorVector4FieldInteractionState& state, int direction, UIEditorVector4FieldInteractionResult& result) { const std::size_t before = ResolveFallbackSelectedComponentIndex(state); const std::size_t after = direction < 0 ? (before == 0u ? 0u : before - 1u) : (before >= 3u ? 3u : before + 1u); state.vector4FieldState.selectedComponentIndex = after; result.selectionChanged = before != after; result.selectedComponentIndex = after; result.consumed = true; return true; } bool SelectComponent( UIEditorVector4FieldInteractionState& state, std::size_t componentIndex, UIEditorVector4FieldInteractionResult& result) { if (componentIndex >= 4u) { return false; } const std::size_t before = ResolveFallbackSelectedComponentIndex(state); state.vector4FieldState.selectedComponentIndex = componentIndex; result.selectionChanged = before != componentIndex; result.selectedComponentIndex = componentIndex; return true; } bool BeginEdit( UIEditorVector4FieldInteractionState& state, const UIEditorVector4FieldSpec& spec, std::size_t componentIndex, bool clearText) { if (spec.readOnly || componentIndex >= spec.values.size()) { return false; } const std::string baseline = FormatUIEditorVector4FieldComponentValue(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.vector4FieldState.editing && state.vector4FieldState.selectedComponentIndex == componentIndex) { return false; } state.vector4FieldState.selectedComponentIndex = componentIndex; state.vector4FieldState.editing = true; state.textInputState.value = clearText ? std::string() : baseline; state.textInputState.caret = state.textInputState.value.size(); state.editModel.UpdateStagedValue(state.textInputState.value); state.vector4FieldState.displayTexts[componentIndex] = state.textInputState.value; return true; } bool CommitEdit( UIEditorVector4FieldInteractionState& state, UIEditorVector4FieldSpec& spec, UIEditorVector4FieldInteractionResult& result) { if (!state.vector4FieldState.editing || !state.editModel.HasActiveEdit() || state.vector4FieldState.selectedComponentIndex >= spec.values.size()) { return false; } const std::size_t componentIndex = state.vector4FieldState.selectedComponentIndex; double parsedValue = spec.values[componentIndex]; if (!TryParseUIEditorVector4FieldComponentValue( spec, state.textInputState.value, parsedValue)) { result.consumed = true; result.editCommitRejected = true; return false; } result.valuesBefore = spec.values; spec.values[componentIndex] = NormalizeUIEditorVector4FieldComponentValue(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 = FormatUIEditorVector4FieldComponentValue(spec, componentIndex); state.editModel.CommitEdit(); state.textInputState = {}; state.vector4FieldState.editing = false; state.vector4FieldState.displayTexts[componentIndex] = result.committedText; return true; } bool CancelEdit( UIEditorVector4FieldInteractionState& state, const UIEditorVector4FieldSpec& spec, UIEditorVector4FieldInteractionResult& result) { if (!state.vector4FieldState.editing || !state.editModel.HasActiveEdit() || state.vector4FieldState.selectedComponentIndex >= spec.values.size()) { return false; } const std::size_t componentIndex = state.vector4FieldState.selectedComponentIndex; state.editModel.CancelEdit(); state.textInputState = {}; state.vector4FieldState.editing = false; state.vector4FieldState.displayTexts[componentIndex] = FormatUIEditorVector4FieldComponentValue(spec, componentIndex); result.consumed = true; result.editCanceled = true; result.valuesBefore = spec.values; result.valuesAfter = spec.values; result.selectedComponentIndex = componentIndex; return true; } bool ApplyStep( UIEditorVector4FieldInteractionState& state, UIEditorVector4FieldSpec& spec, double direction, bool snapToEdge, UIEditorVector4FieldInteractionResult& result) { if (spec.readOnly) { return false; } const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); state.vector4FieldState.selectedComponentIndex = componentIndex; if (state.vector4FieldState.editing && !CommitEdit(state, spec, result)) { return result.editCommitRejected; } result.valuesBefore = spec.values; if (snapToEdge) { spec.values[componentIndex] = direction < 0.0 ? NormalizeUIEditorVector4FieldComponentValue(spec, spec.minValue) : NormalizeUIEditorVector4FieldComponentValue(spec, spec.maxValue); } else { const double step = spec.step == 0.0 ? 1.0 : spec.step; spec.values[componentIndex] = NormalizeUIEditorVector4FieldComponentValue( 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.vector4FieldState.displayTexts[componentIndex] = FormatUIEditorVector4FieldComponentValue(spec, componentIndex); return true; } } // namespace UIEditorVector4FieldInteractionFrame UpdateUIEditorVector4FieldInteraction( UIEditorVector4FieldInteractionState& state, UIEditorVector4FieldSpec& spec, const ::XCEngine::UI::UIRect& bounds, const std::vector& inputEvents, const UIEditorVector4FieldMetrics& metrics) { UIEditorVector4FieldLayout layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); UIEditorVector4FieldInteractionResult interactionResult = {}; interactionResult.selectedComponentIndex = state.vector4FieldState.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; } UIEditorVector4FieldInteractionResult eventResult = {}; eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; switch (event.type) { case UIInputEventType::FocusGained: eventResult.focusChanged = !state.vector4FieldState.focused; state.vector4FieldState.focused = true; if (state.vector4FieldState.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { state.vector4FieldState.selectedComponentIndex = 0u; } eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; break; case UIInputEventType::FocusLost: eventResult.focusChanged = state.vector4FieldState.focused; state.vector4FieldState.focused = false; state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; state.hasPointerPosition = false; if (state.vector4FieldState.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 UIEditorVector4FieldHitTarget hitTarget = state.hasPointerPosition ? HitTestUIEditorVector4Field(layout, state.pointerPosition) : UIEditorVector4FieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton != UIPointerButton::Left) { break; } const bool insideField = state.hasPointerPosition && IsUIEditorVector4FieldPointInside(layout.bounds, state.pointerPosition); if (insideField) { eventResult.focusChanged = !state.vector4FieldState.focused; state.vector4FieldState.focused = true; state.vector4FieldState.activeTarget = hitTarget.kind == UIEditorVector4FieldHitTargetKind::None ? UIEditorVector4FieldHitTargetKind::Row : hitTarget.kind; state.vector4FieldState.activeComponentIndex = hitTarget.componentIndex; if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component) { SelectComponent(state, hitTarget.componentIndex, eventResult); } else if (state.vector4FieldState.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { state.vector4FieldState.selectedComponentIndex = 0u; eventResult.selectedComponentIndex = 0u; } eventResult.consumed = true; } else { if (state.vector4FieldState.editing) { CommitEdit(state, spec, eventResult); if (!eventResult.editCommitRejected) { eventResult.focusChanged = state.vector4FieldState.focused; state.vector4FieldState.focused = false; } } else if (state.vector4FieldState.focused) { eventResult.focusChanged = true; state.vector4FieldState.focused = false; } state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; } break; } case UIInputEventType::PointerButtonUp: { const UIEditorVector4FieldHitTarget hitTarget = state.hasPointerPosition ? HitTestUIEditorVector4Field(layout, state.pointerPosition) : UIEditorVector4FieldHitTarget {}; eventResult.hitTarget = hitTarget; if (event.pointerButton == UIPointerButton::Left) { const UIEditorVector4FieldHitTargetKind activeTarget = state.vector4FieldState.activeTarget; const std::size_t activeComponentIndex = state.vector4FieldState.activeComponentIndex; state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; if (activeTarget == UIEditorVector4FieldHitTargetKind::Component && hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component && activeComponentIndex == hitTarget.componentIndex) { SelectComponent(state, hitTarget.componentIndex, eventResult); if (!state.vector4FieldState.editing) { eventResult.editStarted = BeginEdit(state, spec, hitTarget.componentIndex, false); } eventResult.consumed = true; } else if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Row) { eventResult.consumed = true; } } break; } case UIInputEventType::KeyDown: if (!state.vector4FieldState.focused) { break; } if (state.vector4FieldState.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.vector4FieldState.displayTexts[state.vector4FieldState.selectedComponentIndex] = state.textInputState.value; eventResult.consumed = true; eventResult.selectedComponentIndex = state.vector4FieldState.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.vector4FieldState.focused || spec.readOnly || event.modifiers.control || event.modifiers.alt || event.modifiers.super || !IsPermittedCharacter(spec, event.character)) { break; } if (state.vector4FieldState.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { state.vector4FieldState.selectedComponentIndex = 0u; } eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; if (!state.vector4FieldState.editing) { eventResult.editStarted = BeginEdit( state, spec, state.vector4FieldState.selectedComponentIndex, true); } if (InsertCharacter(state.textInputState, event.character)) { state.editModel.UpdateStagedValue(state.textInputState.value); state.vector4FieldState.displayTexts[state.vector4FieldState.selectedComponentIndex] = state.textInputState.value; eventResult.consumed = true; } break; default: break; } layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); if (eventResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && state.hasPointerPosition) { eventResult.hitTarget = HitTestUIEditorVector4Field(layout, state.pointerPosition); } if (eventResult.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; } if (eventResult.consumed || eventResult.focusChanged || eventResult.valueChanged || eventResult.stepApplied || eventResult.selectionChanged || eventResult.editStarted || eventResult.editCommitted || eventResult.editCommitRejected || eventResult.editCanceled || eventResult.hitTarget.kind != UIEditorVector4FieldHitTargetKind::None) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); SyncDisplayTexts(state, spec); SyncHoverTarget(state, layout); if (interactionResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && state.hasPointerPosition) { interactionResult.hitTarget = HitTestUIEditorVector4Field(layout, state.pointerPosition); } if (interactionResult.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { interactionResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; } return { std::move(layout), std::move(interactionResult) }; } } // namespace XCEngine::UI::Editor