Extract XCUI selection model and layout lab click selection
This commit is contained in:
@@ -32,6 +32,7 @@ Old `editor` replacement is explicitly out of scope for this phase.
|
|||||||
- Shared text-editing primitives now live under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so UTF-8 caret movement, line splitting, and multiline navigation are no longer trapped inside `XCUI Demo`.
|
- Shared text-editing primitives now live under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so UTF-8 caret movement, line splitting, and multiline navigation are no longer trapped inside `XCUI Demo`.
|
||||||
- Shared text-input controller/state now also lives under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so character insertion, backspace/delete, submit, and multiline key handling no longer need to be reimplemented per host.
|
- Shared text-input controller/state now also lives under `engine/include/XCEngine/UI/Text` and `engine/src/UI/Text`, so character insertion, backspace/delete, submit, and multiline key handling no longer need to be reimplemented per host.
|
||||||
- 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 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`.
|
||||||
- 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:
|
||||||
@@ -66,6 +67,7 @@ Current gap:
|
|||||||
- `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`.
|
||||||
- `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 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.
|
||||||
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
- Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths.
|
||||||
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
- The editor bridge layer now has smoke coverage for swapchain after-UI rendering hooks and SRV-backed ImGui texture descriptor registration.
|
||||||
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
|
- `Application` no longer owns the ImGui backend directly; window presentation now routes through `IWindowUICompositor` with an `ImGuiWindowUICompositor` implementation, which currently delegates to `IEditorHostCompositor` / `ImGuiHostCompositor`.
|
||||||
@@ -78,17 +80,17 @@ Current gap:
|
|||||||
|
|
||||||
- The shell is still ImGui-hosted.
|
- The shell is still ImGui-hosted.
|
||||||
- Legacy hosted preview still depends on an active ImGui window context for inline presentation.
|
- 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, selection models, 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/tree expansion state, command routing, property editing models, toolbar/menu chrome, and icon-atlas widgets are not yet extracted into reusable XCUI modules.
|
||||||
|
|
||||||
## Validated This Phase
|
## Validated This Phase
|
||||||
|
|
||||||
- `new_editor_xcui_demo_runtime_tests`: `7/7`
|
- `new_editor_xcui_demo_runtime_tests`: `7/7`
|
||||||
- `new_editor_xcui_layout_lab_runtime_tests`: `6/6`
|
- `new_editor_xcui_layout_lab_runtime_tests`: `7/7`
|
||||||
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
|
||||||
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
- `new_editor_xcui_rhi_render_backend_tests`: `5/5`
|
||||||
- `new_editor_xcui_hosted_preview_presenter_tests`: `12/12`
|
- `new_editor_xcui_hosted_preview_presenter_tests`: `12/12`
|
||||||
- `XCNewEditor` Debug target builds successfully
|
- `XCNewEditor` Debug target builds successfully
|
||||||
- `core_ui_tests`: `28/28`
|
- `core_ui_tests`: `30/30`
|
||||||
- `scene_tests`: `65/65`
|
- `scene_tests`: `65/65`
|
||||||
- `core_ui_style_tests`: `5/5`
|
- `core_ui_style_tests`: `5/5`
|
||||||
- `ui_resource_tests`: `11/11`
|
- `ui_resource_tests`: `11/11`
|
||||||
@@ -101,6 +103,7 @@ Current gap:
|
|||||||
- Common-core `UITextEditing` extraction now owns UTF-8 offset stepping, codepoint counting, line splitting, and vertical caret motion with dedicated `core_ui_tests` coverage.
|
- Common-core `UITextEditing` extraction now owns UTF-8 offset stepping, codepoint counting, line splitting, and vertical caret motion with dedicated `core_ui_tests` coverage.
|
||||||
- Common-core `UITextInputController` extraction now owns per-field text state, character insertion, enter-submit, and multiline keyboard editing behavior with dedicated `core_ui_tests` coverage.
|
- Common-core `UITextInputController` extraction now owns per-field text state, character insertion, enter-submit, and multiline keyboard editing behavior with dedicated `core_ui_tests` coverage.
|
||||||
- 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 `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.
|
||||||
- Demo runtime text editing was extended with:
|
- Demo runtime text editing was extended with:
|
||||||
- click-to-place caret
|
- click-to-place caret
|
||||||
- `Delete` support
|
- `Delete` support
|
||||||
@@ -109,6 +112,7 @@ Current gap:
|
|||||||
- 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.
|
||||||
|
- 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.
|
||||||
- Schema document support extended with:
|
- Schema document support extended with:
|
||||||
- retained `UISchemaDefinition` data on `UIDocumentModel`
|
- retained `UISchemaDefinition` data on `UIDocumentModel`
|
||||||
- artifact schema version bump for UI documents
|
- artifact schema version bump for UI documents
|
||||||
|
|||||||
@@ -521,7 +521,9 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextEditing.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Text/UITextInputController.cpp
|
${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/UIEditorCollectionPrimitives.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/UIEditorCollectionPrimitives.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/UIScreenTypes.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Runtime/UIScreenPlayer.h
|
||||||
|
|||||||
26
engine/include/XCEngine/UI/Widgets/UISelectionModel.h
Normal file
26
engine/include/XCEngine/UI/Widgets/UISelectionModel.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Widgets {
|
||||||
|
|
||||||
|
class UISelectionModel {
|
||||||
|
public:
|
||||||
|
bool HasSelection() const;
|
||||||
|
const std::string& GetSelectedId() const;
|
||||||
|
bool IsSelected(std::string_view id) const;
|
||||||
|
|
||||||
|
bool SetSelection(std::string selectionId);
|
||||||
|
bool ClearSelection();
|
||||||
|
bool ToggleSelection(std::string selectionId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_selectedId = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Widgets
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
49
engine/src/UI/Widgets/UISelectionModel.cpp
Normal file
49
engine/src/UI/Widgets/UISelectionModel.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Widgets {
|
||||||
|
|
||||||
|
bool UISelectionModel::HasSelection() const {
|
||||||
|
return !m_selectedId.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& UISelectionModel::GetSelectedId() const {
|
||||||
|
return m_selectedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UISelectionModel::IsSelected(std::string_view id) const {
|
||||||
|
return !m_selectedId.empty() && m_selectedId == id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UISelectionModel::SetSelection(std::string selectionId) {
|
||||||
|
if (m_selectedId == selectionId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedId = std::move(selectionId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UISelectionModel::ClearSelection() {
|
||||||
|
if (m_selectedId.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedId.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UISelectionModel::ToggleSelection(std::string selectionId) {
|
||||||
|
if (m_selectedId == selectionId) {
|
||||||
|
m_selectedId.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedId = std::move(selectionId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Widgets
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <XCEngine/UI/Style/Theme.h>
|
#include <XCEngine/UI/Style/Theme.h>
|
||||||
#include <XCEngine/UI/Types.h>
|
#include <XCEngine/UI/Types.h>
|
||||||
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
||||||
|
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -77,6 +78,7 @@ struct RuntimeBuildContext {
|
|||||||
std::vector<LayoutNode> nodes = {};
|
std::vector<LayoutNode> nodes = {};
|
||||||
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
|
std::unordered_map<std::string, std::size_t> nodeIndexById = {};
|
||||||
std::unordered_map<std::string, UIRect> rectsById = {};
|
std::unordered_map<std::string, UIRect> rectsById = {};
|
||||||
|
UIWidgets::UISelectionModel selectionModel = {};
|
||||||
bool documentsReady = false;
|
bool documentsReady = false;
|
||||||
std::string statusMessage = {};
|
std::string statusMessage = {};
|
||||||
XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource(
|
XCUIAssetDocumentSource documentSource = XCUIAssetDocumentSource(
|
||||||
@@ -752,6 +754,7 @@ void DrawNode(
|
|||||||
bodyFont);
|
bodyFont);
|
||||||
}
|
}
|
||||||
} else if (node.tagName == "Card") {
|
} else if (node.tagName == "Card") {
|
||||||
|
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||||
const Color cardColor = ResolveColorToken(
|
const Color cardColor = ResolveColorToken(
|
||||||
state.theme,
|
state.theme,
|
||||||
node.tone == "accent"
|
node.tone == "accent"
|
||||||
@@ -799,17 +802,25 @@ void DrawNode(
|
|||||||
UIColor(1.0f, 0.82f, 0.45f, 1.0f),
|
UIColor(1.0f, 0.82f, 0.45f, 1.0f),
|
||||||
2.0f,
|
2.0f,
|
||||||
rounding);
|
rounding);
|
||||||
|
} else if (selected) {
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
node.rect,
|
||||||
|
ToUIColor(ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))),
|
||||||
|
2.0f,
|
||||||
|
rounding);
|
||||||
}
|
}
|
||||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
|
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::TreeItem ||
|
||||||
primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
|
primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::ListItem) {
|
||||||
const bool hovered = node.id == hoveredId;
|
const bool hovered = node.id == hoveredId;
|
||||||
const Color rowColor = hovered
|
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||||
? ResolveColorToken(state.theme, "color.card.alt", Color(0.20f, 0.27f, 0.34f, 1.0f))
|
const Color rowColor = selected
|
||||||
: ResolveColorToken(state.theme, "color.card", Color(0.12f, 0.17f, 0.23f, 1.0f));
|
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
||||||
const Color borderColor = ResolveColorToken(
|
: (hovered
|
||||||
state.theme,
|
? ResolveColorToken(state.theme, "color.card.alt", Color(0.20f, 0.27f, 0.34f, 1.0f))
|
||||||
"color.border",
|
: ResolveColorToken(state.theme, "color.card", Color(0.12f, 0.17f, 0.23f, 1.0f)));
|
||||||
Color(0.24f, 0.34f, 0.43f, 1.0f));
|
const Color borderColor = selected
|
||||||
|
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
||||||
|
: ResolveColorToken(state.theme, "color.border", Color(0.24f, 0.34f, 0.43f, 1.0f));
|
||||||
const Color textColor = ResolveColorToken(
|
const Color textColor = ResolveColorToken(
|
||||||
state.theme,
|
state.theme,
|
||||||
"color.text",
|
"color.text",
|
||||||
@@ -848,6 +859,7 @@ void DrawNode(
|
|||||||
}
|
}
|
||||||
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
|
} else if (primitiveKind == UIWidgets::UIEditorCollectionPrimitiveKind::FieldRow) {
|
||||||
const bool hovered = node.id == hoveredId;
|
const bool hovered = node.id == hoveredId;
|
||||||
|
const bool selected = state.selectionModel.IsSelected(node.id);
|
||||||
const Color textColor = ResolveColorToken(
|
const Color textColor = ResolveColorToken(
|
||||||
state.theme,
|
state.theme,
|
||||||
"color.text",
|
"color.text",
|
||||||
@@ -856,7 +868,7 @@ void DrawNode(
|
|||||||
state.theme,
|
state.theme,
|
||||||
"color.text.muted",
|
"color.text.muted",
|
||||||
Color(0.72f, 0.79f, 0.86f, 1.0f));
|
Color(0.72f, 0.79f, 0.86f, 1.0f));
|
||||||
const Color lineColor = hovered
|
const Color lineColor = selected || hovered
|
||||||
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
? ResolveColorToken(state.theme, "color.accent", Color(0.30f, 0.46f, 0.58f, 1.0f))
|
||||||
: ResolveColorToken(state.theme, "color.border", Color(0.24f, 0.34f, 0.43f, 1.0f));
|
: ResolveColorToken(state.theme, "color.border", Color(0.24f, 0.34f, 0.43f, 1.0f));
|
||||||
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
|
const float inset = ResolveFloatToken(state.theme, "space.cardInset", 12.0f);
|
||||||
@@ -955,6 +967,7 @@ bool XCUILayoutLabRuntime::ReloadDocuments() {
|
|||||||
state.nodes.clear();
|
state.nodes.clear();
|
||||||
state.nodeIndexById.clear();
|
state.nodeIndexById.clear();
|
||||||
state.rectsById.clear();
|
state.rectsById.clear();
|
||||||
|
state.selectionModel.ClearSelection();
|
||||||
|
|
||||||
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
|
state.documentSource.SetPathSet(XCUIAssetDocumentSource::MakeLayoutLabPathSet());
|
||||||
if (!state.documentSource.Reload()) {
|
if (!state.documentSource.Reload()) {
|
||||||
@@ -1027,13 +1040,24 @@ const XCUILayoutLabFrameResult& XCUILayoutLabRuntime::Update(const XCUILayoutLab
|
|||||||
state.rectsById[state.nodes[0].id] = state.nodes[0].rect;
|
state.rectsById[state.nodes[0].id] = state.nodes[0].rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t hoveredIndex = kInvalidIndex;
|
||||||
if (input.pointerInside) {
|
if (input.pointerInside) {
|
||||||
const std::size_t hoveredIndex = HitTest(state, input.pointerPosition);
|
hoveredIndex = HitTest(state, input.pointerPosition);
|
||||||
if (hoveredIndex != kInvalidIndex) {
|
if (hoveredIndex != kInvalidIndex) {
|
||||||
state.frameResult.stats.hoveredElementId = state.nodes[hoveredIndex].id;
|
state.frameResult.stats.hoveredElementId = state.nodes[hoveredIndex].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.pointerPressed) {
|
||||||
|
if (hoveredIndex != kInvalidIndex) {
|
||||||
|
state.selectionModel.SetSelection(state.nodes[hoveredIndex].id);
|
||||||
|
} else {
|
||||||
|
state.selectionModel.ClearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.frameResult.stats.selectedElementId = state.selectionModel.GetSelectedId();
|
||||||
|
|
||||||
UIDrawList drawList("XCUI Layout Lab");
|
UIDrawList drawList("XCUI Layout Lab");
|
||||||
drawList.PushClipRect(input.canvasRect);
|
drawList.PushClipRect(input.canvasRect);
|
||||||
DrawNode(state, 0u, drawList, state.frameResult.stats.hoveredElementId);
|
DrawNode(state, 0u, drawList, state.frameResult.stats.hoveredElementId);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct XCUILayoutLabInputState {
|
|||||||
UI::UIRect canvasRect = {};
|
UI::UIRect canvasRect = {};
|
||||||
UI::UIPoint pointerPosition = {};
|
UI::UIPoint pointerPosition = {};
|
||||||
bool pointerInside = false;
|
bool pointerInside = false;
|
||||||
|
bool pointerPressed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct XCUILayoutLabFrameStats {
|
struct XCUILayoutLabFrameStats {
|
||||||
@@ -47,6 +48,7 @@ struct XCUILayoutLabFrameStats {
|
|||||||
std::size_t propertySectionCount = 0;
|
std::size_t propertySectionCount = 0;
|
||||||
std::size_t fieldRowCount = 0;
|
std::size_t fieldRowCount = 0;
|
||||||
std::string hoveredElementId = {};
|
std::string hoveredElementId = {};
|
||||||
|
std::string selectedElementId = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct XCUILayoutLabFrameResult {
|
struct XCUILayoutLabFrameResult {
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ void XCUILayoutLabPanel::Render() {
|
|||||||
input.pointerPosition = UI::UIPoint(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y);
|
input.pointerPosition = UI::UIPoint(ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y);
|
||||||
input.pointerInside = validCanvas && ImGui::IsItemHovered();
|
input.pointerInside = validCanvas && ImGui::IsItemHovered();
|
||||||
}
|
}
|
||||||
|
input.pointerPressed = input.pointerInside && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||||
|
|
||||||
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& frame = m_runtime.Update(input);
|
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& frame = m_runtime.Update(input);
|
||||||
|
|
||||||
@@ -337,17 +338,19 @@ void XCUILayoutLabPanel::Render() {
|
|||||||
"Native note: %s",
|
"Native note: %s",
|
||||||
stats.nativeOverlayStatusMessage.empty() ? "none" : stats.nativeOverlayStatusMessage.c_str());
|
stats.nativeOverlayStatusMessage.empty() ? "none" : stats.nativeOverlayStatusMessage.c_str());
|
||||||
ImGui::Text(
|
ImGui::Text(
|
||||||
"Hovered: %s | canvas: %.0f x %.0f",
|
"Hovered: %s | Selected: %s | canvas: %.0f x %.0f",
|
||||||
stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(),
|
stats.hoveredElementId.empty() ? "none" : stats.hoveredElementId.c_str(),
|
||||||
|
stats.selectedElementId.empty() ? "none" : stats.selectedElementId.c_str(),
|
||||||
input.canvasRect.width,
|
input.canvasRect.width,
|
||||||
input.canvasRect.height);
|
input.canvasRect.height);
|
||||||
|
|
||||||
ImGui::SeparatorText("Input");
|
ImGui::SeparatorText("Input");
|
||||||
ImGui::Text(
|
ImGui::Text(
|
||||||
"Pointer: %.0f, %.0f | inside %s",
|
"Pointer: %.0f, %.0f | inside %s | pressed %s",
|
||||||
input.pointerPosition.x,
|
input.pointerPosition.x,
|
||||||
input.pointerPosition.y,
|
input.pointerPosition.y,
|
||||||
input.pointerInside ? "yes" : "no");
|
input.pointerInside ? "yes" : "no",
|
||||||
|
input.pointerPressed ? "yes" : "no");
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ set(UI_TEST_SOURCES
|
|||||||
test_ui_core.cpp
|
test_ui_core.cpp
|
||||||
test_ui_editor_collection_primitives.cpp
|
test_ui_editor_collection_primitives.cpp
|
||||||
test_layout_engine.cpp
|
test_layout_engine.cpp
|
||||||
|
test_ui_selection_model.cpp
|
||||||
test_ui_runtime.cpp
|
test_ui_runtime.cpp
|
||||||
test_ui_text_editing.cpp
|
test_ui_text_editing.cpp
|
||||||
test_ui_text_input_controller.cpp
|
test_ui_text_input_controller.cpp
|
||||||
|
|||||||
42
tests/Core/UI/test_ui_selection_model.cpp
Normal file
42
tests/Core/UI/test_ui_selection_model.cpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::Widgets::UISelectionModel;
|
||||||
|
|
||||||
|
TEST(UISelectionModelTest, SetAndClearSelectionTrackCurrentId) {
|
||||||
|
UISelectionModel selection = {};
|
||||||
|
|
||||||
|
EXPECT_FALSE(selection.HasSelection());
|
||||||
|
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||||
|
|
||||||
|
EXPECT_TRUE(selection.SetSelection("assetLighting"));
|
||||||
|
EXPECT_TRUE(selection.HasSelection());
|
||||||
|
EXPECT_TRUE(selection.IsSelected("assetLighting"));
|
||||||
|
EXPECT_EQ(selection.GetSelectedId(), "assetLighting");
|
||||||
|
|
||||||
|
EXPECT_FALSE(selection.SetSelection("assetLighting"));
|
||||||
|
EXPECT_TRUE(selection.ClearSelection());
|
||||||
|
EXPECT_FALSE(selection.HasSelection());
|
||||||
|
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||||
|
EXPECT_FALSE(selection.ClearSelection());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UISelectionModelTest, ToggleSelectionSelectsAndDeselectsMatchingId) {
|
||||||
|
UISelectionModel selection = {};
|
||||||
|
|
||||||
|
EXPECT_TRUE(selection.ToggleSelection("treeScenes"));
|
||||||
|
EXPECT_EQ(selection.GetSelectedId(), "treeScenes");
|
||||||
|
|
||||||
|
EXPECT_TRUE(selection.ToggleSelection("treeScenes"));
|
||||||
|
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||||
|
|
||||||
|
EXPECT_TRUE(selection.ToggleSelection("treeMaterials"));
|
||||||
|
EXPECT_EQ(selection.GetSelectedId(), "treeMaterials");
|
||||||
|
EXPECT_TRUE(selection.ToggleSelection("treeUi"));
|
||||||
|
EXPECT_EQ(selection.GetSelectedId(), "treeUi");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -216,3 +216,37 @@ TEST(NewEditorXCUILayoutLabRuntimeTest, HoverIgnoresClippedScrollViewContent) {
|
|||||||
ASSERT_TRUE(frame.stats.documentsReady);
|
ASSERT_TRUE(frame.stats.documentsReady);
|
||||||
EXPECT_TRUE(frame.stats.hoveredElementId.empty());
|
EXPECT_TRUE(frame.stats.hoveredElementId.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(NewEditorXCUILayoutLabRuntimeTest, ClickSelectionPersistsOnSharedCollectionItems) {
|
||||||
|
XCEngine::Editor::XCUIBackend::XCUILayoutLabRuntime runtime;
|
||||||
|
ASSERT_TRUE(runtime.ReloadDocuments());
|
||||||
|
|
||||||
|
const auto& baseline = runtime.Update(BuildInputState());
|
||||||
|
ASSERT_TRUE(baseline.stats.documentsReady);
|
||||||
|
|
||||||
|
XCEngine::UI::UIRect targetRect = {};
|
||||||
|
ASSERT_TRUE(runtime.TryGetElementRect("fieldPosition", targetRect));
|
||||||
|
|
||||||
|
XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState clickInput = BuildInputState();
|
||||||
|
clickInput.pointerPosition = XCEngine::UI::UIPoint(
|
||||||
|
targetRect.x + targetRect.width * 0.5f,
|
||||||
|
targetRect.y + targetRect.height * 0.5f);
|
||||||
|
clickInput.pointerPressed = true;
|
||||||
|
const auto& selectedFrame = runtime.Update(clickInput);
|
||||||
|
|
||||||
|
ASSERT_TRUE(selectedFrame.stats.documentsReady);
|
||||||
|
ASSERT_FALSE(selectedFrame.stats.hoveredElementId.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
selectedFrame.stats.selectedElementId,
|
||||||
|
selectedFrame.stats.hoveredElementId);
|
||||||
|
|
||||||
|
const std::string selectedElementId = selectedFrame.stats.selectedElementId;
|
||||||
|
XCEngine::UI::UIRect selectedRect = {};
|
||||||
|
EXPECT_TRUE(runtime.TryGetElementRect(selectedElementId, selectedRect));
|
||||||
|
EXPECT_GT(selectedRect.width, 0.0f);
|
||||||
|
EXPECT_GT(selectedRect.height, 0.0f);
|
||||||
|
|
||||||
|
const auto& persistedFrame = runtime.Update(BuildInputState());
|
||||||
|
ASSERT_TRUE(persistedFrame.stats.documentsReady);
|
||||||
|
EXPECT_EQ(persistedFrame.stats.selectedElementId, selectedElementId);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user