mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2025-02-16 12:05:38 +00:00
Added layout and line wrapping support for shaped text and for mixing of LTR and RTL scripts
This commit is contained in:
parent
bd6956d52f
commit
3552c6e228
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"};
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)) &&
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ private:
|
|||
bool mEnabled;
|
||||
bool mFlatStyle;
|
||||
|
||||
float mMinWidth;
|
||||
unsigned int mTextColorFocused;
|
||||
unsigned int mTextColorUnfocused;
|
||||
unsigned int mFlatColorFocused;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue