| #include "rive/constraints/scrolling/scroll_constraint.hpp" | |
| #include "rive/layout/layout_node_provider.hpp" | |
| #include "rive/constraints/scrolling/scroll_virtualizer.hpp" | |
| #include "rive/constraints/scrolling/virtual_layout.hpp" | |
| #include <algorithm> | |
| #include <set> | |
| using namespace rive; | |
| ScrollVirtualizer::~ScrollVirtualizer() { reset(); } | |
| void ScrollVirtualizer::reset() { m_anchorItem = -1; } | |
| float ScrollVirtualizer::anchorMoved( | |
| const VirtualLayout& layout, | |
| std::vector<LayoutNodeProvider*>& children) const | |
| { | |
| if (m_anchorItem < 0) | |
| { | |
| return 0.0f; | |
| } | |
| int item = m_anchorItem; | |
| if (m_anchorInstance == nullptr) | |
| { | |
| // Not a list item: an index only names the same item while the lists | |
| // are unchanged. | |
| if (layout.itemCount() != m_anchorItemCount) | |
| { | |
| return 0.0f; | |
| } | |
| } | |
| else | |
| { | |
| // Follow the instance to wherever a list update moved its item; no | |
| // anchor if it went away. | |
| item = -1; | |
| int segments = std::min((int)children.size(), layout.segmentCount()); | |
| std::vector<int> held; | |
| for (int segment = 0; segment < segments && item < 0; segment++) | |
| { | |
| auto virt = VirtualizingComponent::fromScrollChild(children[segment]); | |
| if (virt == nullptr) | |
| { | |
| continue; | |
| } | |
| int first = layout.segmentStart(segment); | |
| int local = m_anchorItem - first; | |
| if (local >= 0 && local < virt->itemCount() && | |
| virt->item(local) == m_anchorInstance) | |
| { | |
| item = m_anchorItem; | |
| break; | |
| } | |
| held.clear(); | |
| virt->realizedIndices(held); | |
| for (int index : held) | |
| { | |
| if (virt->item(index) == m_anchorInstance) | |
| { | |
| item = first + index; | |
| break; | |
| } | |
| } | |
| } | |
| if (item < 0 || item >= layout.itemCount()) | |
| { | |
| return 0.0f; | |
| } | |
| } | |
| // A row layout hasn't placed yet sits at a guess, not where it moved. | |
| int line = layout.lineOfItem(item); | |
| if (!layout.lineLaidOut(line)) | |
| { | |
| return 0.0f; | |
| } | |
| return layout.lineStart(line) - m_anchorStart; | |
| } | |
| bool ScrollVirtualizer::constrain( | |
| ScrollConstraint* scroll, | |
| std::vector<LayoutNodeProvider*>& children, | |
| float offset, | |
| VirtualizedDirection direction, | |
| float flowOffset, | |
| float columnStart, | |
| float columnViewport, | |
| bool windowsColumns) | |
| { | |
| m_windowsColumns = windowsColumns; | |
| m_flowOffset = flowOffset; | |
| m_columnStart = columnStart; | |
| m_columnViewport = columnViewport; | |
| 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) | |
| { | |
| auto layout = scroll->virtualLayout(); | |
| if (layout == nullptr || layout->segmentCount() != (int)children.size()) | |
| { | |
| return; | |
| } | |
| bool isHorz = m_direction == VirtualizedDirection::horizontal; | |
| int totalItemCount = layout->itemCount(); | |
| // 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 buffer = std::min( | |
| static_cast<int>(scroll->virtualizeBuffer()), | |
| totalItemCount); | |
| auto window = layout->window(m_offset, m_viewportSize, m_infinite, buffer); | |
| // Only realized columns of each realized row exist when columns window. | |
| bool windowsColumns = m_windowsColumns && layout->isGrid(); | |
| // A carousel scrolled both ways cycles its columns too; whole cycles | |
| // move nothing, so drop them to keep offsets small. | |
| bool loopsColumns = windowsColumns && m_infinite; | |
| float flowOffset = m_flowOffset; | |
| float columnStart = m_columnStart; | |
| float columnCycle = layout->columnCycleExtent(); | |
| if (loopsColumns && columnCycle > 0.0f) | |
| { | |
| float cycles = std::floor(columnStart / columnCycle) * columnCycle; | |
| flowOffset -= cycles; | |
| columnStart -= cycles; | |
| } | |
| VirtualWindow columns = windowsColumns ? layout->columnWindow( | |
| columnStart, | |
| m_columnViewport, | |
| buffer, | |
| loopsColumns) | |
| : VirtualWindow(); | |
| // Where a row's column sits in the (unwrapped) column window. | |
| auto windowColumn = [&](int line, int item) { | |
| int column = item - layout->lineFirstItem(line); | |
| return loopsColumns | |
| ? columns.start + layout->wrapColumn(column - columns.start) | |
| : column; | |
| }; | |
| auto inColumns = [&](int line, int item) { | |
| if (!windowsColumns) | |
| { | |
| return true; | |
| } | |
| int column = windowColumn(line, item); | |
| return column >= columns.start && column <= columns.end; | |
| }; | |
| for (auto child : children) | |
| { | |
| auto virt = VirtualizingComponent::fromScrollChild(child); | |
| if (virt != nullptr) | |
| { | |
| virt->clearVirtualWindow(); | |
| } | |
| } | |
| // Recycle what the lists hold that this window doesn't. They're asked | |
| // rather than remembered: a list update moves held items to new indices. | |
| // Line indices wrap through the layout, so these are list items. | |
| // Sized to the window, not the list, so a pass costs what it shows. | |
| std::vector<int> used; | |
| for (int line = window.start; line <= window.end; line++) | |
| { | |
| for (int item = layout->lineFirstItem(line), | |
| last = layout->lineLastItem(line); | |
| item <= last; | |
| item++) | |
| { | |
| if (inColumns(line, item)) | |
| { | |
| used.push_back(item); | |
| } | |
| } | |
| } | |
| std::sort(used.begin(), used.end()); | |
| auto isUsed = [&](int item) { | |
| return std::binary_search(used.begin(), used.end(), item); | |
| }; | |
| // An item listed more than once shares its instance across its indices: | |
| // keep it while any of them is used, and recycle it once. | |
| std::vector<int> held; | |
| std::vector<Virtualizable*> heldItems; | |
| // Few items are realized at once, so linear scans beat sets here. | |
| std::vector<Virtualizable*> kept; | |
| std::vector<Virtualizable*> recycled; | |
| auto contains = [](const std::vector<Virtualizable*>& items, | |
| Virtualizable* item) { | |
| return std::find(items.begin(), items.end(), item) != items.end(); | |
| }; | |
| for (int segment = 0; segment < (int)children.size(); segment++) | |
| { | |
| auto virt = VirtualizingComponent::fromScrollChild(children[segment]); | |
| if (virt == nullptr) | |
| { | |
| continue; | |
| } | |
| held.clear(); | |
| virt->realizedIndices(held); | |
| int first = layout->segmentStart(segment); | |
| int count = (segment + 1 < (int)children.size() | |
| ? layout->segmentStart(segment + 1) | |
| : totalItemCount) - | |
| first; | |
| // Read each index's instance before recycling clears its aliases. | |
| heldItems.clear(); | |
| kept.clear(); | |
| recycled.clear(); | |
| for (int index : held) | |
| { | |
| heldItems.push_back(virt->item(index)); | |
| if (index < count && isUsed(first + index)) | |
| { | |
| kept.push_back(heldItems.back()); | |
| } | |
| } | |
| for (size_t i = 0; i < held.size(); i++) | |
| { | |
| int index = held[i]; | |
| if (index < count && isUsed(first + index)) | |
| { | |
| continue; | |
| } | |
| auto shared = heldItems[i]; | |
| if (!contains(kept, shared) && !contains(recycled, shared)) | |
| { | |
| recycled.push_back(shared); | |
| virt->removeVirtualizable(index); | |
| } | |
| } | |
| } | |
| // Carousel offsets wrap, so there's no fixed place to anchor to. Anchor | |
| // to the first item on screen: in the first line holding one (a grid's | |
| // empty template rows hold none), and in its first column on screen when | |
| // columns window. | |
| m_anchorItem = -1; | |
| for (int line = window.visibleStart; | |
| !m_infinite && !window.isEmpty() && line <= window.visibleEnd && | |
| m_anchorItem < 0; | |
| line++) | |
| { | |
| if (!layout->lineLaidOut(line)) | |
| { | |
| break; | |
| } | |
| int first = layout->lineFirstItem(line); | |
| int last = layout->lineLastItem(line); | |
| int columnsShown = windowsColumns | |
| ? columns.visibleEnd - columns.visibleStart + 1 | |
| : 1; | |
| for (int shown = 0; shown < columnsShown; shown++) | |
| { | |
| int column = 0; | |
| if (windowsColumns) | |
| { | |
| column = columns.visibleStart + shown; | |
| column = loopsColumns ? layout->wrapColumn(column) : column; | |
| } | |
| if (first + column <= last) | |
| { | |
| m_anchorItem = first + column; | |
| m_anchorStart = layout->lineStart(line); | |
| m_anchorItemCount = totalItemCount; | |
| break; | |
| } | |
| } | |
| } | |
| std::set<VirtualizingComponent*> changedVirtualizingComponents; | |
| std::vector<int> lineItems; | |
| for (int line = window.start; line <= window.end; line++) | |
| { | |
| // Buffered items are realized and drawn, but only on screen items | |
| // report their measured size back. | |
| bool isVisible = window.isVisible(line); | |
| float position = layout->lineStart(line) - m_offset; | |
| // Items join the window in the order they paint: on screen order, so | |
| // looping columns that wrap past the last follow it. | |
| lineItems.clear(); | |
| int first = layout->lineFirstItem(line); | |
| int last = layout->lineLastItem(line); | |
| if (loopsColumns) | |
| { | |
| for (int column = columns.start; column <= columns.end; column++) | |
| { | |
| int item = first + layout->wrapColumn(column); | |
| if (item <= last) | |
| { | |
| lineItems.push_back(item); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| for (int item = first; item <= last; item++) | |
| { | |
| if (inColumns(line, item)) | |
| { | |
| lineItems.push_back(item); | |
| } | |
| } | |
| } | |
| for (int item : lineItems) | |
| { | |
| int segment = layout->segmentOf(item); | |
| auto virt = VirtualizingComponent::fromScrollChild(children[segment]); | |
| if (virt == nullptr) | |
| { | |
| continue; | |
| } | |
| int childIndex = item - layout->segmentStart(segment); | |
| bool itemVisible = isVisible && | |
| (!windowsColumns || | |
| columns.isVisible(windowColumn(line, item))); | |
| virt->addToVirtualWindow(childIndex, itemVisible); | |
| if (virt->item(childIndex) == nullptr) | |
| { | |
| virt->addVirtualizable(childIndex); | |
| changedVirtualizingComponents.emplace(virt); | |
| } | |
| // Layout holds every row, so a realized item takes its own cell | |
| // rather than the next free one. Unpin once a pass stops pinning. | |
| if (layout->isGrid() || m_pinnedCells) | |
| { | |
| bool grid = layout->isGrid(); | |
| virt->setVirtualizableCell( | |
| childIndex, | |
| grid ? item - layout->lineFirstItem(line) : -1, | |
| grid ? layout->wrapLine(line) : -1); | |
| } | |
| auto virtualizable = virt->item(childIndex); | |
| if (virtualizable == nullptr) | |
| { | |
| continue; | |
| } | |
| auto virtualizableComponent = virtualizable->virtualizableComponent(); | |
| if (virtualizableComponent == nullptr || | |
| !virtualizableComponent->is<ArtboardInstance>()) | |
| { | |
| continue; | |
| } | |
| Mat2D inverse; | |
| if (!children[segment]->transformComponent()->worldTransform().invert( | |
| &inverse)) | |
| { | |
| continue; | |
| } | |
| auto artboardInstance = virtualizableComponent->as<ArtboardInstance>(); | |
| // Layout already placed a realized item along its line when it saw | |
| // the same lines we did (see VirtualLayout::flowFromLayout). | |
| float flow = !layout->flowFromLayout() ? layout->itemFlowOffset(item) | |
| : isHorz ? artboardInstance->layoutY() | |
| : artboardInstance->layoutX(); | |
| // Lines move along the flow axis when it scrolls too, and a | |
| // cycling column moves to the cycle it's realized in. | |
| flow -= flowOffset; | |
| if (loopsColumns) | |
| { | |
| int column = item - layout->lineFirstItem(line); | |
| flow += layout->columnStart(windowColumn(line, item)) - | |
| layout->columnStart(column); | |
| } | |
| float scroll = position + layout->itemLineOffset(item); | |
| auto location = isHorz ? Vec2D(scroll, flow) : Vec2D(flow, scroll); | |
| virt->setVirtualizablePosition(childIndex, location); | |
| } | |
| } | |
| // The anchor was realized above; remember its instance to follow it. | |
| m_anchorInstance = nullptr; | |
| if (m_anchorItem >= 0) | |
| { | |
| int segment = layout->segmentOf(m_anchorItem); | |
| auto virt = VirtualizingComponent::fromScrollChild(children[segment]); | |
| if (virt != nullptr) | |
| { | |
| m_anchorInstance = virt->item( | |
| m_anchorItem - layout->segmentStart(segment)); | |
| } | |
| } | |
| m_pinnedCells = layout->isGrid(); | |
| for (auto& virtualizingComponent : changedVirtualizingComponents) | |
| { | |
| virtualizingComponent->virtualizableChanged(); | |
| } | |
| } |