Files
pico-launcher/arm9/source/gui/views/RecyclerView.cpp
2025-11-25 17:41:31 +01:00

549 lines
17 KiB
C++

#include "common.h"
#include <algorithm>
#include "gui/materialDesign.h"
#include "gui/input/InputProvider.h"
#include "RecyclerView.h"
RecyclerView::RecyclerView(int x, int y, int width, int height, Mode mode)
: _width(width), _height(height), _mode(mode), _rows(0), _columns(0)
, _viewPoolFreeCount(0), _viewPoolTotalCount(0)
, _xOffset(0), _yOffset(0), _xPadding(0), _yPadding(0)
, _xSpacing(0), _ySpacing(0), _itemWidth(0), _itemHeight(0)
, _itemCount(0), _selectedItem(nullptr)
, _curRangeStart(0), _curRangeLength(0)
{
_position.x = x;
_position.y = y;
}
RecyclerView::~RecyclerView()
{
if (_adapter)
{
for (u32 i = 0; i < _viewPoolTotalCount; i++)
{
_adapter->DestroyView(_viewPool[i].view);
}
}
}
void RecyclerView::SetAdapter(const RecyclerAdapter* adapter, int initialSelectedIndex)
{
if (_adapter)
{
_selectedItem = nullptr;
for (u32 i = 0; i < _viewPoolTotalCount; i++)
{
_adapter->DestroyView(_viewPool[i].view);
}
_viewPool.reset();
_viewPoolFreeCount = 0;
_viewPoolTotalCount = 0;
}
_adapter = adapter;
_adapter->GetViewSize(_itemWidth, _itemHeight);
_itemCount = _adapter->GetItemCount();
if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid)
{
if (_mode == Mode::HorizontalList)
{
_rows = 1;
}
else
{
_rows = std::max(1, _height / _itemHeight);
}
_columns = (_width + _xSpacing + _itemWidth - 1) / (_xSpacing + _itemWidth) + 1;
_viewPoolTotalCount = _rows * (_columns + 1) + 1;
}
else
{
if (_mode == Mode::VerticalList)
{
_columns = 1;
}
else
{
_columns = std::max(1, _width / _itemWidth);
}
_rows = (_height + _ySpacing + _itemHeight - 1) / (_ySpacing + _itemHeight) + 1;
_viewPoolTotalCount = (_rows + 1) * _columns + 1;
}
LOG_DEBUG("_rows: %d\n", _rows);
LOG_DEBUG("_columns: %d\n", _columns);
LOG_DEBUG("_viewPoolTotalCount: %d\n", _viewPoolTotalCount);
_viewPool = std::unique_ptr<ViewPoolEntry[]>(new ViewPoolEntry[_viewPoolTotalCount]);
for (u32 i = 0; i < _viewPoolTotalCount; i++)
{
_viewPool[i].view = _adapter->CreateView();
_viewPool[i].view->SetParent(this);
_viewPool[i].itemIdx = -1;
}
_viewPoolFreeCount = _viewPoolTotalCount;
if (initialSelectedIndex < 0 || initialSelectedIndex >= (int)_itemCount)
{
initialSelectedIndex = 0;
}
EnsureVisible(initialSelectedIndex, false);
if (_itemCount > 0)
{
SetSelectedItem(initialSelectedIndex);
}
}
void RecyclerView::InitVram(const VramContext& vramContext)
{
for (u32 i = 0; i < _viewPoolTotalCount; i++)
{
_viewPool[i].view->InitVram(vramContext);
}
}
void RecyclerView::Update()
{
if (_itemCount == 0)
{
return;
}
if (!_scrollOffsetAnimator.IsFinished())
{
_scrollOffsetAnimator.Update();
}
int rangeStartIndex = 0;
int rangeEndIndex = 0;
if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid)
{
_xOffset = _scrollOffsetAnimator.GetValue();
rangeStartIndex = ((-_xOffset - _xPadding) / (_xSpacing + _itemWidth) - 1) * _rows;
rangeEndIndex = rangeStartIndex + (_columns + 1) * _rows;
}
else
{
_yOffset = _scrollOffsetAnimator.GetValue();
rangeStartIndex = ((-_yOffset - _yPadding) / (_ySpacing + _itemHeight) - 1) * _columns;
rangeEndIndex = rangeStartIndex + (_rows + 1) * _columns;
}
rangeStartIndex = std::clamp(rangeStartIndex, 0, (int)_itemCount - 1);
rangeEndIndex = std::clamp(rangeEndIndex, 0, (int)_itemCount);
if (_curRangeStart != rangeStartIndex || _curRangeLength != rangeEndIndex - rangeStartIndex)
{
LOG_DEBUG("range: %d - %d\n", rangeStartIndex, rangeEndIndex - 1);
if (_curRangeLength != 0)
{
if (_curRangeStart < rangeStartIndex)
{
ReleaseRange(_curRangeStart, rangeStartIndex);
}
if (rangeEndIndex < _curRangeStart + _curRangeLength)
{
ReleaseRange(rangeEndIndex, _curRangeStart + _curRangeLength);
}
}
BindRange(rangeStartIndex, rangeEndIndex);
_curRangeStart = rangeStartIndex;
_curRangeLength = rangeEndIndex - rangeStartIndex;
}
for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++)
{
UpdatePosition(_viewPool[i]);
_viewPool[i].view->Update();
}
}
void RecyclerView::Draw(GraphicsContext& graphicsContext)
{
for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++)
{
_viewPool[i].view->Draw(graphicsContext);
}
}
void RecyclerView::VBlank()
{
for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++)
{
_viewPool[i].view->VBlank();
}
}
View* RecyclerView::MoveFocus(View* currentFocus, FocusMoveDirection direction, View* source)
{
if (_itemCount == 0)
{
return View::MoveFocus(currentFocus, direction, this);
}
if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid)
{
return MoveFocusHorizontal(currentFocus, direction, source);
}
else
{
return MoveFocusVertical(currentFocus, direction, source);
}
}
View* RecyclerView::MoveFocusHorizontal(View* currentFocus, FocusMoveDirection direction, View* source)
{
if (!_selectedItem || currentFocus != _selectedItem->view)
{
// incoming focus
if (direction != FocusMoveDirection::Down)
{
return nullptr;
}
int idx = (-_xOffset + currentFocus->GetPosition().x - _xPadding + ((_xSpacing + _itemWidth) >> 1)) / (_xSpacing + _itemWidth) * _rows;
SetSelectedItem(std::clamp(idx, 0, ((int)_itemCount - 1) / _rows * _rows));
return _selectedItem != nullptr ? _selectedItem->view : this;
}
int row = _selectedItem->itemIdx % _rows;
if ((row == 0 && direction == FocusMoveDirection::Up) ||
(row == _rows - 1 && direction == FocusMoveDirection::Down) ||
(_selectedItem->itemIdx < _rows && direction == FocusMoveDirection::Left) ||
(_selectedItem->itemIdx / _rows >= (int)(_itemCount - 1) / _rows && direction == FocusMoveDirection::Right))
{
return View::MoveFocus(currentFocus, direction, this);
}
if (direction == FocusMoveDirection::Left)
{
int idx = _selectedItem->itemIdx;
if (idx - _rows >= 0)
{
idx -= _rows;
}
SetSelectedItem(idx);
}
else if (direction == FocusMoveDirection::Right)
{
int idx = _selectedItem->itemIdx + _rows;
idx = std::min(idx, (int)_itemCount - 1);
SetSelectedItem(idx);
}
else if (direction == FocusMoveDirection::Up)
{
int idx = (_selectedItem->itemIdx / _rows * _rows) + std::clamp((_selectedItem->itemIdx % _rows) - 1, 0, _rows - 1);
SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1));
}
else if (direction == FocusMoveDirection::Down)
{
int idx = (_selectedItem->itemIdx / _rows * _rows) + std::clamp((_selectedItem->itemIdx % _rows) + 1, 0, _rows - 1);
SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1));
}
return _selectedItem != nullptr ? _selectedItem->view : this;
}
View* RecyclerView::MoveFocusVertical(View* currentFocus, FocusMoveDirection direction, View* source)
{
if (!_selectedItem || currentFocus != _selectedItem->view)
{
// incoming focus
if (direction != FocusMoveDirection::Right)
{
return nullptr;
}
int idx = (-_yOffset + currentFocus->GetPosition().y - _yPadding + ((_ySpacing + _itemHeight) >> 1)) / (_ySpacing + _itemHeight) * _columns;
SetSelectedItem(std::clamp(idx, 0, ((int)_itemCount - 1) / _columns * _columns));
return _selectedItem != nullptr ? _selectedItem->view : this;
}
int column = _selectedItem->itemIdx % _columns;
if ((column == 0 && direction == FocusMoveDirection::Left) ||
(column == _columns - 1 && direction == FocusMoveDirection::Right) ||
(_selectedItem->itemIdx < _columns && direction == FocusMoveDirection::Up) ||
(_selectedItem->itemIdx / _columns >= (int)(_itemCount - 1) / _columns && direction == FocusMoveDirection::Down))
{
return View::MoveFocus(currentFocus, direction, this);
}
if (direction == FocusMoveDirection::Up)
{
int idx = _selectedItem->itemIdx;
if (idx - _columns >= 0)
{
idx -= _columns;
}
SetSelectedItem(idx);
}
else if (direction == FocusMoveDirection::Down)
{
int idx = _selectedItem->itemIdx + _columns;
idx = std::min(idx, (int)_itemCount - 1);
SetSelectedItem(idx);
}
else if (direction == FocusMoveDirection::Left)
{
int idx = (_selectedItem->itemIdx / _columns * _columns) + std::clamp((_selectedItem->itemIdx % _columns) - 1, 0, _columns - 1);
SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1));
}
else if (direction == FocusMoveDirection::Right)
{
int idx = (_selectedItem->itemIdx / _columns * _columns) + std::clamp((_selectedItem->itemIdx % _columns) + 1, 0, _columns - 1);
SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1));
}
return _selectedItem != nullptr ? _selectedItem->view : this;
}
bool RecyclerView::HandleInput(const InputProvider& inputProvider, FocusManager& focusManager)
{
if (inputProvider.Triggered(InputKey::L | InputKey::R))
{
int direction = inputProvider.Triggered(InputKey::L) ? 1 : -1;
int selected = _selectedItem->itemIdx;
if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid)
{
int visibleColumns = _width / (_itemWidth + _xSpacing);
SetScrollOffset(_scrollOffsetAnimator.GetTargetValue() + direction * visibleColumns * (_itemWidth + _xSpacing), true);
int row = selected % _rows;
selected = std::clamp(selected - direction * visibleColumns * _rows, 0, (int)_itemCount - 1);
selected = selected / _rows * _rows + row; // try to stay in the same row
selected = std::clamp(selected, 0, (int)_itemCount - 1); // but clamp to the last item
}
else
{
int visibleRows = _height / (_itemHeight + _ySpacing);
SetScrollOffset(_scrollOffsetAnimator.GetTargetValue() + direction * visibleRows * (_itemHeight + _ySpacing), true);
int column = selected % _columns;
selected = std::clamp(selected - direction * visibleRows * _columns, 0, (int)_itemCount - 1);
selected = selected / _columns * _columns + column; // try to stay in the same column
selected = std::clamp(selected, 0, (int)_itemCount - 1); // but clamp to the last item
}
focusManager.Unfocus();
SetSelectedItem(selected);
focusManager.Focus(_selectedItem->view);
return true;
}
return View::HandleInput(inputProvider, focusManager);
}
Point RecyclerView::GetItemPosition(int itemIdx)
{
int x = 0;
int y = 0;
switch (_mode)
{
case Mode::HorizontalList:
{
x = _xPadding + itemIdx * (_xSpacing + _itemWidth);
y = _yPadding;
break;
}
case Mode::HorizontalGrid:
{
x = _xPadding + (itemIdx / _rows) * (_xSpacing + _itemWidth);
y = _yPadding + (itemIdx % _rows) * (_ySpacing + _itemHeight);
break;
}
case Mode::VerticalList:
{
x = _xPadding;
y = _yPadding + itemIdx * (_ySpacing + _itemHeight);
break;
}
case Mode::VerticalGrid:
{
x = _xPadding + (itemIdx % _columns) * (_xSpacing + _itemWidth);
y = _yPadding + (itemIdx / _columns) * (_ySpacing + _itemHeight);
break;
}
}
return Point(x, y);
}
void RecyclerView::UpdatePosition(ViewPoolEntry& viewPoolEntry)
{
auto itemPosition = GetItemPosition(viewPoolEntry.itemIdx);
viewPoolEntry.view->SetPosition(
_position.x + _xOffset + itemPosition.x,
_position.y + _yOffset + itemPosition.y);
}
RecyclerView::ViewPoolEntry* RecyclerView::GetViewPoolEntryByItemIndex(int itemIdx)
{
for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++)
{
if (_viewPool[i].itemIdx == (int)itemIdx)
{
return &_viewPool[i];
}
}
return nullptr;
}
RecyclerView::ViewPoolEntry* RecyclerView::BindViewPoolEntry(int itemIdx)
{
if (_viewPoolFreeCount == 0)
{
LOG_FATAL("No free view pool entries left\n");
while (true);
return nullptr;
}
auto& entry = _viewPool[_viewPoolFreeCount - 1];
_viewPoolFreeCount--;
entry.itemIdx = itemIdx;
_adapter->BindView(entry.view, itemIdx);
return &entry;
}
void RecyclerView::BindRange(int start, int end)
{
for (int i = start; i < end; i++)
{
if ((_selectedItem && _selectedItem->itemIdx == i) ||
(_curRangeLength != 0 && _curRangeStart <= i && i < _curRangeStart + _curRangeLength))
{
continue;
}
BindViewPoolEntry(i);
}
}
void RecyclerView::ReleaseViewPoolEntry(int itemIdx)
{
for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++)
{
if (_viewPool[i].itemIdx == (int)itemIdx)
{
_adapter->ReleaseView(_viewPool[i].view, _viewPool[i].itemIdx);
_viewPool[i].itemIdx = -1;
std::swap(_viewPool[i], _viewPool[_viewPoolFreeCount]);
if (_selectedItem == &_viewPool[_viewPoolFreeCount])
{
_selectedItem = &_viewPool[i];
}
_viewPoolFreeCount++;
return;
}
}
}
void RecyclerView::ReleaseRange(int start, int end)
{
for (int i = start; i < end; i++)
{
if (_selectedItem && _selectedItem->itemIdx == i)
{
continue;
}
ReleaseViewPoolEntry(i);
}
}
void RecyclerView::SetSelectedItem(int itemIdx)
{
if (_selectedItem)
{
if (_selectedItem->itemIdx == itemIdx)
{
return;
}
if (_selectedItem->itemIdx < _curRangeStart ||
_selectedItem->itemIdx >= _curRangeStart + _curRangeLength)
{
ReleaseViewPoolEntry(_selectedItem->itemIdx);
}
_selectedItem = nullptr;
}
if (itemIdx < 0 || itemIdx >= (int)_itemCount)
{
return;
}
if (itemIdx >= _curRangeStart &&
itemIdx < _curRangeStart + _curRangeLength)
{
_selectedItem = GetViewPoolEntryByItemIndex(itemIdx);
}
else
{
_selectedItem = BindViewPoolEntry(itemIdx);
}
EnsureVisible(itemIdx, true);
}
int RecyclerView::GetMaxScrollOffset()
{
if (_mode == Mode::HorizontalGrid || _mode == Mode::HorizontalList)
{
int totalColumns = (_itemCount + _rows - 1) / _rows;
int contentWidth = totalColumns * _itemWidth + (totalColumns - 1) * _xSpacing + _xPadding * 2;
return std::min(0, _width - contentWidth);
}
else
{
int totalRows = ((_itemCount + _columns - 1) / _columns);
int contentHeight = totalRows * _itemHeight + (totalRows - 1) * _ySpacing + _yPadding * 2;
return std::min(0, _height - contentHeight);
}
}
void RecyclerView::SetScrollOffset(int offset, bool animate)
{
offset = std::clamp(offset, GetMaxScrollOffset(), 0);
if (!animate)
{
_scrollOffsetAnimator = Animator<int>(offset);
}
else
{
if (std::abs(offset - _scrollOffsetAnimator.GetTargetValue()) <= 128)
{
_scrollOffsetAnimator.Goto(offset,
md::sys::motion::duration::medium1, &md::sys::motion::easing::emphasized);
}
else
{
_scrollOffsetAnimator.Goto(offset,
md::sys::motion::duration::long2, &md::sys::motion::easing::standard);
}
}
}
void RecyclerView::EnsureVisible(int itemIdx, bool animate)
{
const auto itemPosition = GetItemPosition(itemIdx);
int minItemScrollOffset;
int maxItemScollOffset;
if (_mode == Mode::HorizontalGrid || _mode == Mode::HorizontalList)
{
minItemScrollOffset = -itemPosition.x + _xPadding;
maxItemScollOffset = -itemPosition.x + _width - _itemWidth - _xPadding;
}
else
{
minItemScrollOffset = -itemPosition.y + _yPadding;
maxItemScollOffset = -itemPosition.y + _height - _itemHeight - _yPadding;
}
int targetScrollOffset = std::clamp(_scrollOffsetAnimator.GetTargetValue(), minItemScrollOffset, maxItemScollOffset);
if (targetScrollOffset != _scrollOffsetAnimator.GetTargetValue())
{
SetScrollOffset(targetScrollOffset, animate);
}
}