Added a number of properties to allow horizontally scrolling text entries with CarouselComponent

Also added support for rotating horizontal scrollable containers
This commit is contained in:
Leon Styhre 2023-08-09 18:57:23 +02:00
parent 950541b261
commit 6ae8c87864
11 changed files with 160 additions and 72 deletions

View file

@ -92,6 +92,7 @@ void GamelistView::onShow()
updateView(CursorState::CURSOR_STOPPED);
mPrimary->finishAnimation(0);
mPrimary->onShowPrimary();
}
void GamelistView::onTransition()

View file

@ -46,6 +46,7 @@ void SystemView::onShow()
stopViewVideos();
mFadeOpacity = 0.0f;
mTransitionAnim = false;
mPrimary->onShowPrimary();
}
void SystemView::onTransition()

View file

@ -145,10 +145,14 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>>
{"colorEnd", COLOR},
{"gradientType", STRING},
{"text", STRING},
{"textRelativeScale", FLOAT},
{"textColor", COLOR},
{"textBackgroundColor", COLOR},
{"textSelectedColor", COLOR},
{"textSelectedBackgroundColor", COLOR},
{"textHorizontalScrolling", BOOLEAN},
{"textHorizontalScrollSpeed", FLOAT},
{"textHorizontalScrollDelay", FLOAT},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"letterCase", STRING},

View file

@ -30,6 +30,7 @@ TextComponent::TextComponent()
, mHorizontalAlignment {ALIGN_LEFT}
, mVerticalAlignment {ALIGN_CENTER}
, mLineSpacing {1.5f}
, mRelativeScale {1.0f}
, mNoTopMargin {false}
, mSelectable {false}
, mVerticalAutoSizing {false}
@ -37,6 +38,7 @@ TextComponent::TextComponent()
, mScrollSpeed {0.0f}
, mScrollSpeedMultiplier {1.0f}
, mScrollDelay {1500.0f}
, mScrollGap {1.5f}
, mScrollOffset1 {0.0f}
, mScrollOffset2 {0.0f}
, mScrollTime {0.0f}
@ -50,7 +52,13 @@ TextComponent::TextComponent(const std::string& text,
Alignment verticalAlignment,
glm::vec3 pos,
glm::vec2 size,
unsigned int bgcolor)
unsigned int bgcolor,
float lineSpacing,
float relativeScale,
bool horizontalScrolling,
float scrollSpeedMultiplier,
float scrollDelay,
float scrollGap)
: mFont {nullptr}
, mRenderer {Renderer::getInstance()}
, mColor {0x000000FF}
@ -66,14 +74,16 @@ TextComponent::TextComponent(const std::string& text,
, mAutoCalcExtent {1, 1}
, mHorizontalAlignment {horizontalAlignment}
, mVerticalAlignment {verticalAlignment}
, mLineSpacing {1.5f}
, mLineSpacing {lineSpacing}
, mRelativeScale {relativeScale}
, mNoTopMargin {false}
, mSelectable {false}
, mVerticalAutoSizing {false}
, mHorizontalScrolling {false}
, mHorizontalScrolling {horizontalScrolling}
, mScrollSpeed {0.0f}
, mScrollSpeedMultiplier {1.0f}
, mScrollDelay {1500.0f}
, mScrollSpeedMultiplier {scrollSpeedMultiplier}
, mScrollDelay {scrollDelay}
, mScrollGap {scrollGap}
, mScrollOffset1 {0.0f}
, mScrollOffset2 {0.0f}
, mScrollTime {0.0f}
@ -81,6 +91,7 @@ TextComponent::TextComponent(const std::string& text,
setFont(font);
setColor(color);
setBackgroundColor(bgcolor);
setHorizontalScrolling(mHorizontalScrolling);
setText(text, false);
setPosition(pos);
setSize(size);
@ -185,7 +196,8 @@ void TextComponent::setCapitalize(bool capitalize)
void TextComponent::render(const glm::mat4& parentTrans)
{
if (!isVisible() || mThemeOpacity == 0.0f || mSize.x == 0.0f || mSize.y == 0.0f)
if (!isVisible() || mTextCache == nullptr || mThemeOpacity == 0.0f || mSize.x == 0.0f ||
mSize.y == 0.0f)
return;
glm::mat4 trans {parentTrans * getTransform()};
@ -198,34 +210,31 @@ void TextComponent::render(const glm::mat4& parentTrans)
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x0000FF33, 0x0000FF33);
}
if (mHorizontalScrolling && mTextCache != nullptr) {
// Clip everything to be inside our bounds.
glm::vec3 dim {mSize.x, mSize.y, 0.0f};
dim.x = (trans[0].x * dim.x + trans[3].x) - trans[3].x;
dim.y = (trans[1].y * dim.y + trans[3].y) - trans[3].y;
const int clipRectPosX {static_cast<int>(std::round(trans[3].x))};
const int clipRectPosY {static_cast<int>(std::round(trans[3].y))};
const int clipRectSizeX {static_cast<int>(std::round(dim.x))};
const int clipRectSizeY {static_cast<int>(std::round(dim.y) + 1.0f)};
mRenderer->pushClipRect(glm::ivec2 {clipRectPosX, clipRectPosY},
glm::ivec2 {clipRectSizeX, clipRectSizeY});
float offsetX {0.0f};
float offsetX {0.0f};
if (mHorizontalScrolling) {
if (mTextCache->metrics.size.x < mSize.x) {
// This is needed for text that does not fill the entire width and thus gets aligned.
if (mHorizontalAlignment == Alignment::ALIGN_CENTER)
offsetX = (mSize.x - mTextCache->metrics.size.x) / 2.0f;
offsetX = ((mSize.x * mRelativeScale) - mTextCache->metrics.size.x) / 2.0f;
else if (mHorizontalAlignment == Alignment::ALIGN_RIGHT)
offsetX = mSize.x - mTextCache->metrics.size.x;
offsetX = (mSize.x * mRelativeScale) - mTextCache->metrics.size.x;
}
if (offsetX < 0.0f)
offsetX = 0.0f;
// Clip the texture using a fragment shader which allows for rotation and other benefits
// as compared to using the pushClipRect() function.
mTextCache->setClipRegion(glm::vec4 {mScrollOffset1, 0.0f,
(mSize.x * mRelativeScale) + mScrollOffset1,
mTextCache->metrics.size.y});
trans = glm::translate(trans, glm::vec3 {offsetX - mScrollOffset1, 0.0f, 0.0f});
}
auto renderFunc = [this](glm::mat4 trans) {
if (mRenderBackground)
auto renderFunc = [this](glm::mat4 trans, bool secondPass) {
if (mRenderBackground && !secondPass)
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, mBgColor, mBgColor, false,
mOpacity * mThemeOpacity, mDimming);
if (mTextCache) {
@ -259,50 +268,73 @@ void TextComponent::render(const glm::mat4& parentTrans)
trans = glm::translate(trans, glm::vec3 {0.0f, yOff, 0.0f});
mRenderer->setMatrix(trans);
// Draw the text area, where the text is actually located.
if (Settings::getInstance()->getBool("DebugText")) {
switch (mHorizontalAlignment) {
case ALIGN_LEFT: {
mRenderer->drawRect(0.0f, 0.0f, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_CENTER: {
mRenderer->drawRect(mHorizontalScrolling ?
0.0f :
(mSize.x - mTextCache->metrics.size.x) / 2.0f,
0.0f, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_RIGHT: {
const float relativeScaleOffset {(mSize.x - (mSize.x * mRelativeScale)) / 2.0f};
if (mHorizontalScrolling && !secondPass) {
if (mScrollOffset1 <= mTextCache->metrics.size.x) {
const float width {mTextCache->metrics.size.x - mScrollOffset1};
mRenderer->drawRect(
mHorizontalScrolling ? 0.0f : mSize.x - mTextCache->metrics.size.x,
0.0f, mTextCache->metrics.size.x, mTextCache->metrics.size.y,
0x00000033, 0x00000033);
break;
mScrollOffset1 + relativeScaleOffset, 0.0f,
width > mSize.x * mRelativeScale ? mSize.x * mRelativeScale : width,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
}
default: {
break;
}
else if (mHorizontalScrolling && secondPass) {
if ((mSize.x * mRelativeScale) - -mScrollOffset2 > 0.0f) {
mRenderer->drawRect(relativeScaleOffset, 0.0f,
(mSize.x * mRelativeScale) - -mScrollOffset2,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
}
}
else {
switch (mHorizontalAlignment) {
case ALIGN_LEFT: {
mRenderer->drawRect(0.0f, 0.0f, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_CENTER: {
mRenderer->drawRect((mSize.x - mTextCache->metrics.size.x) / 2.0f, 0.0f,
mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_RIGHT: {
mRenderer->drawRect(mSize.x - mTextCache->metrics.size.x, 0.0f,
mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
default: {
break;
}
}
}
}
// We need to adjust positioning if the relative scale multiplier is in use.
if (mRelativeScale < 1.0f) {
trans = glm::translate(
trans, glm::vec3 {(mSize.x - (mSize.x * mRelativeScale)) / 2.0f, 0.0f, 0.0f});
mRenderer->setMatrix(trans);
}
mFont->renderTextCache(mTextCache.get());
}
};
renderFunc(trans);
renderFunc(trans, false);
if (mHorizontalScrolling && mTextCache != nullptr && mTextCache->metrics.size.x > mSize.x) {
// Render again if the text has moved far enough to repeat.
if (mHorizontalScrolling && mTextCache->metrics.size.x > mSize.x * mRelativeScale) {
if (mScrollOffset2 < 0.0f) {
mTextCache->setClipRegion(glm::vec4 {mScrollOffset2, 0.0f,
(mSize.x * mRelativeScale) + mScrollOffset2,
mTextCache->metrics.size.y});
trans = glm::translate(parentTrans * getTransform(),
glm::vec3 {-mScrollOffset2, 0.0f, 0.0f});
renderFunc(trans);
glm::vec3 {offsetX - mScrollOffset2, 0.0f, 0.0f});
renderFunc(trans, true);
}
}
if (mHorizontalScrolling && mTextCache != nullptr)
mRenderer->popClipRect();
}
void TextComponent::setValue(const std::string& value)
@ -347,9 +379,9 @@ void TextComponent::update(int deltaTime)
mScrollOffset1 = 0.0f;
mScrollOffset2 = 0.0f;
if (mTextCache->metrics.size.x > mSize.x) {
if (mTextCache->metrics.size.x > mSize.x * mRelativeScale) {
const float scrollLength {mTextCache->metrics.size.x};
const float returnLength {mScrollSpeed * 1.5f / mScrollSpeedMultiplier};
const float returnLength {mScrollSpeed * mScrollGap / mScrollSpeedMultiplier};
const float scrollTime {(scrollLength * 1000.0f) / mScrollSpeed};
const float returnTime {(returnLength * 1000.0f) / mScrollSpeed};
const float maxTime {mScrollDelay + scrollTime + returnTime};
@ -362,7 +394,7 @@ void TextComponent::update(int deltaTime)
mScrollOffset1 = Utils::Math::loop(mScrollDelay, scrollTime + returnTime, mScrollTime,
scrollLength + returnLength);
if (mScrollOffset1 > (scrollLength - (mSize.x - returnLength)))
if (mScrollOffset1 > (scrollLength - (mSize.x * mRelativeScale - returnLength)))
mScrollOffset2 = mScrollOffset1 - (scrollLength + returnLength);
else if (mScrollOffset2 < 0)
mScrollOffset2 = 0;
@ -392,7 +424,6 @@ void TextComponent::onTextChanged()
if (mFont && mAutoCalcExtent.x) {
mSize = mFont->sizeText(text, mLineSpacing);
// This can happen under special circumstances like when a blank/dummy font is used.
if (mSize.x == 0.0f)
return;
}
@ -407,18 +438,20 @@ void TextComponent::onTextChanged()
// Used to initialize all glyphs, which is needed to populate mMaxGlyphHeight.
lineHeight = mFont->loadGlyphs(text + "\n") * mLineSpacing;
const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y > lineHeight};
const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y * mRelativeScale > lineHeight};
if (mHorizontalScrolling) {
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(text, 0.0f, 0.0f, mColor));
mTextCache = std::shared_ptr<TextCache>(
font->buildTextCache(text, 0.0f, 0.0f, mColor, mLineSpacing));
}
else if (isMultiline && !isScrollable) {
const std::string wrappedText {
font->wrapText(text, mSize.x, (mVerticalAutoSizing ? 0.0f : mSize.y - lineHeight),
font->wrapText(text, mSize.x * mRelativeScale,
(mVerticalAutoSizing ? 0.0f : (mSize.y * mRelativeScale) - lineHeight),
mLineSpacing, isMultiline)};
mTextCache = std::shared_ptr<TextCache>(
font->buildTextCache(wrappedText, glm::vec2 {0.0f, 0.0f}, mColor, mSize.x,
mHorizontalAlignment, mLineSpacing, mNoTopMargin));
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
wrappedText, glm::vec2 {0.0f, 0.0f}, mColor, mSize.x * mRelativeScale,
mHorizontalAlignment, mLineSpacing, mNoTopMargin));
}
else {
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
@ -563,8 +596,6 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
glm::clamp(elem->get<float>("containerStartDelay"), 0.0f, 10.0f) * 1000.0f;
}
mHorizontalScrolling = true;
// Rotation can't be combined with a scrolling horizontal container.
mRotation = 0.0f;
}
else if (containerType != "vertical") {
LOG(LogError) << "TextComponent: Invalid theme configuration, property "

View file

@ -29,7 +29,13 @@ public:
Alignment verticalAlignment = ALIGN_CENTER,
glm::vec3 pos = {0.0f, 0.0f, 0.0f},
glm::vec2 size = {0.0f, 0.0f},
unsigned int bgcolor = 0x00000000);
unsigned int bgcolor = 0x00000000,
float lineSpacing = 1.5f,
float relativeScale = 1.0f,
bool horizontalScrolling = false,
float scrollSpeedMultiplier = 1.0f,
float scrollDelay = 1500.0f,
float scrollGap = 1.5f);
void setFont(const std::shared_ptr<Font>& font);
void setUppercase(bool uppercase);
@ -91,6 +97,7 @@ public:
void setHorizontalScrolling(bool state) override;
void setHorizontalScrollingSpeedMultiplier(float speed) { mScrollSpeedMultiplier = speed; }
void setHorizontalScrollingDelay(float delay) { mScrollDelay = delay; }
void setHorizontalScrollingGap(float gap) { mScrollGap = gap; }
void resetComponent() override
{
@ -143,6 +150,7 @@ private:
Alignment mHorizontalAlignment;
Alignment mVerticalAlignment;
float mLineSpacing;
float mRelativeScale;
bool mNoTopMargin;
bool mSelectable;
bool mVerticalAutoSizing;
@ -151,6 +159,7 @@ private:
float mScrollSpeed;
float mScrollSpeedMultiplier;
float mScrollDelay;
float mScrollGap;
float mScrollOffset1;
float mScrollOffset2;
float mScrollTime;

View file

@ -28,6 +28,7 @@ protected:
using List = IList<CarouselEntry, T>;
using List::mCursor;
using List::mEntries;
using List::mLastCursor;
using List::mScrollVelocity;
using List::mSize;
using List::mWindow;
@ -99,6 +100,7 @@ public:
unsigned int properties) override;
private:
void onShowPrimary() override { mEntries.at(mCursor).data.item->resetComponent(); }
void onCursorChanged(const CursorState& state) override;
void onScroll() override
{
@ -192,11 +194,15 @@ private:
unsigned int mCarouselColor;
unsigned int mCarouselColorEnd;
bool mColorGradientHorizontal;
float mTextRelativeScale;
unsigned int mTextColor;
unsigned int mTextBackgroundColor;
unsigned int mTextSelectedColor;
unsigned int mTextSelectedBackgroundColor;
bool mHasTextSelectedColor;
bool mTextHorizontalScrolling;
float mTextHorizontalScrollSpeed;
float mTextHorizontalScrollDelay;
std::shared_ptr<Font> mFont;
LetterCase mLetterCase;
LetterCase mLetterCaseAutoCollections;
@ -262,11 +268,15 @@ CarouselComponent<T>::CarouselComponent()
, mCarouselColor {0}
, mCarouselColorEnd {0}
, mColorGradientHorizontal {true}
, mTextRelativeScale {1.0f}
, mTextColor {0x000000FF}
, mTextBackgroundColor {0xFFFFFF00}
, mTextSelectedColor {0x000000FF}
, mTextSelectedBackgroundColor {0xFFFFFF00}
, mHasTextSelectedColor {false}
, mTextHorizontalScrolling {false}
, mTextHorizontalScrollSpeed {1.0f}
, mTextHorizontalScrollDelay {3000.0f}
, mFont {Font::get(FONT_SIZE_LARGE_FIXED)}
, mLetterCase {LetterCase::NONE}
, mLetterCaseAutoCollections {LetterCase::UNDEFINED}
@ -354,8 +364,9 @@ void CarouselComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeDat
auto text = std::make_shared<TextComponent>(
entry.name, mFont, 0x000000FF, mItemHorizontalAlignment, mItemVerticalAlignment,
glm::vec3 {0.0f, 0.0f, 0.0f},
glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f)), 0x00000000);
text->setLineSpacing(mLineSpacing);
glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f)), 0x00000000,
mLineSpacing, mTextRelativeScale, mTextHorizontalScrolling, mTextHorizontalScrollSpeed,
mTextHorizontalScrollDelay, 1.0f);
if (!mGamelistView)
text->setValue(entry.name);
text->setColor(mTextColor);
@ -654,6 +665,7 @@ template <typename T> bool CarouselComponent<T>::input(InputConfig* config, Inpu
template <typename T> void CarouselComponent<T>::update(int deltaTime)
{
mEntries.at(mCursor).data.item->update(deltaTime);
List::listUpdate(deltaTime);
GuiComponent::update(deltaTime);
}
@ -1646,6 +1658,9 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, false,
(mItemScale >= 1.0f ? mItemScale : 1.0f));
if (elem->has("textRelativeScale"))
mTextRelativeScale = glm::clamp(elem->get<float>("textRelativeScale"), 0.2f, 1.0f);
if (elem->has("textColor"))
mTextColor = elem->get<unsigned int>("textColor");
if (elem->has("textBackgroundColor"))
@ -1663,6 +1678,19 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
mHasTextSelectedColor = true;
}
if (elem->has("textHorizontalScrolling"))
mTextHorizontalScrolling = elem->get<bool>("textHorizontalScrolling");
if (elem->has("textHorizontalScrollSpeed")) {
mTextHorizontalScrollSpeed =
glm::clamp(elem->get<float>("textHorizontalScrollSpeed"), 0.1f, 10.0f);
}
if (elem->has("textHorizontalScrollDelay")) {
mTextHorizontalScrollDelay =
glm::clamp(elem->get<float>("textHorizontalScrollDelay"), 0.0f, 10.0f) * 1000.0f;
}
if (elem->has("lineSpacing"))
mLineSpacing = glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f);
@ -1763,6 +1791,9 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
template <typename T> void CarouselComponent<T>::onCursorChanged(const CursorState& state)
{
if (mEntries.size() > static_cast<size_t>(mLastCursor))
mEntries.at(mLastCursor).data.item->resetComponent();
float startPos {mEntryCamOffset};
float posMax {static_cast<float>(mEntries.size())};
float target {static_cast<float>(mCursor)};

View file

@ -84,6 +84,7 @@ public:
unsigned int properties) override;
private:
void onShowPrimary() override { mEntries.at(mCursor).data.item->resetComponent(); }
void onScroll() override
{
if (mGamelistView)

View file

@ -39,6 +39,7 @@ public:
virtual int size() const = 0;
// Functions used by all primary components.
virtual void onShowPrimary() = 0;
virtual void setCancelTransitionsCallback(const std::function<void()>& func) = 0;
virtual void setCursorChangedCallback(const std::function<void(CursorState state)>& func) = 0;
virtual int getCursor() = 0;

View file

@ -85,7 +85,7 @@ public:
}
private:
void onShow() override { mEntries.at(mCursor).data.entryName->resetComponent(); }
void onShowPrimary() override { mEntries.at(mCursor).data.entryName->resetComponent(); }
void onScroll() override
{
if (mGamelistView &&
@ -278,8 +278,8 @@ template <typename T> bool TextListComponent<T>::input(InputConfig* config, Inpu
template <typename T> void TextListComponent<T>::update(int deltaTime)
{
mEntries.at(mCursor).data.entryName->update(deltaTime);
List::listUpdate(deltaTime);
mEntries.at(static_cast<unsigned int>(mCursor)).data.entryName->update(deltaTime);
GuiComponent::update(deltaTime);
}

View file

@ -222,6 +222,7 @@ TextCache* Font::buildTextCache(const std::string& text,
cache->vertexLists.resize(vertMap.size());
cache->metrics.size = {sizeText(text, lineSpacing)};
cache->metrics.maxGlyphHeight = mMaxGlyphHeight;
cache->clipRegion = {0.0f, 0.0f, 0.0f, 0.0f};
size_t i {0};
for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) {
@ -242,12 +243,18 @@ void Font::renderTextCache(TextCache* cache)
return;
}
const bool clipRegion {cache->clipRegion != glm::vec4 {0.0f, 0.0f, 0.0f, 0.0f}};
for (auto it = cache->vertexLists.begin(); it != cache->vertexLists.end(); ++it) {
assert(*it->textureIdPtr != 0);
auto vertexList = *it;
it->verts[0].shaderFlags = Renderer::ShaderFlags::FONT_TEXTURE;
if (clipRegion) {
it->verts[0].shaderFlags |= Renderer::ShaderFlags::CLIPPING;
it->verts[0].clipregion = cache->clipRegion;
}
mRenderer->bindTexture(*it->textureIdPtr);
mRenderer->drawTriangleStrips(
&it->verts[0], static_cast<const unsigned int>(it->verts.size()),

View file

@ -239,6 +239,7 @@ public:
void setOpacity(float opacity);
void setSaturation(float saturation);
void setDimming(float dimming);
void setClipRegion(const glm::vec4& clip) { clipRegion = clip; }
const glm::vec2& getSize() { return metrics.size; }
friend Font;
@ -250,6 +251,7 @@ protected:
};
std::vector<VertexList> vertexLists;
glm::vec4 clipRegion;
};
#endif // ES_CORE_RESOURCES_FONT_H