blob: c97ce00f0191505a22793ef7468a0c423782cc3d [file]
#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();
}
}