2026-04-10 00:41:28 +08:00
|
|
|
#include <XCEditor/Fields/UIEditorTextFieldInteraction.h>
|
2026-04-08 02:52:28 +08:00
|
|
|
|
|
|
|
|
#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 ::XCEngine::UI::Text::HandleKeyDown;
|
|
|
|
|
using ::XCEngine::UI::Text::InsertCharacter;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::IsUIEditorTextFieldPointInside;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldLayout;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics;
|
|
|
|
|
using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SyncDisplayText(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
const UIEditorTextFieldSpec& spec) {
|
|
|
|
|
if (!state.textFieldState.editing) {
|
|
|
|
|
state.textFieldState.displayText = spec.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SyncHoverTarget(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
const UIEditorTextFieldLayout& layout) {
|
|
|
|
|
if (!state.hasPointerPosition) {
|
|
|
|
|
state.textFieldState.hoveredTarget = UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.textFieldState.hoveredTarget =
|
|
|
|
|
HitTestUIEditorTextField(layout, state.pointerPosition).kind;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BeginEdit(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
const UIEditorTextFieldSpec& spec,
|
|
|
|
|
bool clearText) {
|
|
|
|
|
if (spec.readOnly) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool changed = state.editModel.BeginEdit(spec.fieldId, spec.value);
|
|
|
|
|
if (!changed &&
|
|
|
|
|
state.editModel.HasActiveEdit() &&
|
|
|
|
|
state.editModel.GetActiveFieldId() != spec.fieldId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!changed && state.textFieldState.editing) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.textFieldState.editing = true;
|
|
|
|
|
state.textInputState.value = clearText ? std::string() : spec.value;
|
|
|
|
|
state.textInputState.caret = state.textInputState.value.size();
|
|
|
|
|
state.editModel.UpdateStagedValue(state.textInputState.value);
|
|
|
|
|
state.textFieldState.displayText = state.textInputState.value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CommitEdit(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
UIEditorTextFieldSpec& spec,
|
|
|
|
|
UIEditorTextFieldInteractionResult& result) {
|
|
|
|
|
if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.valueBefore = spec.value;
|
|
|
|
|
spec.value = state.textInputState.value;
|
|
|
|
|
result.valueAfter = spec.value;
|
|
|
|
|
result.valueChanged = result.valueBefore != result.valueAfter;
|
|
|
|
|
result.editCommitted = true;
|
|
|
|
|
result.consumed = true;
|
|
|
|
|
result.committedText = spec.value;
|
|
|
|
|
|
|
|
|
|
state.editModel.CommitEdit();
|
|
|
|
|
state.textInputState = {};
|
|
|
|
|
state.textFieldState.editing = false;
|
|
|
|
|
state.textFieldState.displayText = spec.value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CancelEdit(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
const UIEditorTextFieldSpec& spec,
|
|
|
|
|
UIEditorTextFieldInteractionResult& result) {
|
|
|
|
|
if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.editModel.CancelEdit();
|
|
|
|
|
state.textInputState = {};
|
|
|
|
|
state.textFieldState.editing = false;
|
|
|
|
|
state.textFieldState.displayText = spec.value;
|
|
|
|
|
result.consumed = true;
|
|
|
|
|
result.editCanceled = true;
|
|
|
|
|
result.valueBefore = spec.value;
|
|
|
|
|
result.valueAfter = spec.value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction(
|
|
|
|
|
UIEditorTextFieldInteractionState& state,
|
|
|
|
|
UIEditorTextFieldSpec& spec,
|
|
|
|
|
const ::XCEngine::UI::UIRect& bounds,
|
|
|
|
|
const std::vector<UIInputEvent>& inputEvents,
|
|
|
|
|
const UIEditorTextFieldMetrics& metrics) {
|
|
|
|
|
UIEditorTextFieldLayout layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics);
|
|
|
|
|
SyncDisplayText(state, spec);
|
|
|
|
|
SyncHoverTarget(state, layout);
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldInteractionResult 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldInteractionResult eventResult = {};
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case UIInputEventType::FocusGained:
|
|
|
|
|
eventResult.focusChanged = !state.textFieldState.focused;
|
|
|
|
|
state.textFieldState.focused = true;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::FocusLost:
|
|
|
|
|
eventResult.focusChanged = state.textFieldState.focused;
|
|
|
|
|
state.textFieldState.focused = false;
|
|
|
|
|
state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
state.hasPointerPosition = false;
|
|
|
|
|
if (state.textFieldState.editing) {
|
|
|
|
|
CommitEdit(state, spec, eventResult);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerMove:
|
|
|
|
|
case UIInputEventType::PointerEnter:
|
|
|
|
|
case UIInputEventType::PointerLeave:
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerButtonDown: {
|
|
|
|
|
const UIEditorTextFieldHitTarget hitTarget =
|
|
|
|
|
state.hasPointerPosition
|
|
|
|
|
? HitTestUIEditorTextField(layout, state.pointerPosition)
|
|
|
|
|
: UIEditorTextFieldHitTarget {};
|
|
|
|
|
eventResult.hitTarget = hitTarget;
|
|
|
|
|
|
|
|
|
|
if (event.pointerButton != UIPointerButton::Left) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool insideField =
|
|
|
|
|
state.hasPointerPosition &&
|
|
|
|
|
IsUIEditorTextFieldPointInside(layout.bounds, state.pointerPosition);
|
|
|
|
|
if (insideField) {
|
|
|
|
|
eventResult.focusChanged = !state.textFieldState.focused;
|
|
|
|
|
state.textFieldState.focused = true;
|
|
|
|
|
state.textFieldState.activeTarget =
|
|
|
|
|
hitTarget.kind == UIEditorTextFieldHitTargetKind::None
|
|
|
|
|
? UIEditorTextFieldHitTargetKind::Row
|
|
|
|
|
: hitTarget.kind;
|
|
|
|
|
eventResult.consumed = true;
|
|
|
|
|
} else {
|
|
|
|
|
if (state.textFieldState.editing) {
|
|
|
|
|
CommitEdit(state, spec, eventResult);
|
|
|
|
|
eventResult.focusChanged = state.textFieldState.focused;
|
|
|
|
|
state.textFieldState.focused = false;
|
|
|
|
|
} else if (state.textFieldState.focused) {
|
|
|
|
|
eventResult.focusChanged = true;
|
|
|
|
|
state.textFieldState.focused = false;
|
|
|
|
|
}
|
|
|
|
|
state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::PointerButtonUp: {
|
|
|
|
|
const UIEditorTextFieldHitTarget hitTarget =
|
|
|
|
|
state.hasPointerPosition
|
|
|
|
|
? HitTestUIEditorTextField(layout, state.pointerPosition)
|
|
|
|
|
: UIEditorTextFieldHitTarget {};
|
|
|
|
|
eventResult.hitTarget = hitTarget;
|
|
|
|
|
|
|
|
|
|
if (event.pointerButton == UIPointerButton::Left) {
|
|
|
|
|
const UIEditorTextFieldHitTargetKind activeTarget = state.textFieldState.activeTarget;
|
|
|
|
|
state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
|
|
|
|
|
if (activeTarget == UIEditorTextFieldHitTargetKind::ValueBox &&
|
|
|
|
|
hitTarget.kind == UIEditorTextFieldHitTargetKind::ValueBox) {
|
|
|
|
|
if (!state.textFieldState.editing) {
|
|
|
|
|
eventResult.editStarted = BeginEdit(state, spec, false);
|
|
|
|
|
}
|
|
|
|
|
eventResult.consumed = true;
|
|
|
|
|
} else if (hitTarget.kind == UIEditorTextFieldHitTargetKind::Row) {
|
|
|
|
|
eventResult.consumed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::KeyDown:
|
|
|
|
|
if (!state.textFieldState.focused) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.textFieldState.editing) {
|
|
|
|
|
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape)) {
|
|
|
|
|
CancelEdit(state, spec, eventResult);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto textResult =
|
|
|
|
|
HandleKeyDown(state.textInputState, event.keyCode, event.modifiers);
|
|
|
|
|
if (textResult.handled) {
|
|
|
|
|
state.editModel.UpdateStagedValue(state.textInputState.value);
|
|
|
|
|
state.textFieldState.displayText = state.textInputState.value;
|
|
|
|
|
eventResult.consumed = true;
|
|
|
|
|
eventResult.valueBefore = spec.value;
|
|
|
|
|
eventResult.valueAfter = spec.value;
|
|
|
|
|
if (textResult.submitRequested) {
|
|
|
|
|
CommitEdit(state, spec, eventResult);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (event.keyCode == static_cast<std::int32_t>(KeyCode::Enter)) {
|
|
|
|
|
eventResult.editStarted = BeginEdit(state, spec, false);
|
|
|
|
|
eventResult.consumed = eventResult.editStarted;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIInputEventType::Character:
|
|
|
|
|
if (!state.textFieldState.focused ||
|
|
|
|
|
spec.readOnly ||
|
|
|
|
|
event.modifiers.control ||
|
|
|
|
|
event.modifiers.alt ||
|
|
|
|
|
event.modifiers.super) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!state.textFieldState.editing) {
|
|
|
|
|
eventResult.editStarted = BeginEdit(state, spec, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (InsertCharacter(state.textInputState, event.character)) {
|
|
|
|
|
state.editModel.UpdateStagedValue(state.textInputState.value);
|
|
|
|
|
state.textFieldState.displayText = state.textInputState.value;
|
|
|
|
|
eventResult.consumed = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics);
|
|
|
|
|
SyncDisplayText(state, spec);
|
|
|
|
|
SyncHoverTarget(state, layout);
|
|
|
|
|
if (eventResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None &&
|
|
|
|
|
state.hasPointerPosition) {
|
|
|
|
|
eventResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eventResult.consumed ||
|
|
|
|
|
eventResult.focusChanged ||
|
|
|
|
|
eventResult.valueChanged ||
|
|
|
|
|
eventResult.editStarted ||
|
|
|
|
|
eventResult.editCommitted ||
|
|
|
|
|
eventResult.editCanceled ||
|
|
|
|
|
eventResult.hitTarget.kind != UIEditorTextFieldHitTargetKind::None) {
|
|
|
|
|
interactionResult = std::move(eventResult);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics);
|
|
|
|
|
SyncDisplayText(state, spec);
|
|
|
|
|
SyncHoverTarget(state, layout);
|
|
|
|
|
if (interactionResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None &&
|
|
|
|
|
state.hasPointerPosition) {
|
|
|
|
|
interactionResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
std::move(layout),
|
|
|
|
|
std::move(interactionResult)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace XCEngine::UI::Editor
|