Extract XCUI text editing core and window seam headers
This commit is contained in:
@@ -15,7 +15,7 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
|||||||
- engine runtime coverage was tightened again around `UISystem` and concrete document-host rendering
|
- engine runtime coverage was tightened again around `UISystem` and concrete document-host rendering
|
||||||
- `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls
|
- `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls
|
||||||
- the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope
|
- the demo sandbox and editor bridge APIs were tightened again without touching the old editor replacement scope
|
||||||
- `new_editor` now has an explicit window-level compositor seam through `IEditorHostCompositor` / `ImGuiHostCompositor`
|
- `new_editor` now has an explicit two-step window compositor seam through `IWindowUICompositor` / `ImGuiWindowUICompositor`, layered on top of `IEditorHostCompositor` / `ImGuiHostCompositor`
|
||||||
- Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`.
|
- Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`.
|
||||||
|
|
||||||
## Three-Layer Status
|
## Three-Layer Status
|
||||||
@@ -29,13 +29,14 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
|||||||
- Build-system hardening for MSVC/PDB output paths has started in root CMake, `engine/CMakeLists.txt`, `new_editor/CMakeLists.txt`, and `tests/NewEditor/CMakeLists.txt`.
|
- Build-system hardening for MSVC/PDB output paths has started in root CMake, `engine/CMakeLists.txt`, `new_editor/CMakeLists.txt`, and `tests/NewEditor/CMakeLists.txt`.
|
||||||
- Shared engine-side XCUI runtime scaffolding is now present under `engine/include/XCEngine/UI/Runtime` and `engine/src/UI/Runtime`.
|
- 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 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`.
|
||||||
- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`.
|
- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`.
|
||||||
|
|
||||||
Current gap:
|
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.
|
- 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.
|
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
||||||
- Common widget primitives are still incomplete: multiline text editing, tree/list virtualization, property-grid composition, and native image/source-rect level APIs.
|
- 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.
|
||||||
|
|
||||||
### 2. Runtime/Game Layer
|
### 2. Runtime/Game Layer
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ Current gap:
|
|||||||
- `LayoutLab` now also covers editor-facing widget prototypes: `TreeView`, `TreeItem`, `ListView`, `ListItem`, `PropertySection`, and `FieldRow`.
|
- `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.
|
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||||
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
||||||
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IEditorHostCompositor` with an `ImGuiHostCompositor` implementation.
|
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
|
||||||
- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
|
- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
|
||||||
|
|
||||||
Current gap:
|
Current gap:
|
||||||
@@ -75,7 +76,7 @@ Current gap:
|
|||||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||||
- `XCNewEditor` Debug target builds successfully
|
- `XCNewEditor` Debug target builds successfully
|
||||||
- `core_ui_tests`: `16/16`
|
- `core_ui_tests`: `21/21`
|
||||||
- `core_ui_style_tests`: `5/5`
|
- `core_ui_style_tests`: `5/5`
|
||||||
- `ui_resource_tests`: `11/11`
|
- `ui_resource_tests`: `11/11`
|
||||||
- `editor_tests` targeted bridge smoke: `3/3`
|
- `editor_tests` targeted bridge smoke: `3/3`
|
||||||
@@ -84,6 +85,7 @@ Current gap:
|
|||||||
|
|
||||||
- Demo runtime `TextField` with UTF-8 text insertion, caret state, and backspace.
|
- 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.
|
- 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.
|
||||||
- Demo runtime text editing was extended with:
|
- Demo runtime text editing was extended with:
|
||||||
- click-to-place caret
|
- click-to-place caret
|
||||||
- `Delete` support
|
- `Delete` support
|
||||||
@@ -119,6 +121,8 @@ Current gap:
|
|||||||
- SRV-view based texture descriptor registration in `ImGuiBackendBridge`
|
- SRV-view based texture descriptor registration in `ImGuiBackendBridge`
|
||||||
- smoke tests for window renderer, ImGui backend bridge, and console sink registration
|
- smoke tests for window renderer, ImGui backend bridge, and console sink registration
|
||||||
- `new_editor` host presentation now has a first-class compositor seam:
|
- `new_editor` host presentation now has a first-class compositor seam:
|
||||||
|
- `IWindowUICompositor`
|
||||||
|
- `ImGuiWindowUICompositor`
|
||||||
- `IEditorHostCompositor`
|
- `IEditorHostCompositor`
|
||||||
- `ImGuiHostCompositor`
|
- `ImGuiHostCompositor`
|
||||||
- `Application` frame/present flow routed through the compositor instead of direct `m_imguiBackend` ownership
|
- `Application` frame/present flow routed through the compositor instead of direct `m_imguiBackend` ownership
|
||||||
|
|||||||
@@ -516,6 +516,8 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputRouter.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputRouter.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Text/UITextEditing.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
|
${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/UIScreenDocumentHost.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
||||||
|
|||||||
29
engine/include/XCEngine/UI/Text/UITextEditing.h
Normal file
29
engine/include/XCEngine/UI/Text/UITextEditing.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Text {
|
||||||
|
|
||||||
|
std::size_t CountUtf8Codepoints(const std::string& text);
|
||||||
|
std::size_t AdvanceUtf8Offset(const std::string& text, std::size_t offset);
|
||||||
|
std::size_t RetreatUtf8Offset(const std::string& text, std::size_t offset);
|
||||||
|
void AppendUtf8Codepoint(std::string& text, std::uint32_t codepoint);
|
||||||
|
|
||||||
|
std::vector<std::string> SplitLines(const std::string& text);
|
||||||
|
std::size_t CountTextLines(const std::string& text);
|
||||||
|
std::size_t CountUtf8CodepointsInRange(
|
||||||
|
const std::string& text,
|
||||||
|
std::size_t beginOffset,
|
||||||
|
std::size_t endOffset);
|
||||||
|
std::size_t FindLineStartOffset(const std::string& text, std::size_t caret);
|
||||||
|
std::size_t FindLineEndOffset(const std::string& text, std::size_t caret);
|
||||||
|
std::size_t MoveCaretVertically(const std::string& text, std::size_t caret, int lineDelta);
|
||||||
|
|
||||||
|
} // namespace Text
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
179
engine/src/UI/Text/UITextEditing.cpp
Normal file
179
engine/src/UI/Text/UITextEditing.cpp
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#include <XCEngine/UI/Text/UITextEditing.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Text {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool IsUtf8ContinuationByte(unsigned char value) {
|
||||||
|
return (value & 0xC0u) == 0x80u;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::size_t CountUtf8Codepoints(const std::string& text) {
|
||||||
|
std::size_t count = 0u;
|
||||||
|
for (unsigned char ch : text) {
|
||||||
|
if (!IsUtf8ContinuationByte(ch)) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t AdvanceUtf8Offset(const std::string& text, std::size_t offset) {
|
||||||
|
if (offset >= text.size()) {
|
||||||
|
return text.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
++offset;
|
||||||
|
while (offset < text.size() && IsUtf8ContinuationByte(static_cast<unsigned char>(text[offset]))) {
|
||||||
|
++offset;
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t RetreatUtf8Offset(const std::string& text, std::size_t offset) {
|
||||||
|
if (offset == 0u || text.empty()) {
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = (std::min)(offset, text.size());
|
||||||
|
do {
|
||||||
|
--offset;
|
||||||
|
} while (offset > 0u && IsUtf8ContinuationByte(static_cast<unsigned char>(text[offset])));
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUtf8Codepoint(std::string& text, std::uint32_t codepoint) {
|
||||||
|
if (codepoint <= 0x7Fu) {
|
||||||
|
text.push_back(static_cast<char>(codepoint));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codepoint <= 0x7FFu) {
|
||||||
|
text.push_back(static_cast<char>(0xC0u | ((codepoint >> 6u) & 0x1Fu)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codepoint >= 0xD800u && codepoint <= 0xDFFFu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codepoint <= 0xFFFFu) {
|
||||||
|
text.push_back(static_cast<char>(0xE0u | ((codepoint >> 12u) & 0x0Fu)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | ((codepoint >> 6u) & 0x3Fu)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codepoint <= 0x10FFFFu) {
|
||||||
|
text.push_back(static_cast<char>(0xF0u | ((codepoint >> 18u) & 0x07u)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | ((codepoint >> 12u) & 0x3Fu)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | ((codepoint >> 6u) & 0x3Fu)));
|
||||||
|
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> SplitLines(const std::string& text) {
|
||||||
|
std::vector<std::string> lines = {};
|
||||||
|
std::size_t lineStart = 0u;
|
||||||
|
for (std::size_t index = 0u; index < text.size(); ++index) {
|
||||||
|
if (text[index] != '\n') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push_back(text.substr(lineStart, index - lineStart));
|
||||||
|
lineStart = index + 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push_back(text.substr(lineStart));
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CountTextLines(const std::string& text) {
|
||||||
|
return SplitLines(text).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CountUtf8CodepointsInRange(
|
||||||
|
const std::string& text,
|
||||||
|
std::size_t beginOffset,
|
||||||
|
std::size_t endOffset) {
|
||||||
|
beginOffset = (std::min)(beginOffset, text.size());
|
||||||
|
endOffset = (std::min)(endOffset, text.size());
|
||||||
|
if (endOffset <= beginOffset) {
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t count = 0u;
|
||||||
|
std::size_t offset = beginOffset;
|
||||||
|
while (offset < endOffset) {
|
||||||
|
++count;
|
||||||
|
offset = AdvanceUtf8Offset(text, offset);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t FindLineStartOffset(const std::string& text, std::size_t caret) {
|
||||||
|
caret = (std::min)(caret, text.size());
|
||||||
|
while (caret > 0u && text[caret - 1u] != '\n') {
|
||||||
|
--caret;
|
||||||
|
}
|
||||||
|
return caret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t FindLineEndOffset(const std::string& text, std::size_t caret) {
|
||||||
|
caret = (std::min)(caret, text.size());
|
||||||
|
while (caret < text.size() && text[caret] != '\n') {
|
||||||
|
++caret;
|
||||||
|
}
|
||||||
|
return caret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t MoveCaretVertically(const std::string& text, std::size_t caret, int lineDelta) {
|
||||||
|
if (lineDelta == 0 || text.empty()) {
|
||||||
|
return (std::min)(caret, text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t currentLineStart = FindLineStartOffset(text, caret);
|
||||||
|
const std::size_t currentLineEnd = FindLineEndOffset(text, caret);
|
||||||
|
const std::size_t column = CountUtf8CodepointsInRange(text, currentLineStart, caret);
|
||||||
|
|
||||||
|
if (lineDelta < 0) {
|
||||||
|
if (currentLineStart == 0u) {
|
||||||
|
return caret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t previousLineEnd = currentLineStart - 1u;
|
||||||
|
const std::size_t previousLineStart = FindLineStartOffset(text, previousLineEnd);
|
||||||
|
const std::size_t previousLineLength =
|
||||||
|
CountUtf8CodepointsInRange(text, previousLineStart, previousLineEnd);
|
||||||
|
std::size_t offset = previousLineStart;
|
||||||
|
for (std::size_t step = 0u; step < (std::min)(column, previousLineLength); ++step) {
|
||||||
|
offset = AdvanceUtf8Offset(text, offset);
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLineEnd >= text.size()) {
|
||||||
|
return caret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t nextLineStart = currentLineEnd + 1u;
|
||||||
|
const std::size_t nextLineEnd = FindLineEndOffset(text, nextLineStart);
|
||||||
|
const std::size_t nextLineLength =
|
||||||
|
CountUtf8CodepointsInRange(text, nextLineStart, nextLineEnd);
|
||||||
|
std::size_t offset = nextLineStart;
|
||||||
|
for (std::size_t step = 0u; step < (std::min)(column, nextLineLength); ++step) {
|
||||||
|
offset = AdvanceUtf8Offset(text, offset);
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Text
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
52
new_editor/src/XCUIBackend/IWindowUICompositor.h
Normal file
52
new_editor/src/XCUIBackend/IWindowUICompositor.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Platform/D3D12WindowRenderer.h"
|
||||||
|
|
||||||
|
#include <XCEngine/RHI/RHIDevice.h>
|
||||||
|
#include <XCEngine/RHI/RHITexture.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Editor {
|
||||||
|
namespace XCUIBackend {
|
||||||
|
|
||||||
|
class IWindowUICompositor {
|
||||||
|
public:
|
||||||
|
using ConfigureFontsCallback = std::function<void()>;
|
||||||
|
using UiRenderCallback = std::function<void()>;
|
||||||
|
using RenderCallback = ::XCEngine::Editor::Platform::D3D12WindowRenderer::RenderCallback;
|
||||||
|
|
||||||
|
virtual ~IWindowUICompositor() = default;
|
||||||
|
|
||||||
|
virtual bool Initialize(
|
||||||
|
HWND hwnd,
|
||||||
|
::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer,
|
||||||
|
const ConfigureFontsCallback& configureFonts) = 0;
|
||||||
|
virtual void Shutdown() = 0;
|
||||||
|
virtual bool HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) = 0;
|
||||||
|
virtual void RenderFrame(
|
||||||
|
const float clearColor[4],
|
||||||
|
const UiRenderCallback& renderUi,
|
||||||
|
const RenderCallback& beforeUiRender = {},
|
||||||
|
const RenderCallback& afterUiRender = {}) = 0;
|
||||||
|
virtual bool CreateTextureDescriptor(
|
||||||
|
::XCEngine::RHI::RHIDevice* device,
|
||||||
|
::XCEngine::RHI::RHITexture* texture,
|
||||||
|
D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle,
|
||||||
|
D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle,
|
||||||
|
ImTextureID* outTextureId) = 0;
|
||||||
|
virtual void FreeTextureDescriptor(
|
||||||
|
D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
|
||||||
|
D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<IWindowUICompositor> CreateImGuiWindowUICompositor();
|
||||||
|
|
||||||
|
} // namespace XCUIBackend
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
94
new_editor/src/XCUIBackend/ImGuiWindowUICompositor.h
Normal file
94
new_editor/src/XCUIBackend/ImGuiWindowUICompositor.h
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "XCUIBackend/IEditorHostCompositor.h"
|
||||||
|
#include "XCUIBackend/IWindowUICompositor.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Editor {
|
||||||
|
namespace XCUIBackend {
|
||||||
|
|
||||||
|
class ImGuiWindowUICompositor final : public IWindowUICompositor {
|
||||||
|
public:
|
||||||
|
explicit ImGuiWindowUICompositor(
|
||||||
|
std::unique_ptr<IEditorHostCompositor> hostCompositor = CreateImGuiHostCompositor())
|
||||||
|
: m_hostCompositor(std::move(hostCompositor)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize(
|
||||||
|
HWND hwnd,
|
||||||
|
::XCEngine::Editor::Platform::D3D12WindowRenderer& windowRenderer,
|
||||||
|
const ConfigureFontsCallback& configureFonts) override {
|
||||||
|
if (m_hostCompositor == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_windowRenderer = &windowRenderer;
|
||||||
|
return m_hostCompositor->Initialize(hwnd, windowRenderer, configureFonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() override {
|
||||||
|
if (m_hostCompositor != nullptr) {
|
||||||
|
m_hostCompositor->Shutdown();
|
||||||
|
}
|
||||||
|
m_windowRenderer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HandleWindowMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) override {
|
||||||
|
return m_hostCompositor != nullptr &&
|
||||||
|
m_hostCompositor->HandleWindowMessage(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderFrame(
|
||||||
|
const float clearColor[4],
|
||||||
|
const UiRenderCallback& renderUi,
|
||||||
|
const RenderCallback& beforeUiRender,
|
||||||
|
const RenderCallback& afterUiRender) override {
|
||||||
|
if (m_hostCompositor == nullptr || m_windowRenderer == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hostCompositor->BeginFrame();
|
||||||
|
if (renderUi) {
|
||||||
|
renderUi();
|
||||||
|
}
|
||||||
|
m_hostCompositor->EndFrameAndPresent(*m_windowRenderer, clearColor, beforeUiRender, afterUiRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CreateTextureDescriptor(
|
||||||
|
::XCEngine::RHI::RHIDevice* device,
|
||||||
|
::XCEngine::RHI::RHITexture* texture,
|
||||||
|
D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle,
|
||||||
|
D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle,
|
||||||
|
ImTextureID* outTextureId) override {
|
||||||
|
return m_hostCompositor != nullptr &&
|
||||||
|
m_hostCompositor->CreateTextureDescriptor(
|
||||||
|
device,
|
||||||
|
texture,
|
||||||
|
outCpuHandle,
|
||||||
|
outGpuHandle,
|
||||||
|
outTextureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeTextureDescriptor(
|
||||||
|
D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
|
||||||
|
D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) override {
|
||||||
|
if (m_hostCompositor != nullptr) {
|
||||||
|
m_hostCompositor->FreeTextureDescriptor(cpuHandle, gpuHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
::XCEngine::Editor::Platform::D3D12WindowRenderer* m_windowRenderer = nullptr;
|
||||||
|
std::unique_ptr<IEditorHostCompositor> m_hostCompositor;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::unique_ptr<IWindowUICompositor> CreateImGuiWindowUICompositor() {
|
||||||
|
return std::make_unique<ImGuiWindowUICompositor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCUIBackend
|
||||||
|
} // namespace Editor
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
||||||
#include <XCEngine/UI/Style/StyleResolver.h>
|
#include <XCEngine/UI/Style/StyleResolver.h>
|
||||||
#include <XCEngine/UI/Style/Theme.h>
|
#include <XCEngine/UI/Style/Theme.h>
|
||||||
|
#include <XCEngine/UI/Text/UITextEditing.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -58,6 +59,7 @@ using XCEngine::UI::UIRect;
|
|||||||
using XCEngine::UI::UISize;
|
using XCEngine::UI::UISize;
|
||||||
namespace Layout = XCEngine::UI::Layout;
|
namespace Layout = XCEngine::UI::Layout;
|
||||||
namespace Style = XCEngine::UI::Style;
|
namespace Style = XCEngine::UI::Style;
|
||||||
|
namespace UIText = XCEngine::UI::Text;
|
||||||
|
|
||||||
constexpr std::size_t kInvalidIndex = static_cast<std::size_t>(-1);
|
constexpr std::size_t kInvalidIndex = static_cast<std::size_t>(-1);
|
||||||
constexpr char kViewRelativePath[] = "new_editor/resources/xcui_demo_view.xcui";
|
constexpr char kViewRelativePath[] = "new_editor/resources/xcui_demo_view.xcui";
|
||||||
@@ -147,77 +149,8 @@ float Clamp01(float value) {
|
|||||||
return (std::min)(1.0f, (std::max)(0.0f, value));
|
return (std::min)(1.0f, (std::max)(0.0f, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsUtf8ContinuationByte(unsigned char value) {
|
|
||||||
return (value & 0xC0u) == 0x80u;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t CountUtf8Codepoints(const std::string& text) {
|
|
||||||
std::size_t count = 0u;
|
|
||||||
for (unsigned char ch : text) {
|
|
||||||
if (!IsUtf8ContinuationByte(ch)) {
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
float MeasureGlyphRunWidth(const std::string& text, float fontSize) {
|
float MeasureGlyphRunWidth(const std::string& text, float fontSize) {
|
||||||
return fontSize * kApproximateTextWidthFactor * static_cast<float>(CountUtf8Codepoints(text));
|
return fontSize * kApproximateTextWidthFactor * static_cast<float>(UIText::CountUtf8Codepoints(text));
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t AdvanceUtf8Offset(const std::string& text, std::size_t offset) {
|
|
||||||
if (offset >= text.size()) {
|
|
||||||
return text.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
++offset;
|
|
||||||
while (offset < text.size() && IsUtf8ContinuationByte(static_cast<unsigned char>(text[offset]))) {
|
|
||||||
++offset;
|
|
||||||
}
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t RetreatUtf8Offset(const std::string& text, std::size_t offset) {
|
|
||||||
if (offset == 0u || text.empty()) {
|
|
||||||
return 0u;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset = (std::min)(offset, text.size());
|
|
||||||
do {
|
|
||||||
--offset;
|
|
||||||
} while (offset > 0u && IsUtf8ContinuationByte(static_cast<unsigned char>(text[offset])));
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendUtf8Codepoint(std::string& text, std::uint32_t codepoint) {
|
|
||||||
if (codepoint <= 0x7Fu) {
|
|
||||||
text.push_back(static_cast<char>(codepoint));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codepoint <= 0x7FFu) {
|
|
||||||
text.push_back(static_cast<char>(0xC0u | ((codepoint >> 6u) & 0x1Fu)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codepoint >= 0xD800u && codepoint <= 0xDFFFu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codepoint <= 0xFFFFu) {
|
|
||||||
text.push_back(static_cast<char>(0xE0u | ((codepoint >> 12u) & 0x0Fu)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | ((codepoint >> 6u) & 0x3Fu)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codepoint <= 0x10FFFFu) {
|
|
||||||
text.push_back(static_cast<char>(0xF0u | ((codepoint >> 18u) & 0x07u)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | ((codepoint >> 12u) & 0x3Fu)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | ((codepoint >> 6u) & 0x3Fu)));
|
|
||||||
text.push_back(static_cast<char>(0x80u | (codepoint & 0x3Fu)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||||
@@ -467,26 +400,6 @@ bool IsTextInputNode(const DemoNode& node) {
|
|||||||
return IsTextFieldNode(node) || IsTextAreaNode(node);
|
return IsTextFieldNode(node) || IsTextAreaNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> SplitLines(const std::string& text) {
|
|
||||||
std::vector<std::string> lines = {};
|
|
||||||
std::size_t lineStart = 0u;
|
|
||||||
for (std::size_t index = 0u; index < text.size(); ++index) {
|
|
||||||
if (text[index] != '\n') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push_back(text.substr(lineStart, index - lineStart));
|
|
||||||
lineStart = index + 1u;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push_back(text.substr(lineStart));
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t CountTextLines(const std::string& text) {
|
|
||||||
return SplitLines(text).size();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t CountDecimalDigits(std::size_t value) {
|
std::size_t CountDecimalDigits(std::size_t value) {
|
||||||
std::size_t digits = 1u;
|
std::size_t digits = 1u;
|
||||||
while (value >= 10u) {
|
while (value >= 10u) {
|
||||||
@@ -496,90 +409,6 @@ std::size_t CountDecimalDigits(std::size_t value) {
|
|||||||
return digits;
|
return digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t CountUtf8CodepointsInRange(
|
|
||||||
const std::string& text,
|
|
||||||
std::size_t beginOffset,
|
|
||||||
std::size_t endOffset) {
|
|
||||||
beginOffset = (std::min)(beginOffset, text.size());
|
|
||||||
endOffset = (std::min)(endOffset, text.size());
|
|
||||||
if (endOffset <= beginOffset) {
|
|
||||||
return 0u;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t count = 0u;
|
|
||||||
for (std::size_t index = beginOffset; index < endOffset; ++index) {
|
|
||||||
if (!IsUtf8ContinuationByte(static_cast<unsigned char>(text[index]))) {
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t AdvanceUtf8Codepoints(
|
|
||||||
const std::string& text,
|
|
||||||
std::size_t offset,
|
|
||||||
std::size_t codepointCount) {
|
|
||||||
offset = (std::min)(offset, text.size());
|
|
||||||
for (std::size_t index = 0u; index < codepointCount && offset < text.size(); ++index) {
|
|
||||||
offset = AdvanceUtf8Offset(text, offset);
|
|
||||||
}
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t FindLineStartOffset(const std::string& text, std::size_t caret) {
|
|
||||||
caret = (std::min)(caret, text.size());
|
|
||||||
while (caret > 0u && text[caret - 1u] != '\n') {
|
|
||||||
--caret;
|
|
||||||
}
|
|
||||||
return caret;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t FindLineEndOffset(const std::string& text, std::size_t caret) {
|
|
||||||
caret = (std::min)(caret, text.size());
|
|
||||||
while (caret < text.size() && text[caret] != '\n') {
|
|
||||||
++caret;
|
|
||||||
}
|
|
||||||
return caret;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t MoveCaretVertically(
|
|
||||||
const std::string& text,
|
|
||||||
std::size_t caret,
|
|
||||||
int direction) {
|
|
||||||
caret = (std::min)(caret, text.size());
|
|
||||||
const std::size_t currentLineStart = FindLineStartOffset(text, caret);
|
|
||||||
const std::size_t currentLineEnd = FindLineEndOffset(text, caret);
|
|
||||||
const std::size_t column = CountUtf8CodepointsInRange(text, currentLineStart, caret);
|
|
||||||
|
|
||||||
if (direction < 0) {
|
|
||||||
if (currentLineStart == 0u) {
|
|
||||||
return caret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t previousLineEnd = currentLineStart - 1u;
|
|
||||||
const std::size_t previousLineStart = FindLineStartOffset(text, previousLineEnd);
|
|
||||||
const std::size_t previousLineLength =
|
|
||||||
CountUtf8CodepointsInRange(text, previousLineStart, previousLineEnd);
|
|
||||||
return AdvanceUtf8Codepoints(
|
|
||||||
text,
|
|
||||||
previousLineStart,
|
|
||||||
(std::min)(column, previousLineLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLineEnd >= text.size()) {
|
|
||||||
return caret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t nextLineStart = currentLineEnd + 1u;
|
|
||||||
const std::size_t nextLineEnd = FindLineEndOffset(text, nextLineStart);
|
|
||||||
const std::size_t nextLineLength =
|
|
||||||
CountUtf8CodepointsInRange(text, nextLineStart, nextLineEnd);
|
|
||||||
return AdvanceUtf8Codepoints(
|
|
||||||
text,
|
|
||||||
nextLineStart,
|
|
||||||
(std::min)(column, nextLineLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShouldShowTextAreaLineNumbers(const DemoNode& node) {
|
bool ShouldShowTextAreaLineNumbers(const DemoNode& node) {
|
||||||
bool showLineNumbers = false;
|
bool showLineNumbers = false;
|
||||||
return IsTextAreaNode(node) &&
|
return IsTextAreaNode(node) &&
|
||||||
@@ -612,7 +441,7 @@ std::size_t FindCaretOffsetForHorizontalPosition(
|
|||||||
|
|
||||||
std::size_t caret = lineStart;
|
std::size_t caret = lineStart;
|
||||||
while (caret < lineEnd) {
|
while (caret < lineEnd) {
|
||||||
const std::size_t nextCaret = AdvanceUtf8Offset(text, caret);
|
const std::size_t nextCaret = UIText::AdvanceUtf8Offset(text, caret);
|
||||||
const float currentWidth = MeasureGlyphRunWidth(text.substr(lineStart, caret - lineStart), fontSize);
|
const float currentWidth = MeasureGlyphRunWidth(text.substr(lineStart, caret - lineStart), fontSize);
|
||||||
const float nextWidth = MeasureGlyphRunWidth(text.substr(lineStart, nextCaret - lineStart), fontSize);
|
const float nextWidth = MeasureGlyphRunWidth(text.substr(lineStart, nextCaret - lineStart), fontSize);
|
||||||
if (targetX < (currentWidth + nextWidth) * 0.5f) {
|
if (targetX < (currentWidth + nextWidth) * 0.5f) {
|
||||||
@@ -955,7 +784,7 @@ std::string BuildNodeDisplayText(
|
|||||||
const std::string& promptValue =
|
const std::string& promptValue =
|
||||||
promptIt != state.textFieldValues.end() ? promptIt->second : node.staticText;
|
promptIt != state.textFieldValues.end() ? promptIt->second : node.staticText;
|
||||||
return "Single-line input, Enter submits, " +
|
return "Single-line input, Enter submits, " +
|
||||||
std::to_string(static_cast<unsigned long long>(CountUtf8Codepoints(promptValue))) +
|
std::to_string(static_cast<unsigned long long>(UIText::CountUtf8Codepoints(promptValue))) +
|
||||||
" chars";
|
" chars";
|
||||||
}
|
}
|
||||||
if (node.elementKey == "notesMeta") {
|
if (node.elementKey == "notesMeta") {
|
||||||
@@ -963,7 +792,7 @@ std::string BuildNodeDisplayText(
|
|||||||
const std::string& notesValue =
|
const std::string& notesValue =
|
||||||
notesIt != state.textFieldValues.end() ? notesIt->second : node.staticText;
|
notesIt != state.textFieldValues.end() ? notesIt->second : node.staticText;
|
||||||
return "Multiline input, click caret, Tab indent, " +
|
return "Multiline input, click caret, Tab indent, " +
|
||||||
std::to_string(static_cast<unsigned long long>(CountTextLines(notesValue))) +
|
std::to_string(static_cast<unsigned long long>(UIText::CountTextLines(notesValue))) +
|
||||||
" lines";
|
" lines";
|
||||||
}
|
}
|
||||||
if (node.elementKey == "subtitle" && stats.accentEnabled) {
|
if (node.elementKey == "subtitle" && stats.accentEnabled) {
|
||||||
@@ -1177,7 +1006,7 @@ std::size_t FindCaretOffsetFromPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float lineHeight = MeasureTextHeight(fontSize);
|
const float lineHeight = MeasureTextHeight(fontSize);
|
||||||
const std::size_t lineCount = (std::max)(std::size_t(1u), CountTextLines(value));
|
const std::size_t lineCount = (std::max)(std::size_t(1u), UIText::CountTextLines(value));
|
||||||
const float gutterWidth = ResolveTextAreaGutterWidth(node, lineCount, fontSize);
|
const float gutterWidth = ResolveTextAreaGutterWidth(node, lineCount, fontSize);
|
||||||
const float localY = point.y - (contentRect.y + kTextInputTextInset);
|
const float localY = point.y - (contentRect.y + kTextInputTextInset);
|
||||||
const float clampedY = (std::max)(0.0f, localY);
|
const float clampedY = (std::max)(0.0f, localY);
|
||||||
@@ -1187,13 +1016,13 @@ std::size_t FindCaretOffsetFromPoint(
|
|||||||
|
|
||||||
std::size_t lineStart = 0u;
|
std::size_t lineStart = 0u;
|
||||||
for (std::size_t currentLine = 0u; currentLine < lineIndex && lineStart < value.size(); ++currentLine) {
|
for (std::size_t currentLine = 0u; currentLine < lineIndex && lineStart < value.size(); ++currentLine) {
|
||||||
lineStart = FindLineEndOffset(value, lineStart);
|
lineStart = UIText::FindLineEndOffset(value, lineStart);
|
||||||
if (lineStart < value.size()) {
|
if (lineStart < value.size()) {
|
||||||
++lineStart;
|
++lineStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t lineEnd = FindLineEndOffset(value, lineStart);
|
const std::size_t lineEnd = UIText::FindLineEndOffset(value, lineStart);
|
||||||
const float targetX = point.x - (contentRect.x + kTextInputTextInset + gutterWidth);
|
const float targetX = point.x - (contentRect.x + kTextInputTextInset + gutterWidth);
|
||||||
return FindCaretOffsetForHorizontalPosition(value, lineStart, lineEnd, targetX, fontSize);
|
return FindCaretOffsetForHorizontalPosition(value, lineStart, lineEnd, targetX, fontSize);
|
||||||
}
|
}
|
||||||
@@ -1231,7 +1060,7 @@ bool HandleTextInputCharacterInput(
|
|||||||
caret = (std::min)(caret, value.size());
|
caret = (std::min)(caret, value.size());
|
||||||
|
|
||||||
std::string encoded = {};
|
std::string encoded = {};
|
||||||
AppendUtf8Codepoint(encoded, character);
|
UIText::AppendUtf8Codepoint(encoded, character);
|
||||||
if (encoded.empty()) {
|
if (encoded.empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1260,7 +1089,7 @@ bool HandleTextInputKeyDown(
|
|||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||||
if (caret > 0u) {
|
if (caret > 0u) {
|
||||||
const std::size_t previousCaret = RetreatUtf8Offset(value, caret);
|
const std::size_t previousCaret = UIText::RetreatUtf8Offset(value, caret);
|
||||||
value.erase(previousCaret, caret - previousCaret);
|
value.erase(previousCaret, caret - previousCaret);
|
||||||
caret = previousCaret;
|
caret = previousCaret;
|
||||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||||
@@ -1270,7 +1099,7 @@ bool HandleTextInputKeyDown(
|
|||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
|
||||||
if (caret < value.size()) {
|
if (caret < value.size()) {
|
||||||
const std::size_t nextCaret = AdvanceUtf8Offset(value, caret);
|
const std::size_t nextCaret = UIText::AdvanceUtf8Offset(value, caret);
|
||||||
value.erase(caret, nextCaret - caret);
|
value.erase(caret, nextCaret - caret);
|
||||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||||
}
|
}
|
||||||
@@ -1278,35 +1107,35 @@ bool HandleTextInputKeyDown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
|
||||||
caret = RetreatUtf8Offset(value, caret);
|
caret = UIText::RetreatUtf8Offset(value, caret);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
|
||||||
caret = AdvanceUtf8Offset(value, caret);
|
caret = UIText::AdvanceUtf8Offset(value, caret);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Up) && IsTextAreaNode(*node)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Up) && IsTextAreaNode(*node)) {
|
||||||
caret = MoveCaretVertically(value, caret, -1);
|
caret = UIText::MoveCaretVertically(value, caret, -1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Down) && IsTextAreaNode(*node)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Down) && IsTextAreaNode(*node)) {
|
||||||
caret = MoveCaretVertically(value, caret, 1);
|
caret = UIText::MoveCaretVertically(value, caret, 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::Home)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::Home)) {
|
||||||
caret = IsTextAreaNode(*node)
|
caret = IsTextAreaNode(*node)
|
||||||
? FindLineStartOffset(value, caret)
|
? UIText::FindLineStartOffset(value, caret)
|
||||||
: 0u;
|
: 0u;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyCode == static_cast<std::int32_t>(KeyCode::End)) {
|
if (keyCode == static_cast<std::int32_t>(KeyCode::End)) {
|
||||||
caret = IsTextAreaNode(*node)
|
caret = IsTextAreaNode(*node)
|
||||||
? FindLineEndOffset(value, caret)
|
? UIText::FindLineEndOffset(value, caret)
|
||||||
: value.size();
|
: value.size();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1329,7 +1158,7 @@ bool HandleTextInputKeyDown(
|
|||||||
!inputModifiers.alt &&
|
!inputModifiers.alt &&
|
||||||
!inputModifiers.super) {
|
!inputModifiers.super) {
|
||||||
if (inputModifiers.shift) {
|
if (inputModifiers.shift) {
|
||||||
const std::size_t lineStart = FindLineStartOffset(value, caret);
|
const std::size_t lineStart = UIText::FindLineStartOffset(value, caret);
|
||||||
std::size_t removed = 0u;
|
std::size_t removed = 0u;
|
||||||
while (removed < kTextAreaTabWidth &&
|
while (removed < kTextAreaTabWidth &&
|
||||||
lineStart + removed < value.size() &&
|
lineStart + removed < value.size() &&
|
||||||
@@ -1539,7 +1368,7 @@ UISize MeasureNode(RuntimeBuildContext& state, std::size_t index) {
|
|||||||
float minWidth = 220.0f;
|
float minWidth = 220.0f;
|
||||||
TryParseFloat(GetNodeAttribute(node, "min-width"), minWidth);
|
TryParseFloat(GetNodeAttribute(node, "min-width"), minWidth);
|
||||||
const std::string probeText = value.empty() ? placeholder : value;
|
const std::string probeText = value.empty() ? placeholder : value;
|
||||||
const std::vector<std::string> lines = SplitLines(probeText);
|
const std::vector<std::string> lines = UIText::SplitLines(probeText);
|
||||||
float widestLine = 0.0f;
|
float widestLine = 0.0f;
|
||||||
for (const std::string& line : lines) {
|
for (const std::string& line : lines) {
|
||||||
widestLine = (std::max)(
|
widestLine = (std::max)(
|
||||||
@@ -1881,7 +1710,7 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
|
|||||||
const std::string value = ResolveTextInputValue(state, node);
|
const std::string value = ResolveTextInputValue(state, node);
|
||||||
const std::string placeholder = GetNodeAttribute(node, "placeholder");
|
const std::string placeholder = GetNodeAttribute(node, "placeholder");
|
||||||
const bool showingPlaceholder = value.empty() && !placeholder.empty();
|
const bool showingPlaceholder = value.empty() && !placeholder.empty();
|
||||||
const std::vector<std::string> lines = SplitLines(showingPlaceholder ? placeholder : value);
|
const std::vector<std::string> lines = UIText::SplitLines(showingPlaceholder ? placeholder : value);
|
||||||
const float gutterWidth = ResolveTextAreaGutterWidth(node, (std::max)(std::size_t(1u), lines.size()), fontSize);
|
const float gutterWidth = ResolveTextAreaGutterWidth(node, (std::max)(std::size_t(1u), lines.size()), fontSize);
|
||||||
const Color textColor = showingPlaceholder
|
const Color textColor = showingPlaceholder
|
||||||
? ResolveColorToken(state.activeTheme, "color.text.placeholder", Color(0.49f, 0.56f, 0.64f, 1.0f))
|
? ResolveColorToken(state.activeTheme, "color.text.placeholder", Color(0.49f, 0.56f, 0.64f, 1.0f))
|
||||||
@@ -1931,7 +1760,7 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t caret = ResolveTextInputCaret(state, node);
|
const std::size_t caret = ResolveTextInputCaret(state, node);
|
||||||
const std::size_t lineStart = FindLineStartOffset(value, caret);
|
const std::size_t lineStart = UIText::FindLineStartOffset(value, caret);
|
||||||
std::size_t lineIndex = 0u;
|
std::size_t lineIndex = 0u;
|
||||||
for (std::size_t scan = 0u; scan < lineStart && scan < value.size(); ++scan) {
|
for (std::size_t scan = 0u; scan < lineStart && scan < value.size(); ++scan) {
|
||||||
if (value[scan] == '\n') {
|
if (value[scan] == '\n') {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ set(UI_TEST_SOURCES
|
|||||||
test_ui_core.cpp
|
test_ui_core.cpp
|
||||||
test_layout_engine.cpp
|
test_layout_engine.cpp
|
||||||
test_ui_runtime.cpp
|
test_ui_runtime.cpp
|
||||||
|
test_ui_text_editing.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(core_ui_tests ${UI_TEST_SOURCES})
|
add_executable(core_ui_tests ${UI_TEST_SOURCES})
|
||||||
|
|||||||
59
tests/Core/UI/test_ui_text_editing.cpp
Normal file
59
tests/Core/UI/test_ui_text_editing.cpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Text/UITextEditing.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace UIText = XCEngine::UI::Text;
|
||||||
|
|
||||||
|
TEST(UITextEditingTest, Utf8CountingAndCaretOffsetsRespectCodepointBoundaries) {
|
||||||
|
const std::string text = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||||
|
|
||||||
|
EXPECT_EQ(UIText::CountUtf8Codepoints(text), 3u);
|
||||||
|
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 0u), 1u);
|
||||||
|
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 1u), 4u);
|
||||||
|
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 4u), 5u);
|
||||||
|
EXPECT_EQ(UIText::RetreatUtf8Offset(text, text.size()), 4u);
|
||||||
|
EXPECT_EQ(UIText::RetreatUtf8Offset(text, 4u), 1u);
|
||||||
|
EXPECT_EQ(UIText::RetreatUtf8Offset(text, 1u), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UITextEditingTest, AppendUtf8CodepointEncodesCharactersAndSkipsInvalidSurrogates) {
|
||||||
|
std::string text = {};
|
||||||
|
UIText::AppendUtf8Codepoint(text, 'A');
|
||||||
|
UIText::AppendUtf8Codepoint(text, 0x4F60u);
|
||||||
|
UIText::AppendUtf8Codepoint(text, 0x1F642u);
|
||||||
|
UIText::AppendUtf8Codepoint(text, 0xD800u);
|
||||||
|
|
||||||
|
EXPECT_EQ(text, std::string("A") + "\xE4\xBD\xA0" + "\xF0\x9F\x99\x82");
|
||||||
|
EXPECT_EQ(UIText::CountUtf8Codepoints(text), 3u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UITextEditingTest, SplitLinesAndLineHelpersTrackMultilineRanges) {
|
||||||
|
const std::string text = "alpha\nbeta\n";
|
||||||
|
|
||||||
|
const auto lines = UIText::SplitLines(text);
|
||||||
|
ASSERT_EQ(lines.size(), 3u);
|
||||||
|
EXPECT_EQ(lines[0], "alpha");
|
||||||
|
EXPECT_EQ(lines[1], "beta");
|
||||||
|
EXPECT_EQ(lines[2], "");
|
||||||
|
EXPECT_EQ(UIText::CountTextLines(text), 3u);
|
||||||
|
EXPECT_EQ(UIText::CountUtf8CodepointsInRange(text, 6u, 10u), 4u);
|
||||||
|
EXPECT_EQ(UIText::FindLineStartOffset(text, 7u), 6u);
|
||||||
|
EXPECT_EQ(UIText::FindLineEndOffset(text, 7u), 10u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UITextEditingTest, MoveCaretVerticallyPreservesUtf8ColumnWhenPossible) {
|
||||||
|
const std::string text = std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n";
|
||||||
|
const std::size_t secondColumnCaret = UIText::AdvanceUtf8Offset(text, 1u);
|
||||||
|
|
||||||
|
const std::size_t movedDown = UIText::MoveCaretVertically(text, secondColumnCaret, 1);
|
||||||
|
const std::size_t movedBackUp = UIText::MoveCaretVertically(text, movedDown, -1);
|
||||||
|
|
||||||
|
EXPECT_EQ(movedDown, 8u);
|
||||||
|
EXPECT_EQ(movedBackUp, secondColumnCaret);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user