Add XCUI command routing and widget state models
This commit is contained in:
@@ -34,13 +34,15 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
||||
- Shared editor collection primitive classification and metric helpers now also live under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets`, covering the current `ScrollView` / `TreeView` / `ListView` / `PropertySection` / `FieldRow` prototype taxonomy.
|
||||
- Shared single-selection state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UISelectionModel`, so collection-style widget selection no longer has to stay private to `LayoutLab`.
|
||||
- Shared expansion state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UIExpansionModel`, so collapsible tree/property-style widget state no longer has to stay private to `LayoutLab`.
|
||||
- Shared keyboard-navigation state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UIKeyboardNavigationModel`, so list/tree/property-style widgets can share current-index, anchor, and home/end/step navigation rules instead of re-rolling them per sandbox.
|
||||
- Shared property-edit session state now also lives under `engine/include/XCEngine/UI/Widgets` and `engine/src/UI/Widgets` as `UIPropertyEditModel`, so editor-facing field rows can reuse begin/edit/commit/cancel transaction state instead of baking that directly into sandbox runtimes.
|
||||
- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`.
|
||||
|
||||
Current gap:
|
||||
|
||||
- Minimal schema self-definition support is landed, including consistency checks for enum/document-only schema metadata, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented.
|
||||
- Shared widget/runtime instantiation is still thin and mostly editor-side.
|
||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/keyboard-navigation/virtualized collection state on top of the new editor-primitive helpers, and native image/source-rect level APIs.
|
||||
- Common widget primitives are still incomplete: shared text-input presentation/composition on top of the new text controller, multi-selection/focus-traversal/virtualized collection state on top of the new editor-primitive helpers, and native image/source-rect level APIs.
|
||||
|
||||
### 2. Runtime/Game Layer
|
||||
|
||||
@@ -70,6 +72,8 @@ Current gap:
|
||||
- `LayoutLab` now consumes the shared `UIEditorCollectionPrimitives` helper layer for collection-widget tag classification, clipping flags, and default metric resolution instead of keeping that taxonomy private to the sandbox runtime.
|
||||
- `LayoutLab` now also consumes the shared `UISelectionModel` for click-selection persistence across collection-style widgets, and the diagnostics panel now exposes both hovered and selected element ids.
|
||||
- `LayoutLab` now also consumes the shared `UIExpansionModel` for tree expansion and property-section collapse, with reserved property headers, disclosure glyphs, and persisted click-toggle behavior in the sandbox runtime.
|
||||
- `new_editor` now also has an isolated `XCUIEditorCommandRouter` model with shortcut matching, enable predicates, and direct command invocation semantics covered by dedicated tests, ready for shell-frame integration.
|
||||
- `XCUI Demo` now exports pending per-frame command ids through `DrainPendingCommandIds()`, so editor-side hosts have a clean seam for observing demo/runtime command traffic without parsing draw data.
|
||||
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
||||
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
|
||||
@@ -82,7 +86,7 @@ Current gap:
|
||||
|
||||
- The shell is still ImGui-hosted.
|
||||
- Legacy hosted preview still depends on an active ImGui window context for inline presentation.
|
||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/keyboard-navigation state, command routing, property editing models, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
||||
- Editor-specialized widgets are still incomplete at the shared-module level: the authored prototypes exist, but virtualization, multi-selection/focus traversal, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules, and the new keyboard-navigation/property-edit/command-routing models are still only partially integrated.
|
||||
|
||||
## Validated This Phase
|
||||
|
||||
@@ -90,10 +94,11 @@ Current gap:
|
||||
- `new_editor_xcui_layout_lab_runtime_tests`: `9/9`
|
||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||
- `new_editor_xcui_hosted_preview_presenter_tests`: `12/12`
|
||||
- `new_editor_imgui_window_ui_compositor_tests`: `5/5`
|
||||
- `new_editor_xcui_hosted_preview_presenter_tests`: `14/14`
|
||||
- `new_editor_imgui_window_ui_compositor_tests`: `7/7`
|
||||
- `new_editor_xcui_editor_command_router_tests`: `5/5`
|
||||
- `XCNewEditor` Debug target builds successfully
|
||||
- `core_ui_tests`: `38 total` (`36` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
|
||||
- `core_ui_tests`: `49 total` (`47` passed, `2` skipped because `KeyCode::Delete` currently aliases `Backspace`)
|
||||
- `scene_tests`: `68/68`
|
||||
- `core_ui_style_tests`: `5/5`
|
||||
- `ui_resource_tests`: `11/11`
|
||||
@@ -108,6 +113,8 @@ Current gap:
|
||||
- Common-core `UIEditorCollectionPrimitives` extraction now owns the editor collection tag taxonomy and default metric resolution used by current `LayoutLab` widget prototypes, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UISelectionModel` extraction now owns reusable single-selection state for collection-style widgets, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UIExpansionModel` extraction now owns reusable expansion/collapse state for tree/property-style widgets, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UIKeyboardNavigationModel` extraction now owns reusable current-index/anchor navigation state for collection-style widgets, with dedicated `core_ui_tests` coverage.
|
||||
- Common-core `UIPropertyEditModel` extraction now owns reusable property-field edit session state, including staged values and commit/cancel behavior, with dedicated `core_ui_tests` coverage.
|
||||
- Demo runtime text editing was extended with:
|
||||
- click-to-place caret
|
||||
- `Delete` support
|
||||
@@ -118,6 +125,7 @@ Current gap:
|
||||
- LayoutLab editor-widget prototypes for tree/list/property-style sections with dedicated runtime coverage.
|
||||
- LayoutLab click-selection now persists through the shared `UISelectionModel`, including selected-state diagnostics and reusable visual selection feedback on cards, collection rows, and field rows.
|
||||
- LayoutLab tree expansion and property-section collapse now persist through the shared `UIExpansionModel`, including reserved property headers, disclosure glyphs, and runtime coverage for collapsed/expanded visibility.
|
||||
- `XCUIDemoRuntime` now exposes `DrainPendingCommandIds()` so hosts can observe emitted runtime commands in order across pointer/text interactions without scraping UI text or draw-command payloads.
|
||||
- Schema document support extended with:
|
||||
- retained `UISchemaDefinition` data on `UIDocumentModel`
|
||||
- artifact schema version bump for UI documents
|
||||
@@ -162,6 +170,8 @@ Current gap:
|
||||
- `ImGuiHostCompositor`
|
||||
- `Application` frame/present flow routed through the compositor instead of direct `m_imguiBackend` ownership
|
||||
- The window-level XCUI compositor seam now also has a dedicated regression target around `ImGuiWindowUICompositor`, covering initialization, render-frame ordering, Win32 message forwarding, texture registration forwarding, and shutdown safety.
|
||||
- The window compositor and hosted-preview seams gained more edge-case coverage around no-UI render passes, compositor re-initialization/rebinding, partial logical-size fallback, and descriptor reuse for repeated queued-frame keys.
|
||||
- `new_editor` now has a dedicated `XCUIEditorCommandRouter` test target covering command registration, replacement, enable predicates, accelerator matching, and policy gates around focus/keyboard capture/text input.
|
||||
- `SceneRuntime` layered XCUI routing now has dedicated regression coverage for:
|
||||
- top-interactive layer input ownership
|
||||
- blocking/modal layer suppression of lower layers
|
||||
|
||||
@@ -522,9 +522,13 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIExpansionModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIKeyboardNavigationModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Widgets/UISelectionModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIEditorCollectionPrimitives.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIExpansionModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIKeyboardNavigationModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UIPropertyEditModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Widgets/UISelectionModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenTypes.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
class UIKeyboardNavigationModel {
|
||||
public:
|
||||
static constexpr std::size_t InvalidIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
std::size_t GetItemCount() const;
|
||||
bool SetItemCount(std::size_t itemCount);
|
||||
bool ClampToItemCount();
|
||||
|
||||
bool HasCurrentIndex() const;
|
||||
std::size_t GetCurrentIndex() const;
|
||||
bool SetCurrentIndex(std::size_t index, bool updateAnchor = true);
|
||||
bool ClearCurrentIndex();
|
||||
|
||||
bool HasSelectionAnchor() const;
|
||||
std::size_t GetSelectionAnchorIndex() const;
|
||||
bool SetSelectionAnchorIndex(std::size_t index);
|
||||
bool ClearSelectionAnchor();
|
||||
|
||||
bool MoveNext();
|
||||
bool MovePrevious();
|
||||
bool MoveHome();
|
||||
bool MoveEnd();
|
||||
|
||||
private:
|
||||
std::size_t m_itemCount = 0;
|
||||
std::size_t m_currentIndex = InvalidIndex;
|
||||
std::size_t m_selectionAnchorIndex = InvalidIndex;
|
||||
};
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
32
engine/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
Normal file
32
engine/include/XCEngine/UI/Widgets/UIPropertyEditModel.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
class UIPropertyEditModel {
|
||||
public:
|
||||
bool HasActiveEdit() const;
|
||||
const std::string& GetActiveFieldId() const;
|
||||
const std::string& GetStagedValue() const;
|
||||
bool IsDirty() const;
|
||||
|
||||
bool BeginEdit(std::string fieldId, std::string initialValue);
|
||||
bool UpdateStagedValue(std::string stagedValue);
|
||||
bool CommitEdit(
|
||||
std::string* outFieldId = nullptr,
|
||||
std::string* outCommittedValue = nullptr);
|
||||
bool CancelEdit();
|
||||
|
||||
private:
|
||||
std::string m_activeFieldId = {};
|
||||
std::string m_baselineValue = {};
|
||||
std::string m_stagedValue = {};
|
||||
bool m_dirty = false;
|
||||
};
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
162
engine/src/UI/Widgets/UIKeyboardNavigationModel.cpp
Normal file
162
engine/src/UI/Widgets/UIKeyboardNavigationModel.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ClampIndexToCount(std::size_t itemCount, std::size_t& index) {
|
||||
if (index == UIKeyboardNavigationModel::InvalidIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (itemCount == 0u) {
|
||||
index = UIKeyboardNavigationModel::InvalidIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index < itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = itemCount - 1u;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t UIKeyboardNavigationModel::GetItemCount() const {
|
||||
return m_itemCount;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::SetItemCount(std::size_t itemCount) {
|
||||
const bool countChanged = m_itemCount != itemCount;
|
||||
m_itemCount = itemCount;
|
||||
return ClampToItemCount() || countChanged;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::ClampToItemCount() {
|
||||
bool changed = false;
|
||||
changed = ClampIndexToCount(m_itemCount, m_currentIndex) || changed;
|
||||
changed = ClampIndexToCount(m_itemCount, m_selectionAnchorIndex) || changed;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::HasCurrentIndex() const {
|
||||
return m_currentIndex != InvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t UIKeyboardNavigationModel::GetCurrentIndex() const {
|
||||
return m_currentIndex;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::SetCurrentIndex(std::size_t index, bool updateAnchor) {
|
||||
if (index >= m_itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (m_currentIndex != index) {
|
||||
m_currentIndex = index;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateAnchor && m_selectionAnchorIndex != index) {
|
||||
m_selectionAnchorIndex = index;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::ClearCurrentIndex() {
|
||||
if (m_currentIndex == InvalidIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_currentIndex = InvalidIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::HasSelectionAnchor() const {
|
||||
return m_selectionAnchorIndex != InvalidIndex;
|
||||
}
|
||||
|
||||
std::size_t UIKeyboardNavigationModel::GetSelectionAnchorIndex() const {
|
||||
return m_selectionAnchorIndex;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::SetSelectionAnchorIndex(std::size_t index) {
|
||||
if (index >= m_itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_selectionAnchorIndex == index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_selectionAnchorIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::ClearSelectionAnchor() {
|
||||
if (m_selectionAnchorIndex == InvalidIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_selectionAnchorIndex = InvalidIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::MoveNext() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_currentIndex == InvalidIndex) {
|
||||
return SetCurrentIndex(0u);
|
||||
}
|
||||
|
||||
if ((m_currentIndex + 1u) >= m_itemCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetCurrentIndex(m_currentIndex + 1u);
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::MovePrevious() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_currentIndex == InvalidIndex) {
|
||||
return SetCurrentIndex(m_itemCount - 1u);
|
||||
}
|
||||
|
||||
if (m_currentIndex == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetCurrentIndex(m_currentIndex - 1u);
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::MoveHome() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetCurrentIndex(0u);
|
||||
}
|
||||
|
||||
bool UIKeyboardNavigationModel::MoveEnd() {
|
||||
if (m_itemCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetCurrentIndex(m_itemCount - 1u);
|
||||
}
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
95
engine/src/UI/Widgets/UIPropertyEditModel.cpp
Normal file
95
engine/src/UI/Widgets/UIPropertyEditModel.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Widgets {
|
||||
|
||||
bool UIPropertyEditModel::HasActiveEdit() const {
|
||||
return !m_activeFieldId.empty();
|
||||
}
|
||||
|
||||
const std::string& UIPropertyEditModel::GetActiveFieldId() const {
|
||||
return m_activeFieldId;
|
||||
}
|
||||
|
||||
const std::string& UIPropertyEditModel::GetStagedValue() const {
|
||||
return m_stagedValue;
|
||||
}
|
||||
|
||||
bool UIPropertyEditModel::IsDirty() const {
|
||||
return m_dirty;
|
||||
}
|
||||
|
||||
bool UIPropertyEditModel::BeginEdit(std::string fieldId, std::string initialValue) {
|
||||
if (fieldId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool stateChanged =
|
||||
m_activeFieldId != fieldId ||
|
||||
m_baselineValue != initialValue ||
|
||||
m_stagedValue != initialValue ||
|
||||
m_dirty;
|
||||
if (!stateChanged) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_activeFieldId = std::move(fieldId);
|
||||
m_baselineValue = std::move(initialValue);
|
||||
m_stagedValue = m_baselineValue;
|
||||
m_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIPropertyEditModel::UpdateStagedValue(std::string stagedValue) {
|
||||
if (!HasActiveEdit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_stagedValue == stagedValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_stagedValue = std::move(stagedValue);
|
||||
m_dirty = (m_stagedValue != m_baselineValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIPropertyEditModel::CommitEdit(
|
||||
std::string* outFieldId,
|
||||
std::string* outCommittedValue) {
|
||||
if (!HasActiveEdit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outFieldId != nullptr) {
|
||||
*outFieldId = m_activeFieldId;
|
||||
}
|
||||
if (outCommittedValue != nullptr) {
|
||||
*outCommittedValue = m_stagedValue;
|
||||
}
|
||||
|
||||
m_activeFieldId.clear();
|
||||
m_baselineValue.clear();
|
||||
m_stagedValue.clear();
|
||||
m_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIPropertyEditModel::CancelEdit() {
|
||||
if (!HasActiveEdit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_activeFieldId.clear();
|
||||
m_baselineValue.clear();
|
||||
m_stagedValue.clear();
|
||||
m_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Widgets
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
@@ -116,6 +116,7 @@ struct RuntimeBuildContext {
|
||||
bool accentEnabled = false;
|
||||
std::string resourceError = {};
|
||||
std::string lastCommandId = {};
|
||||
std::vector<std::string> pendingCommandIds = {};
|
||||
XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource(
|
||||
XCUIAssetDocumentSource::MakeDemoPathSet());
|
||||
fs::path repoRoot = {};
|
||||
@@ -945,6 +946,15 @@ std::string ResolveTextInputStateKey(const DemoNode& node) {
|
||||
return !stateKey.empty() ? stateKey : node.elementKey;
|
||||
}
|
||||
|
||||
void RecordCommand(RuntimeBuildContext& state, std::string commandId) {
|
||||
if (commandId.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastCommandId = commandId;
|
||||
state.pendingCommandIds.push_back(std::move(commandId));
|
||||
}
|
||||
|
||||
void EnsureTextInputStateInitialized(RuntimeBuildContext& state, const DemoNode& node) {
|
||||
if (!IsTextInputNode(node)) {
|
||||
return;
|
||||
@@ -1061,7 +1071,7 @@ bool HandleTextInputCharacterInput(
|
||||
return false;
|
||||
}
|
||||
|
||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||
RecordCommand(state, "demo.text.edit." + stateKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1089,9 +1099,9 @@ bool HandleTextInputKeyDown(
|
||||
}
|
||||
|
||||
if (result.submitRequested) {
|
||||
state.lastCommandId = "demo.text.submit." + stateKey;
|
||||
RecordCommand(state, "demo.text.submit." + stateKey);
|
||||
} else if (result.valueChanged) {
|
||||
state.lastCommandId = "demo.text.edit." + stateKey;
|
||||
RecordCommand(state, "demo.text.edit." + stateKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1130,7 +1140,7 @@ void ActivateNode(RuntimeBuildContext& state, UIElementId elementId) {
|
||||
state.accentEnabled = !state.accentEnabled;
|
||||
}
|
||||
|
||||
state.lastCommandId = BuildActivationCommandId(node);
|
||||
RecordCommand(state, BuildActivationCommandId(node));
|
||||
}
|
||||
|
||||
void BuildDemoNodesRecursive(
|
||||
@@ -2149,6 +2159,13 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::GetFrameResult() const {
|
||||
return m_state->data.frameResult;
|
||||
}
|
||||
|
||||
std::vector<std::string> XCUIDemoRuntime::DrainPendingCommandIds() {
|
||||
RuntimeBuildContext& state = m_state->data;
|
||||
std::vector<std::string> drainedCommandIds = std::move(state.pendingCommandIds);
|
||||
state.pendingCommandIds.clear();
|
||||
return drainedCommandIds;
|
||||
}
|
||||
|
||||
bool XCUIDemoRuntime::TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const {
|
||||
const RuntimeBuildContext& state = m_state->data;
|
||||
const auto it = state.nodeIndexByKey.find(elementId);
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
|
||||
const XCUIDemoFrameResult& Update(const XCUIDemoInputState& input);
|
||||
const XCUIDemoFrameResult& GetFrameResult() const;
|
||||
std::vector<std::string> DrainPendingCommandIds();
|
||||
|
||||
bool TryGetElementRect(const std::string& elementId, UI::UIRect& outRect) const;
|
||||
|
||||
|
||||
210
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
Normal file
210
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "XCUIBackend/XCUIEditorCommandRouter.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace XCUIBackend {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ModifiersMatch(
|
||||
const UI::UIInputModifiers& required,
|
||||
const UI::UIInputModifiers& actual,
|
||||
bool exact) {
|
||||
if (exact) {
|
||||
return required.shift == actual.shift &&
|
||||
required.control == actual.control &&
|
||||
required.alt == actual.alt &&
|
||||
required.super == actual.super;
|
||||
}
|
||||
|
||||
return (!required.shift || actual.shift) &&
|
||||
(!required.control || actual.control) &&
|
||||
(!required.alt || actual.alt) &&
|
||||
(!required.super || actual.super);
|
||||
}
|
||||
|
||||
bool AcceleratorMatchesSnapshot(
|
||||
const XCUIEditorCommandAccelerator& accelerator,
|
||||
const XCUIEditorCommandInputSnapshot& snapshot) {
|
||||
if (!accelerator.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandKeyState* keyState = snapshot.FindKeyState(accelerator.keyCode);
|
||||
if (keyState == nullptr || !keyState->down) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyState->repeat && !accelerator.allowRepeat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ModifiersMatch(accelerator.modifiers, snapshot.modifiers, accelerator.exactModifiers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const XCUIEditorCommandKeyState* XCUIEditorCommandInputSnapshot::FindKeyState(std::int32_t keyCode) const {
|
||||
for (const XCUIEditorCommandKeyState& keyState : keys) {
|
||||
if (keyState.keyCode == keyCode) {
|
||||
return &keyState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandInputSnapshot::IsKeyDown(std::int32_t keyCode) const {
|
||||
const XCUIEditorCommandKeyState* keyState = FindKeyState(keyCode);
|
||||
return keyState != nullptr && keyState->down;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandAccelerator::IsValid() const {
|
||||
return keyCode != 0;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandDefinition::IsValid() const {
|
||||
return !commandId.empty() && static_cast<bool>(invoke);
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::RegisterCommand(const XCUIEditorCommandDefinition& definition) {
|
||||
if (!definition.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CommandRecord replacement = {};
|
||||
replacement.commandId = definition.commandId;
|
||||
replacement.invoke = definition.invoke;
|
||||
replacement.isEnabled = definition.isEnabled;
|
||||
replacement.accelerators = definition.accelerators;
|
||||
|
||||
if (CommandRecord* existing = FindRecord(definition.commandId)) {
|
||||
*existing = std::move(replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_commands.push_back(std::move(replacement));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::UnregisterCommand(std::string_view commandId) {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
if (it == m_commands.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_commands.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void XCUIEditorCommandRouter::Clear() {
|
||||
m_commands.clear();
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::HasCommand(std::string_view commandId) const {
|
||||
return FindRecord(commandId) != nullptr;
|
||||
}
|
||||
|
||||
std::size_t XCUIEditorCommandRouter::GetCommandCount() const {
|
||||
return m_commands.size();
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::IsCommandEnabled(std::string_view commandId) const {
|
||||
const CommandRecord* record = FindRecord(commandId);
|
||||
return record != nullptr && EvaluateEnabled(*record);
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::InvokeCommand(std::string_view commandId) const {
|
||||
const CommandRecord* record = FindRecord(commandId);
|
||||
if (record == nullptr || !EvaluateEnabled(*record) || !record->invoke) {
|
||||
return false;
|
||||
}
|
||||
|
||||
record->invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
XCUIEditorCommandShortcutMatch XCUIEditorCommandRouter::MatchShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query) const {
|
||||
XCUIEditorCommandShortcutMatch result = {};
|
||||
if (query.snapshot == nullptr) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandInputSnapshot& snapshot = *query.snapshot;
|
||||
if (query.requireWindowFocused && !snapshot.windowFocused) {
|
||||
return result;
|
||||
}
|
||||
if (!query.allowWhenKeyboardCaptured && snapshot.wantCaptureKeyboard) {
|
||||
return result;
|
||||
}
|
||||
if (!query.allowWhenTextInputActive && snapshot.wantTextInput) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const CommandRecord& record : m_commands) {
|
||||
if (!EvaluateEnabled(record)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const XCUIEditorCommandAccelerator& accelerator : record.accelerators) {
|
||||
if (!AcceleratorMatchesSnapshot(accelerator, snapshot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.matched = true;
|
||||
result.commandId = record.commandId;
|
||||
result.accelerator = accelerator;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::InvokeMatchingShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query,
|
||||
XCUIEditorCommandShortcutMatch* outMatch) const {
|
||||
const XCUIEditorCommandShortcutMatch match = MatchShortcut(query);
|
||||
if (outMatch != nullptr) {
|
||||
*outMatch = match;
|
||||
}
|
||||
|
||||
return match.matched && InvokeCommand(match.commandId);
|
||||
}
|
||||
|
||||
XCUIEditorCommandRouter::CommandRecord* XCUIEditorCommandRouter::FindRecord(std::string_view commandId) {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
return it != m_commands.end() ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
const XCUIEditorCommandRouter::CommandRecord* XCUIEditorCommandRouter::FindRecord(
|
||||
std::string_view commandId) const {
|
||||
const auto it = std::find_if(
|
||||
m_commands.begin(),
|
||||
m_commands.end(),
|
||||
[commandId](const CommandRecord& record) {
|
||||
return record.commandId == commandId;
|
||||
});
|
||||
return it != m_commands.end() ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
bool XCUIEditorCommandRouter::EvaluateEnabled(const CommandRecord& record) {
|
||||
return !record.isEnabled || record.isEnabled();
|
||||
}
|
||||
|
||||
} // namespace XCUIBackend
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
101
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.h
Normal file
101
new_editor/src/XCUIBackend/XCUIEditorCommandRouter.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace XCUIBackend {
|
||||
|
||||
struct XCUIEditorCommandKeyState {
|
||||
std::int32_t keyCode = 0;
|
||||
bool down = false;
|
||||
bool repeat = false;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandInputSnapshot {
|
||||
UI::UIInputModifiers modifiers = {};
|
||||
bool windowFocused = false;
|
||||
bool wantCaptureKeyboard = false;
|
||||
bool wantTextInput = false;
|
||||
std::vector<XCUIEditorCommandKeyState> keys = {};
|
||||
|
||||
const XCUIEditorCommandKeyState* FindKeyState(std::int32_t keyCode) const;
|
||||
bool IsKeyDown(std::int32_t keyCode) const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandAccelerator {
|
||||
std::int32_t keyCode = 0;
|
||||
UI::UIInputModifiers modifiers = {};
|
||||
bool exactModifiers = true;
|
||||
bool allowRepeat = false;
|
||||
|
||||
bool IsValid() const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandDefinition {
|
||||
using InvokeCallback = std::function<void()>;
|
||||
using EnabledPredicate = std::function<bool()>;
|
||||
|
||||
std::string commandId = {};
|
||||
InvokeCallback invoke = {};
|
||||
EnabledPredicate isEnabled = {};
|
||||
std::vector<XCUIEditorCommandAccelerator> accelerators = {};
|
||||
|
||||
bool IsValid() const;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandShortcutQuery {
|
||||
const XCUIEditorCommandInputSnapshot* snapshot = nullptr;
|
||||
bool requireWindowFocused = true;
|
||||
bool allowWhenKeyboardCaptured = false;
|
||||
bool allowWhenTextInputActive = false;
|
||||
};
|
||||
|
||||
struct XCUIEditorCommandShortcutMatch {
|
||||
bool matched = false;
|
||||
std::string commandId = {};
|
||||
XCUIEditorCommandAccelerator accelerator = {};
|
||||
};
|
||||
|
||||
class XCUIEditorCommandRouter {
|
||||
public:
|
||||
bool RegisterCommand(const XCUIEditorCommandDefinition& definition);
|
||||
bool UnregisterCommand(std::string_view commandId);
|
||||
void Clear();
|
||||
|
||||
bool HasCommand(std::string_view commandId) const;
|
||||
std::size_t GetCommandCount() const;
|
||||
bool IsCommandEnabled(std::string_view commandId) const;
|
||||
|
||||
bool InvokeCommand(std::string_view commandId) const;
|
||||
XCUIEditorCommandShortcutMatch MatchShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query) const;
|
||||
bool InvokeMatchingShortcut(
|
||||
const XCUIEditorCommandShortcutQuery& query,
|
||||
XCUIEditorCommandShortcutMatch* outMatch = nullptr) const;
|
||||
|
||||
private:
|
||||
struct CommandRecord {
|
||||
std::string commandId = {};
|
||||
XCUIEditorCommandDefinition::InvokeCallback invoke = {};
|
||||
XCUIEditorCommandDefinition::EnabledPredicate isEnabled = {};
|
||||
std::vector<XCUIEditorCommandAccelerator> accelerators = {};
|
||||
};
|
||||
|
||||
CommandRecord* FindRecord(std::string_view commandId);
|
||||
const CommandRecord* FindRecord(std::string_view commandId) const;
|
||||
static bool EvaluateEnabled(const CommandRecord& record);
|
||||
|
||||
std::vector<CommandRecord> m_commands = {};
|
||||
};
|
||||
|
||||
} // namespace XCUIBackend
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -6,6 +6,8 @@ set(UI_TEST_SOURCES
|
||||
test_ui_core.cpp
|
||||
test_ui_editor_collection_primitives.cpp
|
||||
test_ui_expansion_model.cpp
|
||||
test_ui_keyboard_navigation_model.cpp
|
||||
test_ui_property_edit_model.cpp
|
||||
test_layout_engine.cpp
|
||||
test_ui_selection_model.cpp
|
||||
test_ui_runtime.cpp
|
||||
|
||||
101
tests/Core/UI/test_ui_keyboard_navigation_model.cpp
Normal file
101
tests/Core/UI/test_ui_keyboard_navigation_model.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::UIKeyboardNavigationModel;
|
||||
|
||||
TEST(UIKeyboardNavigationModelTest, EmptyModelStartsWithoutCurrentIndexOrAnchor) {
|
||||
UIKeyboardNavigationModel navigation = {};
|
||||
|
||||
EXPECT_EQ(navigation.GetItemCount(), 0u);
|
||||
EXPECT_FALSE(navigation.HasCurrentIndex());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), UIKeyboardNavigationModel::InvalidIndex);
|
||||
EXPECT_FALSE(navigation.HasSelectionAnchor());
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), UIKeyboardNavigationModel::InvalidIndex);
|
||||
EXPECT_FALSE(navigation.MoveNext());
|
||||
EXPECT_FALSE(navigation.MovePrevious());
|
||||
EXPECT_FALSE(navigation.MoveHome());
|
||||
EXPECT_FALSE(navigation.MoveEnd());
|
||||
}
|
||||
|
||||
TEST(UIKeyboardNavigationModelTest, SetCurrentIndexAndDirectionalMovesTrackCurrentIndexAndAnchor) {
|
||||
UIKeyboardNavigationModel navigation = {};
|
||||
ASSERT_TRUE(navigation.SetItemCount(4u));
|
||||
|
||||
EXPECT_TRUE(navigation.SetCurrentIndex(1u));
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 1u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
|
||||
|
||||
EXPECT_TRUE(navigation.MoveNext());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 2u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 2u);
|
||||
|
||||
EXPECT_TRUE(navigation.MoveEnd());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
|
||||
EXPECT_FALSE(navigation.MoveNext());
|
||||
|
||||
EXPECT_TRUE(navigation.MoveHome());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 0u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 0u);
|
||||
EXPECT_FALSE(navigation.MovePrevious());
|
||||
}
|
||||
|
||||
TEST(UIKeyboardNavigationModelTest, MovePreviousAndEndSeedNavigationWhenCurrentIndexIsUnset) {
|
||||
UIKeyboardNavigationModel navigation = {};
|
||||
ASSERT_TRUE(navigation.SetItemCount(5u));
|
||||
|
||||
EXPECT_TRUE(navigation.MovePrevious());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 4u);
|
||||
|
||||
EXPECT_TRUE(navigation.ClearCurrentIndex());
|
||||
EXPECT_TRUE(navigation.ClearSelectionAnchor());
|
||||
EXPECT_FALSE(navigation.HasCurrentIndex());
|
||||
EXPECT_FALSE(navigation.HasSelectionAnchor());
|
||||
|
||||
EXPECT_TRUE(navigation.MoveEnd());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 4u);
|
||||
}
|
||||
|
||||
TEST(UIKeyboardNavigationModelTest, ExplicitAnchorCanBePreservedUntilNavigationCollapsesIt) {
|
||||
UIKeyboardNavigationModel navigation = {};
|
||||
ASSERT_TRUE(navigation.SetItemCount(6u));
|
||||
|
||||
EXPECT_TRUE(navigation.SetSelectionAnchorIndex(1u));
|
||||
EXPECT_TRUE(navigation.SetCurrentIndex(4u, false));
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
|
||||
|
||||
EXPECT_TRUE(navigation.MovePrevious());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
|
||||
}
|
||||
|
||||
TEST(UIKeyboardNavigationModelTest, ItemCountChangesClampCurrentIndexAndSelectionAnchor) {
|
||||
UIKeyboardNavigationModel navigation = {};
|
||||
ASSERT_TRUE(navigation.SetItemCount(5u));
|
||||
ASSERT_TRUE(navigation.SetSelectionAnchorIndex(3u));
|
||||
ASSERT_TRUE(navigation.SetCurrentIndex(4u, false));
|
||||
|
||||
EXPECT_TRUE(navigation.SetItemCount(4u));
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
|
||||
|
||||
EXPECT_FALSE(navigation.SetCurrentIndex(3u, false));
|
||||
EXPECT_TRUE(navigation.SetSelectionAnchorIndex(2u));
|
||||
EXPECT_TRUE(navigation.SetItemCount(2u));
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), 1u);
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
|
||||
|
||||
EXPECT_TRUE(navigation.SetItemCount(0u));
|
||||
EXPECT_FALSE(navigation.HasCurrentIndex());
|
||||
EXPECT_EQ(navigation.GetCurrentIndex(), UIKeyboardNavigationModel::InvalidIndex);
|
||||
EXPECT_FALSE(navigation.HasSelectionAnchor());
|
||||
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), UIKeyboardNavigationModel::InvalidIndex);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
80
tests/Core/UI/test_ui_property_edit_model.cpp
Normal file
80
tests/Core/UI/test_ui_property_edit_model.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::UIPropertyEditModel;
|
||||
|
||||
TEST(UIPropertyEditModelTest, BeginEditTracksActiveFieldAndInitialValue) {
|
||||
UIPropertyEditModel model = {};
|
||||
|
||||
EXPECT_FALSE(model.HasActiveEdit());
|
||||
EXPECT_TRUE(model.GetActiveFieldId().empty());
|
||||
EXPECT_TRUE(model.GetStagedValue().empty());
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
|
||||
EXPECT_FALSE(model.BeginEdit("", "12.0"));
|
||||
EXPECT_TRUE(model.BeginEdit("transform.position.x", "12.0"));
|
||||
EXPECT_TRUE(model.HasActiveEdit());
|
||||
EXPECT_EQ(model.GetActiveFieldId(), "transform.position.x");
|
||||
EXPECT_EQ(model.GetStagedValue(), "12.0");
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
|
||||
EXPECT_FALSE(model.BeginEdit("transform.position.x", "12.0"));
|
||||
}
|
||||
|
||||
TEST(UIPropertyEditModelTest, UpdateStagedValueTracksDirtyAgainstBaseline) {
|
||||
UIPropertyEditModel model = {};
|
||||
|
||||
EXPECT_FALSE(model.UpdateStagedValue("3.5"));
|
||||
ASSERT_TRUE(model.BeginEdit("light.intensity", "1.0"));
|
||||
|
||||
EXPECT_TRUE(model.UpdateStagedValue("3.5"));
|
||||
EXPECT_EQ(model.GetStagedValue(), "3.5");
|
||||
EXPECT_TRUE(model.IsDirty());
|
||||
|
||||
EXPECT_FALSE(model.UpdateStagedValue("3.5"));
|
||||
EXPECT_TRUE(model.UpdateStagedValue("1.0"));
|
||||
EXPECT_EQ(model.GetStagedValue(), "1.0");
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
}
|
||||
|
||||
TEST(UIPropertyEditModelTest, CommitEditReturnsPayloadAndClearsState) {
|
||||
UIPropertyEditModel model = {};
|
||||
ASSERT_TRUE(model.BeginEdit("material.albedo", "#ffffff"));
|
||||
ASSERT_TRUE(model.UpdateStagedValue("#ffcc00"));
|
||||
|
||||
std::string committedFieldId = {};
|
||||
std::string committedValue = {};
|
||||
EXPECT_TRUE(model.CommitEdit(&committedFieldId, &committedValue));
|
||||
EXPECT_EQ(committedFieldId, "material.albedo");
|
||||
EXPECT_EQ(committedValue, "#ffcc00");
|
||||
|
||||
EXPECT_FALSE(model.HasActiveEdit());
|
||||
EXPECT_TRUE(model.GetActiveFieldId().empty());
|
||||
EXPECT_TRUE(model.GetStagedValue().empty());
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
EXPECT_FALSE(model.CommitEdit(&committedFieldId, &committedValue));
|
||||
}
|
||||
|
||||
TEST(UIPropertyEditModelTest, CancelEditDropsStagedChangesAndResetsSession) {
|
||||
UIPropertyEditModel model = {};
|
||||
ASSERT_TRUE(model.BeginEdit("camera.fov", "60"));
|
||||
ASSERT_TRUE(model.UpdateStagedValue("75"));
|
||||
ASSERT_TRUE(model.IsDirty());
|
||||
|
||||
EXPECT_TRUE(model.CancelEdit());
|
||||
EXPECT_FALSE(model.HasActiveEdit());
|
||||
EXPECT_TRUE(model.GetActiveFieldId().empty());
|
||||
EXPECT_TRUE(model.GetStagedValue().empty());
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
EXPECT_FALSE(model.CancelEdit());
|
||||
|
||||
EXPECT_TRUE(model.BeginEdit("camera.nearClip", "0.3"));
|
||||
EXPECT_EQ(model.GetActiveFieldId(), "camera.nearClip");
|
||||
EXPECT_EQ(model.GetStagedValue(), "0.3");
|
||||
EXPECT_FALSE(model.IsDirty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -58,6 +58,12 @@ set(NEW_EDITOR_INPUT_BRIDGE_HEADER
|
||||
set(NEW_EDITOR_INPUT_BRIDGE_SOURCE
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIInputBridge.cpp
|
||||
)
|
||||
set(NEW_EDITOR_COMMAND_ROUTER_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorCommandRouter.h
|
||||
)
|
||||
set(NEW_EDITOR_COMMAND_ROUTER_SOURCE
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/XCUIEditorCommandRouter.cpp
|
||||
)
|
||||
set(NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src/XCUIBackend/ImGuiXCUIInputAdapter.h
|
||||
)
|
||||
@@ -355,6 +361,33 @@ else()
|
||||
message(STATUS "Skipping new_editor_xcui_input_bridge_tests because input bridge files are missing.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NEW_EDITOR_COMMAND_ROUTER_HEADER}" AND
|
||||
EXISTS "${NEW_EDITOR_COMMAND_ROUTER_SOURCE}" AND
|
||||
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_xcui_editor_command_router.cpp")
|
||||
add_executable(new_editor_xcui_editor_command_router_tests
|
||||
test_xcui_editor_command_router.cpp
|
||||
${NEW_EDITOR_COMMAND_ROUTER_SOURCE}
|
||||
)
|
||||
|
||||
xcengine_configure_new_editor_test_target(new_editor_xcui_editor_command_router_tests)
|
||||
|
||||
target_link_libraries(new_editor_xcui_editor_command_router_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(new_editor_xcui_editor_command_router_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/new_editor/src
|
||||
)
|
||||
|
||||
xcengine_discover_new_editor_gtests(new_editor_xcui_editor_command_router_tests)
|
||||
else()
|
||||
message(STATUS "Skipping new_editor_xcui_editor_command_router_tests because command router files or the test source are missing.")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_HEADER}" AND
|
||||
EXISTS "${NEW_EDITOR_IMGUI_INPUT_ADAPTER_SOURCE}" AND
|
||||
EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test_imgui_xcui_input_adapter.cpp" AND
|
||||
|
||||
@@ -189,6 +189,32 @@ TEST(ImGuiWindowUICompositorTest, RenderFrameCallsHostBeginUiAndPresentInOrder)
|
||||
(std::vector<std::string>{ "begin", "ui", "present" }));
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, RenderFrameWithoutUiCallbackStillBeginsAndPresents) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
D3D12WindowRenderer renderer = {};
|
||||
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), renderer, {}));
|
||||
hostPtr->callOrder.clear();
|
||||
|
||||
constexpr float clearColor[4] = { 0.7f, 0.6f, 0.5f, 0.4f };
|
||||
compositor.RenderFrame(clearColor, {}, {}, {});
|
||||
|
||||
EXPECT_EQ(hostPtr->beginFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->endFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->presentedRenderer, &renderer);
|
||||
EXPECT_FALSE(hostPtr->beforeUiRenderProvided);
|
||||
EXPECT_FALSE(hostPtr->afterUiRenderProvided);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[0], clearColor[0]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[1], clearColor[1]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[2], clearColor[2]);
|
||||
EXPECT_EQ(hostPtr->lastClearColor[3], clearColor[3]);
|
||||
EXPECT_EQ(
|
||||
hostPtr->callOrder,
|
||||
(std::vector<std::string>{ "begin", "present" }));
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, HandleWindowMessageAndTextureRegistrationForwardToHost) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
@@ -222,6 +248,30 @@ TEST(ImGuiWindowUICompositorTest, HandleWindowMessageAndTextureRegistrationForwa
|
||||
EXPECT_EQ(hostPtr->freedRegistration.texture.nativeHandle, registration.texture.nativeHandle);
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, SecondInitializeRebindsRendererForSubsequentFrames) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
|
||||
ImGuiWindowUICompositor compositor(std::move(host));
|
||||
D3D12WindowRenderer firstRenderer = {};
|
||||
D3D12WindowRenderer secondRenderer = {};
|
||||
|
||||
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), firstRenderer, {}));
|
||||
ASSERT_TRUE(compositor.Initialize(MakeFakeHwnd(), secondRenderer, {}));
|
||||
EXPECT_EQ(hostPtr->initializeCount, 2);
|
||||
EXPECT_EQ(hostPtr->initializedRenderer, &secondRenderer);
|
||||
|
||||
compositor.RenderFrame(
|
||||
std::array<float, 4>{ 0.2f, 0.3f, 0.4f, 1.0f }.data(),
|
||||
[]() {},
|
||||
{},
|
||||
{});
|
||||
|
||||
EXPECT_EQ(hostPtr->beginFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->endFrameCount, 1);
|
||||
EXPECT_EQ(hostPtr->presentedRenderer, &secondRenderer);
|
||||
}
|
||||
|
||||
TEST(ImGuiWindowUICompositorTest, ShutdownClearsRendererBindingAndPreventsFurtherRender) {
|
||||
auto host = std::make_unique<RecordingHostCompositor>();
|
||||
RecordingHostCompositor* hostPtr = host.get();
|
||||
|
||||
@@ -219,6 +219,100 @@ TEST(NewEditorXCUIDemoRuntimeTest, InputStateTransitionsAreAcceptedAndFrameStill
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsReturnsEmptyForFramesWithoutCommands) {
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& frame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(frame.stats.documentsReady);
|
||||
|
||||
const std::vector<std::string> drainedCommandIds = runtime.DrainPendingCommandIds();
|
||||
EXPECT_TRUE(drainedCommandIds.empty());
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsCapturesPointerActivationCommands) {
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& baselineFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(baselineFrame.stats.documentsReady);
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
|
||||
XCEngine::UI::UIRect buttonRect = {};
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("toggleAccent", buttonRect));
|
||||
|
||||
const XCEngine::UI::UIPoint buttonCenter(
|
||||
buttonRect.x + buttonRect.width * 0.5f,
|
||||
buttonRect.y + buttonRect.height * 0.5f);
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState();
|
||||
pressedInput.pointerPosition = buttonCenter;
|
||||
pressedInput.pointerPressed = true;
|
||||
pressedInput.pointerDown = true;
|
||||
runtime.Update(pressedInput);
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState();
|
||||
releasedInput.pointerPosition = buttonCenter;
|
||||
releasedInput.pointerReleased = true;
|
||||
const auto& toggledFrame = runtime.Update(releasedInput);
|
||||
|
||||
ASSERT_TRUE(toggledFrame.stats.documentsReady);
|
||||
EXPECT_EQ(runtime.DrainPendingCommandIds(), std::vector<std::string>({ "demo.toggleAccent" }));
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUIDemoRuntimeTest, DrainPendingCommandIdsPreservesMultipleTextEditCommandsPerFrame) {
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
const auto& baselineFrame = runtime.Update(BuildInputState());
|
||||
ASSERT_TRUE(baselineFrame.stats.documentsReady);
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
|
||||
XCEngine::UI::UIRect promptRect = {};
|
||||
ASSERT_TRUE(runtime.TryGetElementRect("agentPrompt", promptRect));
|
||||
|
||||
const XCEngine::UI::UIPoint promptCenter(
|
||||
promptRect.x + promptRect.width * 0.5f,
|
||||
promptRect.y + promptRect.height * 0.5f);
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoInputState pressedInput = BuildInputState();
|
||||
pressedInput.pointerPosition = promptCenter;
|
||||
pressedInput.pointerPressed = true;
|
||||
pressedInput.pointerDown = true;
|
||||
runtime.Update(pressedInput);
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoInputState releasedInput = BuildInputState();
|
||||
releasedInput.pointerPosition = promptCenter;
|
||||
releasedInput.pointerReleased = true;
|
||||
const auto& focusedFrame = runtime.Update(releasedInput);
|
||||
|
||||
ASSERT_TRUE(focusedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(focusedFrame.stats.focusedElementId, "agentPrompt");
|
||||
EXPECT_EQ(
|
||||
runtime.DrainPendingCommandIds(),
|
||||
std::vector<std::string>({ "demo.activate.agentPrompt" }));
|
||||
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoInputState textInput = BuildInputState();
|
||||
textInput.events.push_back(MakeCharacterEvent('A'));
|
||||
textInput.events.push_back(MakeCharacterEvent('I'));
|
||||
textInput.events.push_back(MakeKeyDownEvent(XCEngine::Input::KeyCode::Backspace));
|
||||
const auto& typedFrame = runtime.Update(textInput);
|
||||
|
||||
ASSERT_TRUE(typedFrame.stats.documentsReady);
|
||||
EXPECT_EQ(typedFrame.stats.focusedElementId, "agentPrompt");
|
||||
EXPECT_EQ(
|
||||
runtime.DrainPendingCommandIds(),
|
||||
std::vector<std::string>({
|
||||
"demo.text.edit.agentPrompt",
|
||||
"demo.text.edit.agentPrompt",
|
||||
"demo.text.edit.agentPrompt" }));
|
||||
EXPECT_TRUE(runtime.DrainPendingCommandIds().empty());
|
||||
}
|
||||
|
||||
TEST(NewEditorXCUIDemoRuntimeTest, PointerToggleUpdatesFocusStatusTextAndAccentState) {
|
||||
XCEngine::Editor::XCUIBackend::XCUIDemoRuntime runtime;
|
||||
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||
|
||||
205
tests/NewEditor/test_xcui_editor_command_router.cpp
Normal file
205
tests/NewEditor/test_xcui_editor_command_router.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
#include "XCUIBackend/XCUIEditorCommandRouter.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandAccelerator;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandDefinition;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandKeyState;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandShortcutMatch;
|
||||
using XCEngine::Editor::XCUIBackend::XCUIEditorCommandShortcutQuery;
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
XCUIEditorCommandKeyState MakeKeyState(std::int32_t keyCode, bool down, bool repeat = false) {
|
||||
XCUIEditorCommandKeyState state = {};
|
||||
state.keyCode = keyCode;
|
||||
state.down = down;
|
||||
state.repeat = repeat;
|
||||
return state;
|
||||
}
|
||||
|
||||
XCUIEditorCommandInputSnapshot MakeSnapshot(
|
||||
std::initializer_list<XCUIEditorCommandKeyState> keys,
|
||||
const XCEngine::UI::UIInputModifiers& modifiers = {},
|
||||
bool windowFocused = true) {
|
||||
XCUIEditorCommandInputSnapshot snapshot = {};
|
||||
snapshot.keys.assign(keys.begin(), keys.end());
|
||||
snapshot.modifiers = modifiers;
|
||||
snapshot.windowFocused = windowFocused;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
TEST(XCUIEditorCommandRouterTest, RegisterInvokeAndUnregisterTrackCommandsById) {
|
||||
XCUIEditorCommandRouter router = {};
|
||||
int invokeCount = 0;
|
||||
|
||||
XCUIEditorCommandDefinition definition = {};
|
||||
definition.commandId = "xcui.file.save";
|
||||
definition.invoke = [&invokeCount]() { ++invokeCount; };
|
||||
|
||||
EXPECT_TRUE(router.RegisterCommand(definition));
|
||||
EXPECT_EQ(router.GetCommandCount(), 1u);
|
||||
EXPECT_TRUE(router.HasCommand("xcui.file.save"));
|
||||
EXPECT_TRUE(router.IsCommandEnabled("xcui.file.save"));
|
||||
EXPECT_TRUE(router.InvokeCommand("xcui.file.save"));
|
||||
EXPECT_EQ(invokeCount, 1);
|
||||
|
||||
EXPECT_TRUE(router.UnregisterCommand("xcui.file.save"));
|
||||
EXPECT_FALSE(router.HasCommand("xcui.file.save"));
|
||||
EXPECT_FALSE(router.InvokeCommand("xcui.file.save"));
|
||||
EXPECT_EQ(router.GetCommandCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(XCUIEditorCommandRouterTest, RegisterRejectsMissingIdOrHandlerAndDuplicateIdReplacesEntry) {
|
||||
XCUIEditorCommandRouter router = {};
|
||||
int invokeCount = 0;
|
||||
|
||||
XCUIEditorCommandDefinition invalid = {};
|
||||
invalid.commandId = "xcui.invalid";
|
||||
EXPECT_FALSE(router.RegisterCommand(invalid));
|
||||
|
||||
invalid = {};
|
||||
invalid.invoke = []() {};
|
||||
EXPECT_FALSE(router.RegisterCommand(invalid));
|
||||
|
||||
XCUIEditorCommandDefinition first = {};
|
||||
first.commandId = "xcui.edit.rename";
|
||||
first.invoke = [&invokeCount]() { invokeCount += 1; };
|
||||
first.accelerators.push_back({ static_cast<std::int32_t>(KeyCode::F2), {}, true, false });
|
||||
ASSERT_TRUE(router.RegisterCommand(first));
|
||||
|
||||
XCUIEditorCommandDefinition replacement = {};
|
||||
replacement.commandId = "xcui.edit.rename";
|
||||
replacement.invoke = [&invokeCount]() { invokeCount += 10; };
|
||||
replacement.accelerators.push_back({ static_cast<std::int32_t>(KeyCode::Enter), {}, true, false });
|
||||
ASSERT_TRUE(router.RegisterCommand(replacement));
|
||||
|
||||
EXPECT_EQ(router.GetCommandCount(), 1u);
|
||||
EXPECT_TRUE(router.InvokeCommand("xcui.edit.rename"));
|
||||
EXPECT_EQ(invokeCount, 10);
|
||||
|
||||
const XCUIEditorCommandInputSnapshot f2Snapshot =
|
||||
MakeSnapshot({ MakeKeyState(static_cast<std::int32_t>(KeyCode::F2), true) });
|
||||
const auto f2Match = router.MatchShortcut({ &f2Snapshot });
|
||||
EXPECT_FALSE(f2Match.matched);
|
||||
|
||||
const XCUIEditorCommandInputSnapshot enterSnapshot =
|
||||
MakeSnapshot({ MakeKeyState(static_cast<std::int32_t>(KeyCode::Enter), true) });
|
||||
const auto enterMatch = router.MatchShortcut({ &enterSnapshot });
|
||||
EXPECT_TRUE(enterMatch.matched);
|
||||
EXPECT_EQ(enterMatch.commandId, "xcui.edit.rename");
|
||||
}
|
||||
|
||||
TEST(XCUIEditorCommandRouterTest, DisabledPredicateBlocksDirectAndShortcutInvocation) {
|
||||
XCUIEditorCommandRouter router = {};
|
||||
bool enabled = false;
|
||||
int invokeCount = 0;
|
||||
|
||||
XCUIEditorCommandDefinition definition = {};
|
||||
definition.commandId = "xcui.edit.delete";
|
||||
definition.invoke = [&invokeCount]() { ++invokeCount; };
|
||||
definition.isEnabled = [&enabled]() { return enabled; };
|
||||
definition.accelerators.push_back({ static_cast<std::int32_t>(KeyCode::Delete), {}, true, false });
|
||||
ASSERT_TRUE(router.RegisterCommand(definition));
|
||||
|
||||
const XCUIEditorCommandInputSnapshot snapshot =
|
||||
MakeSnapshot({ MakeKeyState(static_cast<std::int32_t>(KeyCode::Delete), true) });
|
||||
EXPECT_FALSE(router.IsCommandEnabled("xcui.edit.delete"));
|
||||
EXPECT_FALSE(router.InvokeCommand("xcui.edit.delete"));
|
||||
EXPECT_FALSE(router.MatchShortcut({ &snapshot }).matched);
|
||||
EXPECT_FALSE(router.InvokeMatchingShortcut({ &snapshot }));
|
||||
EXPECT_EQ(invokeCount, 0);
|
||||
|
||||
enabled = true;
|
||||
EXPECT_TRUE(router.IsCommandEnabled("xcui.edit.delete"));
|
||||
EXPECT_TRUE(router.InvokeMatchingShortcut({ &snapshot }));
|
||||
EXPECT_EQ(invokeCount, 1);
|
||||
}
|
||||
|
||||
TEST(XCUIEditorCommandRouterTest, ShortcutMatchingRespectsModifiersRepeatAndPolicyFlags) {
|
||||
XCUIEditorCommandRouter router = {};
|
||||
int invokeCount = 0;
|
||||
|
||||
XCEngine::UI::UIInputModifiers modifiers = {};
|
||||
modifiers.control = true;
|
||||
modifiers.shift = true;
|
||||
|
||||
XCUIEditorCommandDefinition definition = {};
|
||||
definition.commandId = "xcui.search.command_palette";
|
||||
definition.invoke = [&invokeCount]() { ++invokeCount; };
|
||||
definition.accelerators.push_back({
|
||||
static_cast<std::int32_t>(KeyCode::P),
|
||||
modifiers,
|
||||
true,
|
||||
false });
|
||||
ASSERT_TRUE(router.RegisterCommand(definition));
|
||||
|
||||
const XCUIEditorCommandInputSnapshot exactSnapshot = MakeSnapshot(
|
||||
{ MakeKeyState(static_cast<std::int32_t>(KeyCode::P), true) },
|
||||
modifiers);
|
||||
auto match = router.MatchShortcut({ &exactSnapshot });
|
||||
ASSERT_TRUE(match.matched);
|
||||
EXPECT_EQ(match.commandId, "xcui.search.command_palette");
|
||||
|
||||
const XCUIEditorCommandInputSnapshot repeatedSnapshot = MakeSnapshot(
|
||||
{ MakeKeyState(static_cast<std::int32_t>(KeyCode::P), true, true) },
|
||||
modifiers);
|
||||
EXPECT_FALSE(router.MatchShortcut({ &repeatedSnapshot }).matched);
|
||||
|
||||
XCUIEditorCommandInputSnapshot captureSnapshot = exactSnapshot;
|
||||
captureSnapshot.wantCaptureKeyboard = true;
|
||||
EXPECT_FALSE(router.MatchShortcut({ &captureSnapshot }).matched);
|
||||
EXPECT_TRUE(router.MatchShortcut({ &captureSnapshot, true, true, false }).matched);
|
||||
|
||||
XCUIEditorCommandInputSnapshot textInputSnapshot = exactSnapshot;
|
||||
textInputSnapshot.wantTextInput = true;
|
||||
EXPECT_FALSE(router.MatchShortcut({ &textInputSnapshot }).matched);
|
||||
EXPECT_TRUE(router.MatchShortcut({ &textInputSnapshot, true, false, true }).matched);
|
||||
|
||||
XCUIEditorCommandInputSnapshot unfocusedSnapshot = exactSnapshot;
|
||||
unfocusedSnapshot.windowFocused = false;
|
||||
EXPECT_FALSE(router.MatchShortcut({ &unfocusedSnapshot }).matched);
|
||||
EXPECT_TRUE(router.MatchShortcut({ &unfocusedSnapshot, false, false, false }).matched);
|
||||
|
||||
XCUIEditorCommandShortcutMatch invokedMatch = {};
|
||||
EXPECT_TRUE(router.InvokeMatchingShortcut({ &exactSnapshot }, &invokedMatch));
|
||||
EXPECT_TRUE(invokedMatch.matched);
|
||||
EXPECT_EQ(invokedMatch.commandId, "xcui.search.command_palette");
|
||||
EXPECT_EQ(invokeCount, 1);
|
||||
}
|
||||
|
||||
TEST(XCUIEditorCommandRouterTest, NonExactModifierAcceleratorsAllowAdditionalPressedModifiers) {
|
||||
XCUIEditorCommandRouter router = {};
|
||||
|
||||
XCEngine::UI::UIInputModifiers requiredModifiers = {};
|
||||
requiredModifiers.control = true;
|
||||
|
||||
XCUIEditorCommandDefinition definition = {};
|
||||
definition.commandId = "xcui.edit.duplicate";
|
||||
definition.invoke = []() {};
|
||||
definition.accelerators.push_back({
|
||||
static_cast<std::int32_t>(KeyCode::D),
|
||||
requiredModifiers,
|
||||
false,
|
||||
false });
|
||||
ASSERT_TRUE(router.RegisterCommand(definition));
|
||||
|
||||
XCEngine::UI::UIInputModifiers actualModifiers = requiredModifiers;
|
||||
actualModifiers.shift = true;
|
||||
const XCUIEditorCommandInputSnapshot permissiveSnapshot = MakeSnapshot(
|
||||
{ MakeKeyState(static_cast<std::int32_t>(KeyCode::D), true) },
|
||||
actualModifiers);
|
||||
EXPECT_TRUE(router.MatchShortcut({ &permissiveSnapshot }).matched);
|
||||
|
||||
XCUIEditorCommandRouter exactRouter = {};
|
||||
definition.accelerators.front().exactModifiers = true;
|
||||
ASSERT_TRUE(exactRouter.RegisterCommand(definition));
|
||||
EXPECT_FALSE(exactRouter.MatchShortcut({ &permissiveSnapshot }).matched);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -242,6 +242,34 @@ TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterFallsBackLogicalSizeTo
|
||||
EXPECT_FLOAT_EQ(image.uvMax.y, 0.5f);
|
||||
}
|
||||
|
||||
TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterFallsBackWhenLogicalSizeIsPartiallySpecified) {
|
||||
XCUIHostedPreviewQueue queue = {};
|
||||
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
|
||||
queue.BeginFrame();
|
||||
|
||||
std::unique_ptr<IXCUIHostedPreviewPresenter> presenter =
|
||||
CreateQueuedNativeXCUIHostedPreviewPresenter(queue, surfaceRegistry);
|
||||
ASSERT_NE(presenter, nullptr);
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
drawData.EmplaceDrawList("HostedPreviewPartialLogicalSize").AddFilledRect(
|
||||
XCEngine::UI::UIRect(2.0f, 3.0f, 18.0f, 9.0f),
|
||||
XCEngine::UI::UIColor(0.1f, 0.2f, 0.3f, 1.0f));
|
||||
|
||||
XCUIHostedPreviewFrame frame = {};
|
||||
frame.drawData = &drawData;
|
||||
frame.debugName = "XCUI Partial Logical Size";
|
||||
frame.canvasRect = XCEngine::UI::UIRect(10.0f, 20.0f, 300.0f, 140.0f);
|
||||
frame.logicalSize = XCEngine::UI::UISize(512.0f, 0.0f);
|
||||
|
||||
ASSERT_TRUE(presenter->Present(frame));
|
||||
ASSERT_EQ(queue.GetQueuedFrames().size(), 1u);
|
||||
|
||||
const XCUIHostedPreviewQueuedFrame& queuedFrame = queue.GetQueuedFrames().front();
|
||||
EXPECT_FLOAT_EQ(queuedFrame.logicalSize.width, 300.0f);
|
||||
EXPECT_FLOAT_EQ(queuedFrame.logicalSize.height, 140.0f);
|
||||
}
|
||||
|
||||
TEST(XCUIHostedPreviewPresenterTest, QueuedNativePresenterRejectsMissingDrawDataAndLeavesQueueUntouched) {
|
||||
XCUIHostedPreviewQueue queue = {};
|
||||
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
|
||||
@@ -430,6 +458,61 @@ TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryTracksQueuedFrameMetadataAlo
|
||||
EXPECT_TRUE(descriptor.image.IsValid());
|
||||
}
|
||||
|
||||
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryReusesDescriptorForRepeatedQueuedFrameKeys) {
|
||||
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
|
||||
surfaceRegistry.BeginFrame();
|
||||
|
||||
surfaceRegistry.UpdateSurface(
|
||||
"XCUI Demo",
|
||||
MakeHostedPreviewTextureHandle(31u, 400u, 200u),
|
||||
XCEngine::UI::UIRect(0.0f, 0.0f, 200.0f, 100.0f));
|
||||
|
||||
XCUIHostedPreviewQueuedFrame firstQueuedFrame = {};
|
||||
firstQueuedFrame.debugName = "XCUI Demo";
|
||||
firstQueuedFrame.debugSource = "tests.hosted_preview.first";
|
||||
firstQueuedFrame.canvasRect = XCEngine::UI::UIRect(8.0f, 12.0f, 320.0f, 180.0f);
|
||||
firstQueuedFrame.logicalSize = XCEngine::UI::UISize(640.0f, 360.0f);
|
||||
firstQueuedFrame.drawData.EmplaceDrawList("First").AddFilledRect(
|
||||
XCEngine::UI::UIRect(0.0f, 0.0f, 16.0f, 16.0f),
|
||||
XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
XCUIHostedPreviewQueuedFrame secondQueuedFrame = {};
|
||||
secondQueuedFrame.debugName = "XCUI Demo";
|
||||
secondQueuedFrame.debugSource = "tests.hosted_preview.second";
|
||||
secondQueuedFrame.canvasRect = XCEngine::UI::UIRect(20.0f, 24.0f, 256.0f, 144.0f);
|
||||
secondQueuedFrame.logicalSize = XCEngine::UI::UISize(512.0f, 288.0f);
|
||||
XCEngine::UI::UIDrawList& secondDrawList = secondQueuedFrame.drawData.EmplaceDrawList("Second");
|
||||
secondDrawList.AddFilledRect(
|
||||
XCEngine::UI::UIRect(1.0f, 2.0f, 20.0f, 10.0f),
|
||||
XCEngine::UI::UIColor(0.0f, 1.0f, 0.0f, 1.0f));
|
||||
secondDrawList.AddText(
|
||||
XCEngine::UI::UIPoint(4.0f, 6.0f),
|
||||
"second",
|
||||
XCEngine::UI::UIColor(1.0f, 1.0f, 1.0f, 1.0f),
|
||||
12.0f);
|
||||
|
||||
surfaceRegistry.RecordQueuedFrame(firstQueuedFrame, 1u);
|
||||
surfaceRegistry.RecordQueuedFrame(secondQueuedFrame, 7u);
|
||||
|
||||
ASSERT_EQ(surfaceRegistry.GetDescriptors().size(), 1u);
|
||||
|
||||
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
|
||||
ASSERT_TRUE(surfaceRegistry.TryGetSurfaceDescriptor("XCUI Demo", descriptor));
|
||||
EXPECT_EQ(descriptor.debugSource, "tests.hosted_preview.second");
|
||||
EXPECT_FLOAT_EQ(descriptor.canvasRect.x, 20.0f);
|
||||
EXPECT_FLOAT_EQ(descriptor.canvasRect.y, 24.0f);
|
||||
EXPECT_FLOAT_EQ(descriptor.canvasRect.width, 256.0f);
|
||||
EXPECT_FLOAT_EQ(descriptor.canvasRect.height, 144.0f);
|
||||
EXPECT_FLOAT_EQ(descriptor.logicalSize.width, 512.0f);
|
||||
EXPECT_FLOAT_EQ(descriptor.logicalSize.height, 288.0f);
|
||||
EXPECT_EQ(descriptor.queuedFrameIndex, 7u);
|
||||
EXPECT_EQ(descriptor.submittedDrawListCount, 1u);
|
||||
EXPECT_EQ(descriptor.submittedCommandCount, 2u);
|
||||
EXPECT_TRUE(descriptor.queuedThisFrame);
|
||||
EXPECT_TRUE(descriptor.image.IsValid());
|
||||
EXPECT_EQ(descriptor.image.texture.nativeHandle, 31u);
|
||||
}
|
||||
|
||||
TEST(XCUIHostedPreviewPresenterTest, SurfaceRegistryRejectsInvalidSurfaceUpdatesWithoutClobberingExistingImage) {
|
||||
XCUIHostedPreviewSurfaceRegistry surfaceRegistry = {};
|
||||
XCUIHostedPreviewSurfaceDescriptor descriptor = {};
|
||||
|
||||
Reference in New Issue
Block a user