blob: 8b0fbdab7997c9d12962cef9d2f0db43073c8a7f [file] [edit]
#include "rive/constraints/scrolling/virtual_layout.hpp"
#include <cmath>
using namespace rive;
void VirtualLayout::beginLinear(float gap)
{
m_itemCount = m_lineCount = 0;
m_gap = gap;
m_running = 0.0f;
m_trailing = -std::numeric_limits<float>::infinity();
m_lineOpen = false;
m_wraps = false;
m_grid = false;
m_hugsLines = false;
m_flowFromLayout = true;
m_lineUsed.clear();
m_lineStart.clear();
m_lineExtent.clear();
m_trailingMax.clear();
m_lineFirstItem.clear();
m_segmentStart.clear();
m_itemExtent.clear();
m_itemFlowExtent.clear();
m_itemFlowOffset.clear();
m_itemLineOffset.clear();
}
void VirtualLayout::beginWrap(
float lineGap,
float flowGap,
float flowStart,
float flowExtent,
LayoutMainDistribute justify,
LayoutCrossAlign align,
bool hugsLines)
{
beginLinear(lineGap);
m_wraps = true;
m_hugsLines = hugsLines;
m_flowGap = flowGap;
m_flowStart = flowStart;
m_flowExtent = flowExtent;
m_justify = justify;
m_align = align;
}
void VirtualLayout::beginGrid(
float rowGap,
int columnCount,
LayoutCrossAlign align,
float columnGap,
int rowCount)
{
m_columnGap = columnGap;
beginLinear(rowGap);
m_wraps = true;
m_grid = true;
m_columnCount = std::max(1, columnCount);
m_rowCount = rowCount;
m_columnContents.assign(m_columnCount, 0.0f);
m_columnStarts.clear();
m_rowStarts.clear();
m_align = align;
}
// Rows sit on layout's lines once it has sized every track from the row and
// column contents; rows it hasn't laid out yet stack at their content size.
void VirtualLayout::placeGridRows()
{
// Template rows nothing fills still take their space.
while (m_lineCount < m_rowCount)
{
pushLine(0.0f, 0.0f);
}
int n = m_lineCount;
m_rowContents.assign(m_lineExtent.begin(), m_lineExtent.begin() + n);
int lines = (int)m_rowStarts.size();
m_running = 0.0f;
m_trailing = -std::numeric_limits<float>::infinity();
for (int line = 0; line < n; line++)
{
float start = m_running;
float extent = m_rowContents[line];
if (line + 1 < lines)
{
// The last line closes the last track; the others open one after
// a gap.
start = m_rowStarts[line] - m_rowStarts[0];
extent = m_rowStarts[line + 1] - m_rowStarts[line] -
(line + 2 < lines ? m_gap : 0.0f);
}
m_lineStart[line] = start;
m_lineExtent[line] = extent;
int last = line + 1 < n ? m_lineFirstItem[line + 1] : m_itemCount;
for (int item = m_lineFirstItem[line]; item < last; item++)
{
// Yoga's grid centers an item in its row but reads the FlexEnd
// that alignment writes as start; match it.
float slack = extent - m_itemExtent[item];
m_itemLineOffset[item] = m_align == LayoutCrossAlign::center
? slack / 2.0f
: 0.0f;
}
m_trailing = std::max(m_trailing, start + extent);
m_trailingMax[line] = m_trailing;
m_running = start + extent + m_gap;
}
}
void VirtualLayout::beginSegment() { m_segmentStart.push_back(m_itemCount); }
void VirtualLayout::pushLine(float start, float extent)
{
m_lineStart.push_back(start);
m_lineExtent.push_back(extent);
m_lineFirstItem.push_back(m_itemCount);
m_trailingMax.push_back(0.0f);
if (m_wraps)
{
m_lineUsed.push_back(0.0f);
}
m_lineCount++;
}
void VirtualLayout::openLine()
{
pushLine(m_running, 0.0f);
m_lineOpen = true;
m_lineFlowUsed = 0.0f;
}
void VirtualLayout::closeLine()
{
int line = m_lineCount - 1;
int first = m_lineFirstItem[line];
if (m_grid)
{
// Rows are placed once all are known (see placeGridRows); until
// then a row's extent is its tallest item.
for (int item = first; item < m_itemCount; item++)
{
int column = item - first;
m_columnContents[column] = std::max(
m_columnContents[column],
m_itemFlowExtent[item]);
m_itemFlowOffset[item] = column < (int)m_columnStarts.size()
? m_columnStarts[column]
: 0.0f;
}
m_lineOpen = false;
return;
}
float lineExtent = m_lineExtent[line];
m_lineUsed[line] = m_lineFlowUsed;
for (int item = first; item < m_itemCount; item++)
{
float slack = lineExtent - m_itemExtent[item];
m_itemLineOffset[item] = m_align == LayoutCrossAlign::start ? 0.0f
: m_align == LayoutCrossAlign::center
? slack / 2.0f
: slack;
}
// Hugging content is as wide as its widest line, known only at end().
if (!m_hugsLines)
{
placeLine(line, m_flowExtent);
}
m_trailing = std::max(m_trailing, m_lineStart[line] + lineExtent);
m_trailingMax[line] = m_trailing;
m_running = m_lineStart[line] + lineExtent + m_gap;
m_lineOpen = false;
}
void VirtualLayout::placeLine(int line, float extent)
{
int first = m_lineFirstItem[line];
int last = line + 1 < (int)m_lineFirstItem.size() && line + 1 < m_lineCount
? m_lineFirstItem[line + 1]
: m_itemCount;
int count = last - first;
float freeSpace = extent - m_lineUsed[line];
float lead = 0.0f;
float between = m_flowGap;
switch (m_justify)
{
case LayoutMainDistribute::start:
break;
case LayoutMainDistribute::center:
lead = freeSpace / 2.0f;
break;
case LayoutMainDistribute::end:
lead = freeSpace;
break;
case LayoutMainDistribute::spaceBetween:
if (count > 1)
{
between += std::max(freeSpace, 0.0f) / (count - 1);
}
break;
}
float flow = m_flowStart + lead;
for (int item = first; item < last; item++)
{
m_itemFlowOffset[item] = flow;
flow += m_itemFlowExtent[item] + between;
}
}
void VirtualLayout::addItem(float extent, float flowExtent)
{
if (!m_wraps)
{
pushLine(m_running, extent);
m_trailing = std::max(m_trailing, m_running + extent);
m_trailingMax.back() = m_trailing;
m_running += extent + m_gap;
m_itemCount++;
return;
}
// Flex wrap breaks before an item that would overflow a non-empty line;
// a grid row breaks once its columns are full.
if (m_lineOpen &&
(m_grid ? m_itemCount - m_lineFirstItem[m_lineCount - 1] >= m_columnCount
: m_lineFlowUsed + m_flowGap + flowExtent > m_flowExtent))
{
closeLine();
}
if (!m_lineOpen)
{
openLine();
m_lineFlowUsed = flowExtent;
}
else
{
m_lineFlowUsed += m_flowGap + flowExtent;
}
int line = m_lineCount - 1;
m_lineExtent[line] = std::max(m_lineExtent[line], extent);
m_itemExtent.push_back(extent);
m_itemFlowExtent.push_back(flowExtent);
m_itemFlowOffset.push_back(0.0f);
m_itemLineOffset.push_back(0.0f);
m_itemCount++;
}
void VirtualLayout::end()
{
if (m_lineOpen)
{
closeLine();
}
if (m_grid)
{
placeGridRows();
}
m_lineFirstItem.push_back(m_itemCount);
m_startSuffixMin.resize(m_lineCount);
// Gaps more negative than a line is long start later lines before
// earlier ones; the lowest start from each line on finds the last line
// starting before a point anyway.
float lowest = std::numeric_limits<float>::infinity();
for (int line = m_lineCount - 1; line >= 0; line--)
{
lowest = std::min(lowest, m_lineStart[line]);
m_startSuffixMin[line] = lowest;
}
if (m_wraps && !m_grid && m_hugsLines)
{
float widest = 0.0f;
for (int line = 0; line < m_lineCount; line++)
{
widest = std::max(widest, m_lineUsed[line]);
}
for (int line = 0; line < m_lineCount; line++)
{
placeLine(line, widest);
}
}
}
float VirtualLayout::itemFlowOffset(int item) const
{
return m_wraps ? m_itemFlowOffset[item] : 0.0f;
}
float VirtualLayout::itemLineOffset(int item) const
{
return m_wraps ? m_itemLineOffset[item] : 0.0f;
}
int VirtualLayout::segmentOf(int item) const
{
// Last segment starting at or before the item; empty segments share a
// start with the next one, so take the last match.
auto it = std::upper_bound(
m_segmentStart.begin(),
m_segmentStart.end(),
item);
return (int)(it - m_segmentStart.begin()) - 1;
}
float VirtualLayout::lineStart(int line) const
{
int wrapped = wrapLine(line);
int cycle = (line - wrapped) / m_lineCount;
return cycle * cycleExtent() + m_lineStart[wrapped];
}
float VirtualLayout::lineExtent(int line) const
{
return m_lineExtent[wrapLine(line)];
}
int VirtualLayout::lineFirstItem(int line) const
{
return m_lineFirstItem[wrapLine(line)];
}
int VirtualLayout::lineLastItem(int line) const
{
return m_lineFirstItem[wrapLine(line) + 1] - 1;
}
int VirtualLayout::lineOfItem(int item) const
{
int lo = 0, hi = m_lineCount - 1;
while (lo < hi)
{
int mid = (lo + hi + 1) >> 1;
if (m_lineFirstItem[mid] <= item)
{
lo = mid;
}
else
{
hi = mid - 1;
}
}
return lo;
}
int VirtualLayout::lastStartBefore(float value) const
{
// The lowest start from each line on only grows, so the first line from
// which every start is at or past value bounds the answer.
int lo = 0, hi = m_lineCount;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (m_startSuffixMin[mid] >= value)
{
hi = mid;
}
else
{
lo = mid + 1;
}
}
return lo - 1;
}
int VirtualLayout::firstTrailingPast(float value, bool inclusive) const
{
int lo = 0, hi = m_lineCount;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
float edge = m_trailingMax[mid];
if (edge > value || (inclusive && edge == value))
{
hi = mid;
}
else
{
lo = mid + 1;
}
}
return lo;
}
VirtualWindow VirtualLayout::window(
float offset,
float viewport,
bool infinite,
int buffer) const
{
int n = m_lineCount;
if (n == 0)
{
return VirtualWindow();
}
// First line still on screen: its trailing edge is past the offset.
int start = firstTrailingPast(offset);
if (start == n)
{
// Only a carousel's trailing gap can be past every line; the next
// cycle's first line is the one coming in.
start = infinite ? n : n - 1;
}
// Last line on screen: the last one starting before the far edge.
float limit = offset + viewport;
int end;
if (!infinite)
{
end = std::max(start, lastStartBefore(limit));
}
else
{
// The latest unwrapped line starting before it, at most a cycle on.
int latest = -1;
float cycle = cycleExtent();
for (int c = 0; c <= 2; c++)
{
int found = lastStartBefore(limit - c * cycle);
if (found >= 0)
{
latest = c * n + found;
}
}
end = std::min(std::max(start, latest), start + n - 1);
}
VirtualWindow window;
window.visibleStart = start;
window.visibleEnd = end;
bufferWindow(window, n, buffer, infinite);
return window;
}
void VirtualLayout::bufferWindow(
VirtualWindow& window,
int count,
int buffer,
bool cycles)
{
int start = window.visibleStart;
int end = window.visibleEnd;
window.start = start;
window.end = end;
if (buffer <= 0)
{
return;
}
int extra = std::max(0, count - (end - start + 1));
int before = std::max(0, std::min(buffer, cycles ? extra : start));
int after = std::max(
0,
std::min(buffer, cycles ? extra - before : count - 1 - end));
window.start = start - before;
window.end = end + after;
}
float VirtualLayout::columnCycleExtent() const
{
int count = columnCount();
if (!m_grid || (int)m_columnStarts.size() < count + 1)
{
return 0.0f;
}
return m_columnStarts[count] - m_columnStarts[0] + m_columnGap;
}
float VirtualLayout::columnStart(int column) const
{
int wrapped = wrapColumn(column);
if (wrapped >= (int)m_columnStarts.size())
{
return 0.0f;
}
int cycle = (column - wrapped) / columnCount();
return cycle * columnCycleExtent() + m_columnStarts[wrapped];
}
VirtualWindow VirtualLayout::columnWindow(
float offset,
float viewport,
int buffer,
bool infinite) const
{
VirtualWindow window;
int count = columnCount();
window.start = window.visibleStart = 0;
window.end = window.visibleEnd = count - 1;
// Column lines bound each column: count + 1 of them, gaps included.
if (!m_grid || (int)m_columnStarts.size() < count + 1)
{
return window;
}
auto columnEnd = [&](int column) {
return column < count - 1 ? m_columnStarts[column + 1] - m_columnGap
: m_columnStarts[count];
};
float cycle = infinite ? columnCycleExtent() : 0.0f;
if (cycle <= 0.0f)
{
// As with lines: the first column whose trailing edge is past the
// offset, through the last that starts before the far edge.
int start = 0;
while (start < count - 1 && columnEnd(start) <= offset)
{
start++;
}
int end = start;
while (end < count - 1 && m_columnStarts[end + 1] < offset + viewport)
{
end++;
}
window.visibleStart = start;
window.visibleEnd = end;
bufferWindow(window, count, buffer, false);
return window;
}
// Find the first column within the offset's cycle, then count on in
// unwrapped columns; past the last column the next cycle's first is in.
int k = (int)std::floor((offset - m_columnStarts[0]) / cycle);
float local = offset - k * cycle;
int start = 0;
while (start < count && columnEnd(start) <= local)
{
start++;
}
start += k * count;
// At most one cycle: each item is realized once.
int end = start;
while (end < start + count - 1 && columnStart(end + 1) < offset + viewport)
{
end++;
}
window.visibleStart = start;
window.visibleEnd = end;
bufferWindow(window, count, buffer, true);
return window;
}