// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. #include <pch.h> #include <common.h> #include "ItemsRepeater.common.h" #include "ViewportManagerWithPlatformFeatures.h" #include "ItemsRepeater.h" #include "layout.h" // Pixel delta by which to inflate the cache buffer on each side. Rather than fill the entire // cache buffer all at once, we chunk the work to make the UI thread more responsive. We inflate // the cache buffer from 0 to a max value determined by the Maximum[Horizontal,Vertical]CacheLength // properties. constexpr double CacheBufferPerSideInflationPixelDelta = 40.0; ViewportManagerWithPlatformFeatures::ViewportManagerWithPlatformFeatures(ItemsRepeater* owner) : m_owner(owner), m_scroller(owner), m_makeAnchorElement(owner), m_cacheBuildAction(owner) { // ItemsRepeater is not fully constructed yet. Don't interact with it. } winrt::UIElement ViewportManagerWithPlatformFeatures::SuggestedAnchor() const { // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick. winrt::UIElement suggestedAnchor = m_makeAnchorElement.get(); winrt::UIElement owner = *m_owner; if (!suggestedAnchor) { const auto anchorElement = m_scroller ? m_scroller.get().CurrentAnchor() : nullptr; if (anchorElement) { // We can't simply return anchorElement because, in case of nested Repeaters, it may not // be a direct child of ours, or even an indirect child. We need to walk up the tree starting // from anchorElement to figure out what child of ours (if any) to use as the suggested element. auto child = anchorElement; auto parent = CachedVisualTreeHelpers::GetParent(child).as<winrt::UIElement>(); while (parent) { if (parent == owner) { suggestedAnchor = child; break; } child = parent; parent = CachedVisualTreeHelpers::GetParent(parent).as<winrt::UIElement>(); } } } return suggestedAnchor; } void ViewportManagerWithPlatformFeatures::HorizontalCacheLength(double value) { if (m_maximumHorizontalCacheLength != value) { ValidateCacheLength(value); m_maximumHorizontalCacheLength = value; ResetCacheBuffer(); } } void ViewportManagerWithPlatformFeatures::VerticalCacheLength(double value) { if (m_maximumVerticalCacheLength != value) { ValidateCacheLength(value); m_maximumVerticalCacheLength = value; ResetCacheBuffer(); } } winrt::Rect ViewportManagerWithPlatformFeatures::GetLayoutVisibleWindowDiscardAnchor() const { auto visibleWindow = m_visibleWindow; if (HasScroller()) { visibleWindow.X += m_layoutExtent.X + m_expectedViewportShift.X + m_unshiftableShift.X; visibleWindow.Y += m_layoutExtent.Y + m_expectedViewportShift.Y + m_unshiftableShift.Y; } return visibleWindow; } winrt::Rect ViewportManagerWithPlatformFeatures::GetLayoutVisibleWindow() const { auto visibleWindow = m_visibleWindow; if (m_makeAnchorElement && m_isAnchorOutsideRealizedRange) { // The anchor is not necessarily laid out yet. Its position should default // to zero and the layout origin is expected to change once layout is done. // Until then, we need a window that's going to protect the anchor from // getting recycled. // Also, we only want to mess with the realization rect iff the anchor is not inside it. // If we fiddle with an anchor that is already inside the realization rect, // shifting the realization rect results in repeater, layout and scroller thinking that it needs to act upon StartBringIntoView. // We do NOT want that! visibleWindow.X = 0.0f; visibleWindow.Y = 0.0f; } else if (HasScroller()) { visibleWindow.X += m_layoutExtent.X + m_expectedViewportShift.X + m_unshiftableShift.X; visibleWindow.Y += m_layoutExtent.Y + m_expectedViewportShift.Y + m_unshiftableShift.Y; } return visibleWindow; } winrt::Rect ViewportManagerWithPlatformFeatures::GetLayoutRealizationWindow() const { auto realizationWindow = GetLayoutVisibleWindow(); if (HasScroller()) { realizationWindow.X -= static_cast<float>(m_horizontalCacheBufferPerSide); realizationWindow.Y -= static_cast<float>(m_verticalCacheBufferPerSide); realizationWindow.Width += static_cast<float>(m_horizontalCacheBufferPerSide) * 2.0f; realizationWindow.Height += static_cast<float>(m_verticalCacheBufferPerSide) * 2.0f; } return realizationWindow; } void ViewportManagerWithPlatformFeatures::SetLayoutExtent(winrt::Rect extent) { m_expectedViewportShift.X += m_layoutExtent.X - extent.X; m_expectedViewportShift.Y += m_layoutExtent.Y - extent.Y; // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. if (std::abs(m_expectedViewportShift.X) > 1.f || std::abs(m_expectedViewportShift.Y) > 1.f) { REPEATER_TRACE_INFO(L"%ls: \tExpecting viewport shift of (%.0f,%.0f) \n", GetLayoutId().data(), m_expectedViewportShift.X, m_expectedViewportShift.Y); // There are cases where we might be expecting a shift but not get it. We will // be waiting for the effective viewport event but if the scroll viewer is not able // to perform the shift (perhaps because it cannot scroll in negative offset), // then we will end up not realizing elements in the visible // window. To avoid this, we register to layout updated for this layout pass. If we // get an effective viewport, we know we have a new viewport and we unregister from // layout updated. If we get the layout updated handler, then we know that the // scroller was unable to perform the shift and we invalidate measure and unregister // from the layout updated event. if (!m_layoutUpdatedRevoker) { m_layoutUpdatedRevoker = m_owner->LayoutUpdated(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnLayoutUpdated }); } } m_layoutExtent = extent; m_pendingViewportShift = m_expectedViewportShift; // We just finished a measure pass and have a new extent. // Let's make sure the scrollers will run its arrange so that they track the anchor. if (m_scroller) { m_scroller.as<winrt::UIElement>().InvalidateArrange(); } } void ViewportManagerWithPlatformFeatures::OnLayoutChanged(bool isVirtualizing) { m_managingViewportDisabled = !isVirtualizing; m_layoutExtent = {}; m_expectedViewportShift = {}; m_pendingViewportShift = {}; if (m_managingViewportDisabled) { m_effectiveViewportChangedRevoker.revoke(); } else if (!m_effectiveViewportChangedRevoker) { m_effectiveViewportChangedRevoker = m_owner->EffectiveViewportChanged(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnEffectiveViewportChanged }); } m_unshiftableShift = {}; ResetCacheBuffer(); } void ViewportManagerWithPlatformFeatures::OnElementPrepared(const winrt::UIElement& element) { // If we have an anchor element, we do not want the // scroll anchor provider to start anchoring some other element. element.CanBeScrollAnchor(true); } void ViewportManagerWithPlatformFeatures::OnElementCleared(const winrt::UIElement& element) { element.CanBeScrollAnchor(false); } void ViewportManagerWithPlatformFeatures::OnOwnerMeasuring() { // This is because of a bug that causes effective viewport to not // fire if you register during arrange. // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport EnsureScroller(); } void ViewportManagerWithPlatformFeatures::OnOwnerArranged() { m_expectedViewportShift = {}; if (!m_managingViewportDisabled) { // This is because of a bug that causes effective viewport to not // fire if you register during arrange. // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport // EnsureScroller(); if (HasScroller()) { const double maximumHorizontalCacheBufferPerSide = m_maximumHorizontalCacheLength * m_visibleWindow.Width / 2.0; const double maximumVerticalCacheBufferPerSide = m_maximumVerticalCacheLength * m_visibleWindow.Height / 2.0; const bool continueBuildingCache = m_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || m_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; if (continueBuildingCache) { m_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; m_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; m_horizontalCacheBufferPerSide = std::min(m_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); m_verticalCacheBufferPerSide = std::min(m_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); // Since we grow the cache buffer at the end of the arrange pass, // we need to register work even if we just reached cache potential. RegisterCacheBuildWork(); } } } } void ViewportManagerWithPlatformFeatures::OnLayoutUpdated(winrt::IInspectable const& sender, winrt::IInspectable const& args) { m_layoutUpdatedRevoker.revoke(); if (m_managingViewportDisabled) { return; } // We were expecting a viewport shift but we never got one and we are not going to in this // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer // that can scroll in the direction where the shift is expected. if (m_pendingViewportShift.X != 0 || m_pendingViewportShift.Y != 0) { REPEATER_TRACE_INFO(L"%ls: \tLayout Updated with pending shift %.0f %.0f- invalidating measure \n", GetLayoutId().data(), m_pendingViewportShift.X, m_pendingViewportShift.Y); // Assume this is never going to come. m_unshiftableShift.X += m_pendingViewportShift.X; m_unshiftableShift.Y += m_pendingViewportShift.Y; m_pendingViewportShift = {}; m_expectedViewportShift = {}; TryInvalidateMeasure(); } } void ViewportManagerWithPlatformFeatures::OnMakeAnchor(const winrt::UIElement& anchor, const bool isAnchorOutsideRealizedRange) { m_makeAnchorElement.set(anchor); m_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; } void ViewportManagerWithPlatformFeatures::OnBringIntoViewRequested(const winrt::BringIntoViewRequestedEventArgs args) { if (!m_managingViewportDisabled) { // We do not animate bring-into-view operations where the anchor is disconnected because // it doesn't look good (the blank space is obvious because the layout can't keep track // of two realized ranges while the animation is going on). if (m_isAnchorOutsideRealizedRange) { args.AnimationDesired(false); } // During the time between a bring into view request and the element coming into view we do not // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the // element that is being brought into view. We can do this by making just that element as a potential // anchor candidate and ensure no other element of this repeater is an anchor candidate. // Once the layout pass is done and we render the frame, the element will be in frame and we can // switch back to letting the anchor provider pick a suitable anchor. // get the targetChild - i.e the immediate child of this repeater that is being brought into view. // Note that the element being brought into view could be a descendant. const auto targetChild = GetImmediateChildOfRepeater(args.TargetElement()); // Make sure that only the target child can be the anchor during the bring into view operation. for (const auto& child : m_owner->Children()) { if (child.CanBeScrollAnchor() && child != targetChild) { child.CanBeScrollAnchor(false); } } // Register to rendering event to go back to how things were before where any child can be the anchor. m_isBringIntoViewInProgress = true; if (!m_renderingToken) { winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); } } } winrt::UIElement ViewportManagerWithPlatformFeatures::GetImmediateChildOfRepeater(winrt::UIElement const& descendant) { winrt::UIElement targetChild = descendant; winrt::UIElement parent = CachedVisualTreeHelpers::GetParent(descendant).as<winrt::UIElement>(); while (parent != nullptr && parent != static_cast<winrt::DependencyObject>(*m_owner)) { targetChild = parent; parent = CachedVisualTreeHelpers::GetParent(parent).as<winrt::UIElement>(); } if (!parent) { throw winrt::hresult_error(E_FAIL, L"OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); } return targetChild; } void ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering(const winrt::IInspectable& /*sender*/, const winrt::IInspectable& /*args*/) { assert(!m_managingViewportDisabled); m_renderingToken.revoke(); m_isBringIntoViewInProgress = false; m_makeAnchorElement.set(nullptr); // Now that the item has been brought into view, we can let the anchor provider pick a new anchor. for (const auto& child : m_owner->Children()) { if (!child.CanBeScrollAnchor()) { auto info = ItemsRepeater::GetVirtualizationInfo(child); if (info->IsRealized() && info->IsHeldByLayout()) { child.CanBeScrollAnchor(true); } } } } void ViewportManagerWithPlatformFeatures::ResetScrollers() { m_scroller.set(nullptr); m_effectiveViewportChangedRevoker.revoke(); m_ensuredScroller = false; } void ViewportManagerWithPlatformFeatures::OnCacheBuildActionCompleted() { m_cacheBuildAction.set(nullptr); if (!m_managingViewportDisabled) { m_owner->InvalidateMeasure(); } } void ViewportManagerWithPlatformFeatures::OnEffectiveViewportChanged(winrt::FrameworkElement const& sender, winrt::EffectiveViewportChangedEventArgs const& args) { assert(!m_managingViewportDisabled); REPEATER_TRACE_INFO(L"%ls: \tEffectiveViewportChanged event callback \n", GetLayoutId().data()); UpdateViewport(args.EffectiveViewport()); m_pendingViewportShift = {}; m_unshiftableShift = {}; if (m_visibleWindow == winrt::Rect()) { // We got cleared. m_layoutExtent = {}; } // We got a new viewport, we dont need to wait for layout updated anymore to // see if our request for a pending shift was handled. m_layoutUpdatedRevoker.revoke(); } void ViewportManagerWithPlatformFeatures::EnsureScroller() { if (!m_ensuredScroller) { ResetScrollers(); auto parent = CachedVisualTreeHelpers::GetParent(*m_owner); while (parent) { if (const auto scroller = parent.try_as<winrt::Controls::IScrollAnchorProvider>()) { m_scroller.set(scroller); break; } parent = CachedVisualTreeHelpers::GetParent(parent); } if (!m_scroller) { // We usually update the viewport in the post arrange handler. But, since we don't have // a scroller, let's do it now. UpdateViewport(winrt::Rect{}); } else if (!m_managingViewportDisabled) { m_effectiveViewportChangedRevoker = m_owner->EffectiveViewportChanged(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnEffectiveViewportChanged }); } m_ensuredScroller = true; } } void ViewportManagerWithPlatformFeatures::UpdateViewport(winrt::Rect const& viewport) { assert(!m_managingViewportDisabled); const auto previousVisibleWindow = m_visibleWindow; REPEATER_TRACE_INFO(L"%ls: \tEffective Viewport: (%.0f,%.0f,%.0f,%.0f)->(%.0f,%.0f,%.0f,%.0f). \n", GetLayoutId().data(), previousVisibleWindow.X, previousVisibleWindow.Y, previousVisibleWindow.Width, previousVisibleWindow.Height, viewport.X, viewport.Y, viewport.Width, viewport.Height); const auto& currentVisibleWindow = viewport; if (-currentVisibleWindow.X <= ItemsRepeater::ClearedElementsArrangePosition.X && -currentVisibleWindow.Y <= ItemsRepeater::ClearedElementsArrangePosition.Y) { REPEATER_TRACE_INFO(L"%ls: \tViewport is invalid. visible window cleared. \n", GetLayoutId().data()); // We got cleared. m_visibleWindow = {}; } else { REPEATER_TRACE_INFO(L"%ls: \tUsed Viewport: (%.0f,%.0f,%.0f,%.0f)->(%.0f,%.0f,%.0f,%.0f). \n", GetLayoutId().data(), previousVisibleWindow.X, previousVisibleWindow.Y, previousVisibleWindow.Width, previousVisibleWindow.Height, currentVisibleWindow.X, currentVisibleWindow.Y, currentVisibleWindow.Width, currentVisibleWindow.Height); m_visibleWindow = currentVisibleWindow; } TryInvalidateMeasure(); } void ViewportManagerWithPlatformFeatures::ResetCacheBuffer() { m_horizontalCacheBufferPerSide = 0.0; m_verticalCacheBufferPerSide = 0.0; if (!m_managingViewportDisabled) { // We need to start building the realization buffer again. RegisterCacheBuildWork(); } } void ViewportManagerWithPlatformFeatures::ValidateCacheLength(double cacheLength) { if (cacheLength < 0.0 || std::isinf(cacheLength) || std::isnan(cacheLength)) { throw winrt::hresult_invalid_argument(L"The maximum cache length must be equal or superior to zero."); } } void ViewportManagerWithPlatformFeatures::RegisterCacheBuildWork() { assert(!m_managingViewportDisabled); if (m_owner->Layout() && !m_cacheBuildAction) { // We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around // when the async action completes. By protecting ItemsRepeater, we also ensure that this instance // of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater // and ViewportManager is the same (see ItemsRepeater::m_viewportManager). // We can't simply hold a strong reference on ViewportManager because it's not a COM object. auto strongOwner = m_owner->get_strong(); m_cacheBuildAction.set( m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&) { OnCacheBuildActionCompleted(); })); } } void ViewportManagerWithPlatformFeatures::TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. if (m_visibleWindow != winrt::Rect()) { // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to // avoid layout cycles. REPEATER_TRACE_INFO(L"%ls: \tInvalidating measure due to viewport change. \n", GetLayoutId().data()); m_owner->InvalidateMeasure(); } } winrt::hstring ViewportManagerWithPlatformFeatures::GetLayoutId() const { if (auto layout = m_owner->Layout()) { return layout.as<Layout>()->LayoutId(); } return winrt::hstring{}; }