| #include "rive/constraints/scrolling/scroll_constraint.hpp" |
| #include "rive/layout/layout_node_provider.hpp" |
| #include "rive/constraints/scrolling/scroll_virtualizer.hpp" |
| #include <algorithm> |
| #include <set> |
| |
| using namespace rive; |
| |
| ScrollVirtualizer::~ScrollVirtualizer() { reset(); } |
| |
| void ScrollVirtualizer::reset() |
| { |
| m_realizedIndexStart = m_realizedIndexEnd = 0; |
| } |
| |
| bool ScrollVirtualizer::constrain(ScrollConstraint* scroll, |
| std::vector<LayoutNodeProvider*>& children, |
| float offset, |
| VirtualizedDirection direction) |
| { |
| bool isHorz = direction == VirtualizedDirection::horizontal; |
| double contentSize = |
| isHorz ? scroll->contentWidth() : scroll->contentHeight(); |
| if (contentSize > 0.0f) |
| { |
| float normalizedOffset = -offset; |
| m_direction = direction; |
| m_viewportSize = |
| isHorz ? scroll->viewportWidth() : scroll->viewportHeight(); |
| m_infinite = scroll->infinite(); |
| if (offset > 0.0f) |
| { |
| if (m_infinite) |
| { |
| int offsetMultiplier = |
| static_cast<int>(std::floor(offset / contentSize)) + 1; |
| m_offset = -1.0f * (offset - (offsetMultiplier * contentSize)); |
| } |
| else |
| { |
| m_offset = -offset; |
| } |
| } |
| else |
| { |
| int offsetMultiplier = |
| static_cast<int>(std::floor(normalizedOffset / contentSize)); |
| m_offset = offsetMultiplier > 0 |
| ? std::fmod(normalizedOffset, |
| offsetMultiplier * contentSize) |
| : normalizedOffset; |
| } |
| virtualize(scroll, children); |
| } |
| return true; |
| } |
| |
| void ScrollVirtualizer::virtualize(ScrollConstraint* scroll, |
| std::vector<LayoutNodeProvider*>& children) |
| { |
| int totalItemCount = 0; |
| for (auto child : children) |
| { |
| totalItemCount += child->numLayoutNodes(); |
| } |
| |
| // All changes in this function are intended to compare the |
| // ranges of the previous render to the ranges of the upcoming list. This is |
| // removing the carousel overflow. |
| // normalizing the two values to the actual indexes of available children |
| int lastRealizedIndexStart = m_infinite && totalItemCount > 0 |
| ? m_realizedIndexStart % totalItemCount |
| : m_realizedIndexStart; |
| int lastRealizedIndexEnd = m_infinite && totalItemCount > 0 |
| ? m_realizedIndexEnd % totalItemCount |
| : m_realizedIndexEnd; |
| |
| m_realizedIndexStart = 0; |
| m_realizedIndexEnd = totalItemCount - 1; |
| float runningSize = 0.0f; |
| float runningOffset = 0.0f; |
| int runningIndex = 0; |
| int childIndex = 0; |
| int currentChildIndex = 0; |
| bool isHorz = m_direction == VirtualizedDirection::horizontal; |
| float gap = isHorz ? scroll->gap().x : scroll->gap().y; |
| std::set<VirtualizingComponent*> changedVirtualizingComponents; |
| |
| for (int i = 0; i < children.size(); i++) |
| { |
| auto child = children[i]; |
| auto component = child->transformComponent(); |
| if (component != nullptr) |
| { |
| auto virt = VirtualizingComponent::from(component); |
| if (virt != nullptr) |
| { |
| virt->setVisibleIndices(-1, -1); |
| virt->setRealizedIndices(-1, -1); |
| } |
| } |
| } |
| |
| for (int i = 0; i < children.size(); i++) |
| { |
| auto child = children[i]; |
| for (int j = 0; j < child->numLayoutNodes(); j++) |
| { |
| auto size = getItemSize(child, j, isHorz); |
| if (runningSize + size > m_offset) |
| { |
| runningOffset = runningSize - m_offset; |
| m_realizedIndexStart = runningIndex; |
| if (currentChildIndex == children.size() - 1) |
| { |
| childIndex++; |
| currentChildIndex = 0; |
| } |
| else |
| { |
| currentChildIndex++; |
| } |
| goto findVisibleEnd; |
| } |
| runningSize += size; |
| currentChildIndex = j; |
| runningIndex++; |
| if (runningSize + gap > m_offset) |
| { |
| if (runningIndex == totalItemCount) |
| { |
| runningIndex = 0; |
| } |
| if (currentChildIndex == children.size() - 1) |
| { |
| childIndex++; |
| currentChildIndex = 0; |
| } |
| else |
| { |
| currentChildIndex++; |
| } |
| runningSize += gap; |
| runningOffset = runningSize - m_offset; |
| m_realizedIndexStart = runningIndex; |
| goto findVisibleEnd; |
| } |
| runningSize += gap; |
| } |
| childIndex++; |
| } |
| |
| findVisibleEnd: |
| childIndex = childIndex % children.size(); |
| int i = m_realizedIndexStart; |
| bool wrapped = false; |
| int cycleCount = 0; |
| while (i < totalItemCount && cycleCount < 2) |
| { |
| auto child = children[childIndex]; |
| for (int j = currentChildIndex; j < child->numLayoutNodes(); j++) |
| { |
| auto size = getItemSize(child, j, isHorz); |
| if (runningSize + size + gap >= m_offset + m_viewportSize) |
| { |
| m_realizedIndexEnd = |
| m_infinite ? (wrapped ? i + totalItemCount : i) : i; |
| goto recycle; |
| } |
| runningSize += size + gap; |
| runningIndex++; |
| if (m_infinite && i == totalItemCount - 1) |
| { |
| wrapped = true; |
| i = -1; // will become 0 after increment |
| cycleCount++; |
| } |
| i++; |
| } |
| currentChildIndex = 0; |
| } |
| |
| recycle: |
| // Keep `virtualizeBuffer` lines realized on each side of the visible range |
| // so items are mounted and advancing before they scroll in. Buffered items |
| // are drawn (clipped away by a normal viewport), but stay out of the |
| // visible range, which is what reports measured sizes back to us. |
| int visibleIndexStart = m_realizedIndexStart; |
| int visibleIndexEnd = m_realizedIndexEnd; |
| int buffer = |
| std::min(static_cast<int>(scroll->virtualizeBuffer()), totalItemCount); |
| if (buffer > 0 && totalItemCount > 0) |
| { |
| int visibleSpan = m_realizedIndexEnd - m_realizedIndexStart + 1; |
| int maxExtra = std::max(0, totalItemCount - visibleSpan); |
| int before = |
| std::min(buffer, m_infinite ? maxExtra : m_realizedIndexStart); |
| int after = |
| std::min(buffer, |
| m_infinite ? maxExtra - before |
| : totalItemCount - 1 - m_realizedIndexEnd); |
| before = std::max(0, before); |
| after = std::max(0, after); |
| for (int k = 1; k <= before; k++) |
| { |
| runningOffset -= getItemSizeAt(m_realizedIndexStart - k, |
| children, |
| totalItemCount, |
| isHorz) + |
| gap; |
| } |
| m_realizedIndexStart -= before; |
| m_realizedIndexEnd += after; |
| if (m_infinite) |
| { |
| // Indices are modular when infinite, so bias the widened range into |
| // positive space and keep the visible bounds in the same frame. |
| m_realizedIndexStart += totalItemCount; |
| m_realizedIndexEnd += totalItemCount; |
| visibleIndexStart += totalItemCount; |
| visibleIndexEnd += totalItemCount; |
| } |
| } |
| |
| std::vector<int> indicesToRecycle; |
| int actualStart = m_infinite && totalItemCount > 0 |
| ? m_realizedIndexStart % totalItemCount |
| : m_realizedIndexStart; |
| int actualEnd = m_infinite && totalItemCount > 0 |
| ? m_realizedIndexEnd % totalItemCount |
| : m_realizedIndexEnd; |
| std::unordered_map<int, bool> usedIndexes = {}; |
| // If start < end it means that the range is not going over |
| // the end of the list, so we know we can add the full range to the used |
| // items. |
| if (actualStart <= actualEnd) |
| { |
| for (int i = actualStart; i <= actualEnd; i++) |
| { |
| usedIndexes[i] = true; |
| } |
| } |
| // If end > start, we know that the range wraps, so we |
| // actually need to add two ranges, from [start to totalIItems] and from [0 |
| // to end] |
| else |
| { |
| for (int i = actualStart; i < totalItemCount; i++) |
| { |
| usedIndexes[i] = true; |
| } |
| for (int i = 0; i <= actualEnd; i++) |
| { |
| usedIndexes[i] = true; |
| } |
| } |
| // Similarly, we check the previous ranges and check which |
| // ones overlap with the new range and which ones can be recycled. |
| if (lastRealizedIndexStart <= lastRealizedIndexEnd) |
| { |
| |
| for (int i = lastRealizedIndexStart; i <= lastRealizedIndexEnd; i++) |
| { |
| if (usedIndexes.find(i) == usedIndexes.end()) |
| { |
| indicesToRecycle.push_back(i); |
| } |
| } |
| } |
| else |
| { |
| |
| for (int i = lastRealizedIndexStart; i < totalItemCount; i++) |
| { |
| if (usedIndexes.find(i) == usedIndexes.end()) |
| { |
| indicesToRecycle.push_back(i); |
| } |
| } |
| for (int i = 0; i <= lastRealizedIndexEnd; i++) |
| { |
| if (usedIndexes.find(i) == usedIndexes.end()) |
| { |
| indicesToRecycle.push_back(i); |
| } |
| } |
| } |
| recycleItems(indicesToRecycle, children, totalItemCount); |
| |
| std::vector<Vec2D> visibleIndices(children.size(), Vec2D(-1, -1)); |
| std::vector<Vec2D> realizedIndices(children.size(), Vec2D(-1, -1)); |
| |
| for (int i = m_realizedIndexStart; i <= m_realizedIndexEnd; ++i) |
| { |
| int actualIndex = m_infinite ? i % totalItemCount : i; |
| // Buffered items are realized and drawn, but only on screen items |
| // report their measured size back. |
| bool isVisible = i >= visibleIndexStart && i <= visibleIndexEnd; |
| int runningTotal = 0; |
| for (int i = 0; i < children.size(); i++) |
| { |
| auto child = children[i]; |
| int start = runningTotal; |
| int end = start + (int)child->numLayoutNodes(); |
| auto component = child->transformComponent(); |
| if (component != nullptr) |
| { |
| auto virt = VirtualizingComponent::from(component); |
| if (virt != nullptr && start < end) |
| { |
| if (actualIndex < end && actualIndex >= start) |
| { |
| int childIndex = actualIndex - start; |
| auto& realizedInd = realizedIndices[i]; |
| if (realizedInd.x == -1) |
| { |
| realizedInd.x = childIndex; |
| } |
| realizedInd.y = childIndex; |
| if (isVisible) |
| { |
| auto& visibleInd = visibleIndices[i]; |
| if (visibleInd.x == -1) |
| { |
| visibleInd.x = childIndex; |
| } |
| visibleInd.y = childIndex; |
| } |
| auto item = virt->item(childIndex); |
| if (item == nullptr) |
| { |
| virt->addVirtualizable(childIndex); |
| changedVirtualizingComponents.emplace(virt); |
| } |
| |
| auto size = getItemSize(child, childIndex, isHorz); |
| auto virtualizable = virt->item(childIndex); |
| if (virtualizable != nullptr) |
| { |
| auto virtualizableComponent = |
| virtualizable->virtualizableComponent(); |
| if (virtualizableComponent != nullptr && |
| virtualizableComponent->is<ArtboardInstance>()) |
| { |
| auto artboardInstance = |
| virtualizableComponent |
| ->as<ArtboardInstance>(); |
| auto parentWorld = component->worldTransform(); |
| Mat2D inverse; |
| if (!parentWorld.invert(&inverse)) |
| { |
| continue; |
| } |
| auto location = |
| isHorz ? Vec2D(runningOffset, |
| artboardInstance->layoutY()) |
| : Vec2D(artboardInstance->layoutX(), |
| runningOffset); |
| virt->setVirtualizablePosition(childIndex, |
| location); |
| } |
| } |
| |
| runningOffset += size + gap; |
| break; |
| } |
| } |
| } |
| runningTotal = end; |
| } |
| } |
| |
| for (int i = 0; i < children.size(); i++) |
| { |
| auto child = children[i]; |
| auto visible = visibleIndices[i]; |
| auto component = child->transformComponent(); |
| if (component != nullptr) |
| { |
| auto virt = VirtualizingComponent::from(component); |
| if (virt != nullptr) |
| { |
| virt->setVisibleIndices(visible.x, visible.y); |
| auto realized = realizedIndices[i]; |
| virt->setRealizedIndices(realized.x, realized.y); |
| } |
| } |
| } |
| for (auto& virtualizingComponent : changedVirtualizingComponents) |
| { |
| virtualizingComponent->virtualizableChanged(); |
| } |
| } |
| |
| void ScrollVirtualizer::recycleItems(std::vector<int> indices, |
| std::vector<LayoutNodeProvider*>& children, |
| int totalItemCount) |
| { |
| if (totalItemCount == 0) |
| { |
| return; |
| } |
| std::sort(indices.begin(), indices.end()); |
| for (auto globalIndex : indices) |
| { |
| auto actualIndex = |
| m_infinite ? globalIndex % totalItemCount : globalIndex; |
| int runningTotal = 0; |
| for (int i = 0; i < children.size(); i++) |
| { |
| auto child = children[i]; |
| int start = runningTotal; |
| int end = start + (int)child->numLayoutNodes(); |
| auto component = child->transformComponent(); |
| if (component != nullptr) |
| { |
| auto virt = VirtualizingComponent::from(component); |
| if (virt != nullptr && start < end) |
| { |
| if (actualIndex < end && actualIndex >= start) |
| { |
| int childIndex = actualIndex - start; |
| virt->removeVirtualizable(childIndex); |
| break; |
| } |
| } |
| } |
| runningTotal = end; |
| } |
| } |
| } |
| |
| float ScrollVirtualizer::getItemSizeAt( |
| int globalIndex, |
| std::vector<LayoutNodeProvider*>& children, |
| int totalItemCount, |
| bool isHorizontal) |
| { |
| if (totalItemCount <= 0) |
| { |
| return 0.0f; |
| } |
| int index = globalIndex % totalItemCount; |
| if (index < 0) |
| { |
| index += totalItemCount; |
| } |
| int runningTotal = 0; |
| for (auto child : children) |
| { |
| int end = runningTotal + (int)child->numLayoutNodes(); |
| if (index < end) |
| { |
| return getItemSize(child, index - runningTotal, isHorizontal); |
| } |
| runningTotal = end; |
| } |
| return 0.0f; |
| } |
| |
| float ScrollVirtualizer::getItemSize(LayoutNodeProvider* child, |
| int index, |
| bool isHorizontal) |
| { |
| auto component = child->transformComponent(); |
| if (component != nullptr) |
| { |
| auto virt = VirtualizingComponent::from(component); |
| if (virt != nullptr) |
| { |
| auto size = virt->itemSize(index); |
| return isHorizontal ? size.x : size.y; |
| } |
| } |
| auto bounds = child->layoutBounds(); |
| return isHorizontal ? bounds.width() : bounds.height(); |
| } |