Added layout and line wrapping support for shaped text and for mixing of LTR and RTL scripts

This commit is contained in:
Leon Styhre 2024-08-20 00:13:44 +02:00
parent bd6956d52f
commit 3552c6e228
36 changed files with 814 additions and 535 deletions

View file

@ -54,7 +54,8 @@ Screensaver::Screensaver()
void Screensaver::startScreensaver(bool generateMediaList)
{
ViewController::getInstance()->pauseViewVideos();
mGameOverlay = std::make_unique<TextComponent>("", Font::get(FONT_SIZE_SMALL), 0xFFFFFFFF);
mGameOverlay = std::make_unique<TextComponent>("", Font::get(FONT_SIZE_SMALL), 0xFFFFFFFF,
ALIGN_LEFT, ALIGN_CENTER, glm::ivec2 {1, 1});
mScreensaverType = Settings::getInstance()->getString("ScreensaverType");
// In case there is an invalid entry in the es_settings.xml file.
@ -701,9 +702,6 @@ void Screensaver::generateOverlayInfo()
mGameOverlay->setText(overlayText);
mGameOverlay->setPosition(posX, posY);
// Setting the Y size to zero makes the text area expand vertically as needed.
mGameOverlay->setSize(mGameOverlay->getSize().x, 0.0f);
const float marginX {mRenderer->getScreenWidth() * 0.01f};
mGameOverlayRectangleCoords.clear();

View file

@ -42,7 +42,8 @@ GuiAlternativeEmulators::GuiAlternativeEmulators()
std::string name {(*it)->getName()};
std::shared_ptr<TextComponent> systemText {
std::make_shared<TextComponent>(name, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)};
std::make_shared<TextComponent>(name, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary,
ALIGN_LEFT, ALIGN_CENTER, glm::ivec2 {0, 0})};
systemText->setSize(systemSizeX, systemText->getSize().y);
row.addElement(systemText, false);
@ -72,15 +73,16 @@ GuiAlternativeEmulators::GuiAlternativeEmulators()
std::shared_ptr<TextComponent> labelText;
if (label == (*it)->getSystemEnvData()->mLaunchCommands.front().second) {
labelText =
std::make_shared<TextComponent>(label, Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
labelText = std::make_shared<TextComponent>(
label, Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT,
ALIGN_CENTER, glm::ivec2 {0, 0});
}
else {
// Mark any non-default value with bold and add a gear symbol as well.
labelText = std::make_shared<TextComponent>(
label + (!invalidEntry ? " " + ViewController::GEAR_CHAR : ""),
Font::get(FONT_SIZE_MEDIUM, FONT_PATH_BOLD), mMenuColorPrimary, ALIGN_RIGHT);
Font::get(FONT_SIZE_MEDIUM, FONT_PATH_BOLD), mMenuColorPrimary, ALIGN_RIGHT,
ALIGN_CENTER, glm::ivec2 {0, 0});
}
// Mark invalid entries with red color.

View file

@ -93,6 +93,8 @@ void GuiGamelistFilter::addFiltersToMenu()
mTextFilterField = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_MEDIUM),
mMenuColorPrimary, ALIGN_RIGHT);
mTextFilterField->setSize(
0.0f, mTextFilterField->getFont()->getHeight(mTextFilterField->getLineSpacing()));
// Don't show the free text filter entry unless there are any games in the system.
if (mSystem->getRootFolder()->getChildren().size() > 0) {

View file

@ -2175,6 +2175,7 @@ void GuiMenu::openQuitMenu()
void GuiMenu::addVersionInfo()
{
mVersion.setFont(Font::get(FONT_SIZE_SMALL));
mVersion.setAutoCalcExtent(glm::ivec2 {0, 0});
mVersion.setColor(mMenuColorTertiary);
const std::string applicationName {"ES-DE"};

View file

@ -139,7 +139,7 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md,
it->type == MD_ALT_EMULATOR) {
ed = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
assert(ed);
ed->setSize(0.0f, ed->getFont()->getHeight());
ed->setValue(mMetaData->get(it->key));
mEditors.push_back(ed);
continue;
@ -197,6 +197,7 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md,
ed =
std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
ed->setSize(0.0f, ed->getFont()->getHeight());
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
@ -283,6 +284,7 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md,
ed =
std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
ed->setSize(0.0f, ed->getFont()->getHeight());
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
@ -430,6 +432,7 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md,
ed =
std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
ed->setSize(0.0f, ed->getFont()->getHeight());
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
@ -550,6 +553,8 @@ GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md,
ed =
std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
ed->setRemoveLineBreaks(true);
ed->setSize(0.0f, ed->getFont()->getHeight());
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();

View file

@ -114,6 +114,7 @@ GuiOfflineGenerator::GuiOfflineGenerator(const std::queue<FileData*>& gameQueue)
// Processing value.
mProcessingVal = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL),
mMenuColorSecondary, ALIGN_LEFT);
mProcessingVal->setRemoveLineBreaks(true);
mGrid.setEntry(mProcessingVal, glm::ivec2 {4, 4}, false, true, glm::ivec2 {1, 1});
// Spacer row.
@ -128,6 +129,7 @@ GuiOfflineGenerator::GuiOfflineGenerator(const std::queue<FileData*>& gameQueue)
// Last error message value.
mLastErrorVal = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL),
mMenuColorSecondary, ALIGN_LEFT);
mLastErrorVal->setRemoveLineBreaks(true);
mGrid.setEntry(mLastErrorVal, glm::ivec2 {1, 10}, false, true, glm::ivec2 {4, 1});
// Right spacer.

View file

@ -114,7 +114,9 @@ GuiOrphanedDataCleanup::GuiOrphanedDataCleanup(std::function<void()> reloadCallb
mMediaDescription,
Font::get(mRenderer->getScreenAspectRatio() < 1.6f ? FONT_SIZE_SMALL : FONT_SIZE_MEDIUM),
mMenuColorPrimary, ALIGN_LEFT, ALIGN_TOP);
mGrid.setEntry(mDescription, glm::ivec2 {1, 4}, false, true, glm::ivec2 {2, 1});
mDescription->setNoSizeUpdate(true);
mGrid.setEntry(mDescription, glm::ivec2 {1, 4}, false, true, glm::ivec2 {2, 1},
GridFlags::BORDER_NONE, GridFlags::UPDATE_ALWAYS, glm::ivec2 {0, 1});
mEntryCountHeader = std::make_shared<TextComponent>(
_("TOTAL ENTRIES REMOVED:"), Font::get(FONT_SIZE_SMALL), mMenuColorPrimary, ALIGN_LEFT);

View file

@ -81,7 +81,8 @@ GuiScraperSearch::GuiScraperSearch(SearchType type, unsigned int scrapeCount, in
mDescContainer->setScrollParameters(6000.0f, 3000.0f, 0.8f);
mResultDesc = std::make_shared<TextComponent>("Result desc", Font::get(FONT_SIZE_SMALL),
mMenuColorPrimary);
mMenuColorPrimary, ALIGN_LEFT, ALIGN_CENTER,
glm::ivec2 {0, 1});
mDescContainer->addChild(mResultDesc.get());
mDescContainer->setAutoScroll(true);
@ -257,12 +258,11 @@ void GuiScraperSearch::resizeMetadata()
float maxLblWidth {0.0f};
for (auto it = mMD_Pairs.cbegin(); it != mMD_Pairs.cend(); ++it) {
it->first->setFont(fontLbl);
it->first->setSize(0, 0);
if (it->first->getSize().x > maxLblWidth)
maxLblWidth =
it->first->getSize().x + (16.0f * (mRenderer->getIsVerticalOrientation() ?
mRenderer->getScreenHeightModifier() :
mRenderer->getScreenWidthModifier()));
if (it->first->getTextCache()->metrics.size.x > maxLblWidth)
maxLblWidth = it->first->getTextCache()->metrics.size.x +
(16.0f * (mRenderer->getIsVerticalOrientation() ?
mRenderer->getScreenHeightModifier() :
mRenderer->getScreenWidthModifier()));
}
for (unsigned int i {0}; i < mMD_Pairs.size(); ++i)
@ -514,7 +514,7 @@ void GuiScraperSearch::onSearchDone(std::vector<ScraperSearchResult>& results)
auto gameEntry =
std::make_shared<TextComponent>(Utils::String::toUpper(gameName), font, color);
gameEntry->setHorizontalScrolling(true);
row.addElement(gameEntry, true);
row.addElement(gameEntry, true, true, glm::ivec2 {1, 0});
row.makeAcceptInputHandler([this, i] { returnResult(mScraperResults.at(i)); });
mResultList->addRow(row);
}

View file

@ -199,7 +199,8 @@ void GuiSettings::addEditableTextComponent(const std::string label,
row.elements.clear();
auto lbl = std::make_shared<TextComponent>(Utils::String::toUpper(label),
Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary);
Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary,
ALIGN_LEFT, ALIGN_CENTER, glm::ivec2 {0, 0});
row.addElement(lbl, true);
row.addElement(ed, true);

View file

@ -360,8 +360,16 @@ void GuiComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
setPosition(glm::vec3 {denormalized.x, denormalized.y, 0.0f});
}
if (properties & ThemeFlags::SIZE && elem->has("size"))
setSize(elem->get<glm::vec2>("size") * scale);
if (properties & ThemeFlags::SIZE && elem->has("size")) {
glm::vec2 size {(elem->get<glm::vec2>("size") * scale)};
if (size.x == 0.0f && size.y == 0.0f)
setAutoCalcExtent(glm::ivec2 {1, 0});
else if (size.x != 0.0f && size.y == 0.0f)
setAutoCalcExtent(glm::ivec2 {0, 1});
else if (size.x != 0.0f && size.y != 0.0f)
setAutoCalcExtent(glm::ivec2 {0, 0});
setSize(size);
}
// Position + size also implies origin.
if ((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) &&

View file

@ -23,6 +23,7 @@ class Animation;
class AnimationController;
class Font;
class InputConfig;
class TextCache;
class ThemeData;
class Window;
@ -185,7 +186,9 @@ public:
mComponentThemeFlags ^= ComponentThemeFlags::METADATA_ELEMENT;
}
virtual int getTextCacheGlyphHeight() { return 0; }
virtual const TextCache* getTextCache() { return nullptr; }
virtual void setRemoveLineBreaks(bool state) {}
virtual void setAutoCalcExtent(glm::ivec2 extent) {};
// Returns the center point of the image (takes origin into account).
const glm::vec2 getCenter() const;

View file

@ -181,7 +181,7 @@ bool Window::init()
mListScrollText = std::make_unique<TextComponent>("", Font::get(FONT_SIZE_LARGE));
mGPUStatisticsText = std::make_unique<TextComponent>(
"", Font::get(FONT_SIZE_SMALL), 0xFF00FFFF, ALIGN_LEFT, ALIGN_CENTER,
"", Font::get(FONT_SIZE_SMALL), 0xFF00FFFF, ALIGN_LEFT, ALIGN_CENTER, glm::vec2 {1, 1},
glm::vec3 {mRenderer->getScreenWidth() * 0.02f, mRenderer->getScreenHeight() * 0.02f, 0.0f},
glm::vec2 {0.0f, 0.0f}, 0x00000000, 1.3f);
@ -383,8 +383,6 @@ void Window::update(int deltaTime)
<< " MiB\nTexture VRAM: " << textureVramUsageMiB
<< " MiB\nMax Texture VRAM: " << textureTotalUsageMiB << " MiB";
mGPUStatisticsText->setText(ss.str());
// Setting the Y size to zero makes the text area expand vertically as needed.
mGPUStatisticsText->setSize(mGPUStatisticsText->getSize().x, 0.0f);
}
mFrameTimeElapsed = 0;

View file

@ -23,6 +23,7 @@ BusyComponent::BusyComponent()
// Col 0 = animation, col 1 = spacer, col 2 = text.
mGrid.setEntry(mAnimation, glm::ivec2 {1, 1}, false, true);
mGrid.setEntry(mText, glm::ivec2 {3, 1}, false, true);
mText->setAutoCalcExtent(glm::ivec2 {1, 0});
addChild(&mBackground);
addChild(&mGrid);
@ -37,7 +38,7 @@ void BusyComponent::onSizeChanged()
const float middleSpacerWidth {0.01f * Renderer::getScreenWidth()};
const float textHeight {mText->getFont()->getLetterHeight()};
mText->setSize(0, textHeight);
mText->setSize(0.0f, textHeight);
const float textWidth {mText->getSize().x + (4.0f * Renderer::getScreenWidthModifier())};
mGrid.setColWidthPerc(1, textHeight / mSize.x); // Animation is square.

View file

@ -23,14 +23,25 @@ ButtonComponent::ButtonComponent(const std::string& text,
, mFocused {false}
, mEnabled {true}
, mFlatStyle {flatStyle}
, mMinWidth {0.0f}
, mTextColorFocused {mMenuColorButtonTextFocused}
, mTextColorUnfocused {mMenuColorButtonTextUnfocused}
, mFlatColorFocused {mMenuColorButtonFlatFocused}
, mFlatColorUnfocused {mMenuColorButtonFlatUnfocused}
{
mButtonText =
std::make_unique<TextComponent>("", Font::get(FONT_SIZE_MEDIUM), 0xFFFFFFFF, ALIGN_CENTER);
if (mFlatStyle) {
mButtonText = std::make_unique<TextComponent>("", Font::get(FONT_SIZE_MEDIUM), 0xFFFFFFFF,
ALIGN_CENTER);
}
else {
mButtonText = std::make_unique<TextComponent>("DELETE", Font::get(FONT_SIZE_MEDIUM),
0xFFFFFFFF, ALIGN_CENTER);
const glm::vec2 textCacheSize {mButtonText->getTextCache() == nullptr ?
glm::vec2 {0.0f, 0.0f} :
mButtonText->getTextCache()->metrics.size};
mMinWidth = textCacheSize.x + (12.0f * mRenderer->getScreenResolutionModifier());
}
mBox.setSharpCorners(true);
setPressedFunc(func);
@ -75,12 +86,10 @@ void ButtonComponent::setText(const std::string& text,
mHelpText = helpText;
mButtonText->setText(mText);
const float minWidth {mButtonText->getFont()->sizeText("DELETE").x +
(12.0f * mRenderer->getScreenResolutionModifier())};
if (resize) {
setSize(
std::max(mButtonText->getSize().x + (12.0f * mRenderer->getScreenResolutionModifier()),
minWidth),
mMinWidth),
mButtonText->getSize().y);
}

View file

@ -67,6 +67,7 @@ private:
bool mEnabled;
bool mFlatStyle;
float mMinWidth;
unsigned int mTextColorFocused;
unsigned int mTextColorUnfocused;
unsigned int mFlatColorFocused;

View file

@ -9,6 +9,7 @@
#include "components/ComponentGrid.h"
#include "Settings.h"
#include "components/TextComponent.h"
#include "utils/LocalizationUtil.h"
using namespace GridFlags;
@ -100,11 +101,13 @@ void ComponentGrid::setEntry(const std::shared_ptr<GuiComponent>& comp,
bool resize,
const glm::ivec2& size,
unsigned int border,
GridFlags::UpdateType updateType)
GridFlags::UpdateType updateType,
glm::ivec2 autoCalcExtent)
{
assert(pos.x >= 0 && pos.x < mGridSize.x && pos.y >= 0 && pos.y < mGridSize.y);
assert(comp != nullptr);
assert(comp->getParent() == nullptr);
comp->setAutoCalcExtent(autoCalcExtent);
GridEntry entry {pos, size, comp, canFocus, resize, updateType, border};
mCells.push_back(entry);

View file

@ -44,7 +44,8 @@ public:
bool resize = true,
const glm::ivec2& size = glm::ivec2 {1, 1},
unsigned int border = GridFlags::BORDER_NONE,
GridFlags::UpdateType updateType = GridFlags::UPDATE_ALWAYS);
GridFlags::UpdateType updateType = GridFlags::UPDATE_ALWAYS,
glm::ivec2 autoCalcExtent = {0, 0});
void setPastBoundaryCallback(const std::function<bool(InputConfig* config, Input input)>& func)
{

View file

@ -36,11 +36,13 @@ struct ComponentListRow {
// is to forward the input to the rightmost element in the currently selected row.
std::function<bool(InputConfig*, Input)> inputHandler;
void addElement(const std::shared_ptr<GuiComponent>& component,
void addElement(const std::shared_ptr<GuiComponent>& comp,
bool resizeWidth,
bool invertWhenSelected = true)
bool invertWhenSelected = true,
glm::ivec2 autoCalcExtent = {0, 0})
{
elements.push_back(ComponentListElement(component, resizeWidth, invertWhenSelected));
comp->setAutoCalcExtent(autoCalcExtent);
elements.push_back(ComponentListElement(comp, resizeWidth, invertWhenSelected));
}
// Utility function for making an input handler for an input event.

View file

@ -28,7 +28,8 @@ DateTimeComponent::DateTimeComponent(const std::string& text,
glm::vec3 pos,
glm::vec2 size,
unsigned int bgcolor)
: TextComponent {text, font, color, horizontalAlignment, ALIGN_CENTER, pos, size, bgcolor}
: TextComponent {text, font, color, horizontalAlignment, ALIGN_CENTER, glm::vec2 {1, 0},
pos, size, bgcolor}
, mRenderer {Renderer::getInstance()}
, mDisplayRelative {false}
{
@ -119,8 +120,8 @@ void DateTimeComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
const std::string& element,
unsigned int properties)
{
GuiComponent::applyTheme(theme, view, element, properties);
using namespace ThemeFlags;
GuiComponent::applyTheme(theme, view, element, properties);
const ThemeData::ThemeElement* elem {theme->getElement(view, element, "datetime")};
if (!elem)
@ -239,15 +240,22 @@ void DateTimeComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
}
float maxHeight {0.0f};
bool hasSize {false};
if (elem->has("size")) {
const glm::vec2 size {elem->get<glm::vec2>("size")};
if (size.x != 0.0f && size.y != 0.0f)
if (size.x != 0.0f && size.y != 0.0f) {
maxHeight = mSize.y * 2.0f;
hasSize = true;
}
}
if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
if (getAutoCalcExtent() == glm::ivec2 {1, 0} && !hasSize)
mSize.y = 0.0f;
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight));
mSize = glm::round(mSize);
}

View file

@ -31,6 +31,7 @@ MenuComponent::MenuComponent(std::string title, const std::shared_ptr<Font>& tit
// Set up title.
mTitle = std::make_shared<TextComponent>();
mTitle->setAutoCalcExtent(glm::ivec2 {0, 0});
mTitle->setHorizontalAlignment(ALIGN_CENTER);
mTitle->setColor(mMenuColorTitle);
setTitle(title, titleFont);

View file

@ -47,9 +47,10 @@ public:
bool invert_when_selected = true)
{
ComponentListRow row;
row.addElement(
std::make_shared<TextComponent>(label, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary),
true);
row.addElement(std::make_shared<TextComponent>(label, Font::get(FONT_SIZE_MEDIUM),
mMenuColorPrimary, ALIGN_LEFT, ALIGN_CENTER,
glm::ivec2 {0, 0}),
true);
row.addElement(comp, false, invert_when_selected);
addRow(row, setCursorHere);
}

View file

@ -45,6 +45,7 @@ public:
{
auto font {Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)};
mText.setFont(font);
mText.setAutoCalcExtent(glm::ivec2 {0, 0});
mText.setColor(mMenuColorPrimary);
mText.setHorizontalAlignment(ALIGN_CENTER);
addChild(&mText);
@ -342,10 +343,10 @@ private:
mText.setText(ss.str());
mText.setSize(0, mText.getSize().y);
setSize(mText.getSize().x + mRightArrow.getSize().x +
setSize(mText.getTextCache()->metrics.size.x + mRightArrow.getSize().x +
Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 0.68f,
mText.getSize().y);
if (mParent) // Hack since there's no "on child size changed" callback.
if (mParent)
mParent->onSizeChanged();
}
else {
@ -362,10 +363,11 @@ private:
}
mText.setSize(0.0f, mText.getSize().y);
setSize(mText.getSize().x + mLeftArrow.getSize().x + mRightArrow.getSize().x +
setSize(mText.getTextCache()->metrics.size.x + mLeftArrow.getSize().x +
mRightArrow.getSize().x +
Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 0.68f,
mText.getSize().y);
if (mParent) // Hack since there's no "on child size changed" callback.
if (mParent)
mParent->onSizeChanged();
if (mSelectedChangedCallback)

View file

@ -68,15 +68,23 @@ void ScrollableContainer::resetComponent()
mAtEnd = false;
mUpdatedSize = false;
// This applies to the actual TextComponent that is getting displayed.
mChildren.front()->setAutoCalcExtent(glm::ivec2 {0, 1});
mChildren.front()->setSize(mSize.x, 0.0f);
// This is needed to resize to the designated area when the background image gets invalidated.
if (!mChildren.empty()) {
float combinedHeight {0.0f};
const float cacheGlyphHeight {
static_cast<float>(mChildren.front()->getTextCacheGlyphHeight())};
mChildren.front()->getTextCache() == nullptr ?
0.0f :
static_cast<float>(mChildren.front()->getTextCache()->metrics.maxGlyphHeight)};
if (cacheGlyphHeight > 0.0f)
combinedHeight = cacheGlyphHeight * mChildren.front()->getLineSpacing();
else
return;
if (mChildren.front()->getSize().y > mSize.y) {
if (mVerticalSnap) {
float numLines {std::floor(mSize.y / combinedHeight)};
@ -132,7 +140,12 @@ void ScrollableContainer::update(int deltaTime)
const float lineSpacing {mChildren.front()->getLineSpacing()};
float combinedHeight {0.0f};
const float cacheGlyphHeight {static_cast<float>(mChildren.front()->getTextCacheGlyphHeight())};
const float cacheGlyphHeight {
mChildren.front()->getTextCache() == nullptr ?
0.0f :
static_cast<float>(mChildren.front()->getTextCache()->metrics.maxGlyphHeight)};
if (cacheGlyphHeight > 0.0f)
combinedHeight = cacheGlyphHeight * lineSpacing;
else

View file

@ -29,14 +29,16 @@ TextComponent::TextComponent()
, mUppercase {false}
, mLowercase {false}
, mCapitalize {false}
, mAutoCalcExtent {1, 1}
, mAutoCalcExtent {1, 0}
, mHorizontalAlignment {ALIGN_LEFT}
, mVerticalAlignment {ALIGN_CENTER}
, mLineSpacing {1.5f}
, mRelativeScale {1.0f}
, mNoTopMargin {false}
, mNeedGlyphsPos {false}
, mRemoveLineBreaks {false}
, mNoSizeUpdate {false}
, mSelectable {false}
, mVerticalAutoSizing {false}
, mHorizontalScrolling {false}
, mDebugRendering {true}
, mScrollSpeed {0.0f}
@ -55,6 +57,7 @@ TextComponent::TextComponent(const std::string& text,
unsigned int color,
Alignment horizontalAlignment,
Alignment verticalAlignment,
glm::ivec2 autoCalcExtent,
glm::vec3 pos,
glm::vec2 size,
unsigned int bgcolor,
@ -79,14 +82,16 @@ TextComponent::TextComponent(const std::string& text,
, mUppercase {false}
, mLowercase {false}
, mCapitalize {false}
, mAutoCalcExtent {1, 1}
, mAutoCalcExtent {autoCalcExtent}
, mHorizontalAlignment {horizontalAlignment}
, mVerticalAlignment {verticalAlignment}
, mLineSpacing {lineSpacing}
, mRelativeScale {relativeScale}
, mNoTopMargin {false}
, mNeedGlyphsPos {false}
, mRemoveLineBreaks {false}
, mNoSizeUpdate {false}
, mSelectable {false}
, mVerticalAutoSizing {false}
, mHorizontalScrolling {horizontalScrolling}
, mDebugRendering {true}
, mScrollSpeed {0.0f}
@ -102,18 +107,9 @@ TextComponent::TextComponent(const std::string& text,
setColor(color);
setBackgroundColor(bgcolor);
setHorizontalScrolling(mHorizontalScrolling);
setText(text, false, mMaxLength);
setSize(size);
setText(text, true, mMaxLength);
setPosition(pos);
if (mMaxLength == 0.0f || mMaxLength > size.x)
setSize(size);
else
setSize(glm::vec2 {mMaxLength, size.y});
}
void TextComponent::onSizeChanged()
{
mAutoCalcExtent = glm::ivec2 {getSize().x == 0, getSize().y == 0};
onTextChanged();
}
void TextComponent::setFont(const std::shared_ptr<Font>& font)
@ -125,7 +121,6 @@ void TextComponent::setFont(const std::shared_ptr<Font>& font)
onTextChanged();
}
// Set the color of the font/text.
void TextComponent::setColor(unsigned int color)
{
if (mColor == color)
@ -136,7 +131,14 @@ void TextComponent::setColor(unsigned int color)
onColorChanged();
}
// Set the color of the background box.
const glm::vec2 TextComponent::getGlyphPosition(int cursor)
{
if (mTextCache == nullptr || mTextCache->glyphPositions.empty())
return glm::vec2 {0.0f, 0.0f};
return mTextCache->glyphPositions.at(cursor);
}
void TextComponent::setBackgroundColor(unsigned int color)
{
if (mBgColor == color)
@ -395,9 +397,6 @@ void TextComponent::setValue(const std::string& value)
mThemeMetadata == "genre" || mThemeMetadata == "players")) {
setText(mDefaultValue);
}
else if (mHorizontalScrolling) {
setText(Utils::String::replace(value, "\n", " "));
}
else {
setText(value);
}
@ -409,8 +408,7 @@ void TextComponent::setHorizontalScrolling(bool state)
mHorizontalScrolling = state;
if (mHorizontalScrolling) {
mScrollSpeed =
mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x * 0.247f * mScrollSpeedMultiplier;
mScrollSpeed = mFont->getSizeReference() * 0.247f * mScrollSpeedMultiplier;
}
else if (mTextCache != nullptr) {
mTextCache->setClipRegion(
@ -464,9 +462,6 @@ void TextComponent::onTextChanged()
{
mTextCache.reset();
if (!mVerticalAutoSizing)
mVerticalAutoSizing = (mSize.x != 0.0f && mSize.y == 0.0f);
std::string text;
if (mText != "") {
@ -480,46 +475,39 @@ void TextComponent::onTextChanged()
text = mText; // Original case.
}
if (mFont && mAutoCalcExtent.x) {
mSize = mFont->sizeText(text, mLineSpacing);
if (mMaxLength > 0.0f && mSize.x > mMaxLength)
mSize.x = std::round(mMaxLength);
else if (mSize.x == 0.0f)
return;
}
if (!mFont || text.empty() || mSize.x < 0.0f)
if (!mFont || text.empty())
return;
std::shared_ptr<Font> font {mFont};
const float lineHeight {mFont->getHeight(mLineSpacing)};
const bool isScrollable {mParent && mParent->isScrollable()};
// Add one extra pixel to lineHeight as the font may be fractional in size.
const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y * mRelativeScale > lineHeight + 1};
float offsetY {0.0f};
if ((!mAutoCalcExtent.y && mSize.y == 0.0f))
mSize.y = lineHeight;
if (mHorizontalScrolling) {
if (lineHeight > mSize.y && mSize.y != 0.0f)
offsetY = (mSize.y - lineHeight) / 2.0f;
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
text, glm::vec2 {0.0f, offsetY}, mColor, 0.0f, 0.0f, ALIGN_LEFT, mLineSpacing));
}
else if (isMultiline && !isScrollable) {
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
text, glm::vec2 {0.0f, 0.0f}, mColor, mSize.x * mRelativeScale,
(mVerticalAutoSizing ? 0.0f : (mSize.y * mRelativeScale) - lineHeight),
mHorizontalAlignment, mLineSpacing, mNoTopMargin, true, isMultiline));
}
else {
if (!isMultiline && lineHeight > mSize.y)
offsetY = (mSize.y - lineHeight) / 2.0f;
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
text, glm::vec2 {0.0f, offsetY}, mColor, mSize.x, 0.0f, mHorizontalAlignment,
mLineSpacing, mNoTopMargin, true, isMultiline));
}
// If the line height is less than the font size then a vertical offset is required to make
// sure the text is correctly centered vertically.
const float offsetY {std::round(lineHeight > mSize.y && mSize.y != 0.0f && !mAutoCalcExtent.y ?
(mSize.y - lineHeight) / 2.0f :
0.0f)};
if (mAutoCalcExtent.y)
const float length {mAutoCalcExtent.x ? 0.0f : mSize.x * mRelativeScale};
const float height {mAutoCalcExtent.y ? 0.0f : (mSize.y * mRelativeScale) - lineHeight};
const Alignment horizontalAlignment {mHorizontalScrolling ? ALIGN_LEFT : mHorizontalAlignment};
const bool multiLine {mAutoCalcExtent.y == 1 || mSize.y > lineHeight};
// Always convert line breaks to spaces for single-line text (or if it's set explicitly).
if (mRemoveLineBreaks || mAutoCalcExtent == glm::ivec2 {1, 0})
text = Utils::String::replace(text, "\n", " ");
mTextCache = std::shared_ptr<TextCache>(mFont->buildTextCache(
text, length, mMaxLength * mRelativeScale, height, offsetY, mLineSpacing,
horizontalAlignment, mColor, mNoTopMargin, multiLine, mNeedGlyphsPos));
if (mHorizontalScrolling && mSize.x == 0.0f)
mSize.x = mTextCache->metrics.size.x;
else if (mAutoCalcExtent.x && !mHorizontalScrolling && !mNoSizeUpdate)
mSize.x = mTextCache->metrics.size.x;
if (mAutoCalcExtent.y && !mNoSizeUpdate)
mSize.y = mTextCache->metrics.size.y;
if (mOpacity != 1.0f || mThemeOpacity != 1.0f)
@ -667,6 +655,7 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("containerScrollGap")) {
mScrollGap = glm::clamp(elem->get<float>("containerScrollGap"), 0.1f, 5.0f);
}
mAutoCalcExtent = glm::ivec2 {1, 0};
mHorizontalScrolling = true;
}
else if (containerType != "vertical") {
@ -791,17 +780,24 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
}
float maxHeight {0.0f};
bool hasSize {false};
if (elem->has("size")) {
const glm::vec2 size {elem->get<glm::vec2>("size")};
if (size.x != 0.0f && size.y != 0.0f)
if (size.x != 0.0f && size.y != 0.0f) {
maxHeight = mSize.y * 2.0f;
hasSize = true;
}
}
if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
if (mAutoCalcExtent == glm::ivec2 {1, 0} && !hasSize)
mSize.y = 0.0f;
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight));
mSize = glm::round(mSize);
// We need to do this after setting the font as the scroll speed is calculated from its size.
if (mHorizontalScrolling)

View file

@ -15,9 +15,16 @@
class ThemeData;
// TextComponent sizing works in the following ways:
// setSize(0.0f, 0.0f) - Automatically sizes single-line text by expanding horizontally.
// setSize(width, 0.0f) - Limits size horizontally and automatically expands vertically.
// setSize(width, height) - Wraps and abbreviates text inside the width and height boundaries.
// autoCalcExtent(1, 0) - Automatically expand horizontally, line breaks are removed.
// autoCalcExtent(0, 0) - Wrap and abbreviate inside the width and height boundaries.
// autoCalcExtent(0, 1) - Limit size horizontally and automatically expand vertically.
// autoCalcExtent(1, 1) - Automatically expand horizontally and wrap by line break.
// The sizing logic above translates to the following theme configuration:
// <size>0 0</size> - autoCalcExtent(1, 0)
// <size>width 0</size> - autoCalcExtent(0, 1)
// <size>width height</size> - autoCalcExtent(0, 0)
class TextComponent : public GuiComponent
{
public:
@ -27,6 +34,7 @@ public:
unsigned int color = 0x000000FF,
Alignment horizontalAlignment = ALIGN_LEFT,
Alignment verticalAlignment = ALIGN_CENTER,
glm::ivec2 autoCalcExtent = {1, 0},
glm::vec3 pos = {0.0f, 0.0f, 0.0f},
glm::vec2 size = {0.0f, 0.0f},
unsigned int bgcolor = 0x00000000,
@ -42,15 +50,22 @@ public:
void setUppercase(bool uppercase);
void setLowercase(bool lowercase);
void setCapitalize(bool capitalize);
void onSizeChanged() override;
void onSizeChanged() override { onTextChanged(); }
void setText(const std::string& text, bool update = true, float maxLength = 0.0f);
void setHiddenText(const std::string& text) { mHiddenText = text; }
void setAutoCalcExtent(glm::ivec2 extent) override { mAutoCalcExtent = extent; }
const glm::ivec2 getAutoCalcExtent() { return mAutoCalcExtent; }
void setColor(unsigned int color) override;
void setHorizontalAlignment(Alignment align);
void setVerticalAlignment(Alignment align) { mVerticalAlignment = align; }
void setLineSpacing(float spacing);
float getLineSpacing() override { return mLineSpacing; }
void setTextShaping(bool state) { mFont->setTextShaping(state); }
void setNoTopMargin(bool margin);
void setNeedGlyphsPos(bool state) { mNeedGlyphsPos = state; }
void setRemoveLineBreaks(bool state) override { mRemoveLineBreaks = state; }
void setNoSizeUpdate(bool state) { mNoSizeUpdate = state; }
const glm::vec2 getGlyphPosition(int cursor);
void setBackgroundColor(unsigned int color) override;
void setRenderBackground(bool render) { mRenderBackground = render; }
void setBackgroundMargins(const glm::vec2 margins) { mBackgroundMargins = margins; }
@ -95,9 +110,9 @@ public:
const bool getSystemNameSuffix() const { return mSystemNameSuffix; }
const LetterCase getLetterCaseSystemNameSuffix() const { return mLetterCaseSystemNameSuffix; }
int getTextCacheGlyphHeight() override
const TextCache* getTextCache() override
{
return (mTextCache == nullptr ? 0 : mTextCache->metrics.maxGlyphHeight);
return (mTextCache == nullptr ? nullptr : mTextCache.get());
}
// Horizontal scrolling for single-line content that is too long to fit.
@ -178,8 +193,10 @@ private:
float mLineSpacing;
float mRelativeScale;
bool mNoTopMargin;
bool mNeedGlyphsPos;
bool mRemoveLineBreaks;
bool mNoSizeUpdate;
bool mSelectable;
bool mVerticalAutoSizing;
bool mHorizontalScrolling;
bool mDebugRendering;
float mScrollSpeed;

View file

@ -4,6 +4,7 @@
// TextEditComponent.cpp
//
// Component for editing text fields.
// TODO: Add support for editing shaped text.
//
#include "components/TextEditComponent.h"
@ -23,13 +24,14 @@
#define BLINKTIME 1000
TextEditComponent::TextEditComponent()
TextEditComponent::TextEditComponent(bool multiLine)
: mRenderer {Renderer::getInstance()}
, mFocused {false}
, mEditing {false}
, mMaskInput {true}
, mMultiLine {false}
, mMultiLine {multiLine}
, mCursor {0}
, mCursorShapedText {0}
, mBlinkTime {0}
, mCursorRepeatDir {0}
, mScrollOffset {0.0f, 0.0f}
@ -37,12 +39,18 @@ TextEditComponent::TextEditComponent()
, mBox {":/graphics/textinput.svg"}
{
mEditText = std::make_unique<TextComponent>("", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT));
mEditText->setNeedGlyphsPos(true);
mEditText->setTextShaping(false);
if (mMultiLine)
mEditText->setAutoCalcExtent(glm::ivec2 {0, 1});
else
mEditText->setAutoCalcExtent(glm::ivec2 {1, 0});
mBox.setSharpCorners(true);
addChild(&mBox);
onFocusLost();
setSize(4096,
getFont()->getHeight() + (TEXT_PADDING_VERT * mRenderer->getScreenHeightModifier()));
}
TextEditComponent::~TextEditComponent()
@ -73,16 +81,22 @@ void TextEditComponent::onSizeChanged()
mBox.fitTo(
mSize, glm::vec3 {0.0f, 0.0f, 0.0f},
glm::vec2 {-34.0f, -32.0f - (TEXT_PADDING_VERT * mRenderer->getScreenHeightModifier())});
glm::vec2 {-32.0f, -32.0f - (TEXT_PADDING_VERT * mRenderer->getScreenHeightModifier())});
if (mMultiLine)
mEditText->setSize(getTextAreaSize().x, 0.0f);
onTextChanged(); // Wrap point probably changed.
}
void TextEditComponent::setValue(const std::string& val, bool multiLine, bool update)
void TextEditComponent::setValue(const std::string& val, bool update)
{
mText = val;
mMultiLine = multiLine;
onTextChanged();
onCursorChanged();
if (update) {
onTextChanged();
onCursorChanged();
}
}
void TextEditComponent::textInput(const std::string& text, const bool pasting)
@ -104,6 +118,7 @@ void TextEditComponent::textInput(const std::string& text, const bool pasting)
size_t newCursor = Utils::String::prevCursor(mText, mCursor);
mText.erase(mText.begin() + newCursor, mText.begin() + mCursor);
mCursor = static_cast<unsigned int>(newCursor);
--mCursorShapedText;
}
}
else {
@ -111,6 +126,7 @@ void TextEditComponent::textInput(const std::string& text, const bool pasting)
(pasting && !mMultiLine ? Utils::String::replace(text, "\n", " ") : text));
mCursor += static_cast<unsigned int>(
(pasting && !mMultiLine ? Utils::String::replace(text, "\n", " ") : text).size());
++mCursorShapedText;
}
}
@ -295,26 +311,33 @@ void TextEditComponent::updateCursorRepeat(int deltaTime)
void TextEditComponent::moveCursor(int amt)
{
mCursor = static_cast<unsigned int>(Utils::String::moveCursor(mText, mCursor, amt));
mCursorShapedText += amt;
if (mCursorShapedText < 0)
mCursorShapedText = 0;
else if (mCursorShapedText > static_cast<int>(Utils::String::unicodeLength(mText)))
mCursorShapedText = static_cast<int>(Utils::String::unicodeLength(mText));
onCursorChanged();
}
void TextEditComponent::setCursor(size_t pos)
{
if (pos == std::string::npos)
if (pos == std::string::npos) {
mCursor = static_cast<unsigned int>(mText.length());
else
mCursorShapedText = Utils::String::unicodeLength(mText);
}
else {
mCursor = static_cast<int>(pos);
mCursorShapedText = Utils::String::unicodeLength(mText.substr(0, pos));
}
moveCursor(0);
}
void TextEditComponent::onTextChanged()
{
if (mMultiLine)
mEditText->setText(mText, true, mSize.x);
else
mEditText->setText(mText);
mEditText->setText(mText);
mEditText->setColor(mMenuColorKeyboardText | static_cast<unsigned char>(mOpacity * 255.0f));
if (mCursor > static_cast<int>(mText.length()))
@ -324,7 +347,7 @@ void TextEditComponent::onTextChanged()
void TextEditComponent::onCursorChanged()
{
if (mMultiLine) {
mCursorPos = getFont()->getWrappedTextCursorOffset(mText, mCursor);
mCursorPos = mEditText->getGlyphPosition(mCursorShapedText);
// Need to scroll down?
if (mScrollOffset.y + getTextAreaSize().y < mCursorPos.y + getFont()->getHeight())
@ -334,8 +357,7 @@ void TextEditComponent::onCursorChanged()
mScrollOffset.y = mCursorPos.y;
}
else {
mCursorPos = getFont()->sizeText(mText.substr(0, mCursor));
mCursorPos.y = 0.0f;
mCursorPos = mEditText->getGlyphPosition(mCursorShapedText);
if (mScrollOffset.x + getTextAreaSize().x < mCursorPos.x)
mScrollOffset.x = mCursorPos.x - getTextAreaSize().x;

View file

@ -4,6 +4,7 @@
// TextEditComponent.h
//
// Component for editing text fields.
// TODO: Add support for editing shaped text.
//
#ifndef ES_CORE_COMPONENTS_TEXT_EDIT_COMPONENT_H
@ -16,7 +17,7 @@
class TextEditComponent : public GuiComponent
{
public:
TextEditComponent();
TextEditComponent(bool multiLine);
~TextEditComponent();
void textInput(const std::string& text, const bool pasting = false) override;
@ -29,7 +30,7 @@ public:
void onSizeChanged() override;
void setValue(const std::string& val, bool multiLine, bool update = true);
void setValue(const std::string& val, bool update = true);
std::string getValue() const override;
void startEditing();
@ -59,7 +60,8 @@ private:
bool mEditing;
bool mMaskInput;
bool mMultiLine;
int mCursor; // Cursor position in characters.
int mCursor; // Cursor position in source text.
int mCursorShapedText; // Cursor position in shaped text.
int mBlinkTime;
int mCursorRepeatTimer;

View file

@ -380,7 +380,7 @@ void CarouselComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeDat
// when quick-jumping as textures are not loaded in this case.
auto text = std::make_shared<TextComponent>(
entry.name, mFont, 0x000000FF, mItemHorizontalAlignment, mItemVerticalAlignment,
glm::vec3 {0.0f, 0.0f, 0.0f},
glm::ivec2 {0, 0}, glm::vec3 {0.0f, 0.0f, 0.0f},
glm::round(mItemSize * (mItemScale >= 1.0f ? mItemScale : 1.0f)), 0x00000000,
mLineSpacing, mTextRelativeScale, mTextHorizontalScrolling, mTextHorizontalScrollSpeed,
mTextHorizontalScrollDelay, mTextHorizontalScrollGap);

View file

@ -381,9 +381,9 @@ void GridComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeData>&
// when quick-jumping as textures are not loaded in this case.
auto text = std::make_shared<TextComponent>(
entry.name, mFont, 0x000000FF, Alignment::ALIGN_CENTER, Alignment::ALIGN_CENTER,
glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize * mTextRelativeScale, 0x00000000, mLineSpacing,
1.0f, mTextHorizontalScrolling, mTextHorizontalScrollSpeed, mTextHorizontalScrollDelay,
mTextHorizontalScrollGap);
glm::ivec2 {0, 0}, glm::vec3 {0.0f, 0.0f, 0.0f}, mItemSize * mTextRelativeScale,
0x00000000, mLineSpacing, 1.0f, mTextHorizontalScrolling, mTextHorizontalScrollSpeed,
mTextHorizontalScrollDelay, mTextHorizontalScrollGap);
text->setOrigin(0.5f, 0.5f);
text->setColor(mTextColor);
text->setBackgroundColor(mTextBackgroundColor);

View file

@ -203,14 +203,14 @@ void TextListComponent<T>::addEntry(Entry& entry, const std::shared_ptr<ThemeDat
{
if (mHorizontalScrolling) {
entry.data.entryName = std::make_shared<TextComponent>(
entry.name, mFont, 0x000000FF, ALIGN_LEFT, ALIGN_CENTER, glm::vec3 {0.0f, 0.0f, 0.0f},
glm::vec2 {mFont->sizeText(entry.name).x, mFont->getSize() * 1.5f});
entry.name, mFont, 0x000000FF, ALIGN_LEFT, ALIGN_CENTER, glm::ivec2 {1, 0},
glm::vec3 {0.0f, 0.0f, 0.0f}, glm::vec2 {0.0f, mFont->getSize() * 1.5f});
}
else {
entry.data.entryName = std::make_shared<TextComponent>(
entry.name, mFont, 0x000000FF, ALIGN_LEFT, ALIGN_CENTER, glm::vec3 {0.0f, 0.0f, 0.0f},
glm::vec2 {mFont->sizeText(entry.name).x, mFont->getSize() * 1.5f}, 0x00000000, 1.5f,
1.0f, false, 1.0f, 1500.0f, 1.5f, mSize.x - (mHorizontalMargin * 2.0f));
entry.name, mFont, 0x000000FF, ALIGN_LEFT, ALIGN_CENTER, glm::ivec2 {1, 0},
glm::vec3 {0.0f, 0.0f, 0.0f}, glm::vec2 {0.0f, mFont->getSize() * 1.5f}, 0x00000000,
1.5f, 1.0f, false, 1.0f, 1500.0f, 1.5f, mSize.x - (mHorizontalMargin * 2.0f));
}
if (mHorizontalScrolling) {

View file

@ -36,20 +36,10 @@ GuiMsgBox::GuiMsgBox(const HelpStyle& helpstyle,
, mDeleteOnButtonPress {deleteOnButtonPress}
, mMaxWidthMultiplier {maxWidthMultiplier}
{
// Adjust the width relative to the aspect ratio of the screen to make the GUI look coherent
// regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference.
const float aspectValue {1.778f / mRenderer->getScreenAspectRatio()};
if (mMaxWidthMultiplier == 0.0f)
mMaxWidthMultiplier = mRenderer->getIsVerticalOrientation() ? 0.90f : 0.80f;
float width {std::floor(glm::clamp(0.60f * aspectValue, 0.60f, mMaxWidthMultiplier) *
mRenderer->getScreenWidth())};
const float minWidth {
floorf(glm::clamp(0.30f * aspectValue, 0.10f, 0.50f) * mRenderer->getScreenWidth())};
// Initially set the text component to wrap by line breaks while maintaining the row lengths.
// This is the "ideal" size for the text as it's exactly how it's written.
mMsg = std::make_shared<TextComponent>(text, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary,
ALIGN_CENTER);
ALIGN_CENTER, ALIGN_CENTER, glm::ivec2 {1, 1});
mGrid.setEntry(mMsg, glm::ivec2 {0, 0}, false, false);
// Create the buttons.
@ -67,23 +57,7 @@ GuiMsgBox::GuiMsgBox(const HelpStyle& helpstyle,
mGrid.setEntry(mButtonGrid, glm::ivec2 {0, 1}, true, false, glm::ivec2 {1, 1},
GridFlags::BORDER_TOP);
// Decide final width.
if (mMsg->getSize().x < width && mButtonGrid->getSize().x < width) {
// mMsg and buttons are narrower than width.
width = std::max(mButtonGrid->getSize().x, mMsg->getSize().x);
width = std::max(width, minWidth);
}
else if (mButtonGrid->getSize().x > width) {
width = mButtonGrid->getSize().x;
}
// Now that we know the width, we can calculate the height.
mMsg->setSize(width, 0.0f); // mMsg->getSize.y() now returns the proper length.
const float msgHeight {std::max(Font::get(FONT_SIZE_LARGE)->getHeight(),
mMsg->getSize().y * VERTICAL_PADDING_MODIFIER)};
setSize(std::round(width + std::ceil(HORIZONTAL_PADDING_PX * 2.0f *
mRenderer->getScreenWidthModifier())),
std::round(msgHeight + mButtonGrid->getSize().y));
calculateSize();
setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f,
(mRenderer->getScreenHeight() - mSize.y) / 2.0f);
@ -92,24 +66,19 @@ GuiMsgBox::GuiMsgBox(const HelpStyle& helpstyle,
addChild(&mGrid);
}
void GuiMsgBox::changeText(const std::string& newText)
void GuiMsgBox::calculateSize()
{
mMsg->setText(newText);
glm::vec2 newSize {mMsg->getFont()->sizeText(newText)};
newSize.y *= VERTICAL_PADDING_MODIFIER;
mMsg->setSize(newSize);
// Adjust the width depending on the aspect ratio of the screen, to make the screen look
// somewhat coherent regardless of screen type. The 1.778 aspect ratio value is the 16:9
// reference.
const float aspectValue {1.778f / Renderer::getScreenAspectRatio()};
// Adjust the width relative to the aspect ratio of the screen to make the GUI look coherent
// regardless of screen type. The 1.778 aspect ratio value is the 16:9 reference.
const float aspectValue {1.778f / mRenderer->getScreenAspectRatio()};
if (mMaxWidthMultiplier == 0.0f)
mMaxWidthMultiplier = mRenderer->getIsVerticalOrientation() ? 0.90f : 0.80f;
float width {floorf(glm::clamp(0.60f * aspectValue, 0.60f, mMaxWidthMultiplier) *
mRenderer->getScreenWidth())};
const float minWidth {mRenderer->getScreenWidth() * 0.3f};
float width {std::floor(glm::clamp(0.60f * aspectValue, 0.60f, mMaxWidthMultiplier) *
mRenderer->getScreenWidth())};
const float minWidth {
floorf(glm::clamp(0.30f * aspectValue, 0.10f, 0.50f) * mRenderer->getScreenWidth())};
// Decide final width.
if (mMsg->getSize().x < width && mButtonGrid->getSize().x < width) {
@ -121,15 +90,24 @@ void GuiMsgBox::changeText(const std::string& newText)
width = mButtonGrid->getSize().x;
}
// Now that we know the width, we can calculate the height.
mMsg->setSize(width, 0.0f); // mMsg->getSize.y() now returns the proper height.
newSize = mMsg->getSize();
newSize.y *= VERTICAL_PADDING_MODIFIER;
mMsg->setSize(newSize);
// As the actual rows may be too wide to fit we change to wrapping by our component width
// while allowing expansion vertically. Setting the width will update the text cache.
mMsg->setAutoCalcExtent(glm::vec2 {0, 1});
mMsg->setSize(width, 0.0f);
const float msgHeight {std::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y)};
setSize(width + std::ceil(HORIZONTAL_PADDING_PX * 2.0f * mRenderer->getScreenWidthModifier()),
msgHeight + mButtonGrid->getSize().y);
const float msgHeight {std::max(Font::get(FONT_SIZE_LARGE)->getHeight(),
mMsg->getSize().y * VERTICAL_PADDING_MODIFIER)};
setSize(std::round(width + std::ceil(HORIZONTAL_PADDING_PX * 2.0f *
mRenderer->getScreenWidthModifier())),
std::round(msgHeight + mButtonGrid->getSize().y));
}
void GuiMsgBox::changeText(const std::string& newText)
{
mMsg->setAutoCalcExtent(glm::vec2 {1, 1});
mMsg->setText(newText);
calculateSize();
}
bool GuiMsgBox::input(InputConfig* config, Input input)
@ -149,7 +127,8 @@ void GuiMsgBox::onSizeChanged()
mGrid.setSize(mSize);
mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y / mSize.y);
mMsg->setSize(mSize.x - HORIZONTAL_PADDING_PX * 2.0f * Renderer::getScreenWidthModifier(),
mMsg->setSize(mSize.x -
std::ceil(HORIZONTAL_PADDING_PX * 2.0f * Renderer::getScreenWidthModifier()),
mGrid.getRowHeight(0));
mGrid.onSizeChanged();

View file

@ -34,6 +34,8 @@ public:
const bool deleteOnButtonPress = true,
const float maxWidthMultiplier = 0.0f);
void calculateSize();
void changeText(const std::string& newText);
bool input(InputConfig* config, Input input) override;

View file

@ -134,8 +134,8 @@ GuiTextEditKeyboardPopup::GuiTextEditKeyboardPopup(
mKeyboardGrid = std::make_shared<ComponentGrid>(
glm::ivec2 {mHorizontalKeyCount, static_cast<int>(kbLayout.size()) / 3});
mText = std::make_shared<TextEditComponent>();
mText->setValue(initValue, mMultiLine, false);
mText = std::make_shared<TextEditComponent>(mMultiLine);
mText->setValue(initValue, false);
// Header.
mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true);
@ -685,12 +685,12 @@ std::shared_ptr<ButtonComponent> GuiTextEditKeyboardPopup::makeButton(
return;
}
else if (key == _("LOAD")) {
mText->setValue(mDefaultValue->getValue(), mMultiLine);
mText->setValue(mDefaultValue->getValue());
mText->setCursor(mDefaultValue->getValue().size());
return;
}
else if (key == _("CLEAR")) {
mText->setValue("", mMultiLine);
mText->setValue("");
return;
}
else if (key == _("CANCEL")) {

View file

@ -55,8 +55,8 @@ GuiTextEditPopup::GuiTextEditPopup(const HelpStyle& helpstyle,
mMenuColorTitle, ALIGN_CENTER);
}
mText = std::make_shared<TextEditComponent>();
mText->setValue(initValue, mMultiLine, false);
mText = std::make_shared<TextEditComponent>(mMultiLine);
mText->setValue(initValue, false);
std::vector<std::shared_ptr<ButtonComponent>> buttons;
buttons.push_back(
@ -67,14 +67,14 @@ GuiTextEditPopup::GuiTextEditPopup(const HelpStyle& helpstyle,
if (mComplexMode) {
buttons.push_back(
std::make_shared<ButtonComponent>(_("LOAD"), loadBtnHelpText, [this, defaultValue] {
mText->setValue(defaultValue, mMultiLine);
mText->setValue(defaultValue);
mText->setCursor(0);
mText->setCursor(defaultValue.size());
}));
}
buttons.push_back(std::make_shared<ButtonComponent>(
_("CLEAR"), clearBtnHelpText, [this] { mText->setValue("", mMultiLine); }));
buttons.push_back(std::make_shared<ButtonComponent>(_("CLEAR"), clearBtnHelpText,
[this] { mText->setValue(""); }));
buttons.push_back(std::make_shared<ButtonComponent>(_("CANCEL"), _("discard changes"),
[this] { delete this; }));
@ -83,7 +83,7 @@ GuiTextEditPopup::GuiTextEditPopup(const HelpStyle& helpstyle,
mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true);
int yPos = 1;
int yPos {1};
if (mComplexMode) {
mGrid.setEntry(mInfoString, glm::ivec2 {0, yPos}, false, true);

View file

@ -14,17 +14,21 @@
#include "utils/PlatformUtil.h"
#include "utils/StringUtil.h"
#define DEBUG_SHAPING false
#define DISABLE_SHAPING false
Font::Font(float size, const std::string& path)
: mRenderer {Renderer::getInstance()}
, mPath(path)
, mFontHB {nullptr}
, mBufHB {nullptr}
, mEllipsisGlyph {0, 0, nullptr}
, mFontSize {size}
, mLetterHeight {0.0f}
, mSizeReference {0.0f}
, mMaxGlyphHeight {static_cast<int>(std::round(size))}
, mWrapMaxLength {0.0f}
, mWrapMaxHeight {0.0f}
, mWrapLineSpacing {1.5f}
, mSpaceGlyph {0}
, mShapeText {true}
{
if (mFontSize < 3.0f) {
mFontSize = 3.0f;
@ -61,6 +65,17 @@ Font::Font(float size, const std::string& path)
// of the font size to avoid some minor sizing issues.
if (getGlyph('\n')->rows > mMaxGlyphHeight)
mMaxGlyphHeight = getGlyph('\n')->rows;
// This is used when abbreviating and wrapping text in wrapText().
std::vector<Font::ShapeSegment> shapedGlyph;
shapeText("", shapedGlyph);
if (!shapedGlyph.empty()) {
mEllipsisGlyph = std::make_tuple(shapedGlyph.front().glyphIndexes.front().first,
shapedGlyph.front().glyphIndexes.front().second,
shapedGlyph.front().fontHB);
}
// This will be zero if there is no space glyph in the font (which hopefully never happens).
mSpaceGlyph = FT_Get_Char_Index(mFontFace->face, ' ');
}
Font::~Font()
@ -174,235 +189,6 @@ int Font::loadGlyphs(const std::string& text)
return mMaxGlyphHeight;
}
std::string Font::wrapText(const std::string& text,
const float maxLength,
const float maxHeight,
const float lineSpacing,
const bool multiLine)
{
assert(maxLength > 0.0f);
const float lineHeight {getHeight(lineSpacing)};
const float ellipsisWidth {sizeText("").x};
float accumHeight {lineHeight};
float lineWidth {0.0f};
float charWidth {0.0f};
float lastSpacePos {0.0f};
unsigned int charID {0};
size_t cursor {0};
size_t lastSpace {0};
size_t spaceAccum {0};
size_t byteCount {0};
std::string wrappedText;
std::string charEntry;
std::vector<std::pair<size_t, float>> ellipsisSection;
bool addEllipsis {false};
float totalWidth {0.0f};
mWrapMaxLength = maxLength;
mWrapMaxHeight = maxHeight;
mWrapLineSpacing = lineSpacing;
// TODO: Fix this rounding issue properly elsewhere.
if (mWrapMaxHeight < 1.0f)
mWrapMaxHeight = 0.0f;
std::vector<ShapeSegment> segmentsHB;
shapeText(text, segmentsHB);
// This should capture a lot of short strings, which are only a single segment.
if (!multiLine && segmentsHB.size() == 1 && segmentsHB.front().shapedWidth <= maxLength)
return text;
// Additionally this should capture many short multi-segment strings that do not require
// more involved line breaking.
bool hasNewline {false};
for (auto& segment : segmentsHB) {
totalWidth += segment.shapedWidth;
if (!segment.doShape && segment.substring == "\n") {
hasNewline = true;
break;
}
}
if (!hasNewline && totalWidth <= maxLength)
return text;
totalWidth = 0.0f;
// TODO: Add proper line breaking logic that takes substituted glyphs and adjusted horizontal
// advance values into consideration.
for (auto& segment : segmentsHB)
totalWidth += segment.shapedWidth;
for (size_t i {0}; i < text.length(); ++i) {
if (text[i] == '\n') {
if (!multiLine) {
addEllipsis = true;
break;
}
accumHeight += lineHeight;
if (mWrapMaxHeight != 0.0f && accumHeight > mWrapMaxHeight) {
addEllipsis = true;
break;
}
wrappedText.append("\n");
lineWidth = 0.0f;
lastSpace = 0;
continue;
}
cursor = i;
// Needed to handle multi-byte Unicode characters.
charID = Utils::String::chars2Unicode(text, cursor);
charEntry = text.substr(i, cursor - i);
Glyph* glyph {getGlyph(charID)};
if (glyph != nullptr) {
charWidth = static_cast<float>(glyph->advance.x);
byteCount = cursor - i;
}
else {
// Missing glyph.
continue;
}
if (multiLine && (charEntry == " " || charEntry == "\t")) {
lastSpace = i;
lastSpacePos = lineWidth;
}
if (lineWidth + charWidth <= maxLength) {
if (lineWidth + charWidth + ellipsisWidth > maxLength)
ellipsisSection.emplace_back(std::make_pair(byteCount, charWidth));
lineWidth += charWidth;
wrappedText.append(charEntry);
}
else if (!multiLine) {
addEllipsis = true;
break;
}
else {
if (mWrapMaxHeight == 0.0f || accumHeight < mWrapMaxHeight) {
// New row.
float spaceOffset {0.0f};
if (lastSpace == wrappedText.size()) {
wrappedText.append("\n");
}
else if (lastSpace != 0) {
if (lastSpace + spaceAccum == wrappedText.size())
wrappedText.append("\n");
else
wrappedText[lastSpace + spaceAccum] = '\n';
spaceOffset = lineWidth - lastSpacePos;
}
else {
if (lastSpace == 0)
++spaceAccum;
wrappedText.append("\n");
}
if (charEntry != " " && charEntry != "\t") {
wrappedText.append(charEntry);
lineWidth = charWidth;
}
else {
lineWidth = 0.0f;
}
accumHeight += lineHeight;
lineWidth += spaceOffset;
lastSpacePos = 0.0f;
lastSpace = 0;
}
else {
if (multiLine)
addEllipsis = true;
break;
}
}
i = cursor - 1;
}
if (addEllipsis) {
if (!wrappedText.empty() && wrappedText.back() == ' ') {
lineWidth -= sizeText(" ").x;
wrappedText.pop_back();
}
else if (!wrappedText.empty() && wrappedText.back() == '\t') {
lineWidth -= sizeText("\t").x;
wrappedText.pop_back();
}
while (!wrappedText.empty() && !ellipsisSection.empty() &&
lineWidth + ellipsisWidth > maxLength) {
lineWidth -= ellipsisSection.back().second;
wrappedText.erase(wrappedText.length() - ellipsisSection.back().first);
ellipsisSection.pop_back();
}
if (!wrappedText.empty() && wrappedText.back() == ' ')
wrappedText.pop_back();
wrappedText.append("");
}
return wrappedText;
}
glm::vec2 Font::getWrappedTextCursorOffset(const std::string& text,
const size_t stop,
const float lineSpacing)
{
float lineWidth {0.0f};
float yPos {0.0f};
size_t cursor {0};
const std::string wrappedText {
wrapText(text, mWrapMaxLength, mWrapMaxHeight, mWrapLineSpacing, true)};
// TODO: Enable this code when shaped text is properly wrapped in wrapText().
// std::vector<ShapeSegment> segmentsHB;
// shapeText(wrappedText, segmentsHB);
// size_t totalPos {0};
// for (auto& segment : segmentsHB) {
// if (totalPos > stop)
// break;
// for (size_t i {0}; i < segment.glyphIndexes.size(); ++i) {
// ++totalPos;
// if (totalPos > stop)
// break;
// const unsigned int character {segment.glyphIndexes[i].first};
// // Invalid character.
// if (!segment.doShape && character == 0)
// continue;
// if (!segment.doShape && character == '\n') {
// lineWidth = 0.0f;
// yPos += getHeight(lineSpacing);
// continue;
// }
// lineWidth += segment.glyphIndexes[i].second;
// }
// }
while (cursor < stop) {
unsigned int character {Utils::String::chars2Unicode(wrappedText, cursor)};
if (character == '\n') {
lineWidth = 0.0f;
yPos += getHeight(lineSpacing);
continue;
}
Glyph* glyph {getGlyph(character)};
if (glyph)
lineWidth += glyph->advance.x;
}
return glm::vec2 {lineWidth, yPos};
}
std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties,
const std::shared_ptr<Font>& orig,
@ -477,24 +263,21 @@ size_t Font::getTotalMemUsage()
return total;
}
TextCache* Font::buildTextCache(const std::string& textArg,
glm::vec2 offset,
unsigned int color,
TextCache* Font::buildTextCache(const std::string& text,
float length,
float maxLength,
float height,
Alignment alignment,
float offsetY,
float lineSpacing,
Alignment alignment,
unsigned int color,
bool noTopMargin,
bool doWrapText,
bool multiLine)
bool multiLine,
bool needGlyphsPos)
{
std::string text;
if (doWrapText)
text = wrapText(textArg, length, height, lineSpacing, multiLine);
else
text = textArg;
if (maxLength == 0.0f)
maxLength = length;
float x {offset.x + (length != 0 ? getNewlineStartOffset(text, 0, length, alignment) : 0)};
int yTop {0};
float yBot {0.0f};
@ -507,15 +290,43 @@ TextCache* Font::buildTextCache(const std::string& textArg,
yBot = getHeight(lineSpacing);
}
float y {offset.y + ((yBot + yTop) / 2.0f)};
std::vector<ShapeSegment> segmentsHB;
shapeText(text, segmentsHB);
wrapText(segmentsHB, maxLength, height, lineSpacing, multiLine);
size_t segmentIndex {0};
float x {0.0f};
float y {offsetY + ((yBot + yTop) / 2.0f)};
float lineWidth {0.0f};
float longestLine {0.0f};
float accumHeight {getHeight(lineSpacing)};
bool isNewLine {false};
// Vertices by texture.
std::map<FontTexture*, std::vector<Renderer::Vertex>> vertMap;
std::vector<ShapeSegment> segmentsHB;
shapeText(text, segmentsHB);
std::vector<glm::vec2> glyphPositions;
if (needGlyphsPos)
glyphPositions.emplace_back(0.0f, 0.0f);
for (auto& segment : segmentsHB) {
if (isNewLine || segmentIndex == 0) {
isNewLine = false;
float totalLength {0.0f};
for (size_t i {segmentIndex}; i < segmentsHB.size(); ++i) {
if (segmentsHB[i].lineBreak)
break;
totalLength += segmentsHB[i].shapedWidth;
}
float lengthTemp {length};
if (length == 0.0f)
lengthTemp = totalLength;
if (alignment == ALIGN_CENTER)
x = (lengthTemp - totalLength) / 2.0f;
else if (alignment == ALIGN_RIGHT)
x = lengthTemp - totalLength;
}
for (size_t cursor {0}; cursor < segment.glyphIndexes.size(); ++cursor) {
const unsigned int character {segment.glyphIndexes[cursor].first};
Glyph* glyph {nullptr};
@ -525,12 +336,38 @@ TextCache* Font::buildTextCache(const std::string& textArg,
continue;
if (!segment.doShape && character == '\n') {
x = 0.0f;
y += getHeight(lineSpacing);
x = offset[0] +
(length != 0 ? getNewlineStartOffset(
text, static_cast<const unsigned int>(segment.startPos + 1),
length, alignment) :
0);
lineWidth = 0.0f;
accumHeight += getHeight(lineSpacing);
// This logic changes the position of any space glyph at the end of a row to the
// beginning of the next row, as that's more intuitive when editing text.
bool spaceMatch {false};
if (needGlyphsPos && segmentIndex > 0) {
unsigned int spaceChar {0};
if (!mShapeText)
spaceChar = 32;
else if (segmentsHB[segmentIndex - 1].fontHB == mFontHB)
spaceChar = mSpaceGlyph;
else if (sFallbackSpaceGlyphs.find(segmentsHB[segmentIndex - 1].fontHB) !=
sFallbackSpaceGlyphs.cend())
spaceChar = sFallbackSpaceGlyphs[segment.fontHB];
unsigned int character {segmentsHB[segmentIndex - 1].glyphIndexes.back().first};
if (character == spaceChar)
spaceMatch = true;
}
if (needGlyphsPos && spaceMatch && glyphPositions.size() > 0) {
glyphPositions.back().x = 0.0f;
glyphPositions.back().y = accumHeight - getHeight(lineSpacing);
}
// Only add positions for "real" line breaks that were part of the original text.
if (needGlyphsPos && !segment.wrapped)
glyphPositions.emplace_back(x, accumHeight - getHeight(lineSpacing));
isNewLine = true;
continue;
}
@ -543,6 +380,8 @@ TextCache* Font::buildTextCache(const std::string& textArg,
if (glyph == nullptr)
continue;
lineWidth += glyph->advance.x;
std::vector<Renderer::Vertex>& verts {vertMap[glyph->texture]};
size_t oldVertSize {verts.size()};
verts.resize(oldVertSize + 6);
@ -574,14 +413,23 @@ TextCache* Font::buildTextCache(const std::string& textArg,
// Advance.
x += glyph->advance.x;
if (needGlyphsPos)
glyphPositions.emplace_back(x, accumHeight - getHeight(lineSpacing));
if (lineWidth > longestLine)
longestLine = lineWidth;
}
++segmentIndex;
}
TextCache* cache {new TextCache()};
cache->vertexLists.resize(vertMap.size());
cache->metrics.size = {sizeText(text, lineSpacing)};
cache->metrics.size = glm::vec2 {longestLine, accumHeight};
cache->metrics.maxGlyphHeight = mMaxGlyphHeight;
cache->clipRegion = {0.0f, 0.0f, 0.0f, 0.0f};
if (needGlyphsPos)
cache->glyphPositions = std::move(glyphPositions);
size_t i {0};
for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) {
@ -620,6 +468,38 @@ void Font::renderTextCache(TextCache* cache)
}
}
float Font::getSizeReference()
{
if (mSizeReference != 0.0f)
return mSizeReference;
const std::string includeChars {"ABCDEFGHIJKLMNOPQRSTUVWXYZ"};
hb_font_t* returnedFont {nullptr};
bool fontError {false};
int advance {0};
FT_Face* face {getFaceForChar('A', &returnedFont)};
if (!face) {
// This is completely inaccurate but it should hopefully never happen.
return static_cast<float>(mMaxGlyphHeight * 16);
}
// We don't check the face for each character, we just assume that if the font includes
// the 'A' character it also includes the other Latin capital letters.
for (auto character : includeChars) {
if (!fontError) {
const FT_GlyphSlot glyphSlot {(*face)->glyph};
if (FT_Load_Char(*face, character, FT_LOAD_RENDER))
return static_cast<float>(mMaxGlyphHeight * 16);
else
advance += glyphSlot->metrics.horiAdvance >> 6;
}
}
mSizeReference = advance;
return mSizeReference;
}
Font::FontTexture::FontTexture(const int mFontSize)
{
textureId = 0;
@ -740,11 +620,9 @@ void Font::shapeText(const std::string& text, std::vector<ShapeSegment>& segment
continue;
byteLength = textCursor - lastCursor;
if (unicode == '\'' || unicode == '\n' || currGlyph->fontHB == nullptr) {
// HarfBuzz converts ' and newline characters to invalid characters, so we
// need to exclude these from getting shaped. This means adding a new segment.
// We also add a segment if there is no font set as it means there was a missing
// glyph and the "no glyph" symbol should be shown.
if (unicode == '\n' || currGlyph->fontHB == nullptr) {
// We need to add a segment if there is a line break, or if no font is set as the
// latter means there was a missing glyph and the "no glyph" symbol should be shown.
addSegment = true;
if (!lastWasNoShaping) {
textCursor -= byteLength;
@ -770,17 +648,29 @@ void Font::shapeText(const std::string& text, std::vector<ShapeSegment>& segment
textCursor -= byteLength;
}
#if (DISABLE_SHAPING)
shapeSegment = false;
#else
if (!mShapeText)
shapeSegment = false;
#endif
if (addSegment) {
ShapeSegment segment;
segment.startPos = static_cast<unsigned int>(lastFlushPos);
segment.length = static_cast<unsigned int>(textCursor - lastFlushPos);
segment.fontHB = (lastFont == nullptr ? currGlyph->fontHB : lastFont);
segment.doShape = shapeSegment;
#if !defined(NDEBUG)
#if (DEBUG_SHAPING)
segment.substring = text.substr(lastFlushPos, textCursor - lastFlushPos);
if (segment.substring == "\n")
segment.lineBreak = true;
#else
if (!shapeSegment)
if (!shapeSegment) {
segment.substring = text.substr(lastFlushPos, textCursor - lastFlushPos);
if (segment.substring == "\n")
segment.lineBreak = true;
}
#endif
segmentsHB.emplace_back(std::move(segment));
@ -851,6 +741,335 @@ void Font::shapeText(const std::string& text, std::vector<ShapeSegment>& segment
}
}
void Font::wrapText(std::vector<ShapeSegment>& segmentsHB,
float maxLength,
const float maxHeight,
const float lineSpacing,
const bool multiLine)
{
std::vector<ShapeSegment> resultSegments;
// We first need to check whether the text is mixing left-to-right and right-to-left script
// as such text always needs to be processed in order to get spacing correct between segments.
bool hasLTR {false};
bool hasRTL {false};
for (auto& segment : segmentsHB) {
if (segment.rightToLeft)
hasRTL = true;
else
hasLTR = true;
// This is a special case where there is text with mixed script directions but with no
// length restriction. This most often means it's horizontally scrolling text. In this
// case we just set the length to a really large number, it's only to correctly get all
// segments processed below.
if (hasRTL && hasLTR && maxLength == 0.0f)
maxLength = 30000.0f;
}
if (!(hasLTR && hasRTL)) {
// This captures all text that is only a single segment and fits within maxLength, or that
// is not length-restricted.
if (maxLength == 0.0f ||
(segmentsHB.size() == 1 && segmentsHB.front().shapedWidth <= maxLength))
return;
// Additionally this captures shorter multi-segment text that does not require more involved
// line breaking or abbreviations.
float combinedWidth {0.0f};
bool hasNewline {false};
for (auto& segment : segmentsHB) {
combinedWidth += segment.shapedWidth;
if (segment.lineBreak) {
hasNewline = true;
break;
}
}
if (!hasNewline && combinedWidth <= maxLength)
return;
}
// All text that makes it this far requires either abbrevation or wrapping, or both.
// TODO: Text that mixes left-to-right and right-to-left script may not wrap and
// abbreviate correctly under all circumstances.
unsigned int newLength {0};
unsigned int spaceChar {0};
int lastSpaceWidth {0};
const float lineHeight {getHeight(lineSpacing)};
float totalWidth {0.0f};
float newShapedWidth {0.0f};
float accumHeight {lineHeight};
bool firstGlyphSpace {false};
bool lastSegmentSpace {false};
bool addEllipsis {false};
for (auto& segment : segmentsHB) {
if (addEllipsis)
break;
size_t lastSpace {0};
size_t spaceAccum {0};
// The space character glyph differs between fonts, so we need to know the correct
// index to be able to detect spaces.
if (segment.doShape == false)
spaceChar = 32;
else if (segment.fontHB == mFontHB)
spaceChar = mSpaceGlyph;
else if (sFallbackSpaceGlyphs.find(segment.fontHB) != sFallbackSpaceGlyphs.cend())
spaceChar = sFallbackSpaceGlyphs[segment.fontHB];
else
spaceChar = 0;
newShapedWidth = 0.0f;
ShapeSegment newSegment;
newSegment.startPos = newLength;
newSegment.fontHB = segment.fontHB;
newSegment.doShape = segment.doShape;
newSegment.rightToLeft = segment.rightToLeft;
newSegment.spaceChar = spaceChar;
#if (DEBUG_SHAPING)
newSegment.substring = segment.substring;
#else
if (!newSegment.doShape)
newSegment.substring = segment.substring;
#endif
// We don't bother to reverse this back later as the segment should only be needed once.
if (segment.rightToLeft) {
if (segment.glyphIndexes.front().first == spaceChar)
std::reverse(segment.glyphIndexes.begin() + 1, segment.glyphIndexes.end());
else
std::reverse(segment.glyphIndexes.begin(), segment.glyphIndexes.end());
}
for (size_t i {0}; i < segment.glyphIndexes.size(); ++i) {
if (multiLine) {
if (segment.lineBreak) {
totalWidth = 0.0f;
accumHeight += lineHeight;
newSegment.lineBreak = true;
}
if (segment.glyphIndexes[i].first == spaceChar) {
lastSpace = i;
lastSpaceWidth = segment.glyphIndexes[i].second;
lastSegmentSpace = false;
if (i == 0)
firstGlyphSpace = true;
}
}
if (totalWidth + segment.glyphIndexes[i].second > maxLength) {
if (multiLine) {
if (maxHeight != 0.0f && accumHeight > maxHeight) {
addEllipsis = true;
break;
}
if (maxHeight == 0.0f || accumHeight < maxHeight) {
// New row.
size_t offset {0};
if (lastSpace == i && !lastSegmentSpace) {
if (segment.rightToLeft)
newSegment.glyphIndexes.insert(newSegment.glyphIndexes.begin(),
segment.glyphIndexes[i]);
else
newSegment.glyphIndexes.emplace_back(segment.glyphIndexes[i]);
++i;
}
else if (lastSpace != 0 || firstGlyphSpace || lastSegmentSpace) {
size_t accum {0};
if (lastSegmentSpace)
++accum;
if (newSegment.rightToLeft &&
segment.glyphIndexes.front().first == spaceChar)
++accum;
lastSegmentSpace = false;
firstGlyphSpace = false;
if (lastSpace + spaceAccum - accum != i) {
offset = i - (lastSpace + spaceAccum - accum) - 1;
newShapedWidth -= lastSpaceWidth;
spaceAccum = 0;
}
}
else {
if (lastSpace == 0)
++spaceAccum;
}
for (size_t o {0}; o < offset; ++o) {
// Remove all glyphs going back to the last space.
--i;
--newLength;
if (newSegment.rightToLeft) {
newShapedWidth -= newSegment.glyphIndexes.front().second;
newSegment.glyphIndexes.erase(newSegment.glyphIndexes.begin());
}
else {
newShapedWidth -= newSegment.glyphIndexes.back().second;
newSegment.glyphIndexes.pop_back();
}
}
newSegment.length = newSegment.glyphIndexes.size();
newSegment.shapedWidth = newShapedWidth;
if (newSegment.glyphIndexes.size() != 0)
resultSegments.emplace_back(newSegment);
ShapeSegment breakSegment;
breakSegment.startPos = newLength;
breakSegment.length = 1;
breakSegment.shapedWidth = 0.0f;
breakSegment.fontHB = nullptr;
breakSegment.doShape = false;
breakSegment.lineBreak = true;
breakSegment.wrapped = true;
breakSegment.rightToLeft = false;
breakSegment.substring = "\n";
breakSegment.glyphIndexes.emplace_back(std::make_pair('\n', 0));
resultSegments.emplace_back(breakSegment);
++newLength;
newSegment.glyphIndexes.clear();
newSegment.startPos = newLength;
newSegment.length = 0;
newSegment.shapedWidth = 0.0f;
newShapedWidth = 0.0f;
totalWidth = 0.0f;
lastSpace = 0;
spaceAccum = 0;
accumHeight += lineHeight;
}
}
else {
addEllipsis = true;
break;
}
}
if (i == segment.glyphIndexes.size())
continue;
if (segment.rightToLeft)
newSegment.glyphIndexes.insert(newSegment.glyphIndexes.begin(),
segment.glyphIndexes[i]);
else
newSegment.glyphIndexes.emplace_back(segment.glyphIndexes[i]);
newShapedWidth += segment.glyphIndexes[i].second;
if (!segment.lineBreak)
totalWidth += segment.glyphIndexes[i].second;
++newLength;
}
// If the last glyph in the segment was a space, then this info may be needed for
// correct wrapping in the following segment.
if (lastSpace != 0 && newSegment.glyphIndexes.size() > 0 &&
newSegment.glyphIndexes.back().first == spaceChar)
lastSegmentSpace = true;
else
lastSegmentSpace = false;
newSegment.length = newSegment.glyphIndexes.size();
newSegment.shapedWidth = newShapedWidth;
if (newSegment.glyphIndexes.size() != 0)
resultSegments.emplace_back(newSegment);
}
if (addEllipsis && resultSegments.size() != 0 &&
resultSegments.back().glyphIndexes.size() > 0) {
std::vector<Font::ShapeSegment> shapedGlyph;
shapeText("", shapedGlyph);
if (!shapedGlyph.empty()) {
mEllipsisGlyph = std::make_tuple(shapedGlyph.front().glyphIndexes.front().first,
shapedGlyph.front().glyphIndexes.front().second,
shapedGlyph.front().fontHB);
}
if (resultSegments.back().rightToLeft) {
std::reverse(resultSegments.back().glyphIndexes.begin(),
resultSegments.back().glyphIndexes.end());
}
// If the last glyph is a space then remove it.
if (resultSegments.back().glyphIndexes.back().first == resultSegments.back().spaceChar) {
totalWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().shapedWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().glyphIndexes.pop_back();
}
// Remove as many glyphs as needed to fit the ellipsis glyph within maxLength.
while (resultSegments.back().glyphIndexes.size() > 0 &&
totalWidth + std::get<1>(mEllipsisGlyph) > maxLength) {
totalWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().shapedWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().glyphIndexes.pop_back();
}
// If the last glyph is a space then remove it before adding the ellipsis. This is
// however only done for a single space character in case there are repeating spaces.
if (resultSegments.back().glyphIndexes.size() > 0 &&
resultSegments.back().glyphIndexes.back().first == resultSegments.back().spaceChar) {
totalWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().shapedWidth -= resultSegments.back().glyphIndexes.back().second;
resultSegments.back().glyphIndexes.pop_back();
}
// This is a special case where the last glyph of the last segment was removed and
// the last glyph of the previous segment is a space, in this case we want to remove
// that space glyph as well.
else if (resultSegments.back().glyphIndexes.empty() && resultSegments.size() > 1 &&
resultSegments[resultSegments.size() - 2].glyphIndexes.size() > 0) {
if (resultSegments[resultSegments.size() - 2].rightToLeft) {
std::reverse(resultSegments[resultSegments.size() - 2].glyphIndexes.begin(),
resultSegments[resultSegments.size() - 2].glyphIndexes.end());
}
if (resultSegments[resultSegments.size() - 2].glyphIndexes.back().first ==
resultSegments[resultSegments.size() - 2].spaceChar) {
totalWidth -= resultSegments[resultSegments.size() - 2].glyphIndexes.back().second;
resultSegments[resultSegments.size() - 2].shapedWidth -=
resultSegments[resultSegments.size() - 2].glyphIndexes.back().second;
resultSegments[resultSegments.size() - 2].glyphIndexes.pop_back();
}
if (resultSegments[resultSegments.size() - 2].rightToLeft) {
std::reverse(resultSegments[resultSegments.size() - 2].glyphIndexes.begin(),
resultSegments[resultSegments.size() - 2].glyphIndexes.end());
}
}
if (resultSegments.back().rightToLeft) {
std::reverse(resultSegments.back().glyphIndexes.begin(),
resultSegments.back().glyphIndexes.end());
}
// Append the ellipsis glyph.
if (std::get<2>(mEllipsisGlyph) != nullptr) {
ShapeSegment newSegment;
newSegment.startPos = 0;
newSegment.fontHB = std::get<2>(mEllipsisGlyph);
#if (DISABLE_SHAPING)
newSegment.doShape = false;
#else
if (mShapeText)
newSegment.doShape = true;
else
newSegment.doShape = false;
#endif
newSegment.rightToLeft = false;
newSegment.shapedWidth += std::get<1>(mEllipsisGlyph);
newSegment.glyphIndexes.emplace_back(
std::make_pair(std::get<0>(mEllipsisGlyph), std::get<1>(mEllipsisGlyph)));
if (resultSegments.back().rightToLeft)
resultSegments.insert(resultSegments.end() - 1, newSegment);
else
resultSegments.emplace_back(newSegment);
}
}
std::swap(resultSegments, segmentsHB);
}
void Font::rebuildTextures()
{
// Recreate all glyph atlas textures.
@ -967,6 +1186,9 @@ std::vector<Font::FallbackFontCache> Font::getFallbackFontPaths()
hb_blob_destroy(blobHB);
ResourceData data {ResourceManager::getInstance().getFileData(path)};
fallbackFont.face = std::make_shared<FontFace>(std::move(data), 10.0f, path, fontHB);
const unsigned int spaceChar {FT_Get_Char_Index(fallbackFont.face->face, ' ')};
if (spaceChar != 0)
sFallbackSpaceGlyphs[fontHB] = spaceChar;
fontPaths.emplace_back(fallbackFont);
}
@ -1147,38 +1369,6 @@ Font::Glyph* Font::getGlyphByIndex(const unsigned int id, hb_font_t* fontArg, in
return &glyph;
}
float Font::getNewlineStartOffset(const std::string& text,
const unsigned int& charStart,
const float& length,
const Alignment& alignment)
{
switch (alignment) {
case ALIGN_LEFT: {
return 0;
}
case ALIGN_CENTER: {
int endChar {0};
endChar = static_cast<int>(text.find('\n', charStart));
return (length - sizeText(text.substr(charStart, static_cast<size_t>(endChar) !=
std::string::npos ?
endChar - charStart :
endChar))
.x) /
2.0f;
}
case ALIGN_RIGHT: {
int endChar = static_cast<int>(text.find('\n', charStart));
return length - (sizeText(text.substr(charStart, static_cast<size_t>(endChar) !=
std::string::npos ?
endChar - charStart :
endChar))
.x);
}
default:
return 0;
}
}
void TextCache::setColor(unsigned int color)
{
for (auto it = vertexLists.begin(); it != vertexLists.end(); ++it)

View file

@ -19,7 +19,6 @@
#include <hb-ft.h>
#include <vector>
class TextCache;
class TextComponent;
#define FONT_SIZE_MINI Font::getMiniFont()
@ -78,27 +77,15 @@ public:
return sLargeFixedFont;
}
// Returns the expected size of a string when rendered. Extra spacing is applied to the Y axis.
// Returns the size of shaped text without applying any wrapping or abbreviations.
glm::vec2 sizeText(std::string text, float lineSpacing = 1.5f);
// This determines mMaxGlyphHeight upfront which is useful for accurate text sizing by
// wrapText and buildTextCache as the requested font height is not guaranteed and could be
// exceeded by a few pixels for some glyphs. However in most instances setting mMaxGlyphHeight
// to the font size is good enough, meaning this somehow expensive operation could be omitted.
// This determines mMaxGlyphHeight upfront which is useful for accurate text sizing as
// the requested font height is not guaranteed and could be exceeded by a few pixels for some
// glyphs. However in most instances setting mMaxGlyphHeight to the font size is good enough,
// meaning this somehow expensive operation could be skipped.
int loadGlyphs(const std::string& text);
// Inserts newlines to make text wrap properly and also abbreviates single-line text.
std::string wrapText(const std::string& text,
const float maxLength,
const float maxHeight = 0.0f,
const float lineSpacing = 1.5f,
const bool multiLine = false);
// Returns the position of the cursor after moving it to the stop position.
glm::vec2 getWrappedTextCursorOffset(const std::string& text,
const size_t stop,
const float lineSpacing = 1.5f);
// Return overall height including line spacing.
const float getHeight(float lineSpacing = 1.5f) const { return mMaxGlyphHeight * lineSpacing; }
// This uses the letter 'S' as a size reference.
@ -124,18 +111,24 @@ public:
static size_t getTotalMemUsage();
protected:
TextCache* buildTextCache(const std::string& textArg,
glm::vec2 offset,
unsigned int color,
TextCache* buildTextCache(const std::string& text,
float length,
float maxLength,
float height,
Alignment alignment = ALIGN_LEFT,
float lineSpacing = 1.5f,
bool noTopMargin = false,
bool doWrapText = false,
bool multiLine = false);
float offsetY,
float lineSpacing,
Alignment alignment,
unsigned int color,
bool noTopMargin,
bool multiLine,
bool needGlyphsPos);
void renderTextCache(TextCache* cache);
// This is used to determine the horizontal text scrolling speed.
float getSizeReference();
// Enable or disable shaping, used by TextEditComponent.
void setTextShaping(bool state) { mShapeText = state; }
friend TextComponent;
@ -188,6 +181,7 @@ private:
std::string path;
std::shared_ptr<FontFace> face;
hb_font_t* fontHB;
unsigned int spaceChar;
};
struct ShapeSegment {
@ -196,7 +190,10 @@ private:
float shapedWidth;
hb_font_t* fontHB;
bool doShape;
bool lineBreak;
bool wrapped;
bool rightToLeft;
unsigned int spaceChar;
std::string substring;
std::vector<std::pair<unsigned int, int>> glyphIndexes;
@ -206,7 +203,10 @@ private:
, shapedWidth {0}
, fontHB {nullptr}
, doShape {false}
, lineBreak {false}
, wrapped {false}
, rightToLeft {false}
, spaceChar {0}
{
}
};
@ -214,6 +214,13 @@ private:
// Shape text using HarfBuzz.
void shapeText(const std::string& text, std::vector<ShapeSegment>& segmentsHB);
// Inserts newlines to make text wrap properly and also abbreviates when necessary.
void wrapText(std::vector<ShapeSegment>& segmentsHB,
float maxLength,
const float maxHeight = 0.0f,
const float lineSpacing = 1.5f,
const bool multiLine = false);
// Completely recreate the texture data for all glyph atlas entries.
void rebuildTextures();
void unloadTextures();
@ -228,14 +235,10 @@ private:
Glyph* getGlyph(const unsigned int id);
Glyph* getGlyphByIndex(const unsigned int id, hb_font_t* fontArg, int xAdvance);
float getNewlineStartOffset(const std::string& text,
const unsigned int& charStart,
const float& length,
const Alignment& alignment);
static inline FT_Library sLibrary {nullptr};
static inline std::map<std::tuple<float, std::string>, std::weak_ptr<Font>> sFontMap;
static inline std::vector<FallbackFontCache> sFallbackFonts;
static inline std::map<hb_font_t*, unsigned int> sFallbackSpaceGlyphs;
Renderer* mRenderer;
std::unique_ptr<FontFace> mFontFace;
@ -247,13 +250,14 @@ private:
const std::string mPath;
hb_font_t* mFontHB;
hb_buffer_t* mBufHB;
std::tuple<unsigned int, unsigned int, hb_font_t*> mEllipsisGlyph;
float mFontSize;
float mLetterHeight;
float mSizeReference;
int mMaxGlyphHeight;
float mWrapMaxLength;
float mWrapMaxHeight;
float mWrapLineSpacing;
unsigned int mSpaceGlyph;
bool mShapeText;
};
// Caching of shaped and rendered text.
@ -276,6 +280,9 @@ public:
void setClipRegion(const glm::vec4& clip) { clipRegion = clip; }
const glm::vec2& getSize() { return metrics.size; }
// Used by TextEditComponent to position the cursor and scroll the text box.
std::vector<glm::vec2> glyphPositions;
friend Font;
protected: