2020-09-21 17:17:34 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-06-28 16:39:18 +00:00
|
|
|
//
|
2020-09-21 17:17:34 +00:00
|
|
|
// EmulationStation Desktop Edition
|
2020-06-28 16:39:18 +00:00
|
|
|
// ImageGridComponent.cpp
|
|
|
|
//
|
|
|
|
// X*Y image grid, used by GridGameListView.
|
|
|
|
//
|
|
|
|
|
2017-10-31 17:12:50 +00:00
|
|
|
#ifndef ES_CORE_COMPONENTS_IMAGE_GRID_COMPONENT_H
|
|
|
|
#define ES_CORE_COMPONENTS_IMAGE_GRID_COMPONENT_H
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2018-04-15 12:29:02 +00:00
|
|
|
#include "Log.h"
|
2019-07-06 14:50:50 +00:00
|
|
|
#include "animations/LambdaAnimation.h"
|
2014-06-20 01:30:09 +00:00
|
|
|
#include "components/IList.h"
|
2017-11-01 22:21:10 +00:00
|
|
|
#include "resources/TextureResource.h"
|
2018-04-07 19:23:10 +00:00
|
|
|
#include "GridTileComponent.h"
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2019-07-06 14:50:50 +00:00
|
|
|
#define EXTRAITEMS 2
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
enum ScrollDirection {
|
|
|
|
SCROLL_VERTICALLY,
|
|
|
|
SCROLL_HORIZONTALLY
|
2018-04-08 22:58:30 +00:00
|
|
|
};
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
enum ImageSource {
|
|
|
|
THUMBNAIL,
|
|
|
|
IMAGE,
|
|
|
|
MARQUEE
|
2019-07-06 14:50:50 +00:00
|
|
|
};
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
struct ImageGridData {
|
|
|
|
std::string texturePath;
|
2014-02-08 03:45:28 +00:00
|
|
|
};
|
|
|
|
|
2013-12-01 01:04:46 +00:00
|
|
|
template<typename T>
|
2014-02-13 23:10:28 +00:00
|
|
|
class ImageGridComponent : public IList<ImageGridData, T>
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2014-02-17 17:40:31 +00:00
|
|
|
protected:
|
2020-06-28 16:39:18 +00:00
|
|
|
using IList<ImageGridData, T>::mEntries;
|
|
|
|
using IList<ImageGridData, T>::mScrollTier;
|
|
|
|
using IList<ImageGridData, T>::listUpdate;
|
|
|
|
using IList<ImageGridData, T>::listInput;
|
|
|
|
using IList<ImageGridData, T>::listRenderTitleOverlay;
|
|
|
|
using IList<ImageGridData, T>::getTransform;
|
|
|
|
using IList<ImageGridData, T>::mSize;
|
|
|
|
using IList<ImageGridData, T>::mCursor;
|
|
|
|
using IList<ImageGridData, T>::mWindow;
|
|
|
|
// The following change is required for compilation with Clang.
|
|
|
|
// http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2070
|
|
|
|
// using IList<ImageGridData, T>::Entry;
|
|
|
|
using IList<ImageGridData, T>::IList;
|
2014-02-17 17:40:31 +00:00
|
|
|
|
2013-12-01 01:04:46 +00:00
|
|
|
public:
|
2020-06-28 16:39:18 +00:00
|
|
|
using IList<ImageGridData, T>::size;
|
|
|
|
using IList<ImageGridData, T>::isScrolling;
|
|
|
|
using IList<ImageGridData, T>::stopScrolling;
|
2014-02-17 17:40:31 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
ImageGridComponent(Window* window);
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
void add(const std::string& name, const std::string& imagePath, const T& obj);
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
bool input(InputConfig* config, Input input) override;
|
|
|
|
void update(int deltaTime) override;
|
|
|
|
void render(const Transform4x4f& parentTrans) override;
|
|
|
|
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view,
|
|
|
|
const std::string& element, unsigned int properties) override;
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
void onSizeChanged() override;
|
|
|
|
inline void setCursorChangedCallback(const std::function<void(CursorState state)>& func)
|
|
|
|
{ mCursorChangedCallback = func; }
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
ImageSource getImageSource() { return mImageSource; };
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2018-04-12 08:25:11 +00:00
|
|
|
protected:
|
2020-06-28 16:39:18 +00:00
|
|
|
virtual void onCursorChanged(const CursorState& state) override;
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2018-04-12 08:25:11 +00:00
|
|
|
private:
|
2020-06-28 16:39:18 +00:00
|
|
|
// Tiles.
|
|
|
|
void buildTiles();
|
|
|
|
void updateTiles(bool ascending = true, bool allowAnimation = true,
|
|
|
|
bool updateSelectedState = true);
|
|
|
|
void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation, bool updateSelectedState);
|
|
|
|
void calcGridDimension();
|
|
|
|
bool isScrollLoop();
|
|
|
|
|
|
|
|
bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY; };
|
|
|
|
|
|
|
|
// Images and entries.
|
|
|
|
bool mEntriesDirty;
|
|
|
|
int mLastCursor;
|
|
|
|
std::string mDefaultGameTexture;
|
|
|
|
std::string mDefaultFolderTexture;
|
|
|
|
|
|
|
|
// Tiles.
|
|
|
|
bool mLastRowPartial;
|
|
|
|
Vector2f mAutoLayout;
|
|
|
|
float mAutoLayoutZoom;
|
|
|
|
|
|
|
|
Vector4f mPadding;
|
|
|
|
Vector2f mMargin;
|
|
|
|
Vector2f mTileSize;
|
|
|
|
Vector2i mGridDimension;
|
|
|
|
std::shared_ptr<ThemeData> mTheme;
|
|
|
|
std::vector< std::shared_ptr<GridTileComponent> > mTiles;
|
|
|
|
|
|
|
|
int mStartPosition;
|
|
|
|
|
|
|
|
float mCamera;
|
|
|
|
float mCameraDirection;
|
|
|
|
|
|
|
|
// Miscellaneous.
|
|
|
|
bool mAnimate;
|
|
|
|
bool mCenterSelection;
|
|
|
|
bool mScrollLoop;
|
|
|
|
ScrollDirection mScrollDirection;
|
|
|
|
ImageSource mImageSource;
|
|
|
|
std::function<void(CursorState state)> mCursorChangedCallback;
|
2013-12-01 01:04:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
template<typename T>
|
2014-02-17 17:40:31 +00:00
|
|
|
ImageGridComponent<T>::ImageGridComponent(Window* window) : IList<ImageGridData, T>(window)
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
Vector2f screen = Vector2f((float)Renderer::getScreenWidth(),
|
|
|
|
(float)Renderer::getScreenHeight());
|
2018-03-22 07:03:12 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mCamera = 0.0;
|
|
|
|
mCameraDirection = 1.0;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mAutoLayout = Vector2f::Zero();
|
|
|
|
mAutoLayoutZoom = 1.0;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mStartPosition = 0;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mEntriesDirty = true;
|
|
|
|
mLastCursor = 0;
|
|
|
|
mDefaultGameTexture = ":/graphics/cartridge.svg";
|
|
|
|
mDefaultFolderTexture = ":/graphics/folder.svg";
|
2018-03-22 07:03:12 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mSize = screen * 0.80f;
|
|
|
|
mMargin = screen * 0.07f;
|
|
|
|
mPadding = Vector4f::Zero();
|
|
|
|
mTileSize = GridTileComponent::getDefaultTileSize();
|
2018-04-07 19:23:10 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mAnimate = true;
|
|
|
|
mCenterSelection = false;
|
|
|
|
mScrollLoop = false;
|
|
|
|
mScrollDirection = SCROLL_VERTICALLY;
|
|
|
|
mImageSource = THUMBNAIL;
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
2014-02-08 03:45:28 +00:00
|
|
|
void ImageGridComponent<T>::add(const std::string& name, const std::string& imagePath, const T& obj)
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
typename IList<ImageGridData, T>::Entry entry;
|
|
|
|
entry.name = name;
|
|
|
|
entry.object = obj;
|
|
|
|
entry.data.texturePath = imagePath;
|
2018-04-15 12:29:02 +00:00
|
|
|
|
2020-08-30 20:25:38 +00:00
|
|
|
static_cast<IList<ImageGridData, T>*>(this)->add(entry);
|
2020-06-28 16:39:18 +00:00
|
|
|
mEntriesDirty = true;
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
bool ImageGridComponent<T>::input(InputConfig* config, Input input)
|
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
if (input.value != 0) {
|
|
|
|
int idx = isVertical() ? 0 : 1;
|
|
|
|
|
|
|
|
Vector2i dir = Vector2i::Zero();
|
|
|
|
if (config->isMappedLike("up", input))
|
|
|
|
dir[1 ^ idx] = -1;
|
|
|
|
else if (config->isMappedLike("down", input))
|
|
|
|
dir[1 ^ idx] = 1;
|
|
|
|
else if (config->isMappedLike("left", input))
|
|
|
|
dir[0 ^ idx] = -1;
|
|
|
|
else if (config->isMappedLike("right", input))
|
|
|
|
dir[0 ^ idx] = 1;
|
|
|
|
|
|
|
|
if (dir != Vector2i::Zero()) {
|
|
|
|
if (isVertical())
|
|
|
|
listInput(dir.x() + dir.y() * mGridDimension.x());
|
|
|
|
else
|
|
|
|
listInput(dir.x() + dir.y() * mGridDimension.y());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (config->isMappedLike("up", input) || config->isMappedLike("down", input) ||
|
|
|
|
config->isMappedLike("left", input) || config->isMappedLike("right", input))
|
|
|
|
stopScrolling();
|
|
|
|
}
|
|
|
|
|
|
|
|
return GuiComponent::input(config, input);
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
void ImageGridComponent<T>::update(int deltaTime)
|
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
GuiComponent::update(deltaTime);
|
|
|
|
listUpdate(deltaTime);
|
2018-04-07 19:23:10 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
for (auto it = mTiles.begin(); it != mTiles.end(); it++)
|
|
|
|
(*it)->update(deltaTime);
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
2017-10-28 20:24:35 +00:00
|
|
|
void ImageGridComponent<T>::render(const Transform4x4f& parentTrans)
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
Transform4x4f trans = getTransform() * parentTrans;
|
|
|
|
Transform4x4f tileTrans = trans;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
float offsetX = isVertical() ? 0.0f : mCamera * mCameraDirection *
|
|
|
|
(mTileSize.x() + mMargin.x());
|
|
|
|
float offsetY = isVertical() ? mCamera * mCameraDirection *
|
|
|
|
(mTileSize.y() + mMargin.y()) : 0.0f;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
tileTrans.translate(Vector3f(offsetX, offsetY, 0.0));
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mEntriesDirty) {
|
|
|
|
updateTiles();
|
|
|
|
mEntriesDirty = false;
|
|
|
|
}
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Create a clipRect to hide tiles used to buffer texture loading.
|
|
|
|
float scaleX = trans.r0().x();
|
|
|
|
float scaleY = trans.r1().y();
|
2018-05-09 15:01:34 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
Vector2i pos((int)Math::round(trans.translation()[0]),
|
|
|
|
(int)Math::round(trans.translation()[1]));
|
|
|
|
Vector2i size((int)Math::round(mSize.x() * scaleX),
|
|
|
|
(int)Math::round(mSize.y() * scaleY));
|
2018-05-09 15:01:34 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
Renderer::pushClipRect(pos, size);
|
2018-05-09 15:01:34 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Render all the tiles but the selected one.
|
|
|
|
std::shared_ptr<GridTileComponent> selectedTile = nullptr;
|
|
|
|
for (auto it = mTiles.begin(); it != mTiles.end(); it++) {
|
|
|
|
std::shared_ptr<GridTileComponent> tile = (*it);
|
|
|
|
// If it's the selected image, keep it for later, otherwise render it now.
|
|
|
|
if (tile->isSelected())
|
|
|
|
selectedTile = tile;
|
|
|
|
else
|
|
|
|
tile->render(tileTrans);
|
|
|
|
}
|
2018-04-12 08:25:11 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
Renderer::popClipRect();
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Render the selected image on top of the others.
|
|
|
|
if (selectedTile != nullptr)
|
|
|
|
selectedTile->render(tileTrans);
|
2018-05-09 15:01:34 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
listRenderTitleOverlay(trans);
|
2018-03-22 07:03:12 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
GuiComponent::renderChildren(trans);
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
2018-03-31 13:59:14 +00:00
|
|
|
template<typename T>
|
2020-06-28 16:39:18 +00:00
|
|
|
void ImageGridComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
|
|
|
const std::string& view, const std::string& element, unsigned int properties)
|
2018-03-31 13:59:14 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
// Apply theme to GuiComponent but not the size property, which will be applied
|
|
|
|
// at the end of this function.
|
|
|
|
GuiComponent::applyTheme(theme, view, element, properties ^ ThemeFlags::SIZE);
|
|
|
|
|
|
|
|
// Keep the theme pointer to apply it on the tiles later on.
|
|
|
|
mTheme = theme;
|
|
|
|
|
|
|
|
Vector2f screen = Vector2f((float)Renderer::getScreenWidth(),
|
|
|
|
(float)Renderer::getScreenHeight());
|
|
|
|
|
|
|
|
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "imagegrid");
|
|
|
|
if (elem) {
|
|
|
|
if (elem->has("margin"))
|
|
|
|
mMargin = elem->get<Vector2f>("margin") * screen;
|
|
|
|
|
|
|
|
if (elem->has("padding"))
|
|
|
|
mPadding = elem->get<Vector4f>("padding") *
|
|
|
|
Vector4f(screen.x(), screen.y(), screen.x(), screen.y());
|
|
|
|
|
|
|
|
if (elem->has("autoLayout"))
|
|
|
|
mAutoLayout = elem->get<Vector2f>("autoLayout");
|
|
|
|
|
|
|
|
if (elem->has("autoLayoutSelectedZoom"))
|
|
|
|
mAutoLayoutZoom = elem->get<float>("autoLayoutSelectedZoom");
|
|
|
|
|
|
|
|
if (elem->has("imageSource")) {
|
|
|
|
auto direction = elem->get<std::string>("imageSource");
|
|
|
|
if (direction == "image")
|
|
|
|
mImageSource = IMAGE;
|
|
|
|
else if (direction == "marquee")
|
|
|
|
mImageSource = MARQUEE;
|
|
|
|
else
|
|
|
|
mImageSource = THUMBNAIL;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
mImageSource = THUMBNAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (elem->has("scrollDirection"))
|
|
|
|
mScrollDirection = (ScrollDirection)(elem->
|
|
|
|
get<std::string>("scrollDirection") == "horizontal");
|
|
|
|
|
|
|
|
if (elem->has("centerSelection")) {
|
|
|
|
mCenterSelection = (elem->get<bool>("centerSelection"));
|
|
|
|
|
|
|
|
if (elem->has("scrollLoop"))
|
|
|
|
mScrollLoop = (elem->get<bool>("scrollLoop"));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (elem->has("animate"))
|
|
|
|
mAnimate = (elem->get<bool>("animate"));
|
|
|
|
else
|
|
|
|
mAnimate = true;
|
|
|
|
|
|
|
|
if (elem->has("gameImage")) {
|
|
|
|
std::string path = elem->get<std::string>("gameImage");
|
|
|
|
|
|
|
|
if (!ResourceManager::getInstance()->fileExists(path)) {
|
|
|
|
LOG(LogWarning) << "Could not replace default game image, check path: " << path;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
std::string oldDefaultGameTexture = mDefaultGameTexture;
|
|
|
|
mDefaultGameTexture = path;
|
|
|
|
|
|
|
|
// mEntries are already loaded at this point, so we need to
|
|
|
|
// update them with the new game image texture.
|
|
|
|
for (auto it = mEntries.begin(); it != mEntries.end(); it++) {
|
|
|
|
if ((*it).data.texturePath == oldDefaultGameTexture)
|
|
|
|
(*it).data.texturePath = mDefaultGameTexture;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (elem->has("folderImage")) {
|
|
|
|
std::string path = elem->get<std::string>("folderImage");
|
|
|
|
|
|
|
|
if (!ResourceManager::getInstance()->fileExists(path)) {
|
|
|
|
LOG(LogWarning) << "Could not replace default folder image, check path: " << path;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
std::string oldDefaultFolderTexture = mDefaultFolderTexture;
|
|
|
|
mDefaultFolderTexture = path;
|
|
|
|
|
|
|
|
// mEntries are already loaded at this point, so we need to
|
|
|
|
// update them with new folder image texture.
|
|
|
|
for (auto it = mEntries.begin(); it != mEntries.end(); it++) {
|
|
|
|
if ((*it).data.texturePath == oldDefaultFolderTexture)
|
|
|
|
(*it).data.texturePath = mDefaultFolderTexture;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We still need to manually get the grid tile size here, so we can recalculate
|
|
|
|
// the new grid dimension, and THEN (re)build the tiles.
|
|
|
|
elem = theme->getElement(view, "default", "gridtile");
|
|
|
|
|
|
|
|
mTileSize = elem && elem->has("size") ?
|
|
|
|
elem->get<Vector2f>("size") * screen :
|
|
|
|
GridTileComponent::getDefaultTileSize();
|
|
|
|
|
|
|
|
// Apply size property which will trigger a call to onSizeChanged() which will build the tiles.
|
|
|
|
GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE);
|
|
|
|
|
|
|
|
// Trigger the call manually if the theme have no "imagegrid" element.
|
|
|
|
if (!elem)
|
|
|
|
buildTiles();
|
2018-03-31 13:59:14 +00:00
|
|
|
}
|
|
|
|
|
2013-12-01 01:04:46 +00:00
|
|
|
template<typename T>
|
2018-04-12 08:25:11 +00:00
|
|
|
void ImageGridComponent<T>::onSizeChanged()
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
buildTiles();
|
|
|
|
updateTiles();
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
2018-04-12 08:25:11 +00:00
|
|
|
void ImageGridComponent<T>::onCursorChanged(const CursorState& state)
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mLastCursor == mCursor) {
|
|
|
|
if (state == CURSOR_STOPPED && mCursorChangedCallback)
|
|
|
|
mCursorChangedCallback(state);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool direction = mCursor >= mLastCursor;
|
|
|
|
int diff = direction ? mCursor - mLastCursor : mLastCursor - mCursor;
|
|
|
|
if (isScrollLoop() && diff == mEntries.size() - 1)
|
|
|
|
direction = !direction;
|
|
|
|
|
|
|
|
int oldStart = mStartPosition;
|
|
|
|
|
|
|
|
int dimScrollable = (isVertical() ? mGridDimension.y() : mGridDimension.x()) - 2 * EXTRAITEMS;
|
|
|
|
int dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y();
|
|
|
|
|
|
|
|
int centralCol = (int)(dimScrollable - 0.5) / 2;
|
|
|
|
int maxCentralCol = dimScrollable / 2;
|
|
|
|
|
|
|
|
int oldCol = (mLastCursor / dimOpposite);
|
|
|
|
int col = (mCursor / dimOpposite);
|
|
|
|
|
|
|
|
int lastCol = ((mEntries.size() - 1) / dimOpposite);
|
|
|
|
|
|
|
|
int lastScroll = std::max(0, (lastCol + 1 - dimScrollable));
|
|
|
|
|
|
|
|
float startPos = 0;
|
|
|
|
float endPos = 1;
|
|
|
|
|
|
|
|
if (((GuiComponent*)this)->isAnimationPlaying(2)) {
|
|
|
|
startPos = 0;
|
|
|
|
((GuiComponent*)this)->cancelAnimation(2);
|
|
|
|
updateTiles(direction, false, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mAnimate) {
|
|
|
|
std::shared_ptr<GridTileComponent> oldTile = nullptr;
|
|
|
|
std::shared_ptr<GridTileComponent> newTile = nullptr;
|
|
|
|
|
|
|
|
int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS);
|
|
|
|
if (oldIdx >= 0 && oldIdx < mTiles.size())
|
|
|
|
oldTile = mTiles[oldIdx];
|
|
|
|
|
|
|
|
int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS);
|
|
|
|
if (isScrollLoop()) {
|
|
|
|
if (newIdx < 0)
|
|
|
|
newIdx += mEntries.size();
|
|
|
|
else if (newIdx >= mTiles.size())
|
|
|
|
newIdx -= mEntries.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newIdx >= 0 && newIdx < mTiles.size())
|
|
|
|
newTile = mTiles[newIdx];
|
|
|
|
|
|
|
|
for (auto it = mTiles.begin(); it != mTiles.end(); it++) {
|
|
|
|
if ((*it)->isSelected() && *it != oldTile && *it != newTile) {
|
|
|
|
startPos = 0;
|
|
|
|
(*it)->setSelected(false, false, nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Vector3f oldPos = Vector3f::Zero();
|
|
|
|
|
|
|
|
if (oldTile != nullptr && oldTile != newTile) {
|
|
|
|
oldPos = oldTile->getBackgroundPosition();
|
|
|
|
oldTile->setSelected(false, true, nullptr, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newTile != nullptr)
|
|
|
|
newTile->setSelected(true, true, oldPos == Vector3f::Zero() ? nullptr : &oldPos, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
int firstVisibleCol = mStartPosition / dimOpposite;
|
|
|
|
|
|
|
|
if ((col < centralCol || (col == 0 && col == centralCol)) && !mCenterSelection) {
|
|
|
|
mStartPosition = 0;
|
|
|
|
}
|
|
|
|
else if ((col - centralCol) > lastScroll && !mCenterSelection && !isScrollLoop()) {
|
|
|
|
mStartPosition = lastScroll * dimOpposite;
|
|
|
|
}
|
|
|
|
else if ((maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol) ||
|
|
|
|
col == firstVisibleCol + centralCol) {
|
|
|
|
if (col == firstVisibleCol + maxCentralCol)
|
|
|
|
mStartPosition = (col - maxCentralCol) * dimOpposite;
|
|
|
|
else
|
|
|
|
mStartPosition = (col - centralCol) * dimOpposite;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (oldCol == firstVisibleCol + maxCentralCol)
|
|
|
|
mStartPosition = (col - maxCentralCol) * dimOpposite;
|
|
|
|
else
|
|
|
|
mStartPosition = (col - centralCol) * dimOpposite;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto lastCursor = mLastCursor;
|
|
|
|
mLastCursor = mCursor;
|
|
|
|
|
|
|
|
mCameraDirection = direction ? -1.0f : 1.0f;
|
|
|
|
mCamera = 0;
|
|
|
|
|
|
|
|
if (lastCursor < 0 || !mAnimate) {
|
|
|
|
updateTiles(direction, mAnimate && (lastCursor >= 0 || isScrollLoop()));
|
|
|
|
|
|
|
|
if (mCursorChangedCallback)
|
|
|
|
mCursorChangedCallback(state);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mCursorChangedCallback)
|
|
|
|
mCursorChangedCallback(state);
|
|
|
|
|
|
|
|
bool moveCamera = (oldStart != mStartPosition);
|
|
|
|
|
|
|
|
auto func = [this, startPos, endPos, moveCamera](float t) {
|
|
|
|
if (!moveCamera)
|
|
|
|
return;
|
|
|
|
|
|
|
|
t -= 1; // cubic ease out
|
|
|
|
float pct = Math::lerp(0, 1, t*t*t + 1);
|
|
|
|
t = startPos * (1.0f - pct) + endPos * pct;
|
|
|
|
mCamera = t;
|
|
|
|
};
|
|
|
|
|
|
|
|
((GuiComponent*)this)->setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] {
|
|
|
|
mCamera = 0;
|
|
|
|
updateTiles(direction, false);
|
|
|
|
}, false, 2);
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Create and position tiles (mTiles).
|
2013-12-01 01:04:46 +00:00
|
|
|
template<typename T>
|
2018-04-12 08:25:11 +00:00
|
|
|
void ImageGridComponent<T>::buildTiles()
|
2013-12-01 01:04:46 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
mStartPosition = 0;
|
|
|
|
mTiles.clear();
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
calcGridDimension();
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mCenterSelection) {
|
|
|
|
int dimScrollable = (isVertical() ? mGridDimension.y() :
|
|
|
|
mGridDimension.x()) - 2 * EXTRAITEMS;
|
|
|
|
mStartPosition -= (int) Math::floorf(dimScrollable / 2.0f);
|
|
|
|
}
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
Vector2f tileDistance = mTileSize + mMargin;
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) {
|
|
|
|
auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - mPadding.x() -
|
|
|
|
mPadding.z()) / (int) mAutoLayout.x();
|
|
|
|
auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - mPadding.y() -
|
|
|
|
mPadding.w()) / (int) mAutoLayout.y();
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mTileSize = Vector2f(x, y);
|
|
|
|
tileDistance = mTileSize + mMargin;
|
|
|
|
}
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
bool vert = isVertical();
|
|
|
|
Vector2f startPosition = mTileSize / 2;
|
|
|
|
startPosition += mPadding.v2();
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
int X;
|
|
|
|
int Y;
|
2018-04-08 22:58:30 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Layout tile size and position.
|
|
|
|
for (int y = 0; y < (vert ? mGridDimension.y() : mGridDimension.x()); y++) {
|
|
|
|
for (int x = 0; x < (vert ? mGridDimension.x() : mGridDimension.y()); x++) {
|
|
|
|
// Create tiles.
|
|
|
|
auto tile = std::make_shared<GridTileComponent>(mWindow);
|
2018-04-07 19:23:10 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// In Vertical mod, tiles are ordered from left to right, then from top to bottom.
|
|
|
|
// In Horizontal mod, tiles are ordered from top to bottom, then from left to right.
|
|
|
|
X = vert ? x : y - EXTRAITEMS;
|
|
|
|
Y = vert ? y - EXTRAITEMS : x;
|
2018-04-08 22:58:30 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
tile->setPosition(X * tileDistance.x() + startPosition.x(), Y *
|
|
|
|
tileDistance.y() + startPosition.y());
|
|
|
|
tile->setOrigin(0.5f, 0.5f);
|
|
|
|
tile->setImage("");
|
2018-04-07 19:23:10 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mTheme)
|
|
|
|
tile->applyTheme(mTheme, "grid", "gridtile", ThemeFlags::ALL);
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0)
|
|
|
|
tile->forceSize(mTileSize, mAutoLayoutZoom);
|
2019-07-06 14:50:50 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
mTiles.push_back(tile);
|
|
|
|
}
|
|
|
|
}
|
2013-12-01 01:04:46 +00:00
|
|
|
}
|
|
|
|
|
2018-04-12 08:25:11 +00:00
|
|
|
template<typename T>
|
2020-06-28 16:39:18 +00:00
|
|
|
void ImageGridComponent<T>::updateTiles(bool ascending, bool allowAnimation,
|
|
|
|
bool updateSelectedState)
|
2018-04-12 08:25:11 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
if (!mTiles.size())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Stop updating the tiles at highest scroll speed.
|
|
|
|
if (mScrollTier == 3) {
|
|
|
|
for (int ti = 0; ti < (int)mTiles.size(); ti++) {
|
|
|
|
std::shared_ptr<GridTileComponent> tile = mTiles.at(ti);
|
|
|
|
|
|
|
|
tile->setSelected(false);
|
|
|
|
tile->setImage(mDefaultGameTexture);
|
|
|
|
tile->setVisible(false);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Temporary store previous texture so they can't be unloaded.
|
|
|
|
std::vector<std::shared_ptr<TextureResource>> previousTextures;
|
|
|
|
for (int ti = 0; ti < (int)mTiles.size(); ti++) {
|
|
|
|
std::shared_ptr<GridTileComponent> tile = mTiles.at(ti);
|
|
|
|
previousTextures.push_back(tile->getTexture());
|
|
|
|
}
|
|
|
|
|
|
|
|
// If going down, update from top to bottom.
|
|
|
|
// If going up, update from bottom to top.
|
|
|
|
int scrollDirection = ascending ? 1 : -1;
|
|
|
|
int ti = ascending ? 0 : (int)mTiles.size() - 1;
|
|
|
|
int end = ascending ? (int)mTiles.size() : -1;
|
|
|
|
int img = mStartPosition + ti;
|
|
|
|
|
|
|
|
img -= EXTRAITEMS * (isVertical() ? mGridDimension.x() : mGridDimension.y());
|
|
|
|
|
|
|
|
// Update the tiles.
|
|
|
|
while (ti != end) {
|
|
|
|
updateTileAtPos(ti, img, allowAnimation, updateSelectedState);
|
|
|
|
|
|
|
|
ti += scrollDirection;
|
|
|
|
img += scrollDirection;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (updateSelectedState)
|
|
|
|
mLastCursor = mCursor;
|
|
|
|
|
|
|
|
mLastCursor = mCursor;
|
2018-04-15 13:20:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template<typename T>
|
2020-06-28 16:39:18 +00:00
|
|
|
void ImageGridComponent<T>::updateTileAtPos(int tilePos, int imgPos,
|
|
|
|
bool allowAnimation, bool updateSelectedState)
|
2018-04-15 13:20:49 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
std::shared_ptr<GridTileComponent> tile = mTiles.at(tilePos);
|
|
|
|
|
|
|
|
if (isScrollLoop()) {
|
|
|
|
if (imgPos < 0)
|
|
|
|
imgPos += mEntries.size();
|
|
|
|
else if (imgPos >= size())
|
|
|
|
imgPos -= mEntries.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have more tiles than we have to display images on screen, hide them.
|
|
|
|
// Same for tiles out of the buffer.
|
|
|
|
if (imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) {
|
|
|
|
if (updateSelectedState)
|
|
|
|
tile->setSelected(false, allowAnimation);
|
|
|
|
tile->reset();
|
|
|
|
tile->setVisible(false);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
tile->setVisible(true);
|
|
|
|
|
|
|
|
std::string imagePath = mEntries.at(imgPos).data.texturePath;
|
|
|
|
|
|
|
|
if (ResourceManager::getInstance()->fileExists(imagePath))
|
|
|
|
tile->setImage(imagePath);
|
|
|
|
else if (mEntries.at(imgPos).object->getType() == 2)
|
|
|
|
tile->setImage(mDefaultFolderTexture);
|
|
|
|
else
|
|
|
|
tile->setImage(mDefaultGameTexture);
|
|
|
|
|
|
|
|
if (updateSelectedState) {
|
|
|
|
if (imgPos == mCursor && mCursor != mLastCursor) {
|
|
|
|
int dif = mCursor - tilePos;
|
|
|
|
int idx = mLastCursor - dif;
|
|
|
|
|
|
|
|
if (idx < 0 || idx >= mTiles.size())
|
|
|
|
idx = 0;
|
|
|
|
|
|
|
|
Vector3f pos = mTiles.at(idx)->getBackgroundPosition();
|
|
|
|
tile->setSelected(true, allowAnimation, &pos);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
tile->setSelected(imgPos == mCursor, allowAnimation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2018-04-07 19:23:10 +00:00
|
|
|
}
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Calculate how much tiles of size mTileSize we can fit in a grid of size mSize using
|
|
|
|
// a margin of size mMargin.
|
2018-04-07 19:23:10 +00:00
|
|
|
template<typename T>
|
2018-04-12 08:25:11 +00:00
|
|
|
void ImageGridComponent<T>::calcGridDimension()
|
2018-04-07 19:23:10 +00:00
|
|
|
{
|
2020-06-28 16:39:18 +00:00
|
|
|
// grid_size = columns * tile_size + (columns - 1) * margin
|
|
|
|
// <=> columns = (grid_size + margin) / (tile_size + margin)
|
|
|
|
Vector2f gridDimension = (mSize + mMargin) / (mTileSize + mMargin);
|
|
|
|
|
|
|
|
if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0)
|
|
|
|
gridDimension = mAutoLayout;
|
|
|
|
|
|
|
|
mLastRowPartial = Math::floorf(gridDimension.y()) != gridDimension.y();
|
|
|
|
|
|
|
|
// Ceil y dim so we can display partial last row.
|
|
|
|
mGridDimension = Vector2i(gridDimension.x(), Math::ceilf(gridDimension.y()));
|
|
|
|
|
|
|
|
// Grid dimension validation.
|
|
|
|
if (mGridDimension.x() < 1) {
|
|
|
|
LOG(LogError) << "Theme defined grid X dimension below 1";
|
|
|
|
}
|
|
|
|
if (mGridDimension.y() < 1) {
|
|
|
|
LOG(LogError) << "Theme defined grid Y dimension below 1";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add extra tiles to both sides: Add EXTRAITEMS before, EXTRAITEMS after.
|
|
|
|
if (isVertical())
|
|
|
|
mGridDimension.y() += 2 * EXTRAITEMS;
|
|
|
|
else
|
|
|
|
mGridDimension.x() += 2 * EXTRAITEMS;
|
2019-07-06 14:50:50 +00:00
|
|
|
}
|
2013-12-01 01:04:46 +00:00
|
|
|
|
2019-07-06 14:50:50 +00:00
|
|
|
template<typename T>
|
|
|
|
bool ImageGridComponent<T>::isScrollLoop() {
|
2020-06-28 16:39:18 +00:00
|
|
|
if (!mScrollLoop)
|
|
|
|
return false;
|
|
|
|
if (isVertical())
|
|
|
|
return (mGridDimension.x() * (mGridDimension.y() - 2 * EXTRAITEMS)) <= mEntries.size();
|
|
|
|
return (mGridDimension.y() * (mGridDimension.x() - 2 * EXTRAITEMS)) <= mEntries.size();
|
2019-07-06 14:50:50 +00:00
|
|
|
};
|
2017-10-31 17:12:50 +00:00
|
|
|
|
|
|
|
#endif // ES_CORE_COMPONENTS_IMAGE_GRID_COMPONENT_H
|