Initial commit

This commit is contained in:
Gericom
2025-11-22 17:21:45 +01:00
commit 5d6f67c612
517 changed files with 63025 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
#pragma once
/// @brief Enum representing a type of dialog.
enum class DialogType
{
// Dialog,
BottomSheet
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "ViewContainer.h"
#include "DialogType.h"
/// @brief View meant to be displayed as a dialog on top of other content.
class DialogView : public ViewContainer
{
public:
/// @brief Gets the type of dialog.
/// @return The type of dialog.
virtual DialogType GetDialogType() const = 0;
/// @brief Moves the focus to this dialog.
/// @param focusManager The focus manager to use.
virtual void Focus(FocusManager& focusManager) = 0;
/// @brief Gets the area of the screen that will be fully covered by
/// this dialog for the purpose of culling views behind it.
/// @return A rectangle that is fully covered by the dialog.
virtual Rectangle GetFullyCoveredArea() const = 0;
};

View File

@@ -0,0 +1,68 @@
#include "common.h"
#include "gui/OamManager.h"
#include "gui/OamBuilder.h"
#include "gui/IVramManager.h"
#include "gui/VramContext.h"
#include "gui/GraphicsContext.h"
#include "core/StringUtil.h"
#include "gui/palette/GradientPalette.h"
#include "Label2DView.h"
void Label2DView::InitVram(const VramContext& vramContext)
{
const auto objVramManager = vramContext.GetObjVramManager();
if (objVramManager)
{
_vramOffset = objVramManager->Alloc((_actualWidth * _actualHeight) >> 1);
_vramAddress = objVramManager->GetVramAddress(_vramOffset);
}
}
void Label2DView::UpdateTileBuffer()
{
LabelView::UpdateTileBuffer();
_tileBufferUpdated = true;
}
void Label2DView::Draw(GraphicsContext& graphicsContext)
{
if (!graphicsContext.IsVisible(GetBounds()))
return;
u32 hCellCount = _actualWidth >> 5;
u32 vCellCount = _actualHeight >> 4;
u32 cellCount = hCellCount * vCellCount;
auto oams = graphicsContext.GetOamManager().AllocOams(cellCount);
int xOffset = _position.x;
if (_hAlign == Alignment::Center)
xOffset += ((int)_width - (int)_stringWidth) / 2;
else if (_hAlign == Alignment::End)
xOffset += (int)_width - (int)_stringWidth;
u32 paletteRow = graphicsContext.GetPaletteManager().AllocRow(
GradientPalette(_backgroundColor, _foregroundColor), _position.y, _position.y + _height);
u32 i = 0;
for (u32 y = 0; y < vCellCount; y++)
{
for (u32 x = 0; x < hCellCount; x++)
{
OamBuilder::OamWithSize<32, 16>(
xOffset + x * 32,
_position.y + y * 16,
(_vramOffset + i * 256) >> 7)
.WithPalette16(paletteRow)
.WithPriority(graphicsContext.GetPriority())
.Build(oams[i]);
i++;
}
}
}
void Label2DView::VBlank()
{
if (_tileBufferUpdated)
{
memcpy((void*)_vramAddress, _tileBuffer.get(), _tileBufferSize);
_tileBufferUpdated = false;
_stringWidth = _newStringWidth;
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "LabelView.h"
class Label2DView : public LabelView
{
public:
Label2DView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font)
: LabelView(width, height, maxStringLength, font, false) { }
void InitVram(const VramContext& vramContext) override;
void Draw(GraphicsContext& graphicsContext) override;
void VBlank() override;
private:
void UpdateTileBuffer() override;
bool _tileBufferUpdated = false;
u32 _vramOffset = 0;
vu16* _vramAddress = nullptr;
};

View File

@@ -0,0 +1,58 @@
#include "common.h"
#include <bit>
#include "gui/IVramManager.h"
#include "gui/Gx.h"
#include "gui/VramContext.h"
#include "gui/GraphicsContext.h"
#include "core/StringUtil.h"
#include "Label3DView.h"
Label3DView::Label3DView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font,
VBlankTextureLoader* vblankTextureLoader)
: LabelView(width, height, maxStringLength, font, true)
, _vblankTextureLoader(vblankTextureLoader) { }
void Label3DView::InitVram(const VramContext& vramContext)
{
const auto texVramManager = vramContext.GetTexVramManager();
if (texVramManager)
{
_texVramOffset = texVramManager->Alloc(_tileBufferSize);
}
}
void Label3DView::UpdateTileBuffer()
{
_vblankTextureLoader->CancelLoad(_textureLoadRequest);
LabelView::UpdateTileBuffer();
_textureLoadRequest = VBlankTextureLoadRequest(_tileBuffer.get(), _tileBufferSize,
_texVramOffset, nullptr, 0, 0, nullptr, nullptr);
_vblankTextureLoader->RequestLoad(_textureLoadRequest);
}
void Label3DView::Draw(GraphicsContext& graphicsContext)
{
if (!graphicsContext.IsVisible(GetBounds()))
return;
Gx::MtxIdentity();
Gx::PolygonAttr(GX_LIGHTMASK_NONE, GX_POLYGON_MODE_MODULATE, GX_DISPLAY_MODE_FRONT,
false, false, false, GX_DEPTH_FUNC_LESS, false, 31, graphicsContext.GetPolygonId());
Gx::TexImageParam(_texVramOffset >> 3, false, false, false, false, (GxTexSize)(std::bit_width(_actualWidth) - 4),
GX_TEXSIZE_1024, GX_TEXFMT_A5I3, false, GX_TEXGEN_NONE);
graphicsContext.GetRgb6Palette()->ApplyColor(Rgb<6, 6, 6>(_foregroundColor));
Gx::Begin(GX_PRIMITIVE_QUAD);
Gx::TexCoord(0, 0);
REG_GX_VTX_16 = GX_VTX_PACK(_position.x << 6, _position.y << 3);
REG_GX_VTX_16 = (200) << 6;
Gx::TexCoord(0, (int)_height);
REG_GX_VTX_16 = GX_VTX_PACK(_position.x << 6, (_position.y + _height) << 3);
REG_GX_VTX_16 = (200) << 6;
Gx::TexCoord((int)_width, (int)_height);
REG_GX_VTX_16 = GX_VTX_PACK((_position.x + _width) << 6, (_position.y + _height) << 3);
REG_GX_VTX_16 = (200) << 6;
Gx::TexCoord((int)_width, 0);
REG_GX_VTX_16 = GX_VTX_PACK((_position.x + _width) << 6, _position.y << 3);
REG_GX_VTX_16 = (200) << 6;
Gx::End();
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "LabelView.h"
#include "gui/VBlankTextureLoader.h"
class alignas(32) Label3DView : public LabelView
{
public:
Label3DView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font,
VBlankTextureLoader* vblankTextureLoader);
void InitVram(const VramContext& vramContext) override;
void Draw(GraphicsContext& graphicsContext) override;
private:
void UpdateTileBuffer() override;
u32 _texVramOffset = 0;
VBlankTextureLoader* _vblankTextureLoader;
VBlankTextureLoadRequest _textureLoadRequest;
};

View File

@@ -0,0 +1,121 @@
#include "common.h"
#include <bit>
#include "gui/OamManager.h"
#include "gui/OamBuilder.h"
#include "gui/IVramManager.h"
#include "gui/VramContext.h"
#include "gui/GraphicsContext.h"
#include "core/StringUtil.h"
#include "gui/palette/GradientPalette.h"
#include "LabelView.h"
LabelView::LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font, bool a5i3)
: _width(width), _height(height)
, _maxStringLength(maxStringLength), _font(font)
, _hAlign(Alignment::Start)
, _stringWidth(0), _newStringWidth(0), _a5i3(a5i3)
{
_textBuffer = std::make_unique_for_overwrite<char16_t[]>(_maxStringLength + 1);
_textBuffer[0] = 0;
if (_a5i3)
{
_actualWidth = std::bit_ceil(width);
_actualHeight = height;
_tileBufferSize = _actualWidth * _actualHeight;
}
else
{
_actualWidth = (width + 31) & ~31;
_actualHeight = (height + 15) & ~15;
_tileBufferSize = (_actualWidth * _actualHeight) >> 1;
}
_tileBuffer = std::make_unique_for_overwrite<u8[]>(_tileBufferSize);
SetText(u"");
}
void LabelView::SetTextBuffer(const char* text)
{
StringUtil::Copy(_textBuffer.get(), text, _maxStringLength + 1);
}
void LabelView::SetTextBuffer(const char16_t* text)
{
StringUtil::Copy(_textBuffer.get(), text, _maxStringLength + 1);
}
void LabelView::SetTextBuffer(const char16_t* text, u32 length)
{
u32 copyLength = std::min(length, _maxStringLength);
if (copyLength > 0)
memcpy(_textBuffer.get(), text, copyLength * 2);
_textBuffer[copyLength] = 0;
}
void LabelView::UpdateTileBuffer()
{
memset(_tileBuffer.get(), 0, _tileBufferSize);
if (_textBuffer[0] != 0)
{
nft2_string_render_params_t renderParams;
renderParams.x = 0;
renderParams.y = 0;
renderParams.width = _width;
renderParams.height = _height;
renderParams.a5i3 = _a5i3;
if (_ellipsis)
nft2_renderStringEllipsis(_font, _textBuffer.get(), _tileBuffer.get(), _actualWidth, &renderParams, u" ... ");
else
nft2_renderString(_font, _textBuffer.get(), _tileBuffer.get(), _actualWidth, &renderParams);
_newStringWidth = renderParams.textWidth;
}
else
{
_newStringWidth = 0;
}
}
QueueTask<void> LabelView::UpdateTileBufferAsync(TaskQueueBase* taskQueue)
{
return taskQueue->Enqueue([this] (const vu8& canceled)
{
UpdateTileBuffer();
return TaskResult<void>::Completed();
});
}
void LabelView::SetText(const char* text)
{
SetTextBuffer(text);
UpdateTileBuffer();
}
void LabelView::SetText(const char16_t* text)
{
SetTextBuffer(text);
UpdateTileBuffer();
}
void LabelView::SetText(const char16_t* text, u32 length)
{
SetTextBuffer(text, length);
UpdateTileBuffer();
}
QueueTask<void> LabelView::SetTextAsync(TaskQueueBase* taskQueue, const char* text)
{
SetTextBuffer(text);
return UpdateTileBufferAsync(taskQueue);
}
QueueTask<void> LabelView::SetTextAsync(TaskQueueBase* taskQueue, const char16_t* text)
{
SetTextBuffer(text);
return UpdateTileBufferAsync(taskQueue);
}
QueueTask<void> LabelView::SetTextAsync(TaskQueueBase* taskQueue, const char16_t* text, u32 length)
{
SetTextBuffer(text, length);
return UpdateTileBufferAsync(taskQueue);
}

View File

@@ -0,0 +1,74 @@
#pragma once
#include "common.h"
#include <memory>
#include "gui/font/nitroFont2.h"
#include "core/task/TaskQueue.h"
#include "gui/Alignment.h"
#include "gui/materialDesign.h"
#include "gui/views/View.h"
#include "core/math/Rgb.h"
class StackVramManager;
class MaterialGraphicsContext;
class LabelView : public View
{
public:
void SetText(const char* text);
void SetText(const char16_t* text);
void SetText(const char16_t* text, u32 length);
QueueTask<void> SetTextAsync(TaskQueueBase* taskQueue, const char* text);
QueueTask<void> SetTextAsync(TaskQueueBase* taskQueue, const char16_t* text);
QueueTask<void> SetTextAsync(TaskQueueBase* taskQueue, const char16_t* text, u32 length);
void SetHorizontalAlignment(Alignment alignment)
{
_hAlign = alignment;
}
u32 GetStringWidth() const { return _stringWidth; }
void SetBackgroundColor(const Rgb<8, 8, 8>& backgroundColor)
{
_backgroundColor = backgroundColor;
}
void SetForegroundColor(const Rgb<8, 8, 8>& foregroundColor)
{
_foregroundColor = foregroundColor;
}
Rectangle GetBounds() const override
{
return Rectangle(_position, _width, _height);
}
void SetEllipsis(bool ellipsis) { _ellipsis = ellipsis; }
protected:
u32 _width;
u32 _height;
u32 _actualWidth;
u32 _actualHeight;
u32 _maxStringLength;
std::unique_ptr<char16_t[]> _textBuffer;
u32 _tileBufferSize;
std::unique_ptr<u8[]> _tileBuffer;
const nft2_header_t* _font;
Alignment _hAlign;
u32 _stringWidth;
u32 _newStringWidth;
Rgb<8, 8, 8> _backgroundColor;
Rgb<8, 8, 8> _foregroundColor;
int _paletteRow = -1;
bool _ellipsis = false;
bool _a5i3;
void SetTextBuffer(const char* text);
void SetTextBuffer(const char16_t* text);
void SetTextBuffer(const char16_t* text, u32 length);
virtual void UpdateTileBuffer();
QueueTask<void> UpdateTileBufferAsync(TaskQueueBase* taskQueue);
LabelView(u32 width, u32 height, u32 maxStringLength, const nft2_header_t* font, bool a5i3);
};

View File

@@ -0,0 +1,38 @@
#pragma once
class View;
/// @brief Adapter class providing the content to show in a \see RecyclerView.
/// All items in the adapter use an identical view that can be reused to
/// show another item of this adapter when it goes off screen.
class RecyclerAdapter
{
public:
virtual ~RecyclerAdapter() { }
/// @brief Returns the total number of items in this adapter.
/// @return The total number of items in this adapter.
virtual u32 GetItemCount() const = 0;
/// @brief Returns the size of each view of this adapter.
/// @param width Returns the with of the view.
/// @param height Returns the height of the view.
virtual void GetViewSize(int& width, int& height) const = 0;
/// @brief Creates and returns a view for this adapter.
/// @return The created view.
virtual View* CreateView() const = 0;
/// @brief Destroys a \p view for this adapter that was previously created with CreateView.
/// @param view The view to destroy.
virtual void DestroyView(View* view) const = 0;
/// @brief Binds the given \p view to the item at the given \p index.
/// @param view The view to bind.
/// @param index The item index to bind to.
virtual void BindView(View* view, int index) const = 0;
/// @brief Releases a \p view that was previously bound with BindView, such that it can be reused.
/// @param view The view to release.
/// @param index The item index that was bound to the view.
virtual void ReleaseView(View* view, int index) const = 0;
};

View File

@@ -0,0 +1,549 @@
#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);
}
}

View File

@@ -0,0 +1,112 @@
#pragma once
#include <memory>
#include "View.h"
#include "RecyclerAdapter.h"
#include "gui/FocusManager.h"
#include "animation/Animator.h"
#include "RecyclerViewBase.h"
class RecyclerView : public RecyclerViewBase
{
public:
enum class Mode
{
/// @brief A single row that grows horizontally.
HorizontalList,
/// @brief A multi-row grid that grows horizontally.
HorizontalGrid,
/// @brief A single column that grows vertically.
VerticalList,
/// @brief A multi-column grid that grows vertically.
VerticalGrid
};
RecyclerView(int x, int y, int width, int height, Mode mode);
~RecyclerView();
void SetAdapter(const RecyclerAdapter* adapter, int initialSelectedIndex = 0) override;
void InitVram(const VramContext& vramContext) override;
void Update() override;
void Draw(GraphicsContext& graphicsContext) override;
void VBlank() override;
Rectangle GetBounds() const override
{
return Rectangle(_position, _width, _height);
}
View* MoveFocus(View* currentFocus, FocusMoveDirection direction, View* source) override;
bool HandleInput(const InputProvider& inputProvider, FocusManager& focusManager) override;
void Focus(FocusManager& focusManager) override
{
if (_selectedItem)
focusManager.Focus(_selectedItem->view);
else
focusManager.Focus(this);
}
int GetSelectedItem() const override
{
return _selectedItem ? _selectedItem->itemIdx : -1;
}
constexpr Mode GetMode() const { return _mode; }
void SetPadding(int x, int y)
{
_xPadding = x;
_yPadding = y;
}
void SetItemSpacing(int x, int y)
{
_xSpacing = x;
_ySpacing = y;
}
private:
struct ViewPoolEntry
{
View* view;
int itemIdx;
};
int _width;
int _height;
Mode _mode;
int _rows;
int _columns;
std::unique_ptr<ViewPoolEntry[]> _viewPool;
u32 _viewPoolFreeCount;
u32 _viewPoolTotalCount;
int _xOffset;
int _yOffset;
int _xPadding;
int _yPadding;
int _xSpacing;
int _ySpacing;
int _itemWidth;
int _itemHeight;
u32 _itemCount;
ViewPoolEntry* _selectedItem;
int _curRangeStart;
int _curRangeLength;
Animator<int> _scrollOffsetAnimator;
void UpdatePosition(ViewPoolEntry& viewPoolEntry);
ViewPoolEntry* GetViewPoolEntryByItemIndex(int itemIdx);
void BindRange(int start, int end);
ViewPoolEntry* BindViewPoolEntry(int itemIdx);
void ReleaseRange(int start, int end);
void ReleaseViewPoolEntry(int itemIdx);
void SetSelectedItem(int itemIdx);
int GetMaxScrollOffset();
void SetScrollOffset(int offset, bool animate);
void EnsureVisible(int itemIdx, bool animate);
Point GetItemPosition(int itemIdx);
View* MoveFocusHorizontal(View* currentFocus, FocusMoveDirection direction, View* source);
View* MoveFocusVertical(View* currentFocus, FocusMoveDirection direction, View* source);
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "View.h"
#include "RecyclerAdapter.h"
#include "gui/FocusManager.h"
/// @brief Abstract base class for a recycler view that displays a possibly large collection of items
/// provided by an adapter in an efficient way.
class RecyclerViewBase : public View
{
public:
virtual void SetAdapter(const RecyclerAdapter* adapter, int initialSelectedIndex = 0) = 0;
virtual void Focus(FocusManager& focusManager) = 0;
virtual int GetSelectedItem() const = 0;
protected:
const RecyclerAdapter* _adapter = nullptr;
};

View File

@@ -0,0 +1,106 @@
#pragma once
#include "core/LinkedListLink.h"
#include "core/math/Point.h"
#include "core/math/Rectangle.h"
#include "../FocusManager.h"
#include "../FocusMoveDirection.h"
class GraphicsContext;
class VramContext;
class InputProvider;
/// @brief Base class for views.
class View
{
public:
/// @brief Link used for views that contain other views.
LinkedListLink listLink;
virtual ~View() { }
/// @brief Initializes the vram for the view.
/// @param vramContext The vram context to use.
virtual void InitVram(const VramContext& vramContext) { }
/// @brief Updates the view.
virtual void Update() { }
/// @brief Draws the view.
/// @param graphicsContext The graphics context to use.
virtual void Draw(GraphicsContext& graphicsContext) = 0;
/// @brief Performs vblank processes for the view.
virtual void VBlank() { }
/// @brief Moves the focus from the currentFocus view into the given direction.
/// @param currentFocus The currently focused view.
/// @param direction The direction to move the focus in.
/// @param source The view that requested this view to move focus.
/// @return The newly focused view, or null if the focus didn't change.
virtual View* MoveFocus(View* currentFocus, FocusMoveDirection direction, View* source)
{
if (_parent && _parent != source)
return _parent->MoveFocus(currentFocus, direction, this);
return nullptr;
}
/// @brief Handles input for the view.
/// @param inputProvider The input provider.
/// @param focusManager The focus manager.
/// @return True if input was handled, or false otherwise.
virtual bool HandleInput(const InputProvider& inputProvider, FocusManager& focusManager)
{
if (_parent)
return _parent->HandleInput(inputProvider, focusManager);
return false;
}
/// @brief Gets the bounds of the view.
/// @return The bounds of the view.
virtual Rectangle GetBounds() const = 0;
/// @brief Sets the position of the view.
/// @param position The position to set.
void SetPosition(const Point& position) { _position = position; }
/// @brief Sets the position of the view.
/// @param x The x position to set.
/// @param y The y position to set.
void SetPosition(int x, int y)
{
_position.x = x;
_position.y = y;
}
/// @brief Gets the position of the view.
/// @return The position of the view.
const Point& GetPosition() const { return _position; }
/// @brief Sets the parent of the view.
/// @param parent The parent to set.
void SetParent(View* parent) { _parent = parent; }
/// @brief Gets the parent of the view.
/// @return The parent of the view, or null if none.
View* GetParent() const { return _parent; }
/// @brief Sets whether the view is currently focused.
/// @param focused True if the view is currently focused, or false otherwise.
void SetFocused(bool focused) { _isFocused = focused; }
/// @brief Gets whether the view is currently focused.
/// @return True if the view is currently focused, or false otherwise.
bool IsFocused() const { return _isFocused; }
protected:
Point _position;
bool _isFocused;
View()
: _position(0, 0), _isFocused(false), _parent(nullptr) { }
private:
View* _parent;
};

View File

@@ -0,0 +1,60 @@
#pragma once
#include "View.h"
#include "core/LinkedList.h"
/// @brief Base class for views that contain other views.
class ViewContainer : public View
{
public:
void InitVram(const VramContext& vramContext) override
{
for (auto& view : _children)
{
view.InitVram(vramContext);
}
}
void Update() override
{
for (auto& view : _children)
{
view.Update();
}
}
void Draw(GraphicsContext& graphicsContext) override
{
for (auto& view : _children)
{
view.Draw(graphicsContext);
}
}
void VBlank() override
{
for (auto& view : _children)
{
view.VBlank();
}
}
protected:
/// @brief Adds a child to the head of the list.
/// @param view The child to add.
void AddChildHead(View* view)
{
_children.InsertHead(view);
view->SetParent(this);
}
/// @brief Adds a child to the tail of the list.
/// @param view The child to add.
void AddChildTail(View* view)
{
_children.InsertTail(view);
view->SetParent(this);
}
private:
LinkedList<View, &View::listLink> _children;
};