Enhance XCUI demo text editing and host bridge
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
22
tests/editor/test_editor_console_sink.cpp
Normal file
22
tests/editor/test_editor_console_sink.cpp
Normal 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
|
||||||
40
tests/editor/test_imgui_backend_bridge_api.cpp
Normal file
40
tests/editor/test_imgui_backend_bridge_api.cpp
Normal 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
|
||||||
28
tests/editor/test_window_renderer_api.cpp
Normal file
28
tests/editor/test_window_renderer_api.cpp
Normal 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
|
||||||
Reference in New Issue
Block a user