From dc0e8b938f857a87287ab3d639572353b99418ff Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 21:52:30 +0800 Subject: [PATCH] Fix UI document host input integration build --- .../UI/Runtime/UIScreenDocumentHost.h | 25 + .../src/UI/Runtime/UIScreenDocumentHost.cpp | 659 ++++++++++++++++-- 2 files changed, 609 insertions(+), 75 deletions(-) diff --git a/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h b/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h index f49efe13..f0358d61 100644 --- a/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h +++ b/engine/include/XCEngine/UI/Runtime/UIScreenDocumentHost.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -22,6 +23,20 @@ public: class UIDocumentScreenHost final : public IUIScreenDocumentHost { public: + struct InputDebugSnapshot { + std::uint64_t totalPointerEventCount = 0u; + UIPoint pointerPosition = {}; + bool pointerInsideViewport = false; + std::string hoveredStateKey = {}; + std::string focusedStateKey = {}; + std::string activeStateKey = {}; + std::string captureStateKey = {}; + std::string lastEventType = {}; + std::string lastTargetStateKey = {}; + std::string lastTargetKind = {}; + std::string lastResult = {}; + }; + struct ScrollDebugSnapshot { std::uint64_t totalWheelEventCount = 0u; std::uint64_t handledWheelEventCount = 0u; @@ -42,10 +57,20 @@ public: UIScreenFrameResult BuildFrame( const UIScreenDocument& document, const UIScreenFrameInput& input) override; + const InputDebugSnapshot& GetInputDebugSnapshot() const; const ScrollDebugSnapshot& GetScrollDebugSnapshot() const; private: + struct PointerState { + UIPoint position = {}; + bool hasPosition = false; + bool insideViewport = false; + }; + + UIInputDispatcher m_inputDispatcher; std::unordered_map m_verticalScrollOffsets = {}; + PointerState m_pointerState = {}; + InputDebugSnapshot m_inputDebugSnapshot = {}; ScrollDebugSnapshot m_scrollDebugSnapshot = {}; }; diff --git a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp index 918cff5b..7aef0312 100644 --- a/engine/src/UI/Runtime/UIScreenDocumentHost.cpp +++ b/engine/src/UI/Runtime/UIScreenDocumentHost.cpp @@ -41,6 +41,8 @@ constexpr float kHeaderTextGap = 2.0f; struct RuntimeLayoutNode { const UIDocumentNode* source = nullptr; std::string stateKey = {}; + UIElementId elementId = 0u; + UIInputPath inputPath = {}; std::vector children = {}; UISize desiredSize = {}; UISize minimumSize = {}; @@ -48,9 +50,19 @@ struct RuntimeLayoutNode { UIRect rect = {}; UIRect scrollViewportRect = {}; float scrollOffsetY = 0.0f; + bool pointerInteractive = false; + bool focusable = false; + bool wantsPointerCapture = false; bool isScrollView = false; }; +struct RuntimeNodeVisualState { + bool hovered = false; + bool focused = false; + bool active = false; + bool capture = false; +}; + UIColor ToUIColor(const Color& color) { return UIColor(color.r, color.g, color.b, color.a); } @@ -59,6 +71,48 @@ std::string ToStdString(const Containers::String& value) { return std::string(value.CStr()); } +std::string ToLowerAscii(std::string value) { + std::transform( + value.begin(), + value.end(), + value.begin(), + [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +bool TryParseBoolString(const std::string& text, bool& outValue) { + if (text.empty()) { + return false; + } + + const std::string normalized = ToLowerAscii(text); + if (normalized == "true" || normalized == "1" || normalized == "yes" || normalized == "on") { + outValue = true; + return true; + } + + if (normalized == "false" || normalized == "0" || normalized == "no" || normalized == "off") { + outValue = false; + return true; + } + + return false; +} + +UIElementId HashStateKeyToElementId(const std::string& stateKey) { + constexpr UIElementId kOffsetBasis = 14695981039346656037ull; + constexpr UIElementId kPrime = 1099511628211ull; + + UIElementId hash = kOffsetBasis; + for (unsigned char value : stateKey) { + hash ^= static_cast(value); + hash *= kPrime; + } + return hash == 0u ? 1u : hash; +} + bool IsUtf8ContinuationByte(unsigned char value) { return (value & 0xC0u) == 0x80u; } @@ -103,6 +157,14 @@ std::string GetAttribute( return attribute != nullptr ? ToStdString(attribute->value) : fallback; } +bool ParseBoolAttribute( + const UIDocumentNode& node, + const char* name, + bool fallback) { + bool value = fallback; + return TryParseBoolString(GetAttribute(node, name), value) ? value : fallback; +} + float MeasureHeaderTextWidth(const UIDocumentNode& node) { float width = 0.0f; @@ -235,6 +297,10 @@ bool IsScrollViewTag(const std::string& tagName) { return tagName == "ScrollView"; } +bool IsButtonTag(const std::string& tagName) { + return tagName == "Button"; +} + bool IsContainerTag(const UIDocumentNode& node) { if (node.children.Size() > 0u) { return true; @@ -249,6 +315,24 @@ bool IsContainerTag(const UIDocumentNode& node) { tagName == "Button"; } +bool IsPointerInteractiveNode(const UIDocumentNode& node) { + const std::string tagName = ToStdString(node.tagName); + return ParseBoolAttribute(node, "interactive", IsButtonTag(tagName) || IsScrollViewTag(tagName)); +} + +bool IsFocusableNode(const UIDocumentNode& node) { + const std::string tagName = ToStdString(node.tagName); + return ParseBoolAttribute(node, "focusable", IsButtonTag(tagName)); +} + +bool WantsPointerCapture(const UIDocumentNode& node) { + if (ParseBoolAttribute(node, "capturePointer", false)) { + return true; + } + + return ToLowerAscii(GetAttribute(node, "capture")) == "pointer"; +} + std::string BuildNodeStateKeySegment( const UIDocumentNode& source, std::size_t siblingIndex) { @@ -280,7 +364,137 @@ bool RectContainsPoint(const UIRect& rect, const UIPoint& point) { point.y <= rect.y + rect.height; } -Color ResolveBackgroundColor(const UIDocumentNode& node) { +bool HasPositiveArea(const UIRect& rect) { + return rect.width > 0.0f && rect.height > 0.0f; +} + +UIRect IntersectRects(const UIRect& lhs, const UIRect& rhs) { + const float left = (std::max)(lhs.x, rhs.x); + const float top = (std::max)(lhs.y, rhs.y); + const float right = (std::min)(lhs.x + lhs.width, rhs.x + rhs.width); + const float bottom = (std::min)(lhs.y + lhs.height, rhs.y + rhs.height); + return UIRect( + left, + top, + (std::max)(0.0f, right - left), + (std::max)(0.0f, bottom - top)); +} + +const UIRect& GetNodeInteractionRect(const RuntimeLayoutNode& node) { + return node.isScrollView ? node.scrollViewportRect : node.rect; +} + +bool IsNodeTargetable(const RuntimeLayoutNode& node) { + return node.pointerInteractive || node.focusable || node.isScrollView; +} + +const RuntimeLayoutNode* FindNodeByElementId( + const RuntimeLayoutNode& node, + UIElementId elementId) { + if (node.elementId == elementId) { + return &node; + } + + for (const RuntimeLayoutNode& child : node.children) { + if (const RuntimeLayoutNode* found = FindNodeByElementId(child, elementId); found != nullptr) { + return found; + } + } + + return nullptr; +} + +RuntimeLayoutNode* FindNodeByElementId( + RuntimeLayoutNode& node, + UIElementId elementId) { + if (node.elementId == elementId) { + return &node; + } + + for (RuntimeLayoutNode& child : node.children) { + if (RuntimeLayoutNode* found = FindNodeByElementId(child, elementId); found != nullptr) { + return found; + } + } + + return nullptr; +} + +bool PathTargetExists( + const RuntimeLayoutNode& root, + const UIInputPath& path) { + return !path.Empty() && FindNodeByElementId(root, path.Target()) != nullptr; +} + +std::string ResolveStateKeyForPathTarget( + const RuntimeLayoutNode& root, + const UIInputPath& path) { + if (path.Empty()) { + return {}; + } + + if (const RuntimeLayoutNode* node = FindNodeByElementId(root, path.Target()); node != nullptr) { + return node->stateKey; + } + + return {}; +} + +const RuntimeLayoutNode* FindDeepestInputTarget( + const RuntimeLayoutNode& node, + const UIPoint& point, + const UIRect* clipRect = nullptr) { + if (clipRect != nullptr && (!HasPositiveArea(*clipRect) || !RectContainsPoint(*clipRect, point))) { + return nullptr; + } + + UIRect nextClip = clipRect != nullptr ? *clipRect : node.rect; + if (!HasPositiveArea(nextClip)) { + nextClip = node.rect; + } + if (node.isScrollView) { + nextClip = clipRect != nullptr + ? IntersectRects(*clipRect, node.scrollViewportRect) + : node.scrollViewportRect; + } + + for (const RuntimeLayoutNode& child : node.children) { + if (const RuntimeLayoutNode* found = FindDeepestInputTarget(child, point, &nextClip); found != nullptr) { + return found; + } + } + + const UIRect& targetRect = GetNodeInteractionRect(node); + if (!IsNodeTargetable(node) || !HasPositiveArea(targetRect)) { + return nullptr; + } + + if (clipRect != nullptr && !RectContainsPoint(*clipRect, point)) { + return nullptr; + } + + return RectContainsPoint(targetRect, point) ? &node : nullptr; +} + +UIInputPath ResolveHoveredPath( + const RuntimeLayoutNode& root, + const UIPoint& pointerPosition, + bool hasPointerPosition, + bool pointerInsideViewport) { + if (!hasPointerPosition || !pointerInsideViewport) { + return {}; + } + + if (const RuntimeLayoutNode* hovered = FindDeepestInputTarget(root, pointerPosition); hovered != nullptr) { + return hovered->inputPath; + } + + return {}; +} + +Color ResolveBackgroundColor( + const UIDocumentNode& node, + const RuntimeNodeVisualState& state) { const std::string tone = GetAttribute(node, "tone"); const std::string tagName = ToStdString(node.tagName); @@ -294,13 +508,33 @@ Color ResolveBackgroundColor(const UIDocumentNode& node) { return Color(0.22f, 0.22f, 0.22f, 1.0f); } if (tagName == "Button") { + if (state.active || state.capture) { + return Color(0.30f, 0.30f, 0.30f, 1.0f); + } + if (state.hovered) { + return Color(0.27f, 0.27f, 0.27f, 1.0f); + } return Color(0.24f, 0.24f, 0.24f, 1.0f); } return Color(0.16f, 0.16f, 0.16f, 1.0f); } -Color ResolveBorderColor(const UIDocumentNode& node) { +Color ResolveBorderColor( + const UIDocumentNode& node, + const RuntimeNodeVisualState& state) { + if (state.capture) { + return Color(0.82f, 0.82f, 0.82f, 1.0f); + } + + if (state.focused || state.active) { + return Color(0.62f, 0.62f, 0.62f, 1.0f); + } + + if (state.hovered) { + return Color(0.45f, 0.45f, 0.45f, 1.0f); + } + const std::string tone = GetAttribute(node, "tone"); if (tone == "accent") { return Color(0.42f, 0.42f, 0.42f, 1.0f); @@ -312,17 +546,46 @@ Color ResolveBorderColor(const UIDocumentNode& node) { return Color(0.30f, 0.30f, 0.30f, 1.0f); } +float ResolveBorderThickness(const RuntimeNodeVisualState& state) { + return (state.focused || state.active || state.capture) ? 2.0f : 1.0f; +} + +bool IsPathTarget( + const UIInputPath& path, + UIElementId elementId) { + return !path.Empty() && path.Target() == elementId; +} + +RuntimeNodeVisualState ResolveNodeVisualState( + const RuntimeLayoutNode& node, + const UIInputPath& hoveredPath, + const UIFocusController& focusController) { + RuntimeNodeVisualState state = {}; + state.hovered = IsPathTarget(hoveredPath, node.elementId); + state.focused = IsPathTarget(focusController.GetFocusedPath(), node.elementId); + state.active = IsPathTarget(focusController.GetActivePath(), node.elementId); + state.capture = IsPathTarget(focusController.GetPointerCapturePath(), node.elementId); + return state; +} + RuntimeLayoutNode BuildLayoutTree( const UIDocumentNode& source, const std::string& parentStateKey, + const UIInputPath& parentInputPath, std::size_t siblingIndex) { RuntimeLayoutNode node = {}; node.source = &source; node.stateKey = parentStateKey + "/" + BuildNodeStateKeySegment(source, siblingIndex); + node.elementId = HashStateKeyToElementId(node.stateKey); + node.inputPath = parentInputPath; + node.inputPath.elements.push_back(node.elementId); + node.pointerInteractive = IsPointerInteractiveNode(source); + node.focusable = ParseBoolAttribute(source, "focusable", IsFocusableNode(source)); + node.wantsPointerCapture = WantsPointerCapture(source); node.isScrollView = IsScrollViewTag(ToStdString(source.tagName)); node.children.reserve(source.children.Size()); for (std::size_t index = 0; index < source.children.Size(); ++index) { - node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, index)); + node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, node.inputPath, index)); } return node; } @@ -534,74 +797,243 @@ const RuntimeLayoutNode* FindFirstScrollView(const RuntimeLayoutNode& node) { return nullptr; } -bool ApplyScrollWheelInput( +bool ApplyScrollWheelEvent( RuntimeLayoutNode& root, - const UIScreenFrameInput& input, + const UIInputEvent& event, std::unordered_map& 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; + if (event.type != UIInputEventType::PointerWheel) { + return false; } - return changed; + ++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) { + return false; + } + + 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"; + return false; + } + + RuntimeLayoutNode* target = FindDeepestScrollTarget(root, event.position); + if (target == nullptr) { + scrollDebugSnapshot.lastResult = "Scroll target resolution failed"; + return false; + } + + 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"; + return false; + } + + verticalScrollOffsets[target->stateKey] = nextOffset; + ++scrollDebugSnapshot.handledWheelEventCount; + scrollDebugSnapshot.lastResult = "Handled"; + return true; +} + +const char* GetInputEventTypeDebugName(UIInputEventType type) { + switch (type) { + case UIInputEventType::PointerMove: + return "PointerMove"; + case UIInputEventType::PointerEnter: + return "PointerEnter"; + case UIInputEventType::PointerLeave: + return "PointerLeave"; + case UIInputEventType::PointerButtonDown: + return "PointerButtonDown"; + case UIInputEventType::PointerButtonUp: + return "PointerButtonUp"; + case UIInputEventType::PointerWheel: + return "PointerWheel"; + case UIInputEventType::KeyDown: + return "KeyDown"; + case UIInputEventType::KeyUp: + return "KeyUp"; + case UIInputEventType::Character: + return "Character"; + case UIInputEventType::FocusGained: + return "FocusGained"; + case UIInputEventType::FocusLost: + return "FocusLost"; + case UIInputEventType::None: + default: + return "None"; + } +} + +const char* GetInputTargetKindDebugName(UIInputTargetKind targetKind) { + switch (targetKind) { + case UIInputTargetKind::Hovered: + return "Hovered"; + case UIInputTargetKind::Focused: + return "Focused"; + case UIInputTargetKind::Captured: + return "Captured"; + case UIInputTargetKind::None: + default: + return "None"; + } +} + +void UpdatePointerTrackingState( + UIPoint& pointerPosition, + bool& hasPointerPosition, + bool& pointerInsideViewport, + const UIInputEvent& event, + const UIRect& viewportRect) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + case UIInputEventType::PointerEnter: + pointerPosition = event.position; + hasPointerPosition = true; + pointerInsideViewport = RectContainsPoint(viewportRect, event.position); + return; + case UIInputEventType::PointerLeave: + pointerPosition = event.position; + hasPointerPosition = true; + pointerInsideViewport = false; + return; + default: + return; + } +} + +void SanitizeInputDispatcherState( + const RuntimeLayoutNode& root, + UIInputDispatcher& inputDispatcher) { + UIFocusController& focusController = inputDispatcher.GetFocusController(); + if (!PathTargetExists(root, focusController.GetFocusedPath())) { + focusController.ClearFocus(); + } + if (!PathTargetExists(root, focusController.GetActivePath())) { + focusController.ClearActivePath(); + } + if (!PathTargetExists(root, focusController.GetPointerCapturePath())) { + focusController.ClearPointerCapturePath(); + } +} + +void UpdateInputDebugSnapshot( + const RuntimeLayoutNode& root, + const UIInputPath& hoveredPath, + const UIInputDispatcher& inputDispatcher, + const UIPoint& pointerPosition, + bool pointerInsideViewport, + UIDocumentScreenHost::InputDebugSnapshot& inputDebugSnapshot) { + inputDebugSnapshot.pointerPosition = pointerPosition; + inputDebugSnapshot.pointerInsideViewport = pointerInsideViewport; + inputDebugSnapshot.hoveredStateKey = ResolveStateKeyForPathTarget(root, hoveredPath); + inputDebugSnapshot.focusedStateKey = ResolveStateKeyForPathTarget( + root, + inputDispatcher.GetFocusController().GetFocusedPath()); + inputDebugSnapshot.activeStateKey = ResolveStateKeyForPathTarget( + root, + inputDispatcher.GetFocusController().GetActivePath()); + inputDebugSnapshot.captureStateKey = ResolveStateKeyForPathTarget( + root, + inputDispatcher.GetFocusController().GetPointerCapturePath()); +} + +void DispatchInputEvent( + RuntimeLayoutNode& root, + const UIInputEvent& event, + const UIInputPath& hoveredPath, + UIInputDispatcher& inputDispatcher, + UIDocumentScreenHost::InputDebugSnapshot& inputDebugSnapshot) { + if (event.type == UIInputEventType::PointerWheel) { + return; + } + + if (UIInputRouter::IsPointerEvent(event.type)) { + ++inputDebugSnapshot.totalPointerEventCount; + } + + inputDebugSnapshot.lastEventType = GetInputEventTypeDebugName(event.type); + inputDebugSnapshot.lastTargetKind = "None"; + inputDebugSnapshot.lastTargetStateKey.clear(); + inputDebugSnapshot.lastResult = "No target"; + + if (event.type == UIInputEventType::FocusLost) { + UIFocusController& focusController = inputDispatcher.GetFocusController(); + focusController.ClearPointerCapturePath(); + focusController.ClearActivePath(); + focusController.ClearFocus(); + inputDebugSnapshot.lastResult = "Focus cleared"; + return; + } + + bool pointerCaptureStarted = false; + const UIInputDispatchSummary summary = inputDispatcher.Dispatch( + event, + hoveredPath, + [&](const UIInputDispatchRequest& request) { + if (!request.isTargetElement) { + return UIInputDispatchDecision{}; + } + + const RuntimeLayoutNode* node = FindNodeByElementId(root, request.elementId); + if (node == nullptr) { + return UIInputDispatchDecision{}; + } + + if (event.type == UIInputEventType::PointerButtonDown && + event.pointerButton == UIPointerButton::Left && + node->wantsPointerCapture) { + inputDispatcher.GetFocusController().SetPointerCapturePath(node->inputPath); + pointerCaptureStarted = true; + return UIInputDispatchDecision{ true, false }; + } + + return UIInputDispatchDecision{}; + }); + + inputDebugSnapshot.lastTargetKind = GetInputTargetKindDebugName(summary.routing.plan.targetKind); + inputDebugSnapshot.lastTargetStateKey = ResolveStateKeyForPathTarget(root, summary.routing.plan.targetPath); + inputDebugSnapshot.lastResult = summary.routing.plan.HasTargetPath() ? "Dispatched" : "No target"; + + if (pointerCaptureStarted) { + inputDebugSnapshot.lastResult = "Pointer capture started"; + } else if (event.type == UIInputEventType::PointerButtonUp && + event.pointerButton == UIPointerButton::Left && + inputDispatcher.GetFocusController().HasPointerCapture()) { + inputDispatcher.GetFocusController().ClearPointerCapturePath(); + inputDebugSnapshot.lastResult = "Pointer capture cleared"; + } else if (summary.routing.handled) { + inputDebugSnapshot.lastResult = "Handled"; + } } void SyncScrollOffsets( @@ -623,19 +1055,29 @@ void SyncScrollOffsets( void EmitNode( const RuntimeLayoutNode& node, + const UIInputPath& hoveredPath, + const UIFocusController& focusController, UIDrawList& drawList, UIScreenFrameStats& stats) { const UIDocumentNode& source = *node.source; const std::string tagName = ToStdString(source.tagName); + const RuntimeNodeVisualState visualState = ResolveNodeVisualState( + node, + hoveredPath, + focusController); ++stats.nodeCount; if (tagName == "View" || tagName == "Card" || tagName == "Button") { - drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source)), 10.0f); + drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source, visualState)), 10.0f); ++stats.filledRectCommandCount; if (tagName != "View") { - drawList.AddRectOutline(node.rect, ToUIColor(ResolveBorderColor(source)), 1.0f, 10.0f); + drawList.AddRectOutline( + node.rect, + ToUIColor(ResolveBorderColor(source, visualState)), + ResolveBorderThickness(visualState), + 10.0f); } } @@ -674,7 +1116,9 @@ void EmitNode( drawList.AddText( UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, kButtonFontSize)), ResolveNodeText(source), - ToUIColor(Color(0.95f, 0.97f, 1.0f, 1.0f)), + ToUIColor(visualState.capture || visualState.focused + ? Color(1.0f, 1.0f, 1.0f, 1.0f) + : Color(0.95f, 0.97f, 1.0f, 1.0f)), kButtonFontSize); ++stats.textCommandCount; } @@ -688,7 +1132,7 @@ void EmitNode( } for (const RuntimeLayoutNode& child : node.children) { - EmitNode(child, drawList, stats); + EmitNode(child, hoveredPath, focusController, drawList, stats); } if (pushScrollClip) { @@ -808,7 +1252,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( const std::string stateRoot = document.sourcePath.empty() ? document.displayName : document.sourcePath; - RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, 0u); + RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, UIInputPath(), 0u); MeasureNode(root); UIRect viewportRect = input.viewportRect; @@ -819,12 +1263,73 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( viewportRect.height = (std::max)(360.0f, root.desiredSize.height); } - ArrangeNode(root, viewportRect, m_verticalScrollOffsets); - if (ApplyScrollWheelInput(root, input, m_verticalScrollOffsets, m_scrollDebugSnapshot)) { - ArrangeNode(root, viewportRect, m_verticalScrollOffsets); + if (!input.focused) { + m_pointerState.insideViewport = false; } + + ArrangeNode(root, viewportRect, m_verticalScrollOffsets); + SanitizeInputDispatcherState(root, m_inputDispatcher); + + UIPoint pointerPosition = m_pointerState.position; + bool hasPointerPosition = m_pointerState.hasPosition; + bool pointerInsideViewport = + input.focused && + m_pointerState.insideViewport && + (!hasPointerPosition || RectContainsPoint(viewportRect, pointerPosition)); + + if (!input.focused) { + UIFocusController& focusController = m_inputDispatcher.GetFocusController(); + focusController.ClearPointerCapturePath(); + focusController.ClearActivePath(); + } + + for (const UIInputEvent& event : input.events) { + UpdatePointerTrackingState( + pointerPosition, + hasPointerPosition, + pointerInsideViewport, + event, + viewportRect); + pointerInsideViewport = input.focused && pointerInsideViewport; + + if (event.type == UIInputEventType::PointerWheel) { + m_inputDebugSnapshot.lastEventType = GetInputEventTypeDebugName(event.type); + m_inputDebugSnapshot.lastTargetKind = "Hovered"; + m_inputDebugSnapshot.lastResult = "No hovered ScrollView"; + if (ApplyScrollWheelEvent(root, event, m_verticalScrollOffsets, m_scrollDebugSnapshot)) { + ArrangeNode(root, viewportRect, m_verticalScrollOffsets); + } + continue; + } + + const UIInputPath eventHoveredPath = ResolveHoveredPath( + root, + pointerPosition, + hasPointerPosition, + pointerInsideViewport); + DispatchInputEvent(root, event, eventHoveredPath, m_inputDispatcher, m_inputDebugSnapshot); + } + + const UIInputPath hoveredPath = ResolveHoveredPath( + root, + pointerPosition, + hasPointerPosition, + pointerInsideViewport); + SanitizeInputDispatcherState(root, m_inputDispatcher); + m_pointerState.position = pointerPosition; + m_pointerState.hasPosition = hasPointerPosition; + m_pointerState.insideViewport = pointerInsideViewport; + UpdateInputDebugSnapshot( + root, + hoveredPath, + m_inputDispatcher, + pointerPosition, + pointerInsideViewport, + m_inputDebugSnapshot); SyncScrollOffsets(root, m_verticalScrollOffsets); + const UIFocusController& focusController = m_inputDispatcher.GetFocusController(); + if (const RuntimeLayoutNode* primaryScrollView = FindFirstScrollView(root); primaryScrollView != nullptr) { m_scrollDebugSnapshot.primaryTargetStateKey = primaryScrollView->stateKey; m_scrollDebugSnapshot.primaryViewportRect = primaryScrollView->scrollViewportRect; @@ -838,7 +1343,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( } UIDrawList& drawList = result.drawData.EmplaceDrawList(document.displayName); - EmitNode(root, drawList, result.stats); + EmitNode(root, hoveredPath, focusController, drawList, result.stats); result.stats.documentLoaded = true; result.stats.drawListCount = result.drawData.GetDrawListCount(); @@ -848,6 +1353,10 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame( return result; } +const UIDocumentScreenHost::InputDebugSnapshot& UIDocumentScreenHost::GetInputDebugSnapshot() const { + return m_inputDebugSnapshot; +} + const UIDocumentScreenHost::ScrollDebugSnapshot& UIDocumentScreenHost::GetScrollDebugSnapshot() const { return m_scrollDebugSnapshot; }