Extract XCUI text input controller to common layer

This commit is contained in:
2026-04-05 06:33:06 +08:00
parent b4c95e4085
commit 6159eef3af
7 changed files with 354 additions and 123 deletions

View File

@@ -12,6 +12,7 @@
#include <XCEngine/UI/Style/StyleResolver.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Text/UITextEditing.h>
#include <XCEngine/UI/Text/UITextInputController.h>
#include <algorithm>
#include <chrono>
@@ -107,8 +108,7 @@ struct RuntimeBuildContext {
std::unordered_map<std::string, std::size_t> nodeIndexByKey = {};
std::unordered_map<UIElementId, std::size_t> nodeIndexById = {};
std::unordered_map<std::string, bool> toggleStates = {};
std::unordered_map<std::string, std::string> textFieldValues = {};
std::unordered_map<std::string, std::size_t> textFieldCarets = {};
std::unordered_map<std::string, UIText::UITextInputState> textInputStates = {};
UIElementId toggleButtonId = 0;
UIElementId registeredShortcutOwnerId = 0;
UIElementId armedElementId = 0;
@@ -780,17 +780,17 @@ std::string BuildNodeDisplayText(
std::string(diagnosticsEnabled ? "on" : "off");
}
if (node.elementKey == "promptMeta") {
const auto promptIt = state.textFieldValues.find("agentPrompt");
const auto promptIt = state.textInputStates.find("agentPrompt");
const std::string& promptValue =
promptIt != state.textFieldValues.end() ? promptIt->second : node.staticText;
promptIt != state.textInputStates.end() ? promptIt->second.value : node.staticText;
return "Single-line input, Enter submits, " +
std::to_string(static_cast<unsigned long long>(UIText::CountUtf8Codepoints(promptValue))) +
" chars";
}
if (node.elementKey == "notesMeta") {
const auto notesIt = state.textFieldValues.find("sessionNotes");
const auto notesIt = state.textInputStates.find("sessionNotes");
const std::string& notesValue =
notesIt != state.textFieldValues.end() ? notesIt->second : node.staticText;
notesIt != state.textInputStates.end() ? notesIt->second.value : node.staticText;
return "Multiline input, click caret, Tab indent, " +
std::to_string(static_cast<unsigned long long>(UIText::CountTextLines(notesValue))) +
" lines";
@@ -951,32 +951,30 @@ void EnsureTextInputStateInitialized(RuntimeBuildContext& state, const DemoNode&
}
const std::string stateKey = ResolveTextInputStateKey(node);
auto valueIt = state.textFieldValues.find(stateKey);
if (valueIt == state.textFieldValues.end()) {
valueIt = state.textFieldValues
.emplace(stateKey, GetNodeAttribute(node, "value"))
auto textInputIt = state.textInputStates.find(stateKey);
if (textInputIt == state.textInputStates.end()) {
UIText::UITextInputState textInput = {};
textInput.value = GetNodeAttribute(node, "value");
textInput.caret = textInput.value.size();
textInputIt = state.textInputStates
.emplace(stateKey, std::move(textInput))
.first;
}
auto caretIt = state.textFieldCarets.find(stateKey);
if (caretIt == state.textFieldCarets.end()) {
caretIt = state.textFieldCarets.emplace(stateKey, valueIt->second.size()).first;
}
caretIt->second = (std::min)(caretIt->second, valueIt->second.size());
UIText::ClampCaret(textInputIt->second);
}
std::string ResolveTextInputValue(RuntimeBuildContext& state, const DemoNode& node) {
EnsureTextInputStateInitialized(state, node);
return state.textFieldValues[ResolveTextInputStateKey(node)];
return state.textInputStates[ResolveTextInputStateKey(node)].value;
}
std::size_t ResolveTextInputCaret(RuntimeBuildContext& state, const DemoNode& node) {
EnsureTextInputStateInitialized(state, node);
const std::string stateKey = ResolveTextInputStateKey(node);
std::size_t& caret = state.textFieldCarets[stateKey];
caret = (std::min)(caret, state.textFieldValues[stateKey].size());
return caret;
UIText::UITextInputState& textInput = state.textInputStates[stateKey];
UIText::ClampCaret(textInput);
return textInput.caret;
}
DemoNode* TryGetNodeByElementId(RuntimeBuildContext& state, UIElementId elementId) {
@@ -1036,8 +1034,11 @@ void SetTextInputCaretFromPoint(
return;
}
EnsureTextInputStateInitialized(state, *node);
const std::string stateKey = ResolveTextInputStateKey(*node);
state.textFieldCarets[stateKey] = FindCaretOffsetFromPoint(state, *node, point);
UIText::UITextInputState& textInput = state.textInputStates[stateKey];
textInput.caret = FindCaretOffsetFromPoint(state, *node, point);
UIText::ClampCaret(textInput);
}
bool HandleTextInputCharacterInput(
@@ -1055,18 +1056,11 @@ bool HandleTextInputCharacterInput(
EnsureTextInputStateInitialized(state, *node);
const std::string stateKey = ResolveTextInputStateKey(*node);
std::string& value = state.textFieldValues[stateKey];
std::size_t& caret = state.textFieldCarets[stateKey];
caret = (std::min)(caret, value.size());
std::string encoded = {};
UIText::AppendUtf8Codepoint(encoded, character);
if (encoded.empty()) {
UIText::UITextInputState& textInput = state.textInputStates[stateKey];
if (!UIText::InsertCharacter(textInput, character)) {
return false;
}
value.insert(caret, encoded);
caret += encoded.size();
state.lastCommandId = "demo.text.edit." + stateKey;
return true;
}
@@ -1083,103 +1077,24 @@ bool HandleTextInputKeyDown(
EnsureTextInputStateInitialized(state, *node);
const std::string stateKey = ResolveTextInputStateKey(*node);
std::string& value = state.textFieldValues[stateKey];
std::size_t& caret = state.textFieldCarets[stateKey];
caret = (std::min)(caret, value.size());
UIText::UITextInputState& textInput = state.textInputStates[stateKey];
UIText::UITextInputOptions options = {};
options.multiline = IsTextAreaNode(*node);
options.tabWidth = kTextAreaTabWidth;
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
if (caret > 0u) {
const std::size_t previousCaret = UIText::RetreatUtf8Offset(value, caret);
value.erase(previousCaret, caret - previousCaret);
caret = previousCaret;
state.lastCommandId = "demo.text.edit." + stateKey;
}
return true;
const UIText::UITextInputEditResult result =
UIText::HandleKeyDown(textInput, keyCode, inputModifiers, options);
if (!result.handled) {
return false;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
if (caret < value.size()) {
const std::size_t nextCaret = UIText::AdvanceUtf8Offset(value, caret);
value.erase(caret, nextCaret - caret);
state.lastCommandId = "demo.text.edit." + stateKey;
}
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
caret = UIText::RetreatUtf8Offset(value, caret);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
caret = UIText::AdvanceUtf8Offset(value, caret);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Up) && IsTextAreaNode(*node)) {
caret = UIText::MoveCaretVertically(value, caret, -1);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Down) && IsTextAreaNode(*node)) {
caret = UIText::MoveCaretVertically(value, caret, 1);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Home)) {
caret = IsTextAreaNode(*node)
? UIText::FindLineStartOffset(value, caret)
: 0u;
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::End)) {
caret = IsTextAreaNode(*node)
? UIText::FindLineEndOffset(value, caret)
: value.size();
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Enter)) {
if (IsTextAreaNode(*node)) {
value.insert(caret, "\n");
++caret;
state.lastCommandId = "demo.text.edit." + stateKey;
return true;
}
if (result.submitRequested) {
state.lastCommandId = "demo.text.submit." + stateKey;
return true;
} else if (result.valueChanged) {
state.lastCommandId = "demo.text.edit." + stateKey;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Tab) &&
IsTextAreaNode(*node) &&
!inputModifiers.control &&
!inputModifiers.alt &&
!inputModifiers.super) {
if (inputModifiers.shift) {
const std::size_t lineStart = UIText::FindLineStartOffset(value, caret);
std::size_t removed = 0u;
while (removed < kTextAreaTabWidth &&
lineStart + removed < value.size() &&
value[lineStart + removed] == ' ') {
++removed;
}
if (removed > 0u) {
value.erase(lineStart, removed);
caret = caret >= lineStart + removed ? caret - removed : lineStart;
state.lastCommandId = "demo.text.edit." + stateKey;
}
} else {
value.insert(caret, std::string(kTextAreaTabWidth, ' '));
caret += kTextAreaTabWidth;
state.lastCommandId = "demo.text.edit." + stateKey;
}
return true;
}
return false;
return true;
}
std::string BuildActivationCommandId(const DemoNode& node) {