Add XCUI core scroll view validation in new_editor
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace UI {
|
namespace UI {
|
||||||
namespace Runtime {
|
namespace Runtime {
|
||||||
@@ -18,10 +22,31 @@ public:
|
|||||||
|
|
||||||
class UIDocumentScreenHost final : public IUIScreenDocumentHost {
|
class UIDocumentScreenHost final : public IUIScreenDocumentHost {
|
||||||
public:
|
public:
|
||||||
|
struct ScrollDebugSnapshot {
|
||||||
|
std::uint64_t totalWheelEventCount = 0u;
|
||||||
|
std::uint64_t handledWheelEventCount = 0u;
|
||||||
|
UIPoint lastPointerPosition = {};
|
||||||
|
UIRect lastViewportRect = {};
|
||||||
|
UIRect primaryViewportRect = {};
|
||||||
|
float lastWheelDelta = 0.0f;
|
||||||
|
float lastOverflow = 0.0f;
|
||||||
|
float lastOffsetBefore = 0.0f;
|
||||||
|
float lastOffsetAfter = 0.0f;
|
||||||
|
float primaryOverflow = 0.0f;
|
||||||
|
std::string lastTargetStateKey = {};
|
||||||
|
std::string primaryTargetStateKey = {};
|
||||||
|
std::string lastResult = {};
|
||||||
|
};
|
||||||
|
|
||||||
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override;
|
UIScreenLoadResult LoadScreen(const UIScreenAsset& asset) override;
|
||||||
UIScreenFrameResult BuildFrame(
|
UIScreenFrameResult BuildFrame(
|
||||||
const UIScreenDocument& document,
|
const UIScreenDocument& document,
|
||||||
const UIScreenFrameInput& input) override;
|
const UIScreenFrameInput& input) override;
|
||||||
|
const ScrollDebugSnapshot& GetScrollDebugSnapshot() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, float> m_verticalScrollOffsets = {};
|
||||||
|
ScrollDebugSnapshot m_scrollDebugSnapshot = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Runtime
|
} // namespace Runtime
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -39,9 +40,15 @@ constexpr float kHeaderTextGap = 2.0f;
|
|||||||
|
|
||||||
struct RuntimeLayoutNode {
|
struct RuntimeLayoutNode {
|
||||||
const UIDocumentNode* source = nullptr;
|
const UIDocumentNode* source = nullptr;
|
||||||
|
std::string stateKey = {};
|
||||||
std::vector<RuntimeLayoutNode> children = {};
|
std::vector<RuntimeLayoutNode> children = {};
|
||||||
UISize desiredSize = {};
|
UISize desiredSize = {};
|
||||||
|
UISize minimumSize = {};
|
||||||
|
UISize contentDesiredSize = {};
|
||||||
UIRect rect = {};
|
UIRect rect = {};
|
||||||
|
UIRect scrollViewportRect = {};
|
||||||
|
float scrollOffsetY = 0.0f;
|
||||||
|
bool isScrollView = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
UIColor ToUIColor(const Color& color) {
|
UIColor ToUIColor(const Color& color) {
|
||||||
@@ -185,23 +192,14 @@ Layout::UILayoutThickness ParsePadding(
|
|||||||
|
|
||||||
Layout::UILayoutItem BuildLayoutItem(
|
Layout::UILayoutItem BuildLayoutItem(
|
||||||
const RuntimeLayoutNode& child,
|
const RuntimeLayoutNode& child,
|
||||||
Layout::UILayoutAxis parentAxis) {
|
Layout::UILayoutAxis parentAxis,
|
||||||
|
const UISize& measuredContentSize) {
|
||||||
Layout::UILayoutItem item = {};
|
Layout::UILayoutItem item = {};
|
||||||
item.desiredContentSize = child.desiredSize;
|
item.desiredContentSize = measuredContentSize;
|
||||||
|
item.minSize = child.minimumSize;
|
||||||
item.width = ParseLengthAttribute(*child.source, "width");
|
item.width = ParseLengthAttribute(*child.source, "width");
|
||||||
item.height = ParseLengthAttribute(*child.source, "height");
|
item.height = ParseLengthAttribute(*child.source, "height");
|
||||||
|
|
||||||
// Pixel-authored lengths act as requested extents, but never below the measured content floor.
|
|
||||||
if (item.width.unit == Layout::UILayoutLengthUnit::Pixels &&
|
|
||||||
item.width.value < child.desiredSize.width) {
|
|
||||||
item.minSize.width = child.desiredSize.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.height.unit == Layout::UILayoutLengthUnit::Pixels &&
|
|
||||||
item.height.value < child.desiredSize.height) {
|
|
||||||
item.minSize.height = child.desiredSize.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentAxis == Layout::UILayoutAxis::Vertical &&
|
if (parentAxis == Layout::UILayoutAxis::Vertical &&
|
||||||
item.width.unit == Layout::UILayoutLengthUnit::Auto) {
|
item.width.unit == Layout::UILayoutLengthUnit::Auto) {
|
||||||
item.horizontalAlignment = Layout::UILayoutAlignment::Stretch;
|
item.horizontalAlignment = Layout::UILayoutAlignment::Stretch;
|
||||||
@@ -233,6 +231,10 @@ bool IsHorizontalTag(const std::string& tagName) {
|
|||||||
return tagName == "Row";
|
return tagName == "Row";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsScrollViewTag(const std::string& tagName) {
|
||||||
|
return tagName == "ScrollView";
|
||||||
|
}
|
||||||
|
|
||||||
bool IsContainerTag(const UIDocumentNode& node) {
|
bool IsContainerTag(const UIDocumentNode& node) {
|
||||||
if (node.children.Size() > 0u) {
|
if (node.children.Size() > 0u) {
|
||||||
return true;
|
return true;
|
||||||
@@ -242,10 +244,42 @@ bool IsContainerTag(const UIDocumentNode& node) {
|
|||||||
return tagName == "View" ||
|
return tagName == "View" ||
|
||||||
tagName == "Column" ||
|
tagName == "Column" ||
|
||||||
tagName == "Row" ||
|
tagName == "Row" ||
|
||||||
|
tagName == "ScrollView" ||
|
||||||
tagName == "Card" ||
|
tagName == "Card" ||
|
||||||
tagName == "Button";
|
tagName == "Button";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string BuildNodeStateKeySegment(
|
||||||
|
const UIDocumentNode& source,
|
||||||
|
std::size_t siblingIndex) {
|
||||||
|
const std::string id = GetAttribute(source, "id");
|
||||||
|
if (!id.empty()) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string name = GetAttribute(source, "name");
|
||||||
|
if (!name.empty()) {
|
||||||
|
return ToStdString(source.tagName) + ":" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToStdString(source.tagName) + "#" + std::to_string(siblingIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
float ComputeScrollOverflow(float contentExtent, float viewportExtent) {
|
||||||
|
return (std::max)(0.0f, contentExtent - viewportExtent);
|
||||||
|
}
|
||||||
|
|
||||||
|
float ClampScrollOffset(float offset, float contentExtent, float viewportExtent) {
|
||||||
|
return (std::max)(0.0f, (std::min)(offset, ComputeScrollOverflow(contentExtent, viewportExtent)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RectContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||||
|
return point.x >= rect.x &&
|
||||||
|
point.x <= rect.x + rect.width &&
|
||||||
|
point.y >= rect.y &&
|
||||||
|
point.y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
Color ResolveBackgroundColor(const UIDocumentNode& node) {
|
Color ResolveBackgroundColor(const UIDocumentNode& node) {
|
||||||
const std::string tone = GetAttribute(node, "tone");
|
const std::string tone = GetAttribute(node, "tone");
|
||||||
const std::string tagName = ToStdString(node.tagName);
|
const std::string tagName = ToStdString(node.tagName);
|
||||||
@@ -278,12 +312,17 @@ Color ResolveBorderColor(const UIDocumentNode& node) {
|
|||||||
return Color(0.30f, 0.30f, 0.30f, 1.0f);
|
return Color(0.30f, 0.30f, 0.30f, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeLayoutNode BuildLayoutTree(const UIDocumentNode& source) {
|
RuntimeLayoutNode BuildLayoutTree(
|
||||||
|
const UIDocumentNode& source,
|
||||||
|
const std::string& parentStateKey,
|
||||||
|
std::size_t siblingIndex) {
|
||||||
RuntimeLayoutNode node = {};
|
RuntimeLayoutNode node = {};
|
||||||
node.source = &source;
|
node.source = &source;
|
||||||
|
node.stateKey = parentStateKey + "/" + BuildNodeStateKeySegment(source, siblingIndex);
|
||||||
|
node.isScrollView = IsScrollViewTag(ToStdString(source.tagName));
|
||||||
node.children.reserve(source.children.Size());
|
node.children.reserve(source.children.Size());
|
||||||
for (const UIDocumentNode& child : source.children) {
|
for (std::size_t index = 0; index < source.children.Size(); ++index) {
|
||||||
node.children.push_back(BuildLayoutTree(child));
|
node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, index));
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -297,6 +336,7 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
|||||||
node.desiredSize = UISize(
|
node.desiredSize = UISize(
|
||||||
MeasureTextWidth(text, kDefaultFontSize),
|
MeasureTextWidth(text, kDefaultFontSize),
|
||||||
MeasureTextHeight(kDefaultFontSize));
|
MeasureTextHeight(kDefaultFontSize));
|
||||||
|
node.minimumSize = node.desiredSize;
|
||||||
return node.desiredSize;
|
return node.desiredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +344,7 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
|||||||
node.desiredSize = UISize(
|
node.desiredSize = UISize(
|
||||||
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
|
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
|
||||||
44.0f);
|
44.0f);
|
||||||
|
node.minimumSize = node.desiredSize;
|
||||||
return node.desiredSize;
|
return node.desiredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,16 +360,21 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
|||||||
source,
|
source,
|
||||||
tagName == "View" ? 16.0f : 12.0f);
|
tagName == "View" ? 16.0f : 12.0f);
|
||||||
|
|
||||||
std::vector<Layout::UILayoutItem> items = {};
|
std::vector<Layout::UILayoutItem> desiredItems = {};
|
||||||
items.reserve(node.children.size());
|
desiredItems.reserve(node.children.size());
|
||||||
|
std::vector<Layout::UILayoutItem> minimumItems = {};
|
||||||
|
minimumItems.reserve(node.children.size());
|
||||||
for (RuntimeLayoutNode& child : node.children) {
|
for (RuntimeLayoutNode& child : node.children) {
|
||||||
MeasureNode(child);
|
MeasureNode(child);
|
||||||
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis);
|
desiredItems.push_back(BuildLayoutItem(child, options.axis, child.desiredSize));
|
||||||
items.push_back(item);
|
minimumItems.push_back(BuildLayoutItem(child, options.axis, child.minimumSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto measured = Layout::MeasureStackLayout(options, items);
|
const auto measured = Layout::MeasureStackLayout(options, desiredItems);
|
||||||
|
const auto minimumMeasured = Layout::MeasureStackLayout(options, minimumItems);
|
||||||
|
node.contentDesiredSize = measured.desiredSize;
|
||||||
node.desiredSize = measured.desiredSize;
|
node.desiredSize = measured.desiredSize;
|
||||||
|
node.minimumSize = minimumMeasured.desiredSize;
|
||||||
|
|
||||||
const float headerHeight = MeasureHeaderHeight(source);
|
const float headerHeight = MeasureHeaderHeight(source);
|
||||||
const float headerTextWidth = MeasureHeaderTextWidth(source);
|
const float headerTextWidth = MeasureHeaderTextWidth(source);
|
||||||
@@ -339,6 +385,16 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
|||||||
? headerTextWidth + options.padding.Horizontal()
|
? headerTextWidth + options.padding.Horizontal()
|
||||||
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||||
node.desiredSize.height += headerHeight;
|
node.desiredSize.height += headerHeight;
|
||||||
|
node.minimumSize.width = (std::max)(
|
||||||
|
node.minimumSize.width,
|
||||||
|
headerTextWidth > 0.0f
|
||||||
|
? headerTextWidth + options.padding.Horizontal()
|
||||||
|
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
||||||
|
node.minimumSize.height += headerHeight;
|
||||||
|
|
||||||
|
if (node.isScrollView) {
|
||||||
|
node.minimumSize.height = headerHeight + options.padding.Vertical();
|
||||||
|
}
|
||||||
|
|
||||||
float explicitWidth = 0.0f;
|
float explicitWidth = 0.0f;
|
||||||
if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) {
|
if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) {
|
||||||
@@ -353,8 +409,13 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
|
|||||||
return node.desiredSize;
|
return node.desiredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArrangeNode(RuntimeLayoutNode& node, const UIRect& rect) {
|
void ArrangeNode(
|
||||||
|
RuntimeLayoutNode& node,
|
||||||
|
const UIRect& rect,
|
||||||
|
const std::unordered_map<std::string, float>& verticalScrollOffsets) {
|
||||||
node.rect = rect;
|
node.rect = rect;
|
||||||
|
node.scrollViewportRect = {};
|
||||||
|
node.scrollOffsetY = 0.0f;
|
||||||
|
|
||||||
const UIDocumentNode& source = *node.source;
|
const UIDocumentNode& source = *node.source;
|
||||||
if (!IsContainerTag(source)) {
|
if (!IsContainerTag(source)) {
|
||||||
@@ -383,13 +444,180 @@ void ArrangeNode(RuntimeLayoutNode& node, const UIRect& rect) {
|
|||||||
std::vector<Layout::UILayoutItem> items = {};
|
std::vector<Layout::UILayoutItem> items = {};
|
||||||
items.reserve(node.children.size());
|
items.reserve(node.children.size());
|
||||||
for (RuntimeLayoutNode& child : node.children) {
|
for (RuntimeLayoutNode& child : node.children) {
|
||||||
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis);
|
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis, child.desiredSize);
|
||||||
items.push_back(item);
|
items.push_back(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.isScrollView) {
|
||||||
|
const auto found = verticalScrollOffsets.find(node.stateKey);
|
||||||
|
const float requestedOffset = found != verticalScrollOffsets.end() ? found->second : 0.0f;
|
||||||
|
node.scrollViewportRect = contentRect;
|
||||||
|
node.scrollOffsetY = ClampScrollOffset(
|
||||||
|
requestedOffset,
|
||||||
|
node.contentDesiredSize.height,
|
||||||
|
contentRect.height);
|
||||||
|
|
||||||
|
UIRect scrolledContentRect = contentRect;
|
||||||
|
scrolledContentRect.y -= node.scrollOffsetY;
|
||||||
|
const auto arranged = Layout::ArrangeStackLayout(options, items, scrolledContentRect);
|
||||||
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||||
|
ArrangeNode(
|
||||||
|
node.children[index],
|
||||||
|
arranged.children[index].arrangedRect,
|
||||||
|
verticalScrollOffsets);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto arranged = Layout::ArrangeStackLayout(options, items, contentRect);
|
const auto arranged = Layout::ArrangeStackLayout(options, items, contentRect);
|
||||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||||
ArrangeNode(node.children[index], arranged.children[index].arrangedRect);
|
ArrangeNode(
|
||||||
|
node.children[index],
|
||||||
|
arranged.children[index].arrangedRect,
|
||||||
|
verticalScrollOffsets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayoutNode* FindDeepestScrollTarget(
|
||||||
|
RuntimeLayoutNode& node,
|
||||||
|
const UIPoint& point) {
|
||||||
|
for (RuntimeLayoutNode& child : node.children) {
|
||||||
|
if (RuntimeLayoutNode* target = FindDeepestScrollTarget(child, point); target != nullptr) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.isScrollView) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RectContainsPoint(node.scrollViewportRect, point)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComputeScrollOverflow(node.contentDesiredSize.height, node.scrollViewportRect.height) <= 0.0f) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &node;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayoutNode* FindDeepestHoveredScrollView(
|
||||||
|
RuntimeLayoutNode& node,
|
||||||
|
const UIPoint& point) {
|
||||||
|
for (RuntimeLayoutNode& child : node.children) {
|
||||||
|
if (RuntimeLayoutNode* hovered = FindDeepestHoveredScrollView(child, point); hovered != nullptr) {
|
||||||
|
return hovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.isScrollView) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RectContainsPoint(node.scrollViewportRect, point)
|
||||||
|
? &node
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeLayoutNode* FindFirstScrollView(const RuntimeLayoutNode& node) {
|
||||||
|
if (node.isScrollView) {
|
||||||
|
return &node;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RuntimeLayoutNode& child : node.children) {
|
||||||
|
if (const RuntimeLayoutNode* found = FindFirstScrollView(child); found != nullptr) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyScrollWheelInput(
|
||||||
|
RuntimeLayoutNode& root,
|
||||||
|
const UIScreenFrameInput& input,
|
||||||
|
std::unordered_map<std::string, float>& verticalScrollOffsets,
|
||||||
|
UIDocumentScreenHost::ScrollDebugSnapshot& scrollDebugSnapshot) {
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (const UIInputEvent& event : input.events) {
|
||||||
|
if (event.type != UIInputEventType::PointerWheel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++scrollDebugSnapshot.totalWheelEventCount;
|
||||||
|
scrollDebugSnapshot.lastPointerPosition = event.position;
|
||||||
|
scrollDebugSnapshot.lastWheelDelta = event.wheelDelta;
|
||||||
|
scrollDebugSnapshot.lastTargetStateKey.clear();
|
||||||
|
scrollDebugSnapshot.lastViewportRect = {};
|
||||||
|
scrollDebugSnapshot.lastOverflow = 0.0f;
|
||||||
|
scrollDebugSnapshot.lastOffsetBefore = 0.0f;
|
||||||
|
scrollDebugSnapshot.lastOffsetAfter = 0.0f;
|
||||||
|
scrollDebugSnapshot.lastResult = "No hovered ScrollView";
|
||||||
|
|
||||||
|
RuntimeLayoutNode* hoveredScrollView = FindDeepestHoveredScrollView(root, event.position);
|
||||||
|
if (hoveredScrollView == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollDebugSnapshot.lastTargetStateKey = hoveredScrollView->stateKey;
|
||||||
|
scrollDebugSnapshot.lastViewportRect = hoveredScrollView->scrollViewportRect;
|
||||||
|
scrollDebugSnapshot.lastOverflow = ComputeScrollOverflow(
|
||||||
|
hoveredScrollView->contentDesiredSize.height,
|
||||||
|
hoveredScrollView->scrollViewportRect.height);
|
||||||
|
|
||||||
|
if (scrollDebugSnapshot.lastOverflow <= 0.0f) {
|
||||||
|
scrollDebugSnapshot.lastResult = "Hovered ScrollView has no overflow";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayoutNode* target = FindDeepestScrollTarget(root, event.position);
|
||||||
|
if (target == nullptr) {
|
||||||
|
scrollDebugSnapshot.lastResult = "Scroll target resolution failed";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto found = verticalScrollOffsets.find(target->stateKey);
|
||||||
|
const float oldOffset = found != verticalScrollOffsets.end()
|
||||||
|
? found->second
|
||||||
|
: target->scrollOffsetY;
|
||||||
|
scrollDebugSnapshot.lastOffsetBefore = oldOffset;
|
||||||
|
|
||||||
|
const float scrollUnits = event.wheelDelta / 120.0f;
|
||||||
|
const float nextOffset = ClampScrollOffset(
|
||||||
|
oldOffset - scrollUnits * 48.0f,
|
||||||
|
target->contentDesiredSize.height,
|
||||||
|
target->scrollViewportRect.height);
|
||||||
|
scrollDebugSnapshot.lastOffsetAfter = nextOffset;
|
||||||
|
if (std::fabs(nextOffset - oldOffset) <= 0.01f) {
|
||||||
|
scrollDebugSnapshot.lastResult = "Scroll delta clamped to current offset";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
verticalScrollOffsets[target->stateKey] = nextOffset;
|
||||||
|
++scrollDebugSnapshot.handledWheelEventCount;
|
||||||
|
scrollDebugSnapshot.lastResult = "Handled";
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncScrollOffsets(
|
||||||
|
const RuntimeLayoutNode& node,
|
||||||
|
std::unordered_map<std::string, float>& verticalScrollOffsets) {
|
||||||
|
if (node.isScrollView) {
|
||||||
|
const float overflow = ComputeScrollOverflow(node.contentDesiredSize.height, node.scrollViewportRect.height);
|
||||||
|
if (overflow <= 0.0f || node.scrollOffsetY <= 0.0f) {
|
||||||
|
verticalScrollOffsets.erase(node.stateKey);
|
||||||
|
} else {
|
||||||
|
verticalScrollOffsets[node.stateKey] = node.scrollOffsetY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RuntimeLayoutNode& child : node.children) {
|
||||||
|
SyncScrollOffsets(child, verticalScrollOffsets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,9 +679,21 @@ void EmitNode(
|
|||||||
++stats.textCommandCount;
|
++stats.textCommandCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool pushScrollClip =
|
||||||
|
node.isScrollView &&
|
||||||
|
node.scrollViewportRect.width > 0.0f &&
|
||||||
|
node.scrollViewportRect.height > 0.0f;
|
||||||
|
if (pushScrollClip) {
|
||||||
|
drawList.PushClipRect(node.scrollViewportRect);
|
||||||
|
}
|
||||||
|
|
||||||
for (const RuntimeLayoutNode& child : node.children) {
|
for (const RuntimeLayoutNode& child : node.children) {
|
||||||
EmitNode(child, drawList, stats);
|
EmitNode(child, drawList, stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pushScrollClip) {
|
||||||
|
drawList.PopClipRect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ResolveDisplayName(
|
std::string ResolveDisplayName(
|
||||||
@@ -565,7 +805,10 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode);
|
const std::string stateRoot = document.sourcePath.empty()
|
||||||
|
? document.displayName
|
||||||
|
: document.sourcePath;
|
||||||
|
RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, 0u);
|
||||||
MeasureNode(root);
|
MeasureNode(root);
|
||||||
|
|
||||||
UIRect viewportRect = input.viewportRect;
|
UIRect viewportRect = input.viewportRect;
|
||||||
@@ -576,7 +819,23 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
|||||||
viewportRect.height = (std::max)(360.0f, root.desiredSize.height);
|
viewportRect.height = (std::max)(360.0f, root.desiredSize.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrangeNode(root, viewportRect);
|
ArrangeNode(root, viewportRect, m_verticalScrollOffsets);
|
||||||
|
if (ApplyScrollWheelInput(root, input, m_verticalScrollOffsets, m_scrollDebugSnapshot)) {
|
||||||
|
ArrangeNode(root, viewportRect, m_verticalScrollOffsets);
|
||||||
|
}
|
||||||
|
SyncScrollOffsets(root, m_verticalScrollOffsets);
|
||||||
|
|
||||||
|
if (const RuntimeLayoutNode* primaryScrollView = FindFirstScrollView(root); primaryScrollView != nullptr) {
|
||||||
|
m_scrollDebugSnapshot.primaryTargetStateKey = primaryScrollView->stateKey;
|
||||||
|
m_scrollDebugSnapshot.primaryViewportRect = primaryScrollView->scrollViewportRect;
|
||||||
|
m_scrollDebugSnapshot.primaryOverflow = ComputeScrollOverflow(
|
||||||
|
primaryScrollView->contentDesiredSize.height,
|
||||||
|
primaryScrollView->scrollViewportRect.height);
|
||||||
|
} else {
|
||||||
|
m_scrollDebugSnapshot.primaryTargetStateKey.clear();
|
||||||
|
m_scrollDebugSnapshot.primaryViewportRect = {};
|
||||||
|
m_scrollDebugSnapshot.primaryOverflow = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
|
UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName);
|
||||||
EmitNode(root, drawList, result.stats);
|
EmitNode(root, drawList, result.stats);
|
||||||
@@ -589,6 +848,10 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UIDocumentScreenHost::ScrollDebugSnapshot& UIDocumentScreenHost::GetScrollDebugSnapshot() const {
|
||||||
|
return m_scrollDebugSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Runtime
|
} // namespace Runtime
|
||||||
} // namespace UI
|
} // namespace UI
|
||||||
} // namespace XCEngine
|
} // namespace XCEngine
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
@@ -22,6 +23,9 @@ namespace {
|
|||||||
using ::XCEngine::UI::UIColor;
|
using ::XCEngine::UI::UIColor;
|
||||||
using ::XCEngine::UI::UIDrawData;
|
using ::XCEngine::UI::UIDrawData;
|
||||||
using ::XCEngine::UI::UIDrawList;
|
using ::XCEngine::UI::UIDrawList;
|
||||||
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
|
using ::XCEngine::UI::UIInputModifiers;
|
||||||
using ::XCEngine::UI::UIPoint;
|
using ::XCEngine::UI::UIPoint;
|
||||||
using ::XCEngine::UI::UIRect;
|
using ::XCEngine::UI::UIRect;
|
||||||
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
|
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||||
@@ -53,6 +57,35 @@ std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
|||||||
return text.substr(0, maxLength - 3u) + "...";
|
return text.substr(0, maxLength - 3u) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string FormatFloat(float value) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream.setf(std::ios::fixed, std::ios::floatfield);
|
||||||
|
stream.precision(1);
|
||||||
|
stream << value;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatPoint(const UIPoint& point) {
|
||||||
|
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatRect(const UIRect& rect) {
|
||||||
|
return "(" + FormatFloat(rect.x) +
|
||||||
|
", " + FormatFloat(rect.y) +
|
||||||
|
", " + FormatFloat(rect.width) +
|
||||||
|
", " + FormatFloat(rect.height) +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputModifiers BuildInputModifiers(size_t wParam) {
|
||||||
|
UIInputModifiers modifiers = {};
|
||||||
|
modifiers.shift = (wParam & MK_SHIFT) != 0;
|
||||||
|
modifiers.control = (wParam & MK_CONTROL) != 0;
|
||||||
|
modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
|
||||||
|
modifiers.super = (GetKeyState(VK_LWIN) & 0x8000) != 0 || (GetKeyState(VK_RWIN) & 0x8000) != 0;
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Application::Application()
|
Application::Application()
|
||||||
@@ -170,11 +203,14 @@ void Application::RenderFrame() {
|
|||||||
m_lastFrameTime = now;
|
m_lastFrameTime = now;
|
||||||
|
|
||||||
RefreshStructuredScreen();
|
RefreshStructuredScreen();
|
||||||
|
std::vector<UIInputEvent> frameEvents = std::move(m_pendingInputEvents);
|
||||||
|
m_pendingInputEvents.clear();
|
||||||
|
|
||||||
UIDrawData drawData = {};
|
UIDrawData drawData = {};
|
||||||
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
||||||
UIScreenFrameInput input = {};
|
UIScreenFrameInput input = {};
|
||||||
input.viewportRect = UIRect(0.0f, 0.0f, width, height);
|
input.viewportRect = UIRect(0.0f, 0.0f, width, height);
|
||||||
|
input.events = std::move(frameEvents);
|
||||||
input.deltaTimeSeconds = deltaTimeSeconds;
|
input.deltaTimeSeconds = deltaTimeSeconds;
|
||||||
input.frameIndex = ++m_frameIndex;
|
input.frameIndex = ++m_frameIndex;
|
||||||
input.focused = GetForegroundWindow() == m_hwnd;
|
input.focused = GetForegroundWindow() == m_hwnd;
|
||||||
@@ -219,6 +255,26 @@ void Application::OnResize(UINT width, UINT height) {
|
|||||||
m_renderer.Resize(width, height);
|
m_renderer.Resize(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT screenPoint = {
|
||||||
|
GET_X_LPARAM(lParam),
|
||||||
|
GET_Y_LPARAM(lParam)
|
||||||
|
};
|
||||||
|
ScreenToClient(m_hwnd, &screenPoint);
|
||||||
|
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::PointerWheel;
|
||||||
|
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
|
||||||
|
event.wheelDelta = static_cast<float>(wheelDelta);
|
||||||
|
event.modifiers = BuildInputModifiers(static_cast<size_t>(wParam));
|
||||||
|
m_pendingInputEvents.push_back(event);
|
||||||
|
m_autoScreenshot.RequestCapture("wheel");
|
||||||
|
}
|
||||||
|
|
||||||
bool Application::LoadStructuredScreen(const char* triggerReason) {
|
bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||||
m_screenAsset = {};
|
m_screenAsset = {};
|
||||||
m_screenAsset.screenId = "new_editor.editor_shell";
|
m_screenAsset.screenId = "new_editor.editor_shell";
|
||||||
@@ -316,13 +372,58 @@ bool Application::DetectTrackedFileChange() const {
|
|||||||
|
|
||||||
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
|
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
|
||||||
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
|
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
|
||||||
const float panelWidth = authoredMode ? 256.0f : 360.0f;
|
const float panelWidth = authoredMode ? 420.0f : 360.0f;
|
||||||
std::vector<std::string> detailLines = {};
|
std::vector<std::string> detailLines = {};
|
||||||
detailLines.push_back(
|
detailLines.push_back(
|
||||||
authoredMode
|
authoredMode
|
||||||
? "Hot reload watches authored UI resources."
|
? "Hot reload watches authored UI resources."
|
||||||
: "Using native fallback while authored UI is invalid.");
|
: "Using native fallback while authored UI is invalid.");
|
||||||
|
|
||||||
|
if (authoredMode) {
|
||||||
|
const auto& scrollDebug = m_documentHost.GetScrollDebugSnapshot();
|
||||||
|
if (!scrollDebug.primaryTargetStateKey.empty()) {
|
||||||
|
detailLines.push_back(
|
||||||
|
"Primary: " +
|
||||||
|
TruncateText(scrollDebug.primaryTargetStateKey, 52u));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Primary viewport: " +
|
||||||
|
FormatRect(scrollDebug.primaryViewportRect));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Primary overflow: " +
|
||||||
|
FormatFloat(scrollDebug.primaryOverflow));
|
||||||
|
}
|
||||||
|
detailLines.push_back(
|
||||||
|
"Wheel total/handled: " +
|
||||||
|
std::to_string(scrollDebug.totalWheelEventCount) +
|
||||||
|
" / " +
|
||||||
|
std::to_string(scrollDebug.handledWheelEventCount));
|
||||||
|
if (scrollDebug.totalWheelEventCount > 0u) {
|
||||||
|
detailLines.push_back(
|
||||||
|
"Last wheel " +
|
||||||
|
FormatFloat(scrollDebug.lastWheelDelta) +
|
||||||
|
" at " +
|
||||||
|
FormatPoint(scrollDebug.lastPointerPosition));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Result: " +
|
||||||
|
(scrollDebug.lastResult.empty() ? std::string("n/a") : scrollDebug.lastResult));
|
||||||
|
if (!scrollDebug.lastTargetStateKey.empty()) {
|
||||||
|
detailLines.push_back(
|
||||||
|
"Target: " +
|
||||||
|
TruncateText(scrollDebug.lastTargetStateKey, 52u));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Viewport: " +
|
||||||
|
FormatRect(scrollDebug.lastViewportRect));
|
||||||
|
detailLines.push_back(
|
||||||
|
"Overflow/offset: " +
|
||||||
|
FormatFloat(scrollDebug.lastOverflow) +
|
||||||
|
" | " +
|
||||||
|
FormatFloat(scrollDebug.lastOffsetBefore) +
|
||||||
|
" -> " +
|
||||||
|
FormatFloat(scrollDebug.lastOffsetAfter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_autoScreenshot.HasPendingCapture()) {
|
if (m_autoScreenshot.HasPendingCapture()) {
|
||||||
detailLines.push_back("Shot pending...");
|
detailLines.push_back("Shot pending...");
|
||||||
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
||||||
@@ -393,6 +494,12 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case WM_MOUSEWHEEL:
|
||||||
|
if (application != nullptr) {
|
||||||
|
application->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case WM_ERASEBKGND:
|
case WM_ERASEBKGND:
|
||||||
return 1;
|
return 1;
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -40,6 +41,7 @@ private:
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
void RenderFrame();
|
void RenderFrame();
|
||||||
void OnResize(UINT width, UINT height);
|
void OnResize(UINT width, UINT height);
|
||||||
|
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam);
|
||||||
bool LoadStructuredScreen(const char* triggerReason);
|
bool LoadStructuredScreen(const char* triggerReason);
|
||||||
void RefreshStructuredScreen();
|
void RefreshStructuredScreen();
|
||||||
void RebuildTrackedFileStates();
|
void RebuildTrackedFileStates();
|
||||||
@@ -60,6 +62,7 @@ private:
|
|||||||
std::chrono::steady_clock::time_point m_lastFrameTime = {};
|
std::chrono::steady_clock::time_point m_lastFrameTime = {};
|
||||||
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
|
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
|
||||||
std::uint64_t m_frameIndex = 0;
|
std::uint64_t m_frameIndex = 0;
|
||||||
|
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||||
bool m_useStructuredScreen = false;
|
bool m_useStructuredScreen = false;
|
||||||
std::string m_runtimeStatus = {};
|
std::string m_runtimeStatus = {};
|
||||||
std::string m_runtimeError = {};
|
std::string m_runtimeError = {};
|
||||||
|
|||||||
@@ -46,13 +46,23 @@
|
|||||||
</Column>
|
</Column>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="Console" subtitle="runtime smoke log" height="220">
|
<Card title="Console" subtitle="wheel-scroll core validation" height="220">
|
||||||
<Column gap="8">
|
<ScrollView id="console-scroll" height="fill">
|
||||||
<Text text="Info XCUI authored screen loaded." />
|
<Column gap="8">
|
||||||
<Text text="Info Theme + schema resources are tracked for reload." />
|
<Text text="Scroll here with the mouse wheel." />
|
||||||
<Text text="Warn Viewport host stays out of scope in this phase." />
|
<Text text="Check that content clips cleanly inside this card." />
|
||||||
<Text text="Todo Replace shell placeholders with Editor widgets." />
|
<Text text="Info XCUI authored screen loaded." />
|
||||||
</Column>
|
<Text text="Info Theme + schema resources are tracked for reload." />
|
||||||
|
<Text text="Warn Viewport host stays out of scope in this phase." />
|
||||||
|
<Text text="Todo Replace shell placeholders with Editor widgets." />
|
||||||
|
<Text text="Trace ScrollView should retain offset across frames." />
|
||||||
|
<Text text="Trace Wheel input should only affect the hovered view." />
|
||||||
|
<Text text="Trace Hidden rows must stay clipped below the footer." />
|
||||||
|
<Text text="Trace Resize should not corrupt the scroll offset clamp." />
|
||||||
|
<Text text="Trace Hot reload should rebuild layout without tearing." />
|
||||||
|
<Text text="Trace This panel exists only to validate XCUI core scroll." />
|
||||||
|
</Column>
|
||||||
|
</ScrollView>
|
||||||
</Card>
|
</Card>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,21 @@ bool TryFindSmallestFilledRectContainingPoint(
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t CountCommandsOfType(
|
||||||
|
const XCEngine::UI::UIDrawData& drawData,
|
||||||
|
XCEngine::UI::UIDrawCommandType type) {
|
||||||
|
std::size_t count = 0u;
|
||||||
|
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
|
||||||
|
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
|
||||||
|
if (command.type == type) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
||||||
UIScreenFrameInput input = {};
|
UIScreenFrameInput input = {};
|
||||||
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f);
|
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f);
|
||||||
@@ -308,6 +323,67 @@ TEST(UIRuntimeTest, DocumentHostDoesNotLetExplicitHeightCrushCardContent) {
|
|||||||
EXPECT_LE(buttonRect.y + buttonRect.height, cardRect.y + cardRect.height - 8.0f);
|
EXPECT_LE(buttonRect.y + buttonRect.height, cardRect.y + cardRect.height - 8.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UIRuntimeTest, DocumentHostScrollViewClipsOverflowingChildrenAndRespondsToWheelInput) {
|
||||||
|
TempFileScope viewFile(
|
||||||
|
"xcui_runtime_scroll_view",
|
||||||
|
".xcui",
|
||||||
|
"<View name=\"Scroll View Test\">\n"
|
||||||
|
" <Column padding=\"18\" gap=\"10\">\n"
|
||||||
|
" <Card title=\"Console\" subtitle=\"Scroll smoke\" height=\"200\">\n"
|
||||||
|
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
|
||||||
|
" <Column gap=\"8\">\n"
|
||||||
|
" <Text text=\"Line 01\" />\n"
|
||||||
|
" <Text text=\"Line 02\" />\n"
|
||||||
|
" <Text text=\"Line 03\" />\n"
|
||||||
|
" <Text text=\"Line 04\" />\n"
|
||||||
|
" <Text text=\"Line 05\" />\n"
|
||||||
|
" <Text text=\"Line 06\" />\n"
|
||||||
|
" <Text text=\"Line 07\" />\n"
|
||||||
|
" <Text text=\"Line 08\" />\n"
|
||||||
|
" <Text text=\"Line 09\" />\n"
|
||||||
|
" <Text text=\"Line 10\" />\n"
|
||||||
|
" <Text text=\"Line 11\" />\n"
|
||||||
|
" <Text text=\"Line 12\" />\n"
|
||||||
|
" </Column>\n"
|
||||||
|
" </ScrollView>\n"
|
||||||
|
" </Card>\n"
|
||||||
|
" </Column>\n"
|
||||||
|
"</View>\n");
|
||||||
|
UIDocumentScreenHost host = {};
|
||||||
|
UIScreenPlayer player(host);
|
||||||
|
|
||||||
|
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.view")));
|
||||||
|
|
||||||
|
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||||
|
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 480.0f, 320.0f);
|
||||||
|
const auto& firstFrame = player.Update(firstInput);
|
||||||
|
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
|
||||||
|
ASSERT_NE(line01Before, nullptr);
|
||||||
|
const float line01BeforeY = line01Before->position.y;
|
||||||
|
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, XCEngine::UI::UIDrawCommandType::PushClipRect), 0u);
|
||||||
|
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, XCEngine::UI::UIDrawCommandType::PopClipRect), 0u);
|
||||||
|
|
||||||
|
UIScreenFrameInput secondInput = BuildInputState(2u);
|
||||||
|
secondInput.viewportRect = firstInput.viewportRect;
|
||||||
|
XCEngine::UI::UIInputEvent wheelEvent = {};
|
||||||
|
wheelEvent.type = XCEngine::UI::UIInputEventType::PointerWheel;
|
||||||
|
wheelEvent.position = XCEngine::UI::UIPoint(90.0f, 130.0f);
|
||||||
|
wheelEvent.wheelDelta = -120.0f;
|
||||||
|
secondInput.events.push_back(wheelEvent);
|
||||||
|
const auto& secondFrame = player.Update(secondInput);
|
||||||
|
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
|
||||||
|
ASSERT_NE(line01After, nullptr);
|
||||||
|
const float line01AfterY = line01After->position.y;
|
||||||
|
EXPECT_LT(line01AfterY, line01BeforeY);
|
||||||
|
|
||||||
|
UIScreenFrameInput thirdInput = BuildInputState(3u);
|
||||||
|
thirdInput.viewportRect = firstInput.viewportRect;
|
||||||
|
const auto& thirdFrame = player.Update(thirdInput);
|
||||||
|
const auto* line01Persisted = FindTextCommand(thirdFrame.drawData, "Line 01");
|
||||||
|
ASSERT_NE(line01Persisted, nullptr);
|
||||||
|
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01AfterY);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
|
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
|
||||||
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
|
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
|
||||||
UIDocumentScreenHost host = {};
|
UIDocumentScreenHost host = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user