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: 104 176 59.1 %
Date: 2022-06-14 00:41:00 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          63 : 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          42 :     if (string.m_Words.empty())
      67          17 :         return;
      68             : 
      69          21 :     CStrIntern font(fontW.ToUTF8());
      70          21 :     float y = bufferZone; // drawing pointer
      71          21 :     float lineWidth = 0.f;
      72          21 :     int from = 0;
      73             : 
      74          21 :     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          25 :     SGenerateTextImages images;
      79          21 :     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          29 :     CFontMetrics currentFont(font);
      86          21 :     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         207 :     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         389 :         CGUIString::SFeedback feedback;
      97             : 
      98             :         // Preliminary line height, used for word-wrapping with floating images.
      99         203 :         float prelimLineHeight = 0.f;
     100             : 
     101             :         // Width and height of all text calls generated.
     102         609 :         string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
     103             : 
     104         203 :         SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
     105             : 
     106         203 :         posLastImage = std::max(posLastImage, i);
     107             : 
     108         203 :         lineWidth += feedback.m_Size.Width;
     109             : 
     110         203 :         prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
     111             : 
     112         203 :         float spaceCorrection = feedback.m_EndsWithSpace ? spaceWidth : 0.f;
     113             : 
     114             :         // If width is 0, then there's no word-wrapping, disable NewLine.
     115         203 :         if ((width != 0 && from != i && (lineWidth - spaceCorrection + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2)
     116             :         {
     117          52 :             if (ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
     118          17 :                 return;
     119             :             lineWidth = 0.f;
     120             :         }
     121             :     }
     122             : }
     123             : 
     124             : // Loop through our images queues, to see if images have been added.
     125         203 : 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         203 :     if (width == 0 || i <= posLastImage)
     138             :         return;
     139             : 
     140             :     // Loop left/right
     141         525 :     for (int j = 0; j < 2; ++j)
     142        1400 :         for (const CStr& imgname : feedbackImages[j])
     143             :         {
     144           0 :             SSpriteCall spriteCall;
     145           0 :             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           0 :             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          52 : 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          52 :     CFontMetrics currentFont(font);
     189          52 :     float spaceWidth = currentFont.GetCharacterWidth(L' ');
     190             : 
     191          52 :     float spaceCorrection = 0.f;
     192             : 
     193          52 :     float x = widthRangeFrom;
     194         224 :     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         203 :         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         609 :         string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
     203             : 
     204             :         // Append X value.
     205         203 :         x += feedback2.m_Size.Width;
     206             : 
     207         203 :         if (width != 0 && x - spaceCorrection > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
     208             :             break;
     209             : 
     210             :         // Update after the line-break detection, because otherwise spaceCorrection above
     211             :         // will refer to the wrapped word and not the last-word-before-the-line-break.
     212         186 :         spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
     213             : 
     214             :         // Let lineSize.cy be the maximum m_Height we encounter.
     215         186 :         lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
     216             : 
     217             :         // If the current word is an explicit new line ("\n"),
     218             :         // break now before adding the width of this character.
     219             :         // ("\n" doesn't have a glyph, thus is given the same width as
     220             :         // the "missing glyph" character by CFont::GetCharacterWidth().)
     221         186 :         if (width != 0 && feedback2.m_NewLine)
     222             :             break;
     223             : 
     224         172 :         lineSize.Width += feedback2.m_Size.Width;
     225             :     }
     226             :     // Remove the space if necessary.
     227          52 :     lineSize.Width -= spaceCorrection;
     228          52 : }
     229             : 
     230          52 : bool CGUIText::ProcessLine(
     231             :     const CGUI& pGUI,
     232             :     const CGUIString& string,
     233             :     const CStrIntern& font,
     234             :     const IGUIObject* pObject,
     235             :     const SGenerateTextImages& images,
     236             :     const EAlign align,
     237             :     const float prelimLineHeight,
     238             :     const float width,
     239             :     const float bufferZone,
     240             :     bool& firstLine,
     241             :     float& y,
     242             :     int& i,
     243             :     int& from)
     244             : {
     245             :     // Change 'from' to 'i', but first keep a copy of its value.
     246          52 :     int tempFrom = from;
     247          52 :     from = i;
     248             : 
     249          52 :     float widthRangeFrom = bufferZone;
     250          52 :     float widthRangeTo = width - bufferZone;
     251          52 :     ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
     252             : 
     253          52 :     CSize2D lineSize;
     254          52 :     ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
     255             : 
     256             :     // Move down, because font drawing starts from the baseline
     257          52 :     y += lineSize.Height;
     258             : 
     259             :     // Do the real processing now
     260          52 :     const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
     261             : 
     262             :     // Update dimensions
     263          52 :     m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
     264          52 :     m_Size.Height = std::max(m_Size.Height, y + bufferZone);
     265             : 
     266          52 :     firstLine = false;
     267             : 
     268             :     // Now if we entered as from = i, then we want
     269             :     //  i being one minus that, so that it will become
     270             :     //  the same i in the next loop. The difference is that
     271             :     //  we're on a new line now.
     272          52 :     i = from - 1;
     273             : 
     274          52 :     return done;
     275             : }
     276             : 
     277             : // Decide width of the line. We need to iterate our floating images.
     278             : //  this won't be exact because we're assuming the lineSize.cy
     279             : //  will be as our preliminary calculation said. But that may change,
     280             : //  although we'd have to add a couple of more loops to try straightening
     281             : //  this problem out, and it is very unlikely to happen noticeably if one
     282             : //  structures his text in a stylistically pure fashion. Even if not, it
     283             : //  is still quite unlikely it will happen.
     284             : // Loop through left and right side, from and to.
     285          52 : void CGUIText::ComputeLineRange(
     286             :     const SGenerateTextImages& images,
     287             :     const float y,
     288             :     const float width,
     289             :     const float prelimLineHeight,
     290             :     float& widthRangeFrom,
     291             :     float& widthRangeTo) const
     292             : {
     293             :     // Floating images are only applicable if word-wrapping is enabled.
     294          52 :     if (width == 0)
     295             :         return;
     296             : 
     297         153 :     for (int j = 0; j < 2; ++j)
     298         408 :         for (const SGenerateTextImage& img : images[j])
     299             :         {
     300             :             // We're working with two intervals here, the image's and the line height's.
     301             :             //  let's find the union of these two.
     302           0 :             float unionFrom, unionTo;
     303             : 
     304           0 :             unionFrom = std::max(y, img.m_YFrom);
     305           0 :             unionTo = std::min(y + prelimLineHeight, img.m_YTo);
     306             : 
     307             :             // The union is not empty
     308           0 :             if (unionTo > unionFrom)
     309             :             {
     310           0 :                 if (j == 0)
     311           0 :                     widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
     312             :                 else
     313           0 :                     widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
     314             :             }
     315             :         }
     316             : }
     317             : 
     318             : // compute offset based on what kind of alignment
     319          52 : float CGUIText::GetLineOffset(
     320             :     const EAlign align,
     321             :     const float widthRangeFrom,
     322             :     const float widthRangeTo,
     323             :     const CSize2D& lineSize) const
     324             : {
     325          52 :     switch (align)
     326             :     {
     327             :     case EAlign::LEFT:
     328             :         return widthRangeFrom;
     329             : 
     330           7 :     case EAlign::CENTER:
     331           7 :         return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
     332             : 
     333          20 :     case EAlign::RIGHT:
     334          20 :         return widthRangeTo - lineSize.Width;
     335             : 
     336           0 :     default:
     337           0 :         debug_warn(L"Broken EAlign in CGUIText()");
     338             :         return 0.f;
     339             :     }
     340             : }
     341             : 
     342          52 : bool CGUIText::AssembleCalls(
     343             :     const CGUI& pGUI,
     344             :     const CGUIString& string,
     345             :     const CStrIntern& font,
     346             :     const IGUIObject* pObject,
     347             :     const bool firstLine,
     348             :     const float width,
     349             :     const float widthRangeTo,
     350             :     const float dx,
     351             :     const float y,
     352             :     const int tempFrom,
     353             :     const int i,
     354             :     int& from)
     355             : {
     356          52 :     bool done = false;
     357          52 :     float x = dx;
     358             : 
     359         224 :     for (int j = tempFrom; j <= i; ++j)
     360             :     {
     361             :         // We don't want to use feedback now, so we'll have to use another one.
     362         372 :         CGUIString::SFeedback feedback2;
     363             : 
     364             :         // Defaults
     365         600 :         string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
     366             : 
     367             :         // Iterate all and set X/Y values
     368             :         // Since X values are not set, we need to make an internal
     369             :         //  iteration with an increment that will append the internal
     370             :         //  x, that is what xPointer is for.
     371         200 :         float xPointer = 0.f;
     372             : 
     373         800 :         for (STextCall& tc : feedback2.m_TextCalls)
     374             :         {
     375         200 :             tc.m_Pos = CVector2D(x + xPointer, y);
     376             : 
     377         200 :             xPointer += tc.m_Size.Width;
     378             : 
     379         200 :             if (tc.m_pSpriteCall)
     380           0 :                 tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
     381             :         }
     382             : 
     383         200 :         x += feedback2.m_Size.Width;
     384             : 
     385         200 :         if (width != 0) // only if word-wrapping is applicable
     386             :         {
     387             :             // Check if we need to wrap, using the same algorithm as ComputeLineSize
     388             :             // This means we must ignore the 'space before the next word' for the purposes of wrapping.
     389         511 :             CFontMetrics currentFont(font);
     390         189 :             float spaceWidth = currentFont.GetCharacterWidth(L' ');
     391         189 :             float spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
     392             : 
     393         189 :             if (feedback2.m_NewLine)
     394             :             {
     395          14 :                 from = j + 1;
     396             : 
     397             :                 // Sprite call can exist within only a newline segment,
     398             :                 //  therefore we need this.
     399          28 :                 if (!feedback2.m_SpriteCalls.empty())
     400             :                 {
     401           0 :                     auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
     402           0 :                     m_TextCalls.insert(
     403           0 :                         m_TextCalls.end(),
     404             :                         std::make_move_iterator(feedback2.m_TextCalls.begin()),
     405           0 :                         std::make_move_iterator(newEnd));
     406           0 :                     m_SpriteCalls.insert(
     407           0 :                         m_SpriteCalls.end(),
     408             :                         std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
     409           0 :                         std::make_move_iterator(feedback2.m_SpriteCalls.end()));
     410             :                 }
     411          56 :                 break;
     412             :             }
     413         175 :             else if (x - spaceCorrection > widthRangeTo && j == tempFrom)
     414             :             {
     415             :                 // The first word overrides the width limit, what we do,
     416             :                 // in those cases, is just drawing that word even
     417             :                 // though it'll extend the object.
     418             :                 // Ergo: do not break, since we want it to be added to m_TextCalls.
     419           7 :                 from = j+1;
     420             :                 // To avoid doing redundant computations, set up j to exit the loop right away.
     421           7 :                 j = i + 1;
     422             :             }
     423         168 :             else if (x - spaceCorrection > widthRangeTo)
     424             :             {
     425          14 :                 from = j;
     426          14 :                 break;
     427             :             }
     428             :         }
     429             : 
     430             :         // Add the whole feedback2.m_TextCalls to our m_TextCalls.
     431         172 :         m_TextCalls.insert(
     432         344 :             m_TextCalls.end(),
     433             :             std::make_move_iterator(feedback2.m_TextCalls.begin()),
     434        1204 :             std::make_move_iterator(feedback2.m_TextCalls.end()));
     435             : 
     436         172 :         m_SpriteCalls.insert(
     437         344 :             m_SpriteCalls.end(),
     438             :             std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
     439        1204 :             std::make_move_iterator(feedback2.m_SpriteCalls.end()));
     440             : 
     441         344 :         if (j == static_cast<int>(string.m_Words.size()) - 2)
     442          17 :             done = true;
     443             :     }
     444             : 
     445          52 :     return done;
     446             : }
     447             : 
     448           0 : void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
     449             : {
     450           0 :     Renderer::Backend::IDeviceCommandContext* deviceCommandContext =
     451           0 :         g_Renderer.GetDeviceCommandContext();
     452             : 
     453           0 :     const bool isClipped = clipping != CRect();
     454           0 :     if (isClipped)
     455             :     {
     456             :         // Make clipping rect as small as possible to prevent rounding errors
     457           0 :         clipping.top = std::ceil(clipping.top);
     458           0 :         clipping.bottom = std::floor(clipping.bottom);
     459           0 :         clipping.left = std::ceil(clipping.left);
     460           0 :         clipping.right = std::floor(clipping.right);
     461             : 
     462           0 :         if (clipping.GetWidth() <= 0.0f || clipping.GetHeight() <= 0.0f)
     463           0 :             return;
     464             : 
     465           0 :         const float scale = g_VideoMode.GetScale();
     466           0 :         Renderer::Backend::IDeviceCommandContext::Rect scissorRect;
     467           0 :         scissorRect.x = std::ceil(clipping.left * scale);
     468           0 :         scissorRect.y = std::ceil(g_yres - clipping.bottom * scale);
     469           0 :         scissorRect.width = std::floor(clipping.GetWidth() * scale);
     470           0 :         scissorRect.height = std::floor(clipping.GetHeight() * scale);
     471             :         // TODO: move scissors to CCanvas2D.
     472           0 :         deviceCommandContext->SetScissors(1, &scissorRect);
     473             :     }
     474             : 
     475           0 :     CTextRenderer textRenderer;
     476           0 :     textRenderer.SetClippingRect(clipping);
     477           0 :     textRenderer.Translate(0.0f, 0.0f);
     478             : 
     479           0 :     for (const STextCall& tc : m_TextCalls)
     480             :     {
     481             :         // If this is just a placeholder for a sprite call, continue
     482           0 :         if (tc.m_pSpriteCall)
     483             :             continue;
     484             : 
     485           0 :         textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
     486           0 :         textRenderer.SetCurrentFont(tc.m_Font);
     487           0 :         textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
     488             :     }
     489             : 
     490           0 :     canvas.DrawText(textRenderer);
     491             : 
     492           0 :     for (const SSpriteCall& sc : m_SpriteCalls)
     493           0 :         pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
     494             : 
     495           0 :     if (isClipped)
     496           0 :         deviceCommandContext->SetScissors(0, nullptr);
     497             : }

Generated by: LCOV version 1.13