LCOV - code coverage report
Current view: top level - source/gui - CGUIText.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 113 182 62.1 %
Date: 2023-01-19 00:18:29 Functions: 7 10 70.0 %

          Line data    Source code
       1             : /* Copyright (C) 2022 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "CGUIText.h"
      21             : 
      22             : #include "graphics/Canvas2D.h"
      23             : #include "graphics/FontMetrics.h"
      24             : #include "graphics/TextRenderer.h"
      25             : #include "gui/CGUI.h"
      26             : #include "gui/ObjectBases/IGUIObject.h"
      27             : #include "gui/SettingTypes/CGUIString.h"
      28             : #include "ps/CStrInternStatic.h"
      29             : #include "ps/VideoMode.h"
      30             : #include "renderer/backend/IDeviceCommandContext.h"
      31             : #include "renderer/Renderer.h"
      32             : 
      33             : #include <math.h>
      34             : 
      35             : extern int g_xres, g_yres;
      36             : 
      37             : // TODO Gee: CRect => CPoint ?
      38           0 : void SGenerateTextImage::SetupSpriteCall(
      39             :     const bool left, CGUIText::SSpriteCall& spriteCall, const float width, const float y,
      40             :     const CSize2D& size, const CStr& textureName, const float bufferZone)
      41             : {
      42             :     // TODO Gee: Temp hardcoded values
      43           0 :     spriteCall.m_Area.top = y + bufferZone;
      44           0 :     spriteCall.m_Area.bottom = y + bufferZone + size.Height;
      45             : 
      46           0 :     if (left)
      47             :     {
      48           0 :         spriteCall.m_Area.left = bufferZone;
      49           0 :         spriteCall.m_Area.right = size.Width + bufferZone;
      50             :     }
      51             :     else
      52             :     {
      53           0 :         spriteCall.m_Area.left = width - bufferZone - size.Width;
      54           0 :         spriteCall.m_Area.right = width - bufferZone;
      55             :     }
      56             : 
      57           0 :     spriteCall.m_Sprite = textureName;
      58             : 
      59           0 :     m_YFrom = spriteCall.m_Area.top - bufferZone;
      60           0 :     m_YTo = spriteCall.m_Area.bottom + bufferZone;
      61           0 :     m_Indentation = size.Width + bufferZone * 2;
      62           0 : }
      63             : 
      64         170 : CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject)
      65             : {
      66         170 :     if (string.m_Words.empty())
      67         104 :         return;
      68             : 
      69         170 :     CStrIntern font(fontW.ToUTF8());
      70         170 :     float y = bufferZone; // drawing pointer
      71         170 :     float lineWidth = 0.f;
      72         170 :     int from = 0;
      73             : 
      74         170 :     bool firstLine = true;  // Necessary because text in the first line is shorter
      75             :                             // (it doesn't count the line spacing)
      76             : 
      77             :     // Images on the left or the right side.
      78         236 :     SGenerateTextImages images;
      79         170 :     int posLastImage = -1;  // Position in the string where last img (either left or right) were encountered.
      80             :                             //  in order to avoid duplicate processing.
      81             : 
      82             :     // The calculated width of each word includes the space between the current
      83             :     // word and the next. When we're wrapping, we need subtract the width of the
      84             :     // space after the last word on the line before the wrap.
      85         236 :     CFontMetrics currentFont(font);
      86         170 :     float spaceWidth = currentFont.GetCharacterWidth(L' ');
      87             : 
      88             :     // Go through string word by word.
      89             :     // a word is defined as [start, end[ in string.m_Words so we skip the last item.
      90         713 :     for (int i = 0; i < static_cast<int>(string.m_Words.size()) - 1; ++i)
      91             :     {
      92             :         // Pre-process each line one time, so we know which floating images
      93             :         //  will be added for that line.
      94             : 
      95             :         // Generated stuff is stored in feedback.
      96        1190 :         CGUIString::SFeedback feedback;
      97             : 
      98             :         // Preliminary line height, used for word-wrapping with floating images.
      99         647 :         float prelimLineHeight = 0.f;
     100             : 
     101             :         // Width and height of all text calls generated.
     102         647 :         string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
     103             : 
     104         647 :         SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
     105             : 
     106         647 :         posLastImage = std::max(posLastImage, i);
     107             : 
     108         647 :         lineWidth += feedback.m_Size.Width;
     109             : 
     110         647 :         prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
     111             : 
     112         647 :         float spaceCorrection = feedback.m_EndsWithSpace ? spaceWidth : 0.f;
     113             : 
     114             :         // If width is 0, then there's no word-wrapping, disable NewLine.
     115         647 :         if ((width != 0 && from != i && (lineWidth - spaceCorrection + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2)
     116             :         {
     117         347 :             if (ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
     118         104 :                 return;
     119         243 :             lineWidth = 0.f;
     120             :         }
     121             :     }
     122             : }
     123             : 
     124             : // Loop through our images queues, to see if images have been added.
     125         647 : void CGUIText::SetupSpriteCalls(
     126             :     const CGUI& pGUI,
     127             :     const std::array<std::vector<CStr>, 2>& feedbackImages,
     128             :     const float y,
     129             :     const float width,
     130             :     const float bufferZone,
     131             :     const int i,
     132             :     const int posLastImage,
     133             :     SGenerateTextImages& images)
     134             : {
     135             :     // Check if this has already been processed.
     136             :     //  Also, floating images are only applicable if Word-Wrapping is on
     137         647 :     if (width == 0 || i <= posLastImage)
     138         174 :         return;
     139             : 
     140             :     // Loop left/right
     141        1419 :     for (int j = 0; j < 2; ++j)
     142         946 :         for (const CStr& imgname : feedbackImages[j])
     143             :         {
     144           0 :             SSpriteCall spriteCall;
     145             :             SGenerateTextImage image;
     146             : 
     147             :             // Y is if no other floating images is above, y. Else it is placed
     148             :             //  after the last image, like a stack downwards.
     149             :             float _y;
     150           0 :             if (!images[j].empty())
     151           0 :                 _y = std::max(y, images[j].back().m_YTo);
     152             :             else
     153           0 :                 _y = y;
     154             : 
     155           0 :             const SGUIIcon& icon = pGUI.GetIcon(imgname);
     156           0 :             image.SetupSpriteCall(j == CGUIString::SFeedback::Left, spriteCall, width, _y, icon.m_Size, icon.m_SpriteName, bufferZone);
     157             : 
     158             :             // Check if image is the lowest thing.
     159           0 :             m_Size.Height = std::max(m_Size.Height, image.m_YTo);
     160             : 
     161           0 :             images[j].emplace_back(image);
     162           0 :             m_SpriteCalls.emplace_back(std::move(spriteCall));
     163             :         }
     164             : }
     165             : 
     166             : // Now we'll do another loop to figure out the height and width of
     167             : //  the line (the height of the largest character and the width is
     168             : //  the sum of all of the individual widths). This
     169             : //  couldn't be determined in the first loop (main loop)
     170             : //  because it didn't regard images, so we don't know
     171             : //  if all characters processed, will actually be involved
     172             : //  in that line.
     173         347 : void CGUIText::ComputeLineSize(
     174             :     const CGUI& pGUI,
     175             :     const CGUIString& string,
     176             :     const CStrIntern& font,
     177             :     const bool firstLine,
     178             :     const float width,
     179             :     const float widthRangeFrom,
     180             :     const float widthRangeTo,
     181             :     const int i,
     182             :     const int tempFrom,
     183             :     CSize2D& lineSize) const
     184             : {
     185             :     // The calculated width of each word includes the space between the current
     186             :     // word and the next. When we're wrapping, we need subtract the width of the
     187             :     // space after the last word on the line before the wrap.
     188         694 :     CFontMetrics currentFont(font);
     189         347 :     float spaceWidth = currentFont.GetCharacterWidth(L' ');
     190             : 
     191         347 :     float spaceCorrection = 0.f;
     192             : 
     193         347 :     float x = widthRangeFrom;
     194         817 :     for (int j = tempFrom; j <= i; ++j)
     195             :     {
     196             :         // We don't want to use feedback now, so we'll have to use another one.
     197        1117 :         CGUIString::SFeedback feedback2;
     198             : 
     199             :         // Don't attach object, it'll suppress the errors
     200             :         //  we want them to be reported in the final GenerateTextCall()
     201             :         //  so that we don't get duplicates.
     202         647 :         string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
     203             : 
     204             :         // Append X value.
     205         647 :         x += feedback2.m_Size.Width;
     206             : 
     207         647 :         const float currentSpaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.0f;
     208             : 
     209         647 :         const bool isLineOverflow = x - currentSpaceCorrection > widthRangeTo;
     210         647 :         if (width != 0 && isLineOverflow && j != tempFrom && !feedback2.m_NewLine)
     211         163 :             break;
     212             : 
     213             :         // Update after the line-break detection, because otherwise spaceCorrection above
     214             :         // will refer to the wrapped word and not the last-word-before-the-line-break.
     215         484 :         spaceCorrection = currentSpaceCorrection;
     216             : 
     217             :         // Let lineSize.cy be the maximum m_Height we encounter.
     218         484 :         lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
     219             : 
     220             :         // If the current word is an explicit new line ("\n"),
     221             :         // break now before adding the width of this character.
     222             :         // ("\n" doesn't have a glyph, thus is given the same width as
     223             :         // the "missing glyph" character by CFont::GetCharacterWidth().)
     224         484 :         if (width != 0 && feedback2.m_NewLine)
     225          14 :             break;
     226             : 
     227         470 :         lineSize.Width += feedback2.m_Size.Width;
     228             :     }
     229             :     // Remove the space if necessary.
     230         347 :     lineSize.Width -= spaceCorrection;
     231         347 : }
     232             : 
     233         347 : bool CGUIText::ProcessLine(
     234             :     const CGUI& pGUI,
     235             :     const CGUIString& string,
     236             :     const CStrIntern& font,
     237             :     const IGUIObject* pObject,
     238             :     const SGenerateTextImages& images,
     239             :     const EAlign align,
     240             :     const float prelimLineHeight,
     241             :     const float width,
     242             :     const float bufferZone,
     243             :     bool& firstLine,
     244             :     float& y,
     245             :     int& i,
     246             :     int& from)
     247             : {
     248             :     // Change 'from' to 'i', but first keep a copy of its value.
     249         347 :     int tempFrom = from;
     250         347 :     from = i;
     251             : 
     252         347 :     float widthRangeFrom = bufferZone;
     253         347 :     float widthRangeTo = width - bufferZone;
     254         347 :     ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
     255             : 
     256         347 :     CSize2D lineSize;
     257         347 :     ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
     258             : 
     259             :     // Move down, because font drawing starts from the baseline
     260         347 :     y += lineSize.Height;
     261             : 
     262             :     // Do the real processing now
     263         347 :     const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
     264             : 
     265             :     // Update dimensions
     266         347 :     m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
     267         347 :     m_Size.Height = std::max(m_Size.Height, y + bufferZone);
     268             : 
     269         347 :     firstLine = false;
     270             : 
     271             :     // Now if we entered as from = i, then we want
     272             :     //  i being one minus that, so that it will become
     273             :     //  the same i in the next loop. The difference is that
     274             :     //  we're on a new line now.
     275         347 :     i = from - 1;
     276             : 
     277         347 :     return done;
     278             : }
     279             : 
     280             : // Decide width of the line. We need to iterate our floating images.
     281             : //  this won't be exact because we're assuming the lineSize.cy
     282             : //  will be as our preliminary calculation said. But that may change,
     283             : //  although we'd have to add a couple of more loops to try straightening
     284             : //  this problem out, and it is very unlikely to happen noticeably if one
     285             : //  structures his text in a stylistically pure fashion. Even if not, it
     286             : //  is still quite unlikely it will happen.
     287             : // Loop through left and right side, from and to.
     288         347 : void CGUIText::ComputeLineRange(
     289             :     const SGenerateTextImages& images,
     290             :     const float y,
     291             :     const float width,
     292             :     const float prelimLineHeight,
     293             :     float& widthRangeFrom,
     294             :     float& widthRangeTo) const
     295             : {
     296             :     // Floating images are only applicable if word-wrapping is enabled.
     297         347 :     if (width == 0)
     298           1 :         return;
     299             : 
     300        1038 :     for (int j = 0; j < 2; ++j)
     301         692 :         for (const SGenerateTextImage& img : images[j])
     302             :         {
     303             :             // We're working with two intervals here, the image's and the line height's.
     304             :             //  let's find the union of these two.
     305             :             float unionFrom, unionTo;
     306             : 
     307           0 :             unionFrom = std::max(y, img.m_YFrom);
     308           0 :             unionTo = std::min(y + prelimLineHeight, img.m_YTo);
     309             : 
     310             :             // The union is not empty
     311           0 :             if (unionTo > unionFrom)
     312             :             {
     313           0 :                 if (j == 0)
     314           0 :                     widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
     315             :                 else
     316           0 :                     widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
     317             :             }
     318             :         }
     319             : }
     320             : 
     321             : // compute offset based on what kind of alignment
     322         347 : float CGUIText::GetLineOffset(
     323             :     const EAlign align,
     324             :     const float widthRangeFrom,
     325             :     const float widthRangeTo,
     326             :     const CSize2D& lineSize) const
     327             : {
     328         347 :     switch (align)
     329             :     {
     330          25 :     case EAlign::LEFT:
     331          25 :         return widthRangeFrom;
     332             : 
     333         302 :     case EAlign::CENTER:
     334         302 :         return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
     335             : 
     336          20 :     case EAlign::RIGHT:
     337          20 :         return widthRangeTo - lineSize.Width;
     338             : 
     339           0 :     default:
     340           0 :         debug_warn(L"Broken EAlign in CGUIText()");
     341           0 :         return 0.f;
     342             :     }
     343             : }
     344             : 
     345         347 : bool CGUIText::AssembleCalls(
     346             :     const CGUI& pGUI,
     347             :     const CGUIString& string,
     348             :     const CStrIntern& font,
     349             :     const IGUIObject* pObject,
     350             :     const bool firstLine,
     351             :     const float width,
     352             :     const float widthRangeTo,
     353             :     const float dx,
     354             :     const float y,
     355             :     const int tempFrom,
     356             :     const int i,
     357             :     int& from)
     358             : {
     359         347 :     bool done = false;
     360         347 :     float x = dx;
     361             : 
     362         817 :     for (int j = tempFrom; j <= i; ++j)
     363             :     {
     364             :         // We don't want to use feedback now, so we'll have to use another one.
     365        1111 :         CGUIString::SFeedback feedback2;
     366             : 
     367             :         // Defaults
     368         641 :         string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
     369             : 
     370             :         // Iterate all and set X/Y values
     371             :         // Since X values are not set, we need to make an internal
     372             :         //  iteration with an increment that will append the internal
     373             :         //  x, that is what xPointer is for.
     374         641 :         float xPointer = 0.f;
     375             : 
     376        1282 :         for (STextCall& tc : feedback2.m_TextCalls)
     377             :         {
     378         641 :             tc.m_Pos = CVector2D(x + xPointer, y);
     379             : 
     380         641 :             xPointer += tc.m_Size.Width;
     381             : 
     382         641 :             if (tc.m_pSpriteCall)
     383           0 :                 tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
     384             :         }
     385             : 
     386         641 :         x += feedback2.m_Size.Width;
     387             : 
     388         641 :         if (width != 0) // only if word-wrapping is applicable
     389             :         {
     390             :             // Check if we need to wrap, using the same algorithm as ComputeLineSize
     391             :             // This means we must ignore the 'space before the next word' for the purposes of wrapping.
     392        1089 :             CFontMetrics currentFont(font);
     393         630 :             float spaceWidth = currentFont.GetCharacterWidth(L' ');
     394         630 :             float spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
     395             : 
     396         630 :             if (feedback2.m_NewLine)
     397             :             {
     398          14 :                 from = j + 1;
     399             : 
     400             :                 // Sprite call can exist within only a newline segment,
     401             :                 //  therefore we need this.
     402          14 :                 if (!feedback2.m_SpriteCalls.empty())
     403             :                 {
     404           0 :                     auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
     405           0 :                     m_TextCalls.insert(
     406           0 :                         m_TextCalls.end(),
     407             :                         std::make_move_iterator(feedback2.m_TextCalls.begin()),
     408           0 :                         std::make_move_iterator(newEnd));
     409           0 :                     m_SpriteCalls.insert(
     410           0 :                         m_SpriteCalls.end(),
     411             :                         std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
     412           0 :                         std::make_move_iterator(feedback2.m_SpriteCalls.end()));
     413             :                 }
     414          14 :                 break;
     415             :             }
     416         616 :             else if (x - spaceCorrection > widthRangeTo && j == tempFrom)
     417             :             {
     418             :                 // The first word overrides the width limit, what we do,
     419             :                 // in those cases, is just drawing that word even
     420             :                 // though it'll extend the object.
     421             :                 // Ergo: do not break, since we want it to be added to m_TextCalls.
     422          72 :                 from = j+1;
     423             :                 // To avoid doing redundant computations, set up j to exit the loop right away.
     424          72 :                 j = i + 1;
     425             :             }
     426         544 :             else if (x - spaceCorrection > widthRangeTo)
     427             :             {
     428         157 :                 from = j;
     429         157 :                 break;
     430             :             }
     431             :         }
     432             : 
     433             :         // Add the whole feedback2.m_TextCalls to our m_TextCalls.
     434         470 :         m_TextCalls.insert(
     435         940 :             m_TextCalls.end(),
     436             :             std::make_move_iterator(feedback2.m_TextCalls.begin()),
     437        1410 :             std::make_move_iterator(feedback2.m_TextCalls.end()));
     438             : 
     439         470 :         m_SpriteCalls.insert(
     440         940 :             m_SpriteCalls.end(),
     441             :             std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
     442        1410 :             std::make_move_iterator(feedback2.m_SpriteCalls.end()));
     443             : 
     444         470 :         if (j == static_cast<int>(string.m_Words.size()) - 2)
     445         104 :             done = true;
     446             :     }
     447             : 
     448         347 :     return done;
     449             : }
     450             : 
     451           0 : void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
     452             : {
     453             :     Renderer::Backend::IDeviceCommandContext* deviceCommandContext =
     454           0 :         g_Renderer.GetDeviceCommandContext();
     455             : 
     456           0 :     const bool isClipped = clipping != CRect();
     457           0 :     if (isClipped)
     458             :     {
     459             :         // Make clipping rect as small as possible to prevent rounding errors
     460           0 :         clipping.top = std::ceil(clipping.top);
     461           0 :         clipping.bottom = std::floor(clipping.bottom);
     462           0 :         clipping.left = std::ceil(clipping.left);
     463           0 :         clipping.right = std::floor(clipping.right);
     464             : 
     465           0 :         if (clipping.GetWidth() <= 0.0f || clipping.GetHeight() <= 0.0f)
     466           0 :             return;
     467             : 
     468           0 :         const float scale = g_VideoMode.GetScale();
     469             :         Renderer::Backend::IDeviceCommandContext::Rect scissorRect;
     470           0 :         scissorRect.x = std::ceil(clipping.left * scale);
     471           0 :         scissorRect.y = std::ceil(g_yres - clipping.bottom * scale);
     472           0 :         scissorRect.width = std::floor(clipping.GetWidth() * scale);
     473           0 :         scissorRect.height = std::floor(clipping.GetHeight() * scale);
     474             :         // TODO: move scissors to CCanvas2D.
     475           0 :         deviceCommandContext->SetScissors(1, &scissorRect);
     476             :     }
     477             : 
     478           0 :     CTextRenderer textRenderer;
     479           0 :     textRenderer.SetClippingRect(clipping);
     480           0 :     textRenderer.Translate(0.0f, 0.0f);
     481             : 
     482           0 :     for (const STextCall& tc : m_TextCalls)
     483             :     {
     484             :         // If this is just a placeholder for a sprite call, continue
     485           0 :         if (tc.m_pSpriteCall)
     486           0 :             continue;
     487             : 
     488           0 :         textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
     489           0 :         textRenderer.SetCurrentFont(tc.m_Font);
     490           0 :         textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
     491             :     }
     492             : 
     493           0 :     canvas.DrawText(textRenderer);
     494             : 
     495           0 :     for (const SSpriteCall& sc : m_SpriteCalls)
     496           0 :         pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
     497             : 
     498           0 :     if (isClipped)
     499           0 :         deviceCommandContext->SetScissors(0, nullptr);
     500             : }

Generated by: LCOV version 1.13