Enhance XCUI demo text editing and host bridge

This commit is contained in:
2026-04-05 05:58:05 +08:00
parent a7662a1d43
commit d1cb0c874b
11 changed files with 479 additions and 24 deletions

View File

@@ -50,6 +50,10 @@
min-width="240" min-width="240"
placeholder="Type a command or note for XCUI..." placeholder="Type a command or note for XCUI..."
value="" /> value="" />
<Text
id="promptMeta"
text="Single-line input, Enter submits"
style="Meta" />
</Column> </Column>
</Card> </Card>
<Card id="notesCard" style="MetricCard"> <Card id="notesCard" style="MetricCard">
@@ -61,8 +65,13 @@
width="stretch" width="stretch"
min-width="240" min-width="240"
rows="4" rows="4"
show-line-numbers="true"
placeholder="Write multiline notes, prompts, or todos for the current screen..." placeholder="Write multiline notes, prompts, or todos for the current screen..."
value="" /> value="" />
<Text
id="notesMeta"
text="Multiline input, click caret, Tab indent"
style="Meta" />
</Column> </Column>
</Card> </Card>
<Button id="toggleAccent" action="demo.toggleAccent" style="AccentButton"> <Button id="toggleAccent" action="demo.toggleAccent" style="AccentButton">

View File

@@ -13,6 +13,9 @@ Old `editor` replacement is explicitly out of scope for this phase.
- schema document definition data is now retained on `UIDocumentModel` and round-trips through the UI artifact path - schema document definition data is now retained on `UIDocumentModel` and round-trips through the UI artifact path
- 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 current editor-host checkpoint additionally hardens the XCUI demo text-editing path and the temporary ImGui host bridge:
- multiline demo text areas now support pointer caret placement, line-number gutters, and tab indent/unindent behavior
- the ImGui bridge now accepts existing shader-resource views and the window renderer exposes an after-UI callback seam
- 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
@@ -26,6 +29,7 @@ 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`.
- The temporary ImGui bridge can now register an existing shader-resource view directly instead of forcing the caller to recreate a texture SRV from scratch.
- 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:
@@ -52,11 +56,12 @@ Current gap:
- `new_editor` remains the isolated XCUI sandbox. - `new_editor` remains the isolated XCUI sandbox.
- Native hosted preview is working as `RHI offscreen surface -> ImGui shell texture embed`. - 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` remains the long-lived effect and behavior testbed.
- `XCUI Demo` now covers both single-line and multiline text authoring behavior. - `XCUI Demo` now covers both single-line and multiline text authoring behavior, including pointer caret placement, line-number gutters, delete/backspace symmetry, and tab indent/unindent in text areas.
- `LayoutLab` now includes a `ScrollView` prototype and a more editor-like three-column authored layout. - `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`. - `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.
- `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`. - `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
- `new_editor` host plumbing now has a narrower swapchain seam via a window-renderer post-ImGui callback, which is the first practical hook for a future window-level compositor split.
Current gap: Current gap:
@@ -73,11 +78,17 @@ Current gap:
- `core_ui_tests`: `14/14` - `core_ui_tests`: `14/14`
- `core_ui_style_tests`: `5/5` - `core_ui_style_tests`: `5/5`
- `ui_resource_tests`: `7/7` - `ui_resource_tests`: `7/7`
- `editor_tests` targeted XCUI/editor host API smoke cases: `3/3`
## Landed This Phase ## Landed This Phase
- 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.
- Demo runtime text editing extended with:
- pointer-based caret placement for text inputs
- multiline text-area line-number gutters
- tab indent / shift-tab unindent
- delete handling symmetry with backspace
- Demo authored resources updated to exercise the input field. - Demo authored resources updated to exercise the input field.
- LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. - LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content.
- LayoutLab editor-widget prototypes for tree/list/property-style sections with dedicated runtime coverage. - LayoutLab editor-widget prototypes for tree/list/property-style sections with dedicated runtime coverage.
@@ -98,6 +109,10 @@ Current gap:
- external texture binding reuse - external texture binding reuse
- per-batch scissor application - per-batch scissor application
- `new_editor` panel/shell diagnostics improvements for hosted preview state. - `new_editor` panel/shell diagnostics improvements for hosted preview state.
- ImGui host bridge improvements:
- existing shader-resource view registration overload
- swapchain render callback seam after ImGui submission
- editor-side API smoke coverage for bridge and window renderer accessors
- XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash. - XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash.
- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression. - `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression.
- MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention. - MSVC debug build hardening was tightened again so large parallel `engine` rebuilds stop tripping over compile-PDB contention.

View File

@@ -182,7 +182,8 @@ public:
void Render( void Render(
UI::ImGuiBackendBridge& imguiBackend, UI::ImGuiBackendBridge& imguiBackend,
const float clearColor[4], const float clearColor[4],
const RenderCallback& beforeUiRender = {}) { const RenderCallback& beforeUiRender = {},
const RenderCallback& afterUiRender = {}) {
auto* d3d12Queue = GetD3D12CommandQueue(); auto* d3d12Queue = GetD3D12CommandQueue();
auto* d3d12CommandList = GetD3D12CommandList(); auto* d3d12CommandList = GetD3D12CommandList();
if (m_swapChain == nullptr || if (m_swapChain == nullptr ||
@@ -222,6 +223,11 @@ public:
d3d12CommandList->SetDescriptorHeaps(1, descriptorHeaps); d3d12CommandList->SetDescriptorHeaps(1, descriptorHeaps);
imguiBackend.RenderDrawData(d3d12CommandList->GetCommandList()); imguiBackend.RenderDrawData(d3d12CommandList->GetCommandList());
if (afterUiRender) {
d3d12CommandList->SetRenderTargets(1, &renderTargetView, nullptr);
afterUiRender(GetRenderContext(), renderSurface);
}
d3d12CommandList->TransitionBarrier( d3d12CommandList->TransitionBarrier(
renderTargetView, renderTargetView,
RHI::ResourceStates::RenderTarget, RHI::ResourceStates::RenderTarget,

View File

@@ -7,8 +7,10 @@
#include <d3d12.h> #include <d3d12.h>
#include <dxgi1_6.h> #include <dxgi1_6.h>
#include <XCEngine/RHI/D3D12/D3D12Device.h> #include <XCEngine/RHI/D3D12/D3D12Device.h>
#include <XCEngine/RHI/D3D12/D3D12ResourceView.h>
#include <XCEngine/RHI/D3D12/D3D12Texture.h> #include <XCEngine/RHI/D3D12/D3D12Texture.h>
#include <XCEngine/RHI/RHIDevice.h> #include <XCEngine/RHI/RHIDevice.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h> #include <XCEngine/RHI/RHITexture.h>
#include <imgui.h> #include <imgui.h>
#include <imgui_impl_dx12.h> #include <imgui_impl_dx12.h>
@@ -168,6 +170,63 @@ public:
return true; return true;
} }
bool CreateTextureDescriptor(
::XCEngine::RHI::RHIDevice* device,
::XCEngine::RHI::RHIResourceView* shaderResourceView,
D3D12_CPU_DESCRIPTOR_HANDLE* outCpuHandle,
D3D12_GPU_DESCRIPTOR_HANDLE* outGpuHandle,
ImTextureID* outTextureId) {
if (device == nullptr ||
shaderResourceView == nullptr ||
outCpuHandle == nullptr ||
outGpuHandle == nullptr ||
outTextureId == nullptr) {
return false;
}
*outCpuHandle = {};
*outGpuHandle = {};
*outTextureId = {};
auto* nativeDevice = dynamic_cast<::XCEngine::RHI::D3D12Device*>(device);
auto* nativeView = dynamic_cast<::XCEngine::RHI::D3D12ResourceView*>(shaderResourceView);
if (nativeDevice == nullptr ||
nativeView == nullptr ||
!nativeView->IsValid() ||
nativeView->GetViewType() != ::XCEngine::RHI::ResourceViewType::ShaderResource) {
return false;
}
if (m_srvHeap == nullptr ||
m_srvDescriptorSize == 0 ||
m_srvUsage.empty() ||
std::find(m_srvUsage.begin(), m_srvUsage.end(), false) == m_srvUsage.end()) {
return false;
}
const D3D12_CPU_DESCRIPTOR_HANDLE sourceCpuHandle = nativeView->GetCPUHandle();
if (sourceCpuHandle.ptr == 0) {
return false;
}
AllocateTextureDescriptor(outCpuHandle, outGpuHandle);
if (outCpuHandle->ptr == 0 || outGpuHandle->ptr == 0) {
*outCpuHandle = {};
*outGpuHandle = {};
return false;
}
// ImGui samples from its own shader-visible heap, so copy the existing SRV into an ImGui-owned slot.
nativeDevice->GetDevice()->CopyDescriptorsSimple(
1,
*outCpuHandle,
sourceCpuHandle,
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
*outTextureId = static_cast<ImTextureID>(outGpuHandle->ptr);
return true;
}
void FreeTextureDescriptor( void FreeTextureDescriptor(
D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) { D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle) {

View File

@@ -1,30 +1,59 @@
<View name="NewEditor Demo" theme="xcui_demo_theme.xctheme" style="Root"> <View name="NewEditor Demo" theme="xcui_demo_theme.xctheme" style="Root">
<Column id="rootColumn" gap="12" padding="18"> <Column id="rootColumn" gap="10" padding="16">
<Card id="hero" style="HeroCard"> <Card id="hero" style="HeroCard">
<Column gap="6"> <Column gap="4">
<Text id="title" text="New XCUI Shell" style="HeroTitle" /> <Text id="title" text="New XCUI Shell" style="HeroTitle" />
<Text id="subtitle" text="Markup -> Layout -> Style -> DrawData" style="HeroSubtitle" /> <Text id="subtitle" text="Markup -> Layout -> Style -> DrawData" style="HeroSubtitle" />
</Column> </Column>
</Card> </Card>
<Row id="metricRow" gap="10"> <Row id="metricRow" gap="10">
<Card id="metric1" style="MetricCard" width="stretch"> <Card id="metric1" style="MetricCard" width="stretch">
<Text text="Tree status" style="MetricLabel" /> <Column gap="6">
<Text text="Driven by runtime" style="MetricValue" /> <Text text="Tree status" style="MetricLabel" />
<Text text="Driven by runtime" style="MetricValue" />
<ProgressBar
id="densityMeter"
text="Density stress"
state-key="toggleDensity"
value-off="0.44"
value-on="0.86"
fill="token:color.accent"
width="stretch" />
</Column>
</Card> </Card>
<Card id="metric2" style="MetricCard" width="stretch"> <Card id="metric2" style="MetricCard" width="stretch">
<Text text="Input" style="MetricLabel" /> <Column gap="6">
<Text text="Hover + shortcuts" style="MetricValue" /> <Text text="Input" style="MetricLabel" />
<Text text="Hover + shortcuts" style="MetricValue" />
<Row gap="6">
<Swatch id="swatchAccent" text="Accent" token="color.accent" width="stretch" height="24" />
<Swatch id="swatchAlt" text="Alt" token="color.accent.alt" width="stretch" height="24" />
</Row>
</Column>
</Card> </Card>
</Row> </Row>
<Row id="toggleRow" gap="8">
<Toggle id="toggleDensity" text="Density stress" width="stretch" />
<Toggle
id="toggleDiagnostics"
text="Debug path"
width="stretch"
fill="token:color.accent.alt" />
</Row>
<Card id="commandCard" style="MetricCard"> <Card id="commandCard" style="MetricCard">
<Column gap="6"> <Column gap="6">
<Text text="Agent command" style="MetricLabel" /> <Text text="Agent command" style="MetricLabel" />
<TextField <TextField
id="agentPrompt" id="agentPrompt"
style="CommandField"
width="stretch" width="stretch"
min-width="240" min-width="240"
placeholder="Type a command or note for XCUI..." placeholder="Type a command or note for XCUI..."
value="" /> value="" />
<Text
id="promptMeta"
text="Single-line input, Enter submits"
style="Meta" />
</Column> </Column>
</Card> </Card>
<Card id="notesCard" style="MetricCard"> <Card id="notesCard" style="MetricCard">
@@ -32,21 +61,28 @@
<Text text="Session notes" style="MetricLabel" /> <Text text="Session notes" style="MetricLabel" />
<TextArea <TextArea
id="sessionNotes" id="sessionNotes"
style="CommandArea"
width="stretch" width="stretch"
min-width="240" min-width="240"
rows="4" rows="4"
show-line-numbers="true"
placeholder="Write multiline notes, prompts, or todos for the current screen..." placeholder="Write multiline notes, prompts, or todos for the current screen..."
value="" /> value="" />
<Text
id="notesMeta"
text="Multiline input, click caret, Tab indent"
style="Meta" />
</Column> </Column>
</Card> </Card>
<Button id="toggleAccent" style="AccentButton"> <Button id="toggleAccent" action="demo.toggleAccent" style="AccentButton">
<Text text="Toggle Accent" style="ButtonLabel" /> <Text text="Toggle Accent" style="ButtonLabel" />
</Button> </Button>
<Card id="debug" style="DebugCard"> <Card id="debug" style="DebugCard">
<Column gap="4"> <Column gap="3">
<Text id="statusFocus" text="Focus: waiting" style="Meta" /> <Text id="statusFocus" text="Focus: waiting" style="Meta" />
<Text id="statusLayout" text="Layout: idle" style="Meta" /> <Text id="statusLayout" text="Layout: idle" style="Meta" />
<Text id="statusCommands" text="Commands: ready" style="Meta" /> <Text id="statusCommands" text="Commands: ready" style="Meta" />
<Text id="statusWidgets" text="Widgets: ready" style="Meta" />
</Column> </Column>
</Card> </Card>
</Column> </Column>

View File

@@ -64,6 +64,9 @@ constexpr char kViewRelativePath[] = "new_editor/resources/xcui_demo_view.xcui";
constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_demo_theme.xctheme"; constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_demo_theme.xctheme";
constexpr char kToggleAccentCommandId[] = "demo.toggleAccent"; constexpr char kToggleAccentCommandId[] = "demo.toggleAccent";
constexpr float kApproximateTextWidthFactor = 0.58f; constexpr float kApproximateTextWidthFactor = 0.58f;
constexpr float kTextInputTextInset = 2.0f;
constexpr float kTextAreaLineNumberGap = 10.0f;
constexpr std::size_t kTextAreaTabWidth = 4u;
struct DemoNode { struct DemoNode {
UIElementId elementId = 0; UIElementId elementId = 0;
@@ -480,6 +483,19 @@ std::vector<std::string> SplitLines(const std::string& text) {
return lines; 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) {
value /= 10u;
++digits;
}
return digits;
}
std::size_t CountUtf8CodepointsInRange( std::size_t CountUtf8CodepointsInRange(
const std::string& text, const std::string& text,
std::size_t beginOffset, std::size_t beginOffset,
@@ -564,6 +580,50 @@ std::size_t MoveCaretVertically(
(std::min)(column, nextLineLength)); (std::min)(column, nextLineLength));
} }
bool ShouldShowTextAreaLineNumbers(const DemoNode& node) {
bool showLineNumbers = false;
return IsTextAreaNode(node) &&
TryParseBool(GetNodeAttribute(node, "show-line-numbers"), showLineNumbers) &&
showLineNumbers;
}
float ResolveTextAreaGutterWidth(
const DemoNode& node,
std::size_t visibleLineCount,
float fontSize) {
if (!ShouldShowTextAreaLineNumbers(node)) {
return 0.0f;
}
const std::size_t digits = CountDecimalDigits((std::max)(std::size_t(1u), visibleLineCount));
const float numberWidth = MeasureGlyphRunWidth(std::string(digits, '8'), fontSize * 0.9f);
return numberWidth + kTextAreaLineNumberGap;
}
std::size_t FindCaretOffsetForHorizontalPosition(
const std::string& text,
std::size_t lineStart,
std::size_t lineEnd,
float targetX,
float fontSize) {
lineStart = (std::min)(lineStart, text.size());
lineEnd = (std::min)(lineEnd, text.size());
targetX = (std::max)(0.0f, targetX);
std::size_t caret = lineStart;
while (caret < lineEnd) {
const std::size_t nextCaret = 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) {
return caret;
}
caret = nextCaret;
}
return lineEnd;
}
Style::UIStyleValue ParseThemeTokenValue( Style::UIStyleValue ParseThemeTokenValue(
const std::string& typeName, const std::string& typeName,
const std::string& rawValue, const std::string& rawValue,
@@ -756,6 +816,9 @@ void ConfigureDemoStyleSheet(Style::UIStyleSheet& styleSheet) {
styleSheet.GetOrCreateNamedStyle("CommandField").SetProperty( styleSheet.GetOrCreateNamedStyle("CommandField").SetProperty(
Style::UIStylePropertyId::BorderColor, Style::UIStylePropertyId::BorderColor,
Style::UIStyleValue::Token("color.accent.alt")); Style::UIStyleValue::Token("color.accent.alt"));
styleSheet.GetOrCreateNamedStyle("CommandArea").SetProperty(
Style::UIStylePropertyId::BorderColor,
Style::UIStyleValue::Token("color.outline"));
styleSheet.GetOrCreateNamedStyle("Meta").SetProperty( styleSheet.GetOrCreateNamedStyle("Meta").SetProperty(
Style::UIStylePropertyId::ForegroundColor, Style::UIStylePropertyId::ForegroundColor,
Style::UIStyleValue::Token("color.text.secondary")); Style::UIStyleValue::Token("color.text.secondary"));
@@ -887,6 +950,22 @@ std::string BuildNodeDisplayText(
" / debug " + " / debug " +
std::string(diagnosticsEnabled ? "on" : "off"); std::string(diagnosticsEnabled ? "on" : "off");
} }
if (node.elementKey == "promptMeta") {
const auto promptIt = state.textFieldValues.find("agentPrompt");
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))) +
" chars";
}
if (node.elementKey == "notesMeta") {
const auto notesIt = state.textFieldValues.find("sessionNotes");
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))) +
" lines";
}
if (node.elementKey == "subtitle" && stats.accentEnabled) { if (node.elementKey == "subtitle" && stats.accentEnabled) {
return "Markup -> Layout -> Style -> DrawData (accent alt active)"; return "Markup -> Layout -> Style -> DrawData (accent alt active)";
} }
@@ -1076,6 +1155,62 @@ DemoNode* TryGetNodeByElementId(RuntimeBuildContext& state, UIElementId elementI
return it != state.nodeIndexById.end() ? &state.nodes[it->second] : nullptr; return it != state.nodeIndexById.end() ? &state.nodes[it->second] : nullptr;
} }
std::size_t FindCaretOffsetFromPoint(
RuntimeBuildContext& state,
const DemoNode& node,
const UIPoint& point) {
EnsureTextInputStateInitialized(state, node);
const std::string value = ResolveTextInputValue(state, node);
const Layout::UILayoutThickness padding =
ResolvePadding(node, state.activeTheme, state.styleSheet);
const UIRect contentRect = InsetRect(node.rect, padding);
const float fontSize = GetFloatProperty(
node,
state.activeTheme,
state.styleSheet,
Style::UIStylePropertyId::FontSize,
14.0f);
if (!IsTextAreaNode(node)) {
const float targetX = point.x - (contentRect.x + kTextInputTextInset);
return FindCaretOffsetForHorizontalPosition(value, 0u, value.size(), targetX, fontSize);
}
const float lineHeight = MeasureTextHeight(fontSize);
const std::size_t lineCount = (std::max)(std::size_t(1u), 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);
const std::size_t lineIndex = (std::min)(
static_cast<std::size_t>(clampedY / (std::max)(1.0f, lineHeight)),
lineCount - 1u);
std::size_t lineStart = 0u;
for (std::size_t currentLine = 0u; currentLine < lineIndex && lineStart < value.size(); ++currentLine) {
lineStart = FindLineEndOffset(value, lineStart);
if (lineStart < value.size()) {
++lineStart;
}
}
const std::size_t lineEnd = FindLineEndOffset(value, lineStart);
const float targetX = point.x - (contentRect.x + kTextInputTextInset + gutterWidth);
return FindCaretOffsetForHorizontalPosition(value, lineStart, lineEnd, targetX, fontSize);
}
void SetTextInputCaretFromPoint(
RuntimeBuildContext& state,
UIElementId elementId,
const UIPoint& point) {
DemoNode* node = TryGetNodeByElementId(state, elementId);
if (node == nullptr || !IsTextInputNode(*node)) {
return;
}
const std::string stateKey = ResolveTextInputStateKey(*node);
state.textFieldCarets[stateKey] = FindCaretOffsetFromPoint(state, *node, point);
}
bool HandleTextInputCharacterInput( bool HandleTextInputCharacterInput(
RuntimeBuildContext& state, RuntimeBuildContext& state,
UIElementId elementId, UIElementId elementId,
@@ -1110,7 +1245,8 @@ bool HandleTextInputCharacterInput(
bool HandleTextInputKeyDown( bool HandleTextInputKeyDown(
RuntimeBuildContext& state, RuntimeBuildContext& state,
UIElementId elementId, UIElementId elementId,
std::int32_t keyCode) { std::int32_t keyCode,
const UI::UIInputModifiers& inputModifiers) {
DemoNode* node = TryGetNodeByElementId(state, elementId); DemoNode* node = TryGetNodeByElementId(state, elementId);
if (node == nullptr || !IsTextInputNode(*node)) { if (node == nullptr || !IsTextInputNode(*node)) {
return false; return false;
@@ -1122,8 +1258,7 @@ bool HandleTextInputKeyDown(
std::size_t& caret = state.textFieldCarets[stateKey]; std::size_t& caret = state.textFieldCarets[stateKey];
caret = (std::min)(caret, value.size()); caret = (std::min)(caret, value.size());
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace) || if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
if (caret > 0u) { if (caret > 0u) {
const std::size_t previousCaret = RetreatUtf8Offset(value, caret); const std::size_t previousCaret = RetreatUtf8Offset(value, caret);
value.erase(previousCaret, caret - previousCaret); value.erase(previousCaret, caret - previousCaret);
@@ -1133,6 +1268,15 @@ bool HandleTextInputKeyDown(
return true; return true;
} }
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
if (caret < value.size()) {
const std::size_t nextCaret = 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)) { if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
caret = RetreatUtf8Offset(value, caret); caret = RetreatUtf8Offset(value, caret);
return true; return true;
@@ -1179,6 +1323,33 @@ bool HandleTextInputKeyDown(
return true; return true;
} }
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 = 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 false;
} }
@@ -1385,11 +1556,13 @@ UISize MeasureNode(RuntimeBuildContext& state, std::size_t index) {
lineCount, lineCount,
static_cast<std::size_t>((std::max)(1.0f, requestedRows))); static_cast<std::size_t>((std::max)(1.0f, requestedRows)));
} }
const float gutterWidth = ResolveTextAreaGutterWidth(node, lineCount, fontSize);
node.desiredSize = UISize( node.desiredSize = UISize(
(std::max)( (std::max)(
minWidth, minWidth,
widestLine + widestLine +
gutterWidth +
padding.Horizontal() + padding.Horizontal() +
18.0f), 18.0f),
lineHeight * static_cast<float>(lineCount) + lineHeight * static_cast<float>(lineCount) +
@@ -1665,7 +1838,7 @@ void DrawTextFieldNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawL
const float textY = contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f; const float textY = contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f;
drawList.AddText( drawList.AddText(
UIPoint(contentRect.x + 2.0f, textY), UIPoint(contentRect.x + kTextInputTextInset, textY),
displayText, displayText,
ToUIColor(textColor), ToUIColor(textColor),
fontSize); fontSize);
@@ -1680,7 +1853,7 @@ void DrawTextFieldNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawL
const std::size_t caret = ResolveTextInputCaret(state, node); const std::size_t caret = ResolveTextInputCaret(state, node);
const float caretX = const float caretX =
contentRect.x + contentRect.x +
2.0f + kTextInputTextInset +
MeasureGlyphRunWidth(value.substr(0u, caret), fontSize); MeasureGlyphRunWidth(value.substr(0u, caret), fontSize);
const Color caretColor = ResolveColorToken( const Color caretColor = ResolveColorToken(
state.activeTheme, state.activeTheme,
@@ -1709,6 +1882,7 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
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 = SplitLines(showingPlaceholder ? placeholder : value);
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))
: GetColorProperty( : GetColorProperty(
@@ -1717,12 +1891,33 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
state.styleSheet, state.styleSheet,
Style::UIStylePropertyId::ForegroundColor, Style::UIStylePropertyId::ForegroundColor,
Color(0.92f, 0.94f, 0.97f, 1.0f)); Color(0.92f, 0.94f, 0.97f, 1.0f));
const Color lineNumberColor = ResolveColorToken(
state.activeTheme,
"color.text.secondary",
Color(0.74f, 0.78f, 0.86f, 1.0f));
if (gutterWidth > 0.0f) {
const float separatorX = contentRect.x + gutterWidth - kTextAreaLineNumberGap * 0.4f;
drawList.AddFilledRect(
UIRect(separatorX, contentRect.y, 1.0f, contentRect.height),
ToUIColor(ResolveColorToken(state.activeTheme, "color.outline", Color(0.23f, 0.29f, 0.35f, 1.0f))),
0.0f);
}
for (std::size_t lineIndex = 0u; lineIndex < lines.size(); ++lineIndex) { for (std::size_t lineIndex = 0u; lineIndex < lines.size(); ++lineIndex) {
if (gutterWidth > 0.0f) {
drawList.AddText(
UIPoint(
contentRect.x,
contentRect.y + kTextInputTextInset + static_cast<float>(lineIndex) * lineHeight),
std::to_string(static_cast<unsigned long long>(lineIndex + 1u)),
ToUIColor(lineNumberColor),
fontSize * 0.9f);
}
drawList.AddText( drawList.AddText(
UIPoint( UIPoint(
contentRect.x + 2.0f, contentRect.x + kTextInputTextInset + gutterWidth,
contentRect.y + 2.0f + static_cast<float>(lineIndex) * lineHeight), contentRect.y + kTextInputTextInset + static_cast<float>(lineIndex) * lineHeight),
lines[lineIndex].empty() ? std::string(" ") : lines[lineIndex], lines[lineIndex].empty() ? std::string(" ") : lines[lineIndex],
ToUIColor(textColor), ToUIColor(textColor),
fontSize); fontSize);
@@ -1746,7 +1941,8 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
const float caretX = const float caretX =
contentRect.x + contentRect.x +
2.0f + kTextInputTextInset +
gutterWidth +
MeasureGlyphRunWidth(value.substr(lineStart, caret - lineStart), fontSize); MeasureGlyphRunWidth(value.substr(lineStart, caret - lineStart), fontSize);
const float caretY = contentRect.y + 3.0f + static_cast<float>(lineIndex) * lineHeight; const float caretY = contentRect.y + 3.0f + static_cast<float>(lineIndex) * lineHeight;
const Color caretColor = ResolveColorToken( const Color caretColor = ResolveColorToken(
@@ -2088,6 +2284,7 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& inp
event.pointerButton == UI::UIPointerButton::Left) { event.pointerButton == UI::UIPointerButton::Left) {
if (state.armedElementId != 0u && if (state.armedElementId != 0u &&
hoveredPath.Target() == state.armedElementId) { hoveredPath.Target() == state.armedElementId) {
SetTextInputCaretFromPoint(state, hoveredPath.Target(), event.position);
ActivateNode(state, hoveredPath.Target()); ActivateNode(state, hoveredPath.Target());
} }
state.armedElementId = 0u; state.armedElementId = 0u;
@@ -2120,7 +2317,12 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& inp
} }
if (event.type == UIInputEventType::KeyDown) { if (event.type == UIInputEventType::KeyDown) {
if (HandleTextInputKeyDown(state, focusedElementId, event.keyCode)) { const UI::UIInputModifiers inputModifiers = event.modifiers;
if (HandleTextInputKeyDown(
state,
focusedElementId,
event.keyCode,
inputModifiers)) {
return; return;
} }
} }

View File

@@ -38,11 +38,21 @@ UIInputEvent MakeCharacterEvent(std::uint32_t character) {
return event; return event;
} }
UIInputEvent MakeKeyDownEvent(XCEngine::Input::KeyCode keyCode, bool repeat = false) { UIInputEvent MakeKeyDownEvent(
XCEngine::Input::KeyCode keyCode,
bool repeat = false,
bool shift = false,
bool control = false,
bool alt = false,
bool super = false) {
UIInputEvent event = {}; UIInputEvent event = {};
event.type = UIInputEventType::KeyDown; event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode); event.keyCode = static_cast<std::int32_t>(keyCode);
event.repeat = repeat; event.repeat = repeat;
event.modifiers.shift = shift;
event.modifiers.control = control;
event.modifiers.alt = alt;
event.modifiers.super = super;
return event; return event;
} }
@@ -372,17 +382,43 @@ TEST(NewEditorXCUIDemoRuntimeTest, TextAreaAcceptsMultilineInputAndCaretMovement
ASSERT_TRUE(typedFrame.stats.documentsReady); ASSERT_TRUE(typedFrame.stats.documentsReady);
EXPECT_EQ(typedFrame.stats.focusedElementId, "sessionNotes"); EXPECT_EQ(typedFrame.stats.focusedElementId, "sessionNotes");
EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.sessionNotes"); EXPECT_EQ(typedFrame.stats.lastCommandId, "demo.text.edit.sessionNotes");
EXPECT_NE(FindTextCommand(typedFrame.drawData, "1"), nullptr);
EXPECT_NE(FindTextCommand(typedFrame.drawData, "2"), nullptr);
EXPECT_NE(FindTextCommand(typedFrame.drawData, "OK"), nullptr); EXPECT_NE(FindTextCommand(typedFrame.drawData, "OK"), nullptr);
EXPECT_NE(FindTextCommand(typedFrame.drawData, "X"), nullptr); EXPECT_NE(FindTextCommand(typedFrame.drawData, "X"), nullptr);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState tabInput = BuildInputState();
tabInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Home));
tabInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Tab));
const auto& indentedFrame = runtime.Update(tabInput);
ASSERT_TRUE(indentedFrame.stats.documentsReady);
EXPECT_EQ(indentedFrame.stats.focusedElementId, "sessionNotes");
EXPECT_EQ(indentedFrame.stats.lastCommandId, "demo.text.edit.sessionNotes");
const XCEngine::UI::UIPoint firstLineStart(
notesRect.x + 8.0f,
notesRect.y + 10.0f);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState caretPressed = BuildInputState();
caretPressed.pointerPosition = firstLineStart;
caretPressed.pointerPressed = true;
caretPressed.pointerDown = true;
runtime.Update(caretPressed);
XCEngine::Editor::XCUIBackend::XCUIDemoInputState caretReleased = BuildInputState();
caretReleased.pointerPosition = firstLineStart;
caretReleased.pointerReleased = true;
const auto& caretFrame = runtime.Update(caretReleased);
ASSERT_TRUE(caretFrame.stats.documentsReady);
EXPECT_EQ(caretFrame.stats.focusedElementId, "sessionNotes");
XCEngine::Editor::XCUIBackend::XCUIDemoInputState caretInput = BuildInputState(); XCEngine::Editor::XCUIBackend::XCUIDemoInputState caretInput = BuildInputState();
caretInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Up));
caretInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::End));
caretInput.events.push_back(MakeCharacterEvent('!')); caretInput.events.push_back(MakeCharacterEvent('!'));
const auto& editedFrame = runtime.Update(caretInput); const auto& editedFrame = runtime.Update(caretInput);
ASSERT_TRUE(editedFrame.stats.documentsReady); ASSERT_TRUE(editedFrame.stats.documentsReady);
EXPECT_NE(FindTextCommand(editedFrame.drawData, "OK!"), nullptr); EXPECT_NE(FindTextCommand(editedFrame.drawData, "!OK"), nullptr);
EXPECT_NE(FindTextCommand(editedFrame.drawData, "X"), nullptr);
EXPECT_EQ(editedFrame.stats.focusedElementId, "sessionNotes"); EXPECT_EQ(editedFrame.stats.focusedElementId, "sessionNotes");
} }

View File

@@ -28,6 +28,8 @@ set(EDITOR_TEST_SOURCES
test_viewport_object_id_picker.cpp test_viewport_object_id_picker.cpp
test_viewport_render_targets.cpp test_viewport_render_targets.cpp
test_viewport_render_flow_utils.cpp test_viewport_render_flow_utils.cpp
test_imgui_backend_bridge_api.cpp
test_window_renderer_api.cpp
test_builtin_icon_layout_utils.cpp test_builtin_icon_layout_utils.cpp
test_xcui_draw_data.cpp test_xcui_draw_data.cpp
test_xcui_imgui_transition_backend.cpp test_xcui_imgui_transition_backend.cpp

View File

@@ -0,0 +1,22 @@
#include <gtest/gtest.h>
#include "Core/EditorConsoleSink.h"
namespace XCEngine::Debug {
namespace {
TEST(EditorConsoleSink, GetInstanceTracksRegisteredSinkOnly) {
EditorConsoleSink* registeredInstance = nullptr;
{
EditorConsoleSink sink;
registeredInstance = &sink;
EXPECT_EQ(EditorConsoleSink::GetInstance(), registeredInstance);
}
EXPECT_EQ(EditorConsoleSink::GetInstance(), nullptr);
}
} // namespace
} // namespace XCEngine::Debug

View File

@@ -0,0 +1,40 @@
#include <gtest/gtest.h>
#include "UI/ImGuiBackendBridge.h"
#include <type_traits>
namespace {
using XCEngine::Editor::UI::ImGuiBackendBridge;
using XCEngine::RHI::RHIDevice;
using XCEngine::RHI::RHIResourceView;
using XCEngine::RHI::RHITexture;
TEST(ImGuiBackendBridgeApiTest, ExposesExistingShaderResourceViewRegistrationOverload) {
using TextureDescriptorFromTextureSignature = bool (ImGuiBackendBridge::*)(
RHIDevice*,
RHITexture*,
D3D12_CPU_DESCRIPTOR_HANDLE*,
D3D12_GPU_DESCRIPTOR_HANDLE*,
ImTextureID*);
using TextureDescriptorFromSrvSignature = bool (ImGuiBackendBridge::*)(
RHIDevice*,
RHIResourceView*,
D3D12_CPU_DESCRIPTOR_HANDLE*,
D3D12_GPU_DESCRIPTOR_HANDLE*,
ImTextureID*);
static_assert(std::is_same_v<
decltype(static_cast<TextureDescriptorFromTextureSignature>(
&ImGuiBackendBridge::CreateTextureDescriptor)),
TextureDescriptorFromTextureSignature>);
static_assert(std::is_same_v<
decltype(static_cast<TextureDescriptorFromSrvSignature>(
&ImGuiBackendBridge::CreateTextureDescriptor)),
TextureDescriptorFromSrvSignature>);
SUCCEED();
}
} // namespace

View File

@@ -0,0 +1,28 @@
#include <gtest/gtest.h>
#include "Platform/D3D12WindowRenderer.h"
#include <type_traits>
namespace {
using XCEngine::Editor::Platform::D3D12WindowRenderer;
using XCEngine::Rendering::RenderSurface;
TEST(D3D12WindowRendererApiTest, ExposesSurfaceAwareRenderCallbackAndAccessor) {
using Callback = D3D12WindowRenderer::RenderCallback;
static_assert(std::is_same_v<
decltype(std::declval<D3D12WindowRenderer&>().GetCurrentRenderSurface()),
const RenderSurface*>);
static_assert(std::is_same_v<
decltype(std::declval<D3D12WindowRenderer&>().Render(
std::declval<::XCEngine::Editor::UI::ImGuiBackendBridge&>(),
std::declval<const float*>(),
std::declval<const Callback&>())),
void>);
SUCCEED();
}
} // namespace