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,258 @@
#include "common.h"
#include <libtwl/gfx/gfxStatus.h>
#include <algorithm>
#include "AdvancedPaletteManager.h"
bool AdvancedPaletteManagerBase::PaletteRow::ColorsEqualTo(const PaletteRow& other) const
{
if (hash != other.hash)
{
return false;
}
for (int i = 0; i < 16; i++)
{
if (colors[i] != other.colors[i])
{
return false;
}
}
return true;
}
int AdvancedPaletteManagerBase::HwPaletteRow::FindPlace(const PaletteRow* rows, u32 yStart, u32 yEnd) const
{
if (count == 0)
{
return 0xFF;
}
u32 idx = first;
u32 prevIdx = 0xFF;
while (idx != 0xFF && rows[idx].endY < yStart)
{
prevIdx = idx;
idx = rows[idx].next;
}
if (idx == 0xFF || rows[idx].startY >= yEnd)
{
return prevIdx;
}
return -1;
}
u32 AdvancedPaletteManagerBase::TryMerge(PaletteRow* rows, const PaletteRow& newRow, int yStart, int yEnd)
{
for (u32 i = 0; i < _usedRows; i++)
{
if (rows[i].ColorsEqualTo(newRow))
{
if (yStart < rows[i].startY)
{
u32 prev = rows[i].prev;
if (prev != 0xFF && yStart - rows[prev].endY <= 1)
{
continue;
}
}
if (yEnd > rows[i].endY)
{
u32 next = rows[i].next;
if (next != 0xFF && rows[next].startY - yEnd <= 1)
{
continue;
}
}
// LOG_DEBUG("Could merge [%d,%d) into [%d,%d)\n", yStart, yEnd, rows[i].startY, rows[i].endY);
if ((yEnd < rows[i].startY && rows[i].startY - yEnd > 1) ||
(yStart > rows[i].endY && yStart - rows[i].endY > 1))
{
continue;
}
// LOG_DEBUG("Merging [%d,%d) into [%d,%d)\n", yStart, yEnd, rows[i].startY, rows[i].endY);
// merging
rows[i].startY = std::min((int)rows[i].startY, yStart);
rows[i].endY = std::max((int)rows[i].endY, yEnd);
return rows[i].hwRow;
}
}
return PALETTE_MANAGER_ROW_IDX_INVALID;
}
u32 AdvancedPaletteManagerBase::AllocRowInternal(PaletteRow* rows, const IPalette& palette, int yStart, int yEnd)
{
if (yStart < 0)
{
yStart = 0;
}
if (yEnd > 192)
{
yEnd = 192;
}
u32 newIdx = _usedRows;
PaletteRow& newRow = rows[newIdx];
newRow.hash = palette.GetHashCode();
palette.GetColors(newRow.colors);
u32 rowIdx = TryMerge(rows, newRow, yStart, yEnd);
if (rowIdx != PALETTE_MANAGER_ROW_IDX_INVALID)
{
return rowIdx;
}
_usedRows++;
int bestSlack = 0x100;//-1;
u32 bestPrevIdx = 0xFF;
u32 bestHwRow = 0xFF;
for (int i = 0; i < HW_PALETTE_ROWS; i++)
{
int result = _hwRows[i].FindPlace(rows, yStart, yEnd);
if (result < 0)
{
continue;
}
int slack = result >= 0xFF ? result : yStart - rows[result].endY;
if (slack /*>*/ < bestSlack && slack > 1)
{
bestSlack = slack;
bestPrevIdx = result;
bestHwRow = i;
}
}
rowIdx = bestHwRow;
if (bestHwRow == 0xFF)
{
LOG_FATAL("Failed to allocate palette row\n");
while (true);
return PALETTE_MANAGER_ROW_IDX_INVALID;
}
// LOG_DEBUG("Allocated hw palette row %d for y range %d - %d\n", bestHwRow, yStart, yEnd);
newRow.startY = yStart;
newRow.endY = yEnd;
if (bestPrevIdx >= 0xFF)
{
newRow.next = _hwRows[bestHwRow].first;
if (newRow.next != 0xFF)
{
rows[newRow.next].prev = newIdx;
}
newRow.prev = 0xFF;
_hwRows[bestHwRow].first = newIdx;
}
else
{
newRow.prev = bestPrevIdx;
newRow.next = rows[bestPrevIdx].next;
rows[bestPrevIdx].next = newIdx;
if (newRow.next != 0xFF)
{
rows[newRow.next].prev = newIdx;
}
}
_hwRows[bestHwRow].count++;
newRow.hwRow = rowIdx;
return rowIdx;
}
void AdvancedPaletteManagerBase::Reset()
{
_usedRows = 0;
for (int i = 0; i < HW_PALETTE_ROWS; i++)
{
_hwRows[i].count = 0;
_hwRows[i].first = 0xFF;
}
}
void AdvancedPaletteManagerBase::CreateScheme(const PaletteRow* rows, SchemeEntry* scheme)
{
_nextSchemeEntry = scheme;
u32 y = 0;
u8 curRow[HW_PALETTE_ROWS];
for (int i = 0; i < HW_PALETTE_ROWS; i++)
{
curRow[i] = _hwRows[i].first;
}
while (y != 0xFF)
{
u32 minY = 0xFF;
for (int i = 0; i < HW_PALETTE_ROWS; i++)
{
if (curRow[i] == 0xFF)
{
continue;
}
const PaletteRow& row = rows[curRow[i]];
u32 uploadLine = row.prev == 0xFF ? 0 : rows[row.prev].endY;
if (uploadLine == y)
{
// LOG_DEBUG("At %d upload row %d (%d-%d) to hw row %d\n", uploadLine, curRow[i], row.startY, row.endY, i);
scheme->src = row.colors;
scheme->dst = _targetPalette + i * 16;
scheme++;
curRow[i] = row.next;
uploadLine = row.endY;
}
if (curRow[i] == 0xFF)
{
continue;
}
minY = std::min(minY, uploadLine);
}
if (y != minY)
{
scheme->endMarker = 0;
scheme->nextVCount = minY == 255 ? 0 : minY;
scheme++;
}
y = minY;
}
scheme->endMarker = 0;
scheme->nextVCount = 0;
}
void AdvancedPaletteManagerBase::VCount()
{
while (_curSchemeEntry->endMarker != 0)
{
struct palette_row_t
{
u32 data[8];
};
*(palette_row_t*)_curSchemeEntry->dst = *(palette_row_t*)_curSchemeEntry->src;
_curSchemeEntry++;
}
if (_curSchemeEntry->nextVCount == 0)
{
gfx_setVCountMatchIrqEnabled(false);
}
else
{
// LOG_DEBUG("vcount %d\n", _curSchemeEntry->nextVCount);
gfx_setVCountMatchLine(_curSchemeEntry->nextVCount);
gfx_setVCountMatchIrqEnabled(true);
_curSchemeEntry++;
}
}

View File

@@ -0,0 +1,101 @@
#pragma once
#include "common.h"
#include "PaletteManager.h"
#define HW_PALETTE_ROWS 16
class AdvancedPaletteManagerBase : public PaletteManager
{
public:
void Reset();
void VCount();
void VBlank()
{
_curSchemeEntry = _nextSchemeEntry;
VCount();
}
protected:
struct PaletteRow
{
u16 colors[16];
u8 startY;
u8 endY;
u8 hwRow;
u8 prev;
u32 next;
u32 hash;
bool ColorsEqualTo(const PaletteRow& other) const;
};
union SchemeEntry
{
struct
{
const u16* src;
vu16* dst;
};
struct
{
u32 endMarker;
u32 nextVCount;
};
};
explicit AdvancedPaletteManagerBase(vu16* targetPalette)
: _targetPalette(targetPalette), _curSchemeEntry(nullptr), _nextSchemeEntry(nullptr)
{
Reset();
}
u32 AllocRowInternal(PaletteRow* rows, const IPalette& palette, int yStart, int yEnd);
void CreateScheme(const PaletteRow* rows, SchemeEntry* scheme);
private:
struct HwPaletteRow
{
u8 count;
u8 first;
int FindPlace(const PaletteRow* rows, u32 yStart, u32 yEnd) const;
};
u32 _usedRows = 0;
HwPaletteRow _hwRows[HW_PALETTE_ROWS];
vu16* _targetPalette;
const SchemeEntry* _curSchemeEntry;
const SchemeEntry* _nextSchemeEntry;
u32 TryMerge(PaletteRow* rows, const PaletteRow& newRow, int yStart, int yEnd);
};
/// @brief Advanced palette manager that manages dynamic palette rows that are valid on a range of scanlines.
/// By reloading the palette during display this allows to use more than 16 palette rows in total
/// as long as only up to 16 are in use on a scanline.
/// @tparam MaxPaletteRows The maximum number of palette rows to manage.
template <u32 MaxPaletteRows>
class AdvancedPaletteManager : public AdvancedPaletteManagerBase
{
public:
explicit AdvancedPaletteManager(vu16* targetPalette)
: AdvancedPaletteManagerBase(targetPalette) { }
u32 AllocRow(const IPalette& palette, int yStart, int yEnd) override
{
return AllocRowInternal(_rows[_curSet], palette, yStart, yEnd);
}
void EndOfFrame()
{
CreateScheme(_rows[_curSet], _scheme[_curSet]);
_curSet = 1 - _curSet;
Reset();
}
private:
u32 _curSet = 0;
PaletteRow _rows[2][MaxPaletteRows];
SchemeEntry _scheme[2][MaxPaletteRows * 2];
};

View File

@@ -0,0 +1,8 @@
#pragma once
enum class Alignment
{
Start,
Center,
End
};

View File

@@ -0,0 +1,22 @@
#pragma once
#include "StackVramManager.h"
/// @brief Stack vram manager that increases the stack pointer after an allocation.
class AscendingStackVramManager : public StackVramManager
{
public:
/// @brief Creates a new AscendingStackVramManager.
AscendingStackVramManager() { }
/// @brief Creates a new AscendingStackVramManager.
/// @param vramAddress Pointer to the start of the vram block to manage.
explicit AscendingStackVramManager(vu16* vramAddress)
: StackVramManager(vramAddress) { }
u32 Alloc(u32 length) override
{
u32 result = _offset;
_offset += length;
return result;
}
};

View File

@@ -0,0 +1,35 @@
#pragma once
#include "StackVramManager.h"
/// @brief Stack vram manager that decreases the stack pointer before an allocation.
class DescendingStackVramManager : public StackVramManager
{
public:
/// @brief Creates a new DescendingStackVramManager.
/// @param vramSize The size of the vram block to manage.
/// This will be used as the initial offset.
explicit DescendingStackVramManager(u32 vramSize)
: _size(vramSize)
{
_offset = vramSize;
}
/// @brief Creates a new DescendingStackVramManager.
/// @param vramAddress Pointer to the start of the vram block to manage.
/// @param vramSize The size of the vram block to manage.
/// This will be used as the initial offset.
DescendingStackVramManager(vu16* vramAddress, u32 vramSize)
: StackVramManager(vramAddress), _size(vramSize)
{
_offset = vramSize;
}
u32 Alloc(u32 length) override
{
_offset -= length;
return _offset;
}
private:
u32 _size;
};

View File

@@ -0,0 +1,58 @@
#include "common.h"
#include "gui/views/View.h"
#include "input/InputProvider.h"
#include "FocusManager.h"
void FocusManager::Focus(View* newFocus)
{
if (!newFocus)
{
Unfocus();
return;
}
if (_currentFocus)
_currentFocus->SetFocused(false);
newFocus->SetFocused(true);
_currentFocus = newFocus;
}
void FocusManager::Unfocus()
{
if (_currentFocus)
_currentFocus->SetFocused(false);
_currentFocus = nullptr;
}
void FocusManager::Update(const InputProvider& inputProvider)
{
if (!_currentFocus || !_currentFocus->GetParent())
return; // todo
View* newFocus = nullptr;
if (inputProvider.Triggered(InputKey::DpadUp))
newFocus = _currentFocus->GetParent()->MoveFocus(_currentFocus, FocusMoveDirection::Up, _currentFocus);
else if (inputProvider.Triggered(InputKey::DpadDown))
newFocus = _currentFocus->GetParent()->MoveFocus(_currentFocus, FocusMoveDirection::Down, _currentFocus);
else if (inputProvider.Triggered(InputKey::DpadLeft))
newFocus = _currentFocus->GetParent()->MoveFocus(_currentFocus, FocusMoveDirection::Left, _currentFocus);
else if (inputProvider.Triggered(InputKey::DpadRight))
newFocus = _currentFocus->GetParent()->MoveFocus(_currentFocus, FocusMoveDirection::Right, _currentFocus);
else
_currentFocus->HandleInput(inputProvider, *this);
if (newFocus)
Focus(newFocus);
}
bool FocusManager::IsFocusInside(const View* view) const
{
auto focusView = _currentFocus;
while (focusView)
{
if (view == focusView)
return true;
focusView = focusView->GetParent();
}
return false;
}

View File

@@ -0,0 +1,32 @@
#pragma once
class View;
class InputProvider;
/// @brief Class for managing focus.
class FocusManager
{
public:
/// @brief Focuses the given view.
/// @param newFocus The view to focus.
void Focus(View* newFocus);
/// @brief Clears the current focus.
void Unfocus();
/// @brief Updates the focus using the given input provider.
/// @param inputProvider The input provider to use.
void Update(const InputProvider& inputProvider);
/// @brief Gets the currently focused view.
/// @return A pointer to the view that is currently focused, or null if none.
constexpr View* GetCurrentFocus() const { return _currentFocus; }
/// @brief Checks whether the current focus lies inside the given view.
/// @param view The view to check for.
/// @return True if the current focus lies inside the given view, or false otherwise.
bool IsFocusInside(const View* view) const;
private:
View* _currentFocus = nullptr;
};

View File

@@ -0,0 +1,9 @@
#pragma once
enum class FocusMoveDirection
{
Up,
Down,
Left,
Right
};

View File

@@ -0,0 +1,67 @@
#pragma once
#include "core/math/Point.h"
#include "core/math/Rectangle.h"
#include "OamManager.h"
#include "PaletteManager.h"
#include "Rgb6Palette.h"
class GraphicsContext
{
public:
GraphicsContext(OamManager* oamManager, PaletteManager* paletteManager, const Rgb6Palette* rgb6Palette)
: _priority(3), _polygonId(0), _oamManager(oamManager)
, _paletteManager(paletteManager), _rgb6Palette(rgb6Palette)
, _clipArea(0, 0, 256, 192) { }
u32 SetPriority(u32 priority)
{
u32 oldPriority = _priority;
_priority = priority;
return oldPriority;
}
constexpr u32 GetPriority() const { return _priority; }
u32 SetPolygonId(u32 polygonId)
{
u32 oldPolygonId = _polygonId;
_polygonId = polygonId;
return oldPolygonId;
}
constexpr u32 GetPolygonId() const { return _polygonId; }
OamManager& GetOamManager() const { return *_oamManager; }
PaletteManager& GetPaletteManager() const { return *_paletteManager; }
const Rgb6Palette* GetRgb6Palette() const { return _rgb6Palette; }
virtual PaletteManager& GetPaletteManager(int scanline) const { return *_paletteManager; }
void SetClipArea(const Rectangle& clipArea, bool inverse = false)
{
_clipArea = clipArea;
_inverseClipArea = inverse;
}
constexpr bool IsVisible(const Rectangle& bounds) const
{
if (!Rectangle(0, 0, 256, 192).OverlapsWith(bounds))
return false;
return _inverseClipArea ? !_clipArea.Contains(bounds) : _clipArea.OverlapsWith(bounds);
}
void ResetClipArea()
{
SetClipArea(Rectangle(0, 0, 256, 192));
}
private:
u32 _priority;
u8 _polygonId;
OamManager* _oamManager;
PaletteManager* _paletteManager;
const Rgb6Palette* _rgb6Palette;
Rectangle _clipArea;
bool _inverseClipArea = false;
};

198
arm9/source/gui/Gx.h Normal file
View File

@@ -0,0 +1,198 @@
#pragma once
#include <libtwl/gfx/gfx3dCmd.h>
#include "core/math/ColorConverter.h"
#include "core/math/fixed.h"
namespace Gx
{
static inline void MtxMode(GxMtxMode mode)
{
gx_mtxMode(mode);
}
static inline void MtxIdentity()
{
gx_mtxIdentity();
}
static inline void MtxLoad44(const mtx44_t* matrix)
{
gx_mtxLoad44(matrix);
}
static inline void MtxLoad43(const mtx43_t* matrix)
{
gx_mtxLoad43(matrix);
}
static inline void MtxMult44(const mtx44_t* matrix)
{
gx_mtxMult44(matrix);
}
static inline void MtxMult43(const mtx43_t* matrix)
{
gx_mtxMult43(matrix);
}
static inline void MtxMult33(const mtx33_t* matrix)
{
gx_mtxMult33(matrix);
}
static inline void MtxTranslate(fix32<12> x, fix32<12> y, fix32<12> z)
{
gx_mtxTranslate(x.GetRawValue(), y.GetRawValue(), z.GetRawValue());
}
static inline void MtxScale(fix32<12> x, fix32<12> y, fix32<12> z)
{
gx_mtxScale(x.GetRawValue(), y.GetRawValue(), z.GetRawValue());
}
static inline void MtxPush()
{
gx_mtxPush();
}
static inline void MtxPop(int count)
{
gx_mtxPop(count);
}
static inline void PolygonAttr(GxLightMask lightMask, GxPolygonMode polygonMode,
GxDisplayMode displayMode, bool xluDepthUpdate, bool clipFarPlane, bool render1Dot,
GxDepthFunc depthFunc, bool fogEnable, u32 alpha, u32 polygonId)
{
gx_polygonAttr(lightMask, polygonMode, displayMode, xluDepthUpdate, clipFarPlane,
render1Dot, depthFunc, fogEnable, alpha, polygonId);
}
static inline void PolygonAttr(u32 polygonAttr)
{
REG_GX_POLYGON_ATTR = polygonAttr;
}
static inline void TexImageParam(u16 address, bool repeatS, bool repeatT,
bool flipS, bool flipT, GxTexSize sizeS, GxTexSize sizeT, GxTexFormat format,
bool color0Transparent, GxTexGen texGen)
{
gx_texImageParam(address, repeatS, repeatT, flipS, flipT,
sizeS, sizeT, format, color0Transparent, texGen);
}
static inline void TexPlttBase(u32 address)
{
gx_texPlttBase(address);
}
static inline void TexImageParam(u32 texImageParam)
{
REG_GX_TEXIMAGE_PARAM = texImageParam;
}
static inline void TexCoord(fix16<4> s, fix16<4> t)
{
gx_texCoord(s.GetRawValue(), t.GetRawValue());
}
static inline void Normal(int x, int y, int z)
{
gx_normal(x, y, z);
}
static inline void Color(const Rgb<5, 5, 5>& color)
{
gx_color(ColorConverter::ToXBGR555(color));
}
static inline void Color(u32 color)
{
gx_color(color);
}
static inline void Color(u32 r, u32 g, u32 b)
{
Color(Rgb<5, 5, 5>(r, g, b));
}
static inline void Vtx16(fix16<12> x, fix16<12> y, fix16<12> z)
{
gx_vtx16(x.GetRawValue(), y.GetRawValue(), z.GetRawValue());
}
static inline void Vtx10(fix16<6> x, fix16<6> y, fix16<6> z)
{
gx_vtx10(x.GetRawValue(), y.GetRawValue(), z.GetRawValue());
}
static inline void VtxXY(fix16<12> x, fix16<12> y)
{
gx_vtxXY(x.GetRawValue(), y.GetRawValue());
}
static inline void VtxXZ(fix16<12> x, fix16<12> z)
{
gx_vtxXZ(x.GetRawValue(), z.GetRawValue());
}
static inline void VtxYZ(fix16<12> y, fix16<12> z)
{
gx_vtxYZ(y.GetRawValue(), z.GetRawValue());
}
static inline void VtxDiff(fix16<12> x, fix16<12> y, fix16<12> z)
{
gx_vtxDiff(x.GetRawValue(), y.GetRawValue(), z.GetRawValue());
}
static inline void Begin(GxPrimitiveType type)
{
gx_beginVtxs(type);
}
static inline void End()
{
gx_endVtxs();
}
static inline void LightVector(int light, int x, int y, int z)
{
gx_lightVector(light, x, y, z);
}
static inline void LightColor(int light, u32 color)
{
gx_lightColor(light, color);
}
static inline void DiffuseAmbient(u32 diffuse, bool setDiffuseVtxColor, u32 ambient)
{
gx_diffuseAmbient(diffuse, setDiffuseVtxColor, ambient);
}
static inline void DiffuseAmbient(const Rgb<5, 5, 5>& diffuse, bool setDiffuseVtxColor, const Rgb<5, 5, 5>& ambient)
{
gx_diffuseAmbient(ColorConverter::ToXBGR555(diffuse), setDiffuseVtxColor, ColorConverter::ToXBGR555(ambient));
}
static inline void SpecularEmission(u32 specular, bool useShininessTable, u32 emission)
{
gx_specularEmission(specular, useShininessTable, emission);
}
static inline void SpecularEmission(const Rgb<5, 5, 5>& specular, bool useShininessTable, const Rgb<5, 5, 5>& emission)
{
gx_specularEmission(ColorConverter::ToXBGR555(specular), useShininessTable, ColorConverter::ToXBGR555(emission));
}
static inline void SwapBuffers(GxXluSortMode xluSortMode, GxDepthMode depthMode)
{
gx_swapBuffers(xluSortMode, depthMode);
}
static inline void Viewport(int x1, int y1, int x2, int y2)
{
gx_viewport(x1, y1, x2, y2);
}
};

View File

@@ -0,0 +1,20 @@
#pragma once
/// @brief Interface for managing and allocating vram.
class IVramManager
{
public:
virtual ~IVramManager() = 0;
/// @brief Allocates a block of vram of the given length.
/// @param length The size to allocate.
/// @return The vram offset of the allocated block.
virtual u32 Alloc(u32 length) = 0;
/// @brief Converts the vram offset to a vram pointer.
/// @param offset The offset to convert.
/// @return The pointer corresponding to the vram offset.
virtual vu16* GetVramAddress(u32 offset) const = 0;
};
inline IVramManager::~IVramManager() { }

View File

@@ -0,0 +1,220 @@
#pragma once
#include "common.h"
#include "core/math/Point.h"
#include <libtwl/gfx/gfxOam.h>
/// @brief Helper class for building oams.
class OamBuilder
{
u16 _attr0;
u16 _attr1;
u16 _attr2;
OamBuilder()
: _attr0(0), _attr1(0), _attr2(0) { }
public:
template <int width, int height>
static constexpr OamBuilder OamWithSize(const Point& position, u32 vramOffset)
{
return OamWithSize<width, height>(position.x, position.y, vramOffset);
}
template <int width, int height>
static constexpr OamBuilder OamWithSize(int x, int y, u32 vramOffset)
{
OamBuilder builder;
switch (width)
{
case 8:
{
switch (height)
{
case 8:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_8_8;
builder._attr1 = GFX_OAM_ATTR1_SIZE_8_8;
break;
case 16:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_8_16;
builder._attr1 = GFX_OAM_ATTR1_SIZE_8_16;
break;
case 32:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_8_32;
builder._attr1 = GFX_OAM_ATTR1_SIZE_8_32;
break;
default:
static_assert(width != 8 || (height == 8 || height == 16 || height == 32), "Invalid oam height for width 8");
break;
}
break;
}
case 16:
{
switch (height)
{
case 8:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_16_8;
builder._attr1 = GFX_OAM_ATTR1_SIZE_16_8;
break;
case 16:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_16_16;
builder._attr1 = GFX_OAM_ATTR1_SIZE_16_16;
break;
case 32:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_16_32;
builder._attr1 = GFX_OAM_ATTR1_SIZE_16_32;
break;
default:
static_assert(width != 16 || (height == 8 || height == 16 || height == 32), "Invalid oam height for width 16");
break;
}
break;
}
case 32:
{
switch (height)
{
case 8:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_32_8;
builder._attr1 = GFX_OAM_ATTR1_SIZE_32_8;
break;
case 16:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_32_16;
builder._attr1 = GFX_OAM_ATTR1_SIZE_32_16;
break;
case 32:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_32_32;
builder._attr1 = GFX_OAM_ATTR1_SIZE_32_32;
break;
case 64:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_32_64;
builder._attr1 = GFX_OAM_ATTR1_SIZE_32_64;
break;
default:
static_assert(width != 32 || (height == 8 || height == 16 || height == 32 || height == 64), "Invalid oam height for width 32");
break;
}
break;
}
case 64:
{
switch (height)
{
case 32:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_64_32;
builder._attr1 = GFX_OAM_ATTR1_SIZE_64_32;
break;
case 64:
builder._attr0 = GFX_OAM_ATTR0_SHAPE_64_64;
builder._attr1 = GFX_OAM_ATTR1_SIZE_64_64;
break;
default:
static_assert(width != 64 || (height == 32 || height == 64), "Invalid oam height for width 64");
break;
}
break;
}
default:
static_assert(width == 8 || width == 16 || width == 32 || width == 64, "Invalid oam width");
break;
}
builder._attr0 |= GFX_OAM_ATTR0_Y(y);
builder._attr1 |= GFX_OAM_ATTR1_X(x);
builder._attr2 = GFX_OAM_ATTR2_VRAM_OFFS(vramOffset);
return builder;
}
constexpr OamBuilder& Hidden()
{
_attr0 &= ~GFX_OAM_ATTR0_DOUBLE_AFFINE;
_attr0 |= GFX_OAM_ATTR0_HIDDEN;
return *this;
}
constexpr OamBuilder& AsWindow()
{
_attr0 &= ~GFX_OAM_ATTR0_MODE_BITMAP;
_attr0 |= GFX_OAM_ATTR0_MODE_WINDOW;
return *this;
}
constexpr OamBuilder& AsTranslucent()
{
_attr0 &= ~GFX_OAM_ATTR0_MODE_BITMAP;
_attr0 |= GFX_OAM_ATTR0_MODE_TRANSLUCENT;
return *this;
}
constexpr OamBuilder& AsBitmap(int alpha = 16)
{
_attr0 &= ~GFX_OAM_ATTR0_PLTT256;
_attr0 |= GFX_OAM_ATTR0_MODE_BITMAP;
if (alpha == 0)
return Hidden();
_attr2 &= ~GFX_OAM_ATTR2_BMP_ALPHA(0xF);
_attr2 |= GFX_OAM_ATTR2_BMP_ALPHA(alpha - 1);
return *this;
}
constexpr OamBuilder& WithAffineMtx(int mtx, bool doubleSize = false)
{
_attr0 &= ~GFX_OAM_ATTR0_DOUBLE_AFFINE;
if (doubleSize)
_attr0 |= GFX_OAM_ATTR0_DOUBLE_AFFINE;
else
_attr0 |= GFX_OAM_ATTR0_AFFINE;
_attr1 &= ~GFX_OAM_ATTR1_MTX(0x1F);
_attr1 |= GFX_OAM_ATTR1_MTX(mtx);
return *this;
}
constexpr OamBuilder& WithHFlip(bool hFlip = true)
{
if (hFlip)
_attr1 |= GFX_OAM_ATTR1_HFLIP;
else
_attr1 &= ~GFX_OAM_ATTR1_HFLIP;
return *this;
}
constexpr OamBuilder& WithVFlip(bool vFlip = true)
{
if (vFlip)
_attr1 |= GFX_OAM_ATTR1_VFLIP;
else
_attr1 &= ~GFX_OAM_ATTR1_VFLIP;
return *this;
}
constexpr OamBuilder& WithPriority(int priority)
{
_attr2 &= ~GFX_OAM_ATTR2_PRIORITY(3);
_attr2 |= GFX_OAM_ATTR2_PRIORITY(priority);
return *this;
}
constexpr OamBuilder& WithPalette16(int palette)
{
_attr0 &= ~GFX_OAM_ATTR0_PLTT256;
_attr2 &= ~GFX_OAM_ATTR2_PLTT(0xF);
_attr2 |= GFX_OAM_ATTR2_PLTT(palette);
return *this;
}
constexpr OamBuilder& WithPalette256(int palette = 0)
{
_attr0 |= GFX_OAM_ATTR0_PLTT256;
_attr2 &= ~GFX_OAM_ATTR2_PLTT(0xF);
_attr2 |= GFX_OAM_ATTR2_PLTT(palette);
return *this;
}
constexpr void Build(gfx_oam_entry_t& oamEntry) const
{
oamEntry.attr0 = _attr0;
oamEntry.attr1 = _attr1;
oamEntry.attr2 = _attr2;
}
};

View File

@@ -0,0 +1,16 @@
#include "common.h"
#include <nds/arm9/cache.h>
#include "OamManager.h"
void OamManager::Apply(vu16* dst)
{
DC_FlushRange(&_oamTable, sizeof(gfx_oam_table_t));
dma_ntrCopy32(3, &_oamTable, dst, sizeof(gfx_oam_table_t));
}
void OamManager::Clear()
{
memset(&_oamTable, 0x2, sizeof(gfx_oam_table_t));
_oamPtr = &_oamTable.objs[128];
_mtxIdx = 0;
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <libtwl/gfx/gfxOam.h>
#include <libtwl/dma/dmaNitro.h>
#include <string.h>
/// @brief Class for managing oams and affine matrices.
class alignas(32) OamManager
{
public:
OamManager()
{
Clear();
}
gfx_oam_entry_t* AllocOams(u32 count)
{
_oamPtr -= count;
return _oamPtr;
}
gfx_oam_mtx_t* AllocMatrices(u32 count, u32& mtxId)
{
mtxId = _mtxIdx;
gfx_oam_mtx_t* result = &_oamTable.mtxs[_mtxIdx];
_mtxIdx += count;
return result;
}
void Apply(vu16* dst);
void Clear();
private:
gfx_oam_table_t _oamTable alignas(32);
gfx_oam_entry_t* _oamPtr;
u32 _mtxIdx;
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include "palette/IPalette.h"
#define PALETTE_MANAGER_ROW_IDX_INVALID 0xFF
/// @brief Base class for palette management.
class PaletteManager
{
public:
virtual ~PaletteManager() { }
/// @brief Allocates a palette row for the given palette, which is used
/// in the display line range from yStart to yEnd (exclusive).
/// @param palette The palette to allocate a row for.
/// @param yStart The first display line the palette will be used on.
/// @param yEnd The display line from which the palette will not be used anymore.
/// @return The allocated palette row number.
virtual u32 AllocRow(const IPalette& palette, int yStart, int yEnd) = 0;
/// @brief Allocates a palette row for the given palette, assuming it
/// will be used on all display lines.
/// @param palette The palette to allocate a row for.
/// @return The allocated palette row number.
/// @note It is recommended to specify the start and end display line
/// for more efficient palette management.
u32 AllocRow(const IPalette& palette)
{
return AllocRow(palette, 0, 192);
}
};

View File

@@ -0,0 +1,118 @@
#include "common.h"
#include "gui/VramContext.h"
#include "gui/Gx.h"
#include "core/math/ColorConverter.h"
#include "Rgb6Palette.h"
#define PLTT_IDX_15 0
#define PLTT_IDX_30 1
#define PLTT_IDX_31 2
struct color_mapping_t
{
u8 vtxColorComponent;
u8 plttIdx;
};
static const color_mapping_t sMappingTable[64] =
{
{ 0, PLTT_IDX_31 }, // 0
{ 1, PLTT_IDX_15 }, // 1
{ 2, PLTT_IDX_15 }, // 2
{ 1, PLTT_IDX_31 }, // 3
{ 4, PLTT_IDX_15 }, // 4
{ 2, PLTT_IDX_31 }, // 5
{ 6, PLTT_IDX_15 }, // 6
{ 3, PLTT_IDX_31 }, // 7
{ 8, PLTT_IDX_15 }, // 8
{ 4, PLTT_IDX_31 }, // 9
{ 10, PLTT_IDX_15 }, // 10
{ 5, PLTT_IDX_31 }, // 11
{ 12, PLTT_IDX_15 }, // 12
{ 6, PLTT_IDX_31 }, // 13
{ 14, PLTT_IDX_15 }, // 14
{ 7, PLTT_IDX_31 }, // 15
{ 16, PLTT_IDX_15 }, // 16
{ 8, PLTT_IDX_31 }, // 17
{ 18, PLTT_IDX_15 }, // 18
{ 9, PLTT_IDX_31 }, // 19
{ 20, PLTT_IDX_15 }, // 20
{ 10, PLTT_IDX_31 }, // 21
{ 22, PLTT_IDX_15 }, // 22
{ 11, PLTT_IDX_31 }, // 23
{ 24, PLTT_IDX_15 }, // 24
{ 12, PLTT_IDX_31 }, // 25
{ 26, PLTT_IDX_15 }, // 26
{ 13, PLTT_IDX_31 }, // 27
{ 28, PLTT_IDX_15 }, // 28
{ 14, PLTT_IDX_31 }, // 29
{ 30, PLTT_IDX_15 }, // 30
{ 15, PLTT_IDX_31 }, // 31
{ 16, PLTT_IDX_30 }, // 32
{ 16, PLTT_IDX_31 }, // 33
{ 17, PLTT_IDX_30 }, // 34
{ 17, PLTT_IDX_31 }, // 35
{ 18, PLTT_IDX_30 }, // 36
{ 18, PLTT_IDX_31 }, // 37
{ 19, PLTT_IDX_30 }, // 38
{ 19, PLTT_IDX_31 }, // 39
{ 20, PLTT_IDX_30 }, // 40
{ 20, PLTT_IDX_31 }, // 41
{ 21, PLTT_IDX_30 }, // 42
{ 21, PLTT_IDX_31 }, // 43
{ 22, PLTT_IDX_30 }, // 44
{ 22, PLTT_IDX_31 }, // 45
{ 23, PLTT_IDX_30 }, // 46
{ 23, PLTT_IDX_31 }, // 47
{ 24, PLTT_IDX_30 }, // 48
{ 24, PLTT_IDX_31 }, // 49
{ 25, PLTT_IDX_30 }, // 50
{ 25, PLTT_IDX_31 }, // 51
{ 26, PLTT_IDX_30 }, // 52
{ 26, PLTT_IDX_31 }, // 53
{ 27, PLTT_IDX_30 }, // 54
{ 27, PLTT_IDX_31 }, // 55
{ 28, PLTT_IDX_30 }, // 56
{ 28, PLTT_IDX_31 }, // 57
{ 29, PLTT_IDX_30 }, // 58
{ 29, PLTT_IDX_31 }, // 59
{ 30, PLTT_IDX_30 }, // 60
{ 31, PLTT_IDX_30 }, // 61
{ 31, PLTT_IDX_30 }, // 62 -> 61
{ 31, PLTT_IDX_31 } // 63
};
void Rgb6Palette::UploadGraphics(const VramContext& vramContext)
{
const auto paletteVramManager = vramContext.GetTexPlttVramManager();
if (paletteVramManager)
{
_vramOffset = paletteVramManager->Alloc(432);
static const u8 colorValues[3] = { 15, 30, 31 };
for (int r = 0; r < 3; r++)
{
for (int g = 0; g < 3; g++)
{
for (int b = 0; b < 3; b++)
{
int index = b * 3 * 3 + g * 3 + r;
paletteVramManager->GetVramAddress(_vramOffset)[index * 8]
= ColorConverter::ToXBGR555(Rgb<5, 5, 5>(colorValues[r], colorValues[g], colorValues[b]));
}
}
}
}
}
void Rgb6Palette::ApplyColor(const Rgb<6,6,6>& color) const
{
const auto& rConversion = sMappingTable[color.r];
const auto& gConversion = sMappingTable[color.g];
const auto& bConversion = sMappingTable[color.b];
Gx::Color(ColorConverter::ToXBGR555(Rgb<5, 5, 5>(
rConversion.vtxColorComponent, gConversion.vtxColorComponent, bConversion.vtxColorComponent)));
u32 colorOffset = bConversion.plttIdx * 3 * 3 + gConversion.plttIdx * 3 + rConversion.plttIdx;
Gx::TexPlttBase((_vramOffset >> 4) + colorOffset);
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "core/math/Rgb.h"
class VramContext;
class Rgb6Palette
{
public:
void UploadGraphics(const VramContext& vramContext);
void ApplyColor(const Rgb<6, 6, 6>& color) const;
private:
u32 _vramOffset = 0;
};

View File

@@ -0,0 +1,15 @@
#include "common.h"
#include <nds/arm9/cache.h>
#include <libtwl/dma/dmaNitro.h>
#include "SimplePaletteManager.h"
void SimplePaletteManager::Apply(vu16* dst)
{
if (_idxOffset > 0 && _curRow == _idxOffset)
return;
u32 rows = _idxOffset == 0 ? 16 : _curRow - _idxOffset;
dst += _idxOffset * 16;
DC_FlushRange(_palette[_idxOffset], rows * 16 * 2);
dma_ntrCopy32(3, _palette[_idxOffset], dst, rows * 16 * 2);
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "PaletteManager.h"
/// @brief Simple palette manager that manages 16 static palette rows.
class alignas(32) SimplePaletteManager : public PaletteManager
{
public:
using PaletteManager::AllocRow;
u32 GetState() const { return _curRow; }
void SetState(u32 state) { _curRow = state; }
void Reset(u32 idxOffset = 0)
{
_curRow = idxOffset;
_idxOffset = idxOffset;
}
u32 AllocRow(const IPalette& palette, int yStart, int yEnd) override
{
u32 rowIdx = _curRow++;
palette.GetColors(_palette[rowIdx]);
return rowIdx;
}
u16* GetRow(u32 rowIdx)
{
return _palette[rowIdx];
}
void SetColor0(u16 color)
{
_palette[0][0] = color;
}
void Apply(vu16* dst);
private:
u16 _palette[16][16] alignas(32);
u8 _curRow = 0;
u8 _idxOffset = 0;
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include "IVramManager.h"
/// @brief Base class for vram managers that work like a stack.
class StackVramManager : public IVramManager
{
public:
/// @brief Gets the current state of the stack.
/// @return The current state of the stack.
u32 GetState() const { return _offset; }
/// @brief Restores the stack to a previously stored state.
/// @param state The state to restore.
void SetState(u32 state) { _offset = state; }
vu16* GetVramAddress(u32 offset) const override
{
return (vu16*)((vu8*)_vramAddress + offset);
}
protected:
u32 _offset;
vu16* _vramAddress;
StackVramManager()
: _offset(0), _vramAddress(nullptr) { }
explicit StackVramManager(vu16* vramAddress)
: _offset(0), _vramAddress(vramAddress) { }
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include "core/LinkedListLink.h"
#include "VBlankTextureLoadRequestState.h"
class VBlankTextureLoadRequest
{
friend class VBlankTextureLoader;
public:
typedef void (*CallbackFunc)(const VBlankTextureLoadRequest* request, void* arg);
VBlankTextureLoadRequest() { }
VBlankTextureLoadRequest(const void* textureData, u32 textureDataLength, u32 textureVramOffset,
const void* paletteData, u32 paletteDataLength, u32 paletteVramOffset,
CallbackFunc callbackFunc, void* callbackArg)
: _textureData(textureData), _textureDataLength(textureDataLength), _textureVramOffset(textureVramOffset)
, _paletteData(paletteData), _paletteDataLength(paletteDataLength), _paletteVramOffset(paletteVramOffset)
, _callbackFunc(callbackFunc), _callbackArg(callbackArg) { }
VBlankTextureLoadRequestState GetState() const
{
return _state;
}
private:
VBlankTextureLoadRequestState _state = VBlankTextureLoadRequestState::NotLoaded;
LinkedListLink _listLink;
const void* _textureData = nullptr;
u32 _textureDataLength = 0;
u32 _textureVramOffset = 0;
const void* _paletteData = nullptr;
u32 _paletteDataLength = 0;
u32 _paletteVramOffset = 0;
CallbackFunc _callbackFunc = nullptr;
void* _callbackArg = nullptr;
};

View File

@@ -0,0 +1,10 @@
#pragma once
enum class VBlankTextureLoadRequestState
{
NotLoaded,
LoadRequested,
Loading,
LoadComplete,
Canceled
};

View File

@@ -0,0 +1,177 @@
#include "common.h"
#include <string.h>
#include <libtwl/mem/memVram.h>
#include <libtwl/gfx/gfxStatus.h>
#include <libtwl/dma/dmaNitro.h>
#include "VBlankTextureLoader.h"
#define TEXLOADER_BYTES_PER_LINE 2048
#define TEXLOADER_G3_VBLANK_START_LINE 191
#define TEXLOADER_G3_VBLANK_END_LINE 213
void VBlankTextureLoader::VBlank()
{
rtos_lockMutex(&_mutex);
{
while (true)
{
auto request = _requests.GetHead();
if (!request)
{
_currentRequest = nullptr;
break;
}
if (request->GetState() == VBlankTextureLoadRequestState::Canceled)
{
_currentRequest = nullptr;
continue;
}
if (_currentRequest != request)
{
_currentRequest = request;
_currentLoadingOffset = 0;
_currentLoadingStage = LoadingStage::Texture;
}
if (_currentLoadingStage == LoadingStage::Texture)
{
if (!LoadTexture())
{
break;
}
}
if (_currentRequest->GetState() == VBlankTextureLoadRequestState::Canceled)
{
_currentRequest = nullptr;
continue;
}
if (_currentLoadingStage == LoadingStage::Palette)
{
if (!LoadPalette())
{
break;
}
}
_currentRequest->_state = VBlankTextureLoadRequestState::LoadComplete;
if (_currentRequest->_callbackFunc)
{
_currentRequest->_callbackFunc(_currentRequest, _currentRequest->_callbackArg);
}
_requests.Remove(_currentRequest);
_currentRequest = nullptr;
}
}
rtos_unlockMutex(&_mutex);
}
bool VBlankTextureLoader::LoadTexture()
{
int size = _currentRequest->_textureDataLength - _currentLoadingOffset;
if (size > 0)
{
int line = gfx_getVCount();
if (line >= TEXLOADER_G3_VBLANK_END_LINE)
{
return false;
}
int maxLength = (TEXLOADER_G3_VBLANK_END_LINE - line) * TEXLOADER_BYTES_PER_LINE;
auto vramDMapping = mem_getVramDMapping();
mem_setVramDMapping(MEM_VRAM_D_LCDC);
if (size <= maxLength)
{
dma_ntrCopy32(3,
(u8*)_currentRequest->_textureData + _currentLoadingOffset,
(u8*)0x6860000 + _currentRequest->_textureVramOffset + _currentLoadingOffset,
size);
mem_setVramDMapping(vramDMapping);
}
else
{
dma_ntrCopy32(3,
(u8*)_currentRequest->_textureData + _currentLoadingOffset,
(u8*)0x6860000 + _currentRequest->_textureVramOffset + _currentLoadingOffset,
maxLength);
mem_setVramDMapping(vramDMapping);
_currentLoadingOffset += maxLength;
return false;
}
}
_currentLoadingOffset = 0;
_currentLoadingStage = LoadingStage::Palette;
return true;
}
bool VBlankTextureLoader::LoadPalette()
{
int size = _currentRequest->_paletteDataLength - _currentLoadingOffset;
if (size > 0)
{
int line = gfx_getVCount();
if (line >= TEXLOADER_G3_VBLANK_END_LINE)
{
return false;
}
int maxLength = (TEXLOADER_G3_VBLANK_END_LINE - line) * TEXLOADER_BYTES_PER_LINE;
auto vramEMapping = mem_getVramEMapping();
mem_setVramEMapping(MEM_VRAM_E_LCDC);
if (size <= maxLength)
{
dma_ntrCopy32(3,
(u8*)_currentRequest->_paletteData + _currentLoadingOffset,
(u8*)0x6880000 + _currentRequest->_paletteVramOffset + _currentLoadingOffset,
size);
mem_setVramEMapping(vramEMapping);
}
else
{
dma_ntrCopy32(3,
(u8*)_currentRequest->_paletteData + _currentLoadingOffset,
(u8*)0x6880000 + _currentRequest->_paletteVramOffset + _currentLoadingOffset,
maxLength);
mem_setVramEMapping(vramEMapping);
_currentLoadingOffset += maxLength;
return false;
}
}
return true;
}
void VBlankTextureLoader::RequestLoad(VBlankTextureLoadRequest& request)
{
rtos_lockMutex(&_mutex);
{
if (request._state == VBlankTextureLoadRequestState::NotLoaded)
{
_requests.InsertTail(&request);
request._state = VBlankTextureLoadRequestState::LoadRequested;
}
}
rtos_unlockMutex(&_mutex);
}
void VBlankTextureLoader::CancelLoad(VBlankTextureLoadRequest& request)
{
rtos_lockMutex(&_mutex);
{
if (request._state == VBlankTextureLoadRequestState::LoadRequested || request._state == VBlankTextureLoadRequestState::Loading)
{
_requests.Remove(&request);
request._state = VBlankTextureLoadRequestState::Canceled;
if (_currentRequest == &request)
{
_currentRequest = nullptr;
_currentLoadingOffset = 0;
_currentLoadingStage = LoadingStage::Texture;
}
}
}
rtos_unlockMutex(&_mutex);
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <libtwl/rtos/rtosMutex.h>
#include "VBlankTextureLoadRequest.h"
#include "core/LinkedList.h"
/// @brief Class for loading texture data during vblank.
class VBlankTextureLoader
{
public:
VBlankTextureLoader()
{
rtos_createMutex(&_mutex);
}
void VBlank();
void RequestLoad(VBlankTextureLoadRequest& request);
void CancelLoad(VBlankTextureLoadRequest& request);
private:
enum class LoadingStage
{
Texture,
Palette
};
rtos_mutex_t _mutex;
LinkedList<VBlankTextureLoadRequest, &VBlankTextureLoadRequest::_listLink> _requests;
VBlankTextureLoadRequest* _currentRequest = nullptr;
u32 _currentLoadingOffset = 0;
LoadingStage _currentLoadingStage;
bool LoadTexture();
bool LoadPalette();
};

View File

@@ -0,0 +1,23 @@
#pragma once
#include "IVramManager.h"
class VramContext
{
public:
VramContext(IVramManager* bgVramManager, IVramManager* objVramManager,
IVramManager* texVramManager, IVramManager* texPlttVramManager)
: _bgVramManager(bgVramManager), _objVramManager(objVramManager)
, _texVramManager(texVramManager), _texPlttVramManager(texPlttVramManager)
{ }
IVramManager* GetBgVramManager() const { return _bgVramManager; }
IVramManager* GetObjVramManager() const { return _objVramManager; }
IVramManager* GetTexVramManager() const { return _texVramManager; }
IVramManager* GetTexPlttVramManager() const { return _texPlttVramManager; }
private:
IVramManager* _bgVramManager;
IVramManager* _objVramManager;
IVramManager* _texVramManager;
IVramManager* _texPlttVramManager;
};

View File

@@ -0,0 +1,324 @@
#include "common.h"
#include "nitroFont2.h"
bool nft2_unpack(nft2_header_t* font)
{
if (font->signature != NFT2_SIGNATURE)
return false;
font->glyphInfoPtr = (const nft2_glyph_t*)((u32)font + (u32)font->glyphInfoPtr);
font->charMapPtr = (const nft2_char_map_entry_t*)((u32)font + (u32)font->charMapPtr);
font->glyphDataPtr = (const u8*)((u32)font + (u32)font->glyphDataPtr);
return true;
}
int nft2_findGlyphIdxForCharacter(const nft2_header_t* font, u16 character)
{
const nft2_char_map_entry_t* charMapEntry = font->charMapPtr;
while (charMapEntry->count > 0)
{
if (charMapEntry->startChar <= character && character < charMapEntry->startChar + charMapEntry->count)
return charMapEntry->glyphs[character - charMapEntry->startChar];
charMapEntry = (const nft2_char_map_entry_t*)((u32)charMapEntry + 4 + 2 * charMapEntry->count);
}
return 0;
}
static inline void renderGlyph(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride, bool a5i3)
{
int yOffset = glyph->spacingTop;
u32 xStart = xPos < 0 ? -xPos : 0;
u32 yStart = yPos + yOffset < 0 ? -(yPos + yOffset) : 0;
int xEnd = glyph->glyphWidth;
if (xPos + xEnd > width)
{
// by returning we only render complete glyphs
return;
// old code for rendering partial glyphs
// xEnd = width - xPos;
}
int yEnd = glyph->glyphHeight;
if (yPos + yOffset + yEnd > height)
yEnd = height - (yPos + yOffset); // allow partial glyphs in the vertical direction
const u8* glyphData = &font->glyphDataPtr[glyph->dataOffset];
glyphData += yStart * ((glyph->glyphWidth + 1) >> 1);
for (int y = yStart; y < yEnd; y++)
{
for (int x = xStart; x < xEnd; x++)
{
u32 data = glyphData[x >> 1];
if ((x & 1) == 0)
data &= 0xF;
else
data >>= 4;
if (data == 0)
continue;
u32 finalX = x + xPos;
u32 finalY = y + yPos + yOffset;
if (a5i3)
{
dst[finalY * stride + finalX] = (data << 4) | (data ? (1 << 3) : 0);
}
else
{
u32 tileX = finalX >> 3;
u32 tileY = finalY >> 3;
u32 tileIdx = (tileY >> 1) * stride + (tileX >> 2) * 8 + (tileY & 1) * 4 + (tileX & 3);
u32 offset = tileIdx * 64 + ((finalY & 7) << 3) + (finalX & 7);
u32 value = dst[offset >> 1];
if (offset & 1)
{
u32 prevData = value >> 4;
if (prevData < data)
dst[offset >> 1] = (value & 0xF) | (data << 4);
}
else
{
u32 prevData = value & 0xF;
if (prevData < data)
dst[offset >> 1] = (value & 0xF0) | data;
}
}
}
glyphData += (glyph->glyphWidth + 1) >> 1;
}
}
static ITCM_CODE void renderGlyphTiled(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride)
{
renderGlyph(font, glyph, xPos, yPos, width, height, dst, stride, false);
}
static ITCM_CODE void renderGlyphA5I3(const nft2_header_t* font, const nft2_glyph_t* glyph,
int xPos, int yPos, int width, int height, u8* dst, u32 stride)
{
renderGlyph(font, glyph, xPos, yPos, width, height, dst, stride, true);
}
ITCM_CODE void nft2_renderString(const nft2_header_t* font, const char16_t* string, u8* dst, u32 stride,
nft2_string_render_params_t* renderParams)
{
int xPos = renderParams->x;
int yPos = renderParams->y;
bool a5i3 = renderParams->a5i3;
u32 textWidth = 0;
while (true)
{
u16 c = *string++;
if (c == 0)
break;
if (c == '\n')
{
xPos = renderParams->x;
yPos += font->ascend + font->descend + 1;
if (yPos >= (int)renderParams->height)
break;
continue;
}
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
renderParams->textWidth = textWidth;
}
ITCM_CODE void nft2_measureString(const nft2_header_t* font, const char16_t* string, u32& width, u32& height)
{
int xPos = 0;
int yPos = 0;
u32 textWidth = 0;
while (true)
{
u16 c = *string++;
if (c == 0)
break;
if (c == '\n')
{
xPos = 0;
yPos += font->ascend + font->descend + 1;
continue;
}
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft;
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
width = textWidth;
height = yPos + font->ascend + font->descend;
}
static ITCM_CODE const char16_t* findFirstCharacterThatDoesNotFit(const nft2_header_t* font, const char16_t* string, u32 width)
{
int xPos = 0;
while (true)
{
u16 c = *string++;
if (c == 0 || c == '\n')
return string - 1;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft + glyph->glyphWidth + glyph->spacingRight;
if (xPos > (int)width)
return string - 1;
}
}
static ITCM_CODE const char16_t* findLastCharacterThatFitsBackwards(const nft2_header_t* font, const char16_t* stringStart, const char16_t* stringEnd, u32 width)
{
const char16_t* string = stringEnd;
int xPos = (int)width;
while (string >= stringStart)
{
u16 c = *--string;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
if (string != stringEnd - 1)
xPos -= glyph->spacingRight;
xPos -= glyph->glyphWidth;
xPos -= glyph->spacingLeft;
if (xPos < 0)
return string + 1;
}
return stringStart;
}
static ITCM_CODE int measureEllipsisWidth(const nft2_header_t* font, const char16_t* ellipsisString)
{
int ellipsisWidth = 0;
const char16_t* stringPtr = ellipsisString;
while (true)
{
u16 c = *stringPtr++;
if (c == 0)
break;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
ellipsisWidth += glyph->spacingLeft + glyph->glyphWidth + glyph->spacingRight;
}
return ellipsisWidth;
}
ITCM_CODE void nft2_renderStringEllipsis(const nft2_header_t* font, const char16_t* string, u8* dst,
u32 stride, nft2_string_render_params_t* renderParams, const char16_t* ellipsisString)
{
u32 stringWidth, stringHeight;
nft2_measureString(font, string, stringWidth, stringHeight);
if (stringWidth <= renderParams->width)
{
// no ellipsis needed
nft2_renderString(font, string, dst, stride, renderParams);
return;
}
u32 splitPoint = (renderParams->width - renderParams->x) * 3 / 4;
int ellipsisWidth = measureEllipsisWidth(font, ellipsisString);
u32 splitLeftEnd = splitPoint - (ellipsisWidth >> 1);
u32 splitRightStart = splitPoint + ((ellipsisWidth + 1) >> 1);
const char16_t* endOfFirstPart = findFirstCharacterThatDoesNotFit(font, string, splitLeftEnd);
u32 stringLength = 0;
while (string[stringLength] != 0)
stringLength++;
const char16_t* startOfSecondPart = findLastCharacterThatFitsBackwards(
font, endOfFirstPart, string + stringLength, renderParams->width - renderParams->x - splitRightStart);
int xPos = renderParams->x;
int yPos = renderParams->y;
bool a5i3 = renderParams->a5i3;
u32 textWidth = 0;
const char16_t* stringPtr = string;
while (stringPtr < endOfFirstPart)
{
u16 c = *stringPtr++;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
stringPtr = ellipsisString;
while (true)
{
u16 c = *stringPtr++;
if (c == 0)
break;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
stringPtr = startOfSecondPart;
while (true)
{
u16 c = *stringPtr++;
if (c == 0)
break;
int glyphIdx = nft2_findGlyphIdxForCharacter(font, c);
const nft2_glyph_t* glyph = &font->glyphInfoPtr[glyphIdx];
xPos += glyph->spacingLeft;
if (a5i3)
{
renderGlyphA5I3(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
else
{
renderGlyphTiled(font, glyph, xPos, yPos, renderParams->width, renderParams->height, dst, stride);
}
xPos += glyph->glyphWidth;
if (xPos > (int)textWidth)
textWidth = xPos;
xPos += glyph->spacingRight;
}
renderParams->textWidth = textWidth;
}

View File

@@ -0,0 +1,84 @@
#pragma once
#define NFT2_SIGNATURE 0x3254464E
struct nft2_glyph_t
{
u32 dataOffset : 24;
u32 glyphWidth : 8;
s8 spacingLeft;
s8 spacingRight;
u8 glyphHeight;
s8 spacingTop;
};
struct nft2_char_map_entry_t
{
u16 count;
u16 startChar;
u16 glyphs[1];
};
struct nft2_header_t
{
u32 signature;
const nft2_glyph_t* glyphInfoPtr;
const nft2_char_map_entry_t* charMapPtr;
const u8* glyphDataPtr;
u8 ascend;
u8 descend;
u16 glyphCount;
};
struct nft2_string_render_params_t
{
int x;
int y;
u32 width;
u32 height;
u32 textWidth;
// u32 textHeight;
bool a5i3;
};
/// @brief Prepares the ntf2 data of the given \p font for runtime use.
/// Call this method once after loading a font file.
/// @param font The font to prepare.
/// @return True if preparing was successful, or false otherwise.
bool nft2_unpack(nft2_header_t* font);
/// @brief Finds the glyph index in the given \p font that corresponds to the given \p character.
/// @param font The font the find the glyph index in.
/// @param character The character to find the glyph index for.
/// @return The glyph index if found, or 0 otherwise.
int nft2_findGlyphIdxForCharacter(const nft2_header_t* font, u16 character);
/// @brief Renders the given \p string with the given \p font to the \p dst buffer
/// with the given \p stride and \p renderParams.
/// @param font The font to use.
/// @param string The string to render.
/// @param dst The destination buffer.
/// @param stride The stride of the destination buffer.
/// @param renderParams The render params.
void nft2_renderString(const nft2_header_t* font, const char16_t* string, u8* dst,
u32 stride, nft2_string_render_params_t* renderParams);
/// @brief Measures the given \p string with the given \p font. Returns the \p width and \p height.
/// @param font The font to use.
/// @param string The string to measure.
/// @param width The measured width.
/// @param height The measured height.
void nft2_measureString(const nft2_header_t* font, const char16_t* string, u32& width, u32& height);
/// @brief Renders the given \p string with the given \p font to the \p dst buffer
/// with the given \p stride and \p renderParams. If the string is too long to
/// fit the buffer, \p ellipsisString is rendered at 3/4rd of the string, cutting
/// it in two parts.
/// @param font The font to use.
/// @param string The string to render.
/// @param dst The destination buffer.
/// @param stride The stride of the destination buffer.
/// @param renderParams The render params.
/// @param ellipsisString The ellipsis string.
void nft2_renderStringEllipsis(const nft2_header_t* font, const char16_t* string, u8* dst,
u32 stride, nft2_string_render_params_t* renderParams, const char16_t* ellipsisString);

View File

@@ -0,0 +1,11 @@
#pragma once
#include "InputKey.h"
/// @brief Interface for a source of key input.
class IInputSource
{
public:
virtual ~IInputSource() { }
virtual InputKey Sample() const = 0;
};

View File

@@ -0,0 +1,45 @@
#pragma once
enum class InputKey : u16
{
None = 0,
A = 1 << 0,
B = 1 << 1,
Select = 1 << 2,
Start = 1 << 3,
DpadRight = 1 << 4,
DpadLeft = 1 << 5,
DpadUp = 1 << 6,
DpadDown = 1 << 7,
R = 1 << 8,
L = 1 << 9,
X = 1 << 10,
Y = 1 << 11,
Debug = 1 << 13
};
inline InputKey operator&(InputKey lhs, InputKey rhs)
{
return static_cast<InputKey>(static_cast<u16>(lhs) & static_cast<u16>(rhs));
}
inline InputKey operator|(InputKey lhs, InputKey rhs)
{
return static_cast<InputKey>(static_cast<u16>(lhs) | static_cast<u16>(rhs));
}
inline InputKey operator~(InputKey lhs)
{
return static_cast<InputKey>(~static_cast<u16>(lhs));
}
inline InputKey operator^(InputKey lhs, InputKey rhs)
{
return static_cast<InputKey>(static_cast<u16>(lhs) ^ static_cast<u16>(rhs));
}
inline InputKey operator|=(InputKey& lhs, InputKey rhs)
{
lhs = lhs | rhs; // don't use |= to prevent recursion
return lhs;
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "InputKey.h"
class InputProvider
{
public:
/// @brief Returns a bitmask of the keys currently being held.
/// @return A bitmask of the keys currently being held.
InputKey GetCurrentKeys() const { return _currentKeys; }
/// @brief Returns whether any of the keys in the given \p mask is currently being held.
/// @param mask The mask to check for.
/// @return \c true if any of the keys in the \p mask is being held, or \c false otherwise.
bool Current(InputKey mask) const { return static_cast<bool>(_currentKeys & mask); }
/// @brief Returns a bitmask of the keys that went from unpressed to pressed in the latest update.
/// @return A bitmask of the keys that went from unpressed to pressed in the latest update.
InputKey GetTriggeredKeys() const
{
return _triggeredKeys;
}
/// @brief Returns whether any of the keys in the given \p mask went from unpressed to pressed in the latest update.
/// @param mask The mask to check for.
/// @return \c true if any of the keys in the \p mask went from unpressed to pressed
/// in the latest update, or \c false otherwise.
bool Triggered(InputKey mask) const { return static_cast<bool>(_triggeredKeys & mask); }
/// @brief Returns a bitmask of the keys that went from pressed to unpressed in the latest update.
/// @return A bitmask of the keys that went from pressed to unpressed in the latest update.
InputKey GetReleasedKeys() const { return _releasedKeys; }
/// @brief Returns whether any of the keys in the given \p mask went from pressed to unpressed in the latest update.
/// @param mask The mask to check for.
/// @return \c true if any of the keys in the \p mask went from pressed to unpressed
/// in the latest update, or \c false otherwise.
bool Released(InputKey mask) const { return static_cast<bool>(_releasedKeys & mask); }
/// @brief Updates the input provider.
virtual void Update() = 0;
/// @brief Resets the input provider.
virtual void Reset()
{
_currentKeys = InputKey::None;
_triggeredKeys = InputKey::None;
_releasedKeys = InputKey::None;
}
protected:
InputKey _currentKeys;
InputKey _triggeredKeys;
InputKey _releasedKeys;
InputProvider()
: _currentKeys(InputKey::None), _triggeredKeys(InputKey::None), _releasedKeys(InputKey::None) { }
};

View File

@@ -0,0 +1,58 @@
#include "common.h"
#include "InputRepeater.h"
void InputRepeater::Update()
{
_inputProvider->Update();
InputKey curKeys = _inputProvider->GetCurrentKeys();
InputKey repKeys = InputKey::None;
if (_state == State::Idle)
{
if (static_cast<bool>(curKeys & _repeatMask))
{
_state = State::FirstRepeat;
_frameCounter = 0;
repKeys = curKeys & _repeatMask;
}
}
else if (_state == State::FirstRepeat)
{
if (static_cast<bool>(curKeys & _repeatMask))
{
if (++_frameCounter >= _firstRepeatDelayFrames)
{
_state = State::NextRepeat;
_frameCounter = 0;
repKeys = curKeys & _repeatMask;
}
}
else
_state = State::Idle;
}
else if (_state == State::NextRepeat)
{
if (static_cast<bool>(curKeys & _repeatMask))
{
if (++_frameCounter >= _nextRepeatDelayFrames)
{
_frameCounter = 0;
repKeys = curKeys & _repeatMask;
}
}
else
_state = State::Idle;
}
InputKey lastRepKeys = _currentKeys & _repeatMask;
_currentKeys = curKeys | repKeys;
_triggeredKeys = _inputProvider->GetTriggeredKeys() | repKeys;
_releasedKeys = _inputProvider->GetReleasedKeys() | (lastRepKeys & (lastRepKeys ^ repKeys));
}
void InputRepeater::Reset()
{
InputProvider::Reset();
_inputProvider->Reset();
_state = State::Idle;
_frameCounter = 0;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "InputProvider.h"
/// @brief Input provider that wraps another input provider and adds key repetition on top of it.
class InputRepeater : public InputProvider
{
public:
InputRepeater(InputProvider* inputProvider, InputKey repeatMask, u16 firstRepeatDelay, u16 nextRepeatDelay)
: _inputProvider(inputProvider), _state(State::Idle)
, _frameCounter(0), _repeatMask(repeatMask)
, _firstRepeatDelayFrames(firstRepeatDelay)
, _nextRepeatDelayFrames(nextRepeatDelay) { }
void Update() override;
void Reset() override;
private:
enum class State
{
Idle,
FirstRepeat,
NextRepeat
};
InputProvider* _inputProvider;
State _state;
u16 _frameCounter;
InputKey _repeatMask;
u16 _firstRepeatDelayFrames;
u16 _nextRepeatDelayFrames;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "common.h"
#include <nds/input.h>
#include "IInputSource.h"
#include "sharedMemory.h"
/// @brief Input source from the physical DS buttons.
class PadInputSource : public IInputSource
{
public:
InputKey Sample() const override
{
u16 arm9Mask = (~REG_KEYINPUT & 0x3FF);
u16 arm7Mask = (~SHARED_KEY_XY & 0xB);
return static_cast<InputKey>((arm9Mask | (arm7Mask << 10)));
}
};

View File

@@ -0,0 +1,22 @@
#include "common.h"
#include "SampledInputProvider.h"
void SampledInputProvider::Update()
{
InputKey curKeys = _currentKeys;
InputKey trig = InputKey::None;
InputKey rel = InputKey::None;
while (_inputBufferReadPtr != _inputBufferWritePtr)
{
InputKey nextKeys = _inputBuffer[_inputBufferReadPtr];
trig |= (nextKeys ^ curKeys) & nextKeys;
rel |= (nextKeys ^ curKeys) & curKeys;
curKeys = nextKeys;
_inputBufferReadPtr = (_inputBufferReadPtr + 1) & 3;
}
_triggeredKeys = trig;
_releasedKeys = rel;
_currentKeys = curKeys;
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "InputProvider.h"
#include "IInputSource.h"
/// @brief Input provider providing input from an \see IInputSource.
class SampledInputProvider : public InputProvider
{
public:
explicit SampledInputProvider(const IInputSource* inputSource)
: _inputSource(inputSource), _inputBufferReadPtr(0), _inputBufferWritePtr(0) { }
void Update() override;
/// @brief Samples the input source.
void Sample()
{
_inputBuffer[_inputBufferWritePtr] = _inputSource->Sample();
_inputBufferWritePtr = (_inputBufferWritePtr + 1) & 3;
}
void Reset() override
{
InputProvider::Reset();
_inputBufferReadPtr = 0;
_inputBufferWritePtr = 0;
}
private:
const IInputSource* _inputSource;
InputKey _inputBuffer[4];
u8 _inputBufferReadPtr;
u8 _inputBufferWritePtr;
};

View File

@@ -0,0 +1,62 @@
#pragma once
#include "animation/LinearCurve.h"
#include "animation/CubicBezierCurve.h"
#include "animation/ThreePointCubicBezierCurve.h"
namespace md::sys
{
enum class color
{
primary,
onPrimary,
secondaryContainer,
onSecondaryContainer,
onSurfaceVariant,
outline,
onSurface,
inverseOnSurface,
surfaceContainerLow = inverseOnSurface, // basically the same color
surfaceContainerHighest,
surfaceBright,
Max = surfaceBright
};
}
namespace md::sys::motion::duration
{
#define MILLISECONDS_TO_FRAMES(x) ((x) * 60 / 1000)
constexpr int short1 = MILLISECONDS_TO_FRAMES(50);
constexpr int short2 = MILLISECONDS_TO_FRAMES(100);
constexpr int short3 = MILLISECONDS_TO_FRAMES(150);
constexpr int short4 = MILLISECONDS_TO_FRAMES(200);
constexpr int medium1 = MILLISECONDS_TO_FRAMES(250);
constexpr int medium2 = MILLISECONDS_TO_FRAMES(300);
constexpr int medium3 = MILLISECONDS_TO_FRAMES(350);
constexpr int medium4 = MILLISECONDS_TO_FRAMES(400);
constexpr int long1 = MILLISECONDS_TO_FRAMES(450);
constexpr int long2 = MILLISECONDS_TO_FRAMES(500);
constexpr int long3 = MILLISECONDS_TO_FRAMES(550);
constexpr int long4 = MILLISECONDS_TO_FRAMES(600);
constexpr int extraLong1 = MILLISECONDS_TO_FRAMES(700);
constexpr int extraLong2 = MILLISECONDS_TO_FRAMES(800);
constexpr int extraLong3 = MILLISECONDS_TO_FRAMES(900);
constexpr int extraLong4 = MILLISECONDS_TO_FRAMES(1000);
#undef MILLISECONDS_TO_FRAMES
}
namespace md::sys::motion::easing
{
constexpr auto linear = LinearCurve();
constexpr auto standard = CubicBezierCurve(0.2, 0, 0, 1);
constexpr auto standardAccelerate = CubicBezierCurve(0.3, 0, 1, 1);
constexpr auto standardDecelerate = CubicBezierCurve(0, 0, 0, 1);
constexpr auto emphasized = ThreePointCubicBezierCurve(
0.05, 0, 0.133333, 0.06,
0.166666, 0.4,
0.208333, 0.82, 0.25, 1);
constexpr auto emphasizedDecelerate = CubicBezierCurve(0.05, 0.7, 0.1, 1);
constexpr auto emphasizedAccelerate = CubicBezierCurve(0.3, 0, 0.8, 0.15);
}

View File

@@ -0,0 +1,13 @@
#include "common.h"
#include "DirectPalette.h"
u32 DirectPalette::GetHashCode() const
{
u32 hashcode = 1430287;
#pragma GCC unroll 8
for (int i = 0; i < 8; i++)
{
hashcode = hashcode * 7302013 + ((u32*)_colors)[i];
}
return hashcode;
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include <string.h>
#include "IPalette.h"
/// @brief Palette of 16 specified colors.
class alignas(4) DirectPalette : public IPalette
{
public:
explicit DirectPalette(const u16* colors)
{
memcpy(_colors, colors, sizeof(_colors));
}
void GetColors(u16* dst) const override
{
memcpy(dst, _colors, sizeof(_colors));
}
u32 GetHashCode() const override;
private:
u16 _colors[16];
};

View File

@@ -0,0 +1,14 @@
#include "common.h"
#include "GradientPalette.h"
u32 GradientPalette::GetHashCode() const
{
u32 hashcode = 1430287;
hashcode = hashcode * 7302013 + _fromColor.r;
hashcode = hashcode * 7302013 + _fromColor.g;
hashcode = hashcode * 7302013 + _fromColor.b;
hashcode = hashcode * 7302013 + _toColor.r;
hashcode = hashcode * 7302013 + _toColor.g;
hashcode = hashcode * 7302013 + _toColor.b;
return hashcode;
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "core/math/Rgb.h"
#include "core/math/RgbMixer.h"
#include "IPalette.h"
/// @brief Gradient palette between two colors.
class GradientPalette : public IPalette
{
public:
GradientPalette(const Rgb<8, 8, 8>& from, const Rgb<8, 8, 8>& to)
: _fromColor(from), _toColor(to) { }
void GetColors(u16* dst) const override
{
RgbMixer::MakeGradientPalette(dst, _fromColor, _toColor);
}
u32 GetHashCode() const override;
private:
Rgb<8, 8, 8> _fromColor;
Rgb<8, 8, 8> _toColor;
};

View File

@@ -0,0 +1,18 @@
#pragma once
/// @brief Interface for a color palette.
class IPalette
{
public:
virtual ~IPalette() = 0;
/// @brief Gets the colors of the palette.
/// @param dst A pointer to an array the colors should be written to.
virtual void GetColors(u16* dst) const = 0;
/// @brief Gets a hash that represents this color palette.
/// @return The hash.
virtual u32 GetHashCode() const = 0;
};
inline IPalette::~IPalette() { }

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;
};