Extract XCUI text input controller to common layer
This commit is contained in:
@@ -30,13 +30,14 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
||||
- Shared engine-side XCUI runtime scaffolding is now present under `engine/include/XCEngine/UI/Runtime` and `engine/src/UI/Runtime`.
|
||||
- Shared engine-side `UIDocumentScreenHost` now compiles `.xcui` / `.xctheme` screen documents into a runtime-facing document host path instead of leaving all document ownership in `new_editor`.
|
||||
- Shared text-editing primitives now live under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so UTF-8 caret movement, line splitting, and multiline navigation are no longer trapped inside `XCUI Demo`.
|
||||
- Shared text-input controller/state now also lives under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so character insertion, backspace/delete, submit, and multiline key handling no longer need to be reimplemented per host.
|
||||
- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`.
|
||||
|
||||
Current gap:
|
||||
|
||||
- Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented.
|
||||
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
||||
- Common widget primitives are still incomplete: reusable text-input state/view composition on top of the new text helpers, tree/list virtualization, property-grid composition, and native image/source-rect level APIs.
|
||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, tree/list virtualization, property-grid composition, and native image/source-rect level APIs.
|
||||
|
||||
### 2. Runtime/Game Layer
|
||||
|
||||
@@ -57,6 +58,7 @@ Current gap:
|
||||
- Native hosted preview is working as `RHI offscreen surface -> ImGui shell texture embed`.
|
||||
- `XCUI Demo` remains the long-lived effect and behavior testbed.
|
||||
- `XCUI Demo` now covers both single-line and multiline text authoring behavior, including click caret placement, delete/backspace, tab indentation, and optional text-area line numbers.
|
||||
- `XCUI Demo` now consumes the shared `UITextInputController` path for text editing instead of carrying a private key-handling state machine.
|
||||
- `LayoutLab` now includes a `ScrollView` prototype and a more editor-like three-column authored layout.
|
||||
- `LayoutLab` now also covers editor-facing widget prototypes: `TreeView`, `TreeItem`, `ListView`, `ListItem`, `PropertySection`, and `FieldRow`.
|
||||
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||
@@ -76,7 +78,7 @@ Current gap:
|
||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||
- `XCNewEditor` Debug target builds successfully
|
||||
- `core_ui_tests`: `21/21`
|
||||
- `core_ui_tests`: `26/26`
|
||||
- `core_ui_style_tests`: `5/5`
|
||||
- `ui_resource_tests`: `11/11`
|
||||
- `editor_tests` targeted bridge smoke: `3/3`
|
||||
@@ -86,6 +88,7 @@ Current gap:
|
||||
- Demo runtime `TextField` with UTF-8 text insertion, caret state, and backspace.
|
||||
- Demo runtime multiline `TextArea` path in the sandbox and test coverage for caret movement / multiline input.
|
||||
- Common-core `UITextEditing` extraction now owns UTF-8 offset stepping, codepoint counting, line splitting, and vertical caret motion with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UITextInputController` extraction now owns per-field text state, character insertion, enter-submit, and multiline keyboard editing behavior with dedicated `core_ui_tests` coverage.
|
||||
- Demo runtime text editing was extended with:
|
||||
- click-to-place caret
|
||||
- `Delete` support
|
||||
|
||||
@@ -517,7 +517,9 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextEditing.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextInputController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
||||
|
||||
39
engine/include/XCEngine/UI/Text/UITextInputController.h
Normal file
39
engine/include/XCEngine/UI/Text/UITextInputController.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Text {
|
||||
|
||||
struct UITextInputState {
|
||||
std::string value = {};
|
||||
std::size_t caret = 0u;
|
||||
};
|
||||
|
||||
struct UITextInputOptions {
|
||||
bool multiline = false;
|
||||
std::size_t tabWidth = 4u;
|
||||
};
|
||||
|
||||
struct UITextInputEditResult {
|
||||
bool handled = false;
|
||||
bool valueChanged = false;
|
||||
bool submitRequested = false;
|
||||
};
|
||||
|
||||
void ClampCaret(UITextInputState& state);
|
||||
bool InsertCharacter(UITextInputState& state, std::uint32_t character);
|
||||
UITextInputEditResult HandleKeyDown(
|
||||
UITextInputState& state,
|
||||
std::int32_t keyCode,
|
||||
const UIInputModifiers& modifiers,
|
||||
const UITextInputOptions& options = {});
|
||||
|
||||
} // namespace Text
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
156
engine/src/UI/Text/UITextInputController.cpp
Normal file
156
engine/src/UI/Text/UITextInputController.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Text/UITextEditing.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Text {
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
} // namespace
|
||||
|
||||
void ClampCaret(UITextInputState& state) {
|
||||
state.caret = (std::min)(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
bool InsertCharacter(UITextInputState& state, std::uint32_t character) {
|
||||
if (character < 32u || character == 127u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string encoded = {};
|
||||
AppendUtf8Codepoint(encoded, character);
|
||||
if (encoded.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ClampCaret(state);
|
||||
state.value.insert(state.caret, encoded);
|
||||
state.caret += encoded.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
UITextInputEditResult HandleKeyDown(
|
||||
UITextInputState& state,
|
||||
std::int32_t keyCode,
|
||||
const UIInputModifiers& modifiers,
|
||||
const UITextInputOptions& options) {
|
||||
ClampCaret(state);
|
||||
|
||||
UITextInputEditResult result = {};
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||
result.handled = true;
|
||||
if (state.caret > 0u) {
|
||||
const std::size_t previousCaret = RetreatUtf8Offset(state.value, state.caret);
|
||||
state.value.erase(previousCaret, state.caret - previousCaret);
|
||||
state.caret = previousCaret;
|
||||
result.valueChanged = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
|
||||
result.handled = true;
|
||||
if (state.caret < state.value.size()) {
|
||||
const std::size_t nextCaret = AdvanceUtf8Offset(state.value, state.caret);
|
||||
state.value.erase(state.caret, nextCaret - state.caret);
|
||||
result.valueChanged = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
|
||||
result.handled = true;
|
||||
state.caret = RetreatUtf8Offset(state.value, state.caret);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
|
||||
result.handled = true;
|
||||
state.caret = AdvanceUtf8Offset(state.value, state.caret);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Up) && options.multiline) {
|
||||
result.handled = true;
|
||||
state.caret = MoveCaretVertically(state.value, state.caret, -1);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Down) && options.multiline) {
|
||||
result.handled = true;
|
||||
state.caret = MoveCaretVertically(state.value, state.caret, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Home)) {
|
||||
result.handled = true;
|
||||
state.caret = options.multiline
|
||||
? FindLineStartOffset(state.value, state.caret)
|
||||
: 0u;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::End)) {
|
||||
result.handled = true;
|
||||
state.caret = options.multiline
|
||||
? FindLineEndOffset(state.value, state.caret)
|
||||
: state.value.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Enter)) {
|
||||
result.handled = true;
|
||||
if (options.multiline) {
|
||||
state.value.insert(state.caret, "\n");
|
||||
++state.caret;
|
||||
result.valueChanged = true;
|
||||
} else {
|
||||
result.submitRequested = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Tab) &&
|
||||
options.multiline &&
|
||||
!modifiers.control &&
|
||||
!modifiers.alt &&
|
||||
!modifiers.super) {
|
||||
result.handled = true;
|
||||
|
||||
if (modifiers.shift) {
|
||||
const std::size_t lineStart = FindLineStartOffset(state.value, state.caret);
|
||||
std::size_t removed = 0u;
|
||||
while (removed < options.tabWidth &&
|
||||
lineStart + removed < state.value.size() &&
|
||||
state.value[lineStart + removed] == ' ') {
|
||||
++removed;
|
||||
}
|
||||
|
||||
if (removed > 0u) {
|
||||
state.value.erase(lineStart, removed);
|
||||
state.caret = state.caret >= lineStart + removed ? state.caret - removed : lineStart;
|
||||
result.valueChanged = true;
|
||||
}
|
||||
} else {
|
||||
state.value.insert(state.caret, std::string(options.tabWidth, ' '));
|
||||
state.caret += options.tabWidth;
|
||||
result.valueChanged = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Text
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ set(UI_TEST_SOURCES
|
||||
test_layout_engine.cpp
|
||||
test_ui_runtime.cpp
|
||||
test_ui_text_editing.cpp
|
||||
test_ui_text_input_controller.cpp
|
||||
)
|
||||
|
||||
add_executable(core_ui_tests ${UI_TEST_SOURCES})
|
||||
|
||||
115
tests/Core/UI/test_ui_text_input_controller.cpp
Normal file
115
tests/Core/UI/test_ui_text_input_controller.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace UIText = XCEngine::UI::Text;
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
TEST(UITextInputControllerTest, InsertCharacterTracksUtf8CaretMovement) {
|
||||
UIText::UITextInputState state = {};
|
||||
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, 'A'));
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, 0x4F60u));
|
||||
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, BackspaceAndArrowKeysUseUtf8Boundaries) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||
state.caret = state.value.size();
|
||||
|
||||
const auto moveLeft = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Left),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(moveLeft.handled);
|
||||
EXPECT_EQ(state.caret, 4u);
|
||||
|
||||
const auto backspace = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Backspace),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(backspace.handled);
|
||||
EXPECT_TRUE(backspace.valueChanged);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, 1u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, SingleLineEnterRequestsSubmitWithoutMutatingValue) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "prompt";
|
||||
state.caret = state.value.size();
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Enter),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_TRUE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "prompt");
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineEnterAndVerticalMovementStayInController) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "Z\nBC";
|
||||
state.caret = 4u;
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto moveDown = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Down),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(moveDown.handled);
|
||||
EXPECT_EQ(state.caret, 8u);
|
||||
|
||||
const auto enter = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Enter),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(enter.handled);
|
||||
EXPECT_TRUE(enter.valueChanged);
|
||||
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n");
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLine) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto indent = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(indent.handled);
|
||||
EXPECT_TRUE(indent.valueChanged);
|
||||
EXPECT_EQ(state.value, "root\n node");
|
||||
EXPECT_EQ(state.caret, 9u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
const auto outdent = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
modifiers,
|
||||
options);
|
||||
EXPECT_TRUE(outdent.handled);
|
||||
EXPECT_TRUE(outdent.valueChanged);
|
||||
EXPECT_EQ(state.value, "root\nnode");
|
||||
EXPECT_EQ(state.caret, 5u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user