mirror of
				https://github.com/RetroDECK/ES-DE.git
				synced 2025-04-10 19:15:13 +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