Extract XCUI text editing core and window seam headers

This commit is contained in:
2026-04-05 06:23:49 +08:00
parent da85109a31
commit b4c95e4085
9 changed files with 446 additions and 197 deletions

View 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

View 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

View File

@@ -11,6 +11,7 @@
#include <XCEngine/UI/Layout/LayoutEngine.h>
#include <XCEngine/UI/Style/StyleResolver.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Text/UITextEditing.h>
#include <algorithm>
#include <chrono>
@@ -58,6 +59,7 @@ using XCEngine::UI::UIRect;
using XCEngine::UI::UISize;
namespace Layout = XCEngine::UI::Layout;
namespace Style = XCEngine::UI::Style;
namespace UIText = XCEngine::UI::Text;
constexpr std::size_t kInvalidIndex = static_cast<std::size_t>(-1);
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));
}
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) {
return fontSize * kApproximateTextWidthFactor * static_cast<float>(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)));
}
return fontSize * kApproximateTextWidthFactor * static_cast<float>(UIText::CountUtf8Codepoints(text));
}
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
@@ -467,26 +400,6 @@ bool IsTextInputNode(const DemoNode& 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 digits = 1u;
while (value >= 10u) {
@@ -496,90 +409,6 @@ std::size_t CountDecimalDigits(std::size_t value) {
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 showLineNumbers = false;
return IsTextAreaNode(node) &&
@@ -612,7 +441,7 @@ std::size_t FindCaretOffsetForHorizontalPosition(
std::size_t caret = lineStart;
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 nextWidth = MeasureGlyphRunWidth(text.substr(lineStart, nextCaret - lineStart), fontSize);
if (targetX < (currentWidth + nextWidth) * 0.5f) {
@@ -955,7 +784,7 @@ std::string BuildNodeDisplayText(
const std::string& promptValue =
promptIt != state.textFieldValues.end() ? promptIt->second : node.staticText;
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";
}
if (node.elementKey == "notesMeta") {
@@ -963,7 +792,7 @@ std::string BuildNodeDisplayText(
const std::string& notesValue =
notesIt != state.textFieldValues.end() ? notesIt->second : node.staticText;
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";
}
if (node.elementKey == "subtitle" && stats.accentEnabled) {
@@ -1177,7 +1006,7 @@ std::size_t FindCaretOffsetFromPoint(
}
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 localY = point.y - (contentRect.y + kTextInputTextInset);
const float clampedY = (std::max)(0.0f, localY);
@@ -1187,13 +1016,13 @@ std::size_t FindCaretOffsetFromPoint(
std::size_t lineStart = 0u;
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()) {
++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);
return FindCaretOffsetForHorizontalPosition(value, lineStart, lineEnd, targetX, fontSize);
}
@@ -1231,7 +1060,7 @@ bool HandleTextInputCharacterInput(
caret = (std::min)(caret, value.size());
std::string encoded = {};
AppendUtf8Codepoint(encoded, character);
UIText::AppendUtf8Codepoint(encoded, character);
if (encoded.empty()) {
return false;
}
@@ -1260,7 +1089,7 @@ bool HandleTextInputKeyDown(
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
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);
caret = previousCaret;
state.lastCommandId = "demo.text.edit." + stateKey;
@@ -1270,7 +1099,7 @@ bool HandleTextInputKeyDown(
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
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);
state.lastCommandId = "demo.text.edit." + stateKey;
}
@@ -1278,35 +1107,35 @@ bool HandleTextInputKeyDown(
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
caret = RetreatUtf8Offset(value, caret);
caret = UIText::RetreatUtf8Offset(value, caret);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
caret = AdvanceUtf8Offset(value, caret);
caret = UIText::AdvanceUtf8Offset(value, caret);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Up) && IsTextAreaNode(*node)) {
caret = MoveCaretVertically(value, caret, -1);
caret = UIText::MoveCaretVertically(value, caret, -1);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Down) && IsTextAreaNode(*node)) {
caret = MoveCaretVertically(value, caret, 1);
caret = UIText::MoveCaretVertically(value, caret, 1);
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Home)) {
caret = IsTextAreaNode(*node)
? FindLineStartOffset(value, caret)
? UIText::FindLineStartOffset(value, caret)
: 0u;
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::End)) {
caret = IsTextAreaNode(*node)
? FindLineEndOffset(value, caret)
? UIText::FindLineEndOffset(value, caret)
: value.size();
return true;
}
@@ -1329,7 +1158,7 @@ bool HandleTextInputKeyDown(
!inputModifiers.alt &&
!inputModifiers.super) {
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;
while (removed < kTextAreaTabWidth &&
lineStart + removed < value.size() &&
@@ -1539,7 +1368,7 @@ UISize MeasureNode(RuntimeBuildContext& state, std::size_t index) {
float minWidth = 220.0f;
TryParseFloat(GetNodeAttribute(node, "min-width"), minWidth);
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;
for (const std::string& line : lines) {
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 placeholder = GetNodeAttribute(node, "placeholder");
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 Color textColor = showingPlaceholder
? 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 lineStart = FindLineStartOffset(value, caret);
const std::size_t lineStart = UIText::FindLineStartOffset(value, caret);
std::size_t lineIndex = 0u;
for (std::size_t scan = 0u; scan < lineStart && scan < value.size(); ++scan) {
if (value[scan] == '\n') {