LCOV - code coverage report
Current view: top level - source/gui/SettingTypes - CGUIString.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 76 240 31.7 %
Date: 2023-01-19 00:18:29 Functions: 3 5 60.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 "CGUIString.h"
      21             : 
      22             : #include "graphics/FontMetrics.h"
      23             : #include "gui/CGUI.h"
      24             : #include "gui/ObjectBases/IGUIObject.h"
      25             : #include "lib/utf8.h"
      26             : #include "ps/CLogger.h"
      27             : 
      28             : #include <algorithm>
      29             : #include <array>
      30             : 
      31             : // List of word delimiter bounds
      32             : // The list contains ranges of word delimiters. The odd indexed chars are the start
      33             : // of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
      34             : static const int NUM_WORD_DELIMITERS = 4*2;
      35             : static const u16 WordDelimiters[NUM_WORD_DELIMITERS] = {
      36             :     ' '   , ' ',    // spaces
      37             :     '-'   , '-',    // hyphens
      38             :     0x3000, 0x31FF, // ideographic symbols
      39             :     0x3400, 0x9FFF
      40             : // TODO add unicode blocks of other languages that don't use spaces
      41             : };
      42             : 
      43        1935 : void CGUIString::SFeedback::Reset()
      44             : {
      45        1935 :     m_Images[Left].clear();
      46        1935 :     m_Images[Right].clear();
      47        1935 :     m_TextCalls.clear();
      48        1935 :     m_SpriteCalls.clear();
      49        1935 :     m_Size = CSize2D();
      50        1935 :     m_NewLine = false;
      51        1935 :     m_EndsWithSpace = false;
      52        1935 : }
      53             : 
      54        1935 : void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject) const
      55             : {
      56             :     // Reset width and height, because they will be determined with incrementation
      57             :     //  or comparisons.
      58        1935 :     Feedback.Reset();
      59             : 
      60             :     // Check out which text chunk this is within.
      61        3870 :     for (const TextChunk& textChunk : m_TextChunks)
      62             :     {
      63             :         // Get the area that is overlapped by both the TextChunk and
      64             :         //  by the from/to inputted.
      65        1935 :         int _from = std::max(from, textChunk.m_From);
      66        1935 :         int _to = std::min(to, textChunk.m_To);
      67             : 
      68             :         // If from is larger than to, then they are not overlapping
      69        1935 :         if (_to == _from && textChunk.m_From == textChunk.m_To)
      70             :         {
      71             :             // These should never be able to have more than one tag.
      72           0 :             ENSURE(textChunk.m_Tags.size() == 1);
      73             : 
      74             :             // Icons and images are placed on exactly one position
      75             :             //  in the words-list, and they can be counted twice if placed
      76             :             //  on an edge. But there is always only one logical preference
      77             :             //  that we want. This check filters the unwanted.
      78             : 
      79             :             // it's in the end of one word, and the icon
      80             :             //  should really belong to the beginning of the next one
      81           0 :             if (_to == to && to >= 1 && to < (int)m_RawString.length())
      82             :             {
      83           0 :                 if (m_RawString[to-1] == ' ' ||
      84           0 :                     m_RawString[to-1] == '-' ||
      85           0 :                     m_RawString[to-1] == '\n')
      86           0 :                     continue;
      87             :             }
      88             :             // This std::string is just a break
      89           0 :             if (_from == from && from >= 1)
      90             :             {
      91           0 :                 if (m_RawString[from] == '\n' &&
      92           0 :                     m_RawString[from-1] != '\n' &&
      93           0 :                     m_RawString[from-1] != ' ' &&
      94           0 :                     m_RawString[from-1] != '-')
      95           0 :                     continue;
      96             :             }
      97             : 
      98           0 :             const TextChunk::Tag& tag = textChunk.m_Tags[0];
      99           0 :             ENSURE(tag.m_TagType == TextChunk::Tag::TAG_IMGLEFT ||
     100             :                    tag.m_TagType == TextChunk::Tag::TAG_IMGRIGHT ||
     101             :                    tag.m_TagType == TextChunk::Tag::TAG_ICON);
     102             : 
     103           0 :             const std::string& path = utf8_from_wstring(tag.m_TagValue);
     104           0 :             if (!pGUI.HasIcon(path))
     105             :             {
     106           0 :                 if (pObject)
     107           0 :                     LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path.c_str());
     108           0 :                 continue;
     109             :             }
     110             : 
     111           0 :             switch (tag.m_TagType)
     112             :             {
     113           0 :             case TextChunk::Tag::TAG_IMGLEFT:
     114           0 :                 Feedback.m_Images[SFeedback::Left].push_back(path);
     115           0 :                 break;
     116           0 :             case TextChunk::Tag::TAG_IMGRIGHT:
     117           0 :                 Feedback.m_Images[SFeedback::Right].push_back(path);
     118           0 :                 break;
     119           0 :             case TextChunk::Tag::TAG_ICON:
     120             :             {
     121             :                 // We'll need to setup a text-call that will point
     122             :                 //  to the icon, this is to be able to iterate
     123             :                 //  through the text-calls without having to
     124             :                 //  complex the structure virtually for nothing more.
     125           0 :                 CGUIText::STextCall TextCall;
     126             : 
     127             :                 // Also add it to the sprites being rendered.
     128           0 :                 CGUIText::SSpriteCall SpriteCall;
     129             : 
     130             :                 // Get Icon from icon database in pGUI
     131           0 :                 const SGUIIcon& icon = pGUI.GetIcon(path);
     132             : 
     133           0 :                 const CSize2D& size = icon.m_Size;
     134             : 
     135             :                 // append width, and make maximum height the height.
     136           0 :                 Feedback.m_Size.Width += size.Width;
     137           0 :                 Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
     138             : 
     139             :                 // These are also needed later
     140           0 :                 TextCall.m_Size = size;
     141           0 :                 SpriteCall.m_Area = size;
     142             : 
     143             :                 // Handle additional attributes
     144           0 :                 for (const TextChunk::Tag::TagAttribute& tagAttrib : tag.m_TagAttributes)
     145             :                 {
     146           0 :                     if (tagAttrib.attrib == L"displace" && !tagAttrib.value.empty())
     147             :                     {
     148             :                         // Displace the sprite
     149           0 :                         CSize2D displacement;
     150             :                         // Parse the value
     151           0 :                         if (!CGUI::ParseString<CSize2D>(&pGUI, tagAttrib.value, displacement))
     152           0 :                             LOGERROR("Error parsing 'displace' value for tag [ICON]");
     153             :                         else
     154           0 :                             SpriteCall.m_Area += displacement;
     155             :                     }
     156           0 :                     else if (tagAttrib.attrib == L"tooltip")
     157           0 :                         TextCall.m_Tooltip = tagAttrib.value;
     158             :                 }
     159             : 
     160           0 :                 SpriteCall.m_Sprite = icon.m_SpriteName;
     161             : 
     162             :                 // Add sprite call
     163           0 :                 Feedback.m_SpriteCalls.push_back(std::move(SpriteCall));
     164             : 
     165             :                 // Finalize text call
     166           0 :                 TextCall.m_pSpriteCall = &Feedback.m_SpriteCalls.back();
     167             : 
     168             :                 // Add text call
     169           0 :                 Feedback.m_TextCalls.emplace_back(std::move(TextCall));
     170             : 
     171           0 :                 break;
     172             :             }
     173           0 :             NODEFAULT;
     174           0 :             }
     175             :         }
     176        1935 :         else if (_to > _from && !Feedback.m_NewLine)
     177             :         {
     178        3870 :             CGUIText::STextCall TextCall;
     179             : 
     180             :             // Set defaults
     181        1935 :             TextCall.m_Font = DefaultFont;
     182        1935 :             TextCall.m_UseCustomColor = false;
     183             : 
     184        1935 :             TextCall.m_String = m_RawString.substr(_from, _to-_from);
     185             : 
     186             :             // Go through tags and apply changes.
     187        1935 :             for (const TextChunk::Tag& tag : textChunk.m_Tags)
     188             :             {
     189           0 :                 switch (tag.m_TagType)
     190             :                 {
     191           0 :                 case TextChunk::Tag::TAG_COLOR:
     192           0 :                     TextCall.m_UseCustomColor = true;
     193             : 
     194           0 :                     if (!CGUI::ParseString<CGUIColor>(&pGUI, tag.m_TagValue, TextCall.m_Color) && pObject)
     195           0 :                         LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject->GetPresentableName().c_str());
     196           0 :                     break;
     197           0 :                 case TextChunk::Tag::TAG_FONT:
     198             :                     // TODO Gee: (2004-08-15) Check if Font exists?
     199           0 :                     TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
     200           0 :                     break;
     201           0 :                 case TextChunk::Tag::TAG_TOOLTIP:
     202           0 :                     TextCall.m_Tooltip = tag.m_TagValue;
     203           0 :                     break;
     204           0 :                 default:
     205           0 :                     LOGERROR("Encountered unexpected tag applied to text");
     206           0 :                     break;
     207             :                 }
     208             :             }
     209             : 
     210             :             // Calculate the size of the font.
     211        1935 :             CSize2D size;
     212             :             int cx, cy;
     213        3870 :             CFontMetrics font (TextCall.m_Font);
     214        1935 :             font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
     215             : 
     216        1935 :             size.Width = static_cast<float>(cx);
     217             :             // For anything other than the first line, the line spacing
     218             :             // needs to be considered rather than just the height of the text.
     219        1935 :             if (FirstLine)
     220        1188 :                 size.Height = static_cast<float>(font.GetHeight());
     221             :             else
     222         747 :                 size.Height = static_cast<float>(font.GetLineSpacing());
     223             : 
     224             :             // Append width, and make maximum height the height.
     225        1935 :             Feedback.m_Size.Width += size.Width;
     226        1935 :             Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
     227             : 
     228             :             // These are also needed later
     229        1935 :             TextCall.m_Size = size;
     230             : 
     231        1935 :             if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
     232          45 :                 Feedback.m_NewLine = true;
     233             :             // Multiple empty spaces are treated as individual words (one per space),
     234             :             // and for coherence we'll do the 'ignore space after the word' thing
     235             :             // only if the word actually has some other text in it, so process this only if size >= 2
     236        1890 :             else if (TextCall.m_String.size() >= 2)
     237             :             {
     238        1731 :                 const wchar_t lastChar = TextCall.m_String.back();
     239             :                 // If the last word ends with a 'space', we'll ignore it when aligning, so mark it.
     240        1731 :                 Feedback.m_EndsWithSpace = lastChar == ' ' || lastChar == 0x3000;
     241             :             }
     242             :             else
     243         159 :                 Feedback.m_EndsWithSpace = false;
     244             : 
     245             :             // Add text-chunk
     246        1935 :             Feedback.m_TextCalls.emplace_back(std::move(TextCall));
     247             :         }
     248             :     }
     249        1935 : }
     250             : 
     251           0 : bool CGUIString::TextChunk::Tag::SetTagType(const CStrW& tagtype)
     252             : {
     253           0 :     TagType t = GetTagType(tagtype);
     254           0 :     if (t == TAG_INVALID)
     255           0 :         return false;
     256             : 
     257           0 :     m_TagType = t;
     258           0 :     return true;
     259             : }
     260             : 
     261           0 : CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const CStrW& tagtype) const
     262             : {
     263           0 :     if (tagtype == L"color")
     264           0 :         return TAG_COLOR;
     265           0 :     if (tagtype == L"font")
     266           0 :         return TAG_FONT;
     267           0 :     if (tagtype == L"icon")
     268           0 :         return TAG_ICON;
     269           0 :     if (tagtype == L"imgleft")
     270           0 :         return TAG_IMGLEFT;
     271           0 :     if (tagtype == L"imgright")
     272           0 :         return TAG_IMGRIGHT;
     273           0 :     if (tagtype == L"tooltip")
     274           0 :         return TAG_TOOLTIP;
     275             : 
     276           0 :     return TAG_INVALID;
     277             : }
     278             : 
     279           7 : void CGUIString::SetValue(const CStrW& str)
     280             : {
     281           7 :     m_OriginalString = str;
     282             : 
     283           7 :     m_TextChunks.clear();
     284           7 :     m_Words.clear();
     285           7 :     m_RawString.clear();
     286             : 
     287             :     // Current Text Chunk
     288          13 :     CGUIString::TextChunk CurrentTextChunk;
     289           7 :     CurrentTextChunk.m_From = 0;
     290             : 
     291           7 :     int l = str.length();
     292           7 :     int rawpos = 0;
     293          13 :     CStrW tag;
     294          13 :     std::vector<CStrW> tags;
     295           7 :     bool closing = false;
     296         311 :     for (int p = 0; p < l; ++p)
     297             :     {
     298         608 :         TextChunk::Tag tag_;
     299         304 :         switch (str[p])
     300             :         {
     301           0 :         case L'[':
     302           0 :             CurrentTextChunk.m_To = rawpos;
     303             :             // Add the current chunks if it is not empty
     304           0 :             if (CurrentTextChunk.m_From != rawpos)
     305           0 :                 m_TextChunks.push_back(CurrentTextChunk);
     306           0 :             CurrentTextChunk.m_From = rawpos;
     307             : 
     308           0 :             closing = false;
     309           0 :             if (++p == l)
     310             :             {
     311           0 :                 LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str));
     312           0 :                 break;
     313             :             }
     314           0 :             if (str[p] == L'/')
     315             :             {
     316           0 :                 closing = true;
     317           0 :                 if (tags.empty())
     318             :                 {
     319           0 :                     LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p, utf8_from_wstring(str));
     320           0 :                     break;
     321             :                 }
     322           0 :                 if (++p == l)
     323             :                 {
     324           0 :                     LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str));
     325           0 :                     break;
     326             :                 }
     327             :             }
     328           0 :             tag.clear();
     329             :             // Parse tag
     330           0 :             for (; p < l && str[p] != L']'; ++p)
     331             :             {
     332           0 :                 CStrW name, param;
     333           0 :                 switch (str[p])
     334             :                 {
     335           0 :                 case L' ':
     336           0 :                     if (closing) // We still parse them to make error handling cleaner
     337           0 :                         LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
     338             : 
     339             :                     // parse something="something else"
     340           0 :                     for (++p; p < l && str[p] != L'='; ++p)
     341           0 :                         name.push_back(str[p]);
     342             : 
     343           0 :                     if (p == l)
     344             :                     {
     345           0 :                         LOGERROR("Parameter without value at pos %d '%s'", p, utf8_from_wstring(str));
     346           0 :                         break;
     347             :                     }
     348             :                     FALLTHROUGH;
     349             :                 case L'=':
     350             :                     // parse a quoted parameter
     351           0 :                     if (closing) // We still parse them to make error handling cleaner
     352           0 :                         LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
     353             : 
     354           0 :                     if (++p == l)
     355             :                     {
     356           0 :                         LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str));
     357           0 :                         break;
     358             :                     }
     359           0 :                     if (str[p] != L'"')
     360             :                     {
     361           0 :                         LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p, utf8_from_wstring(str));
     362           0 :                         break;
     363             :                     }
     364           0 :                     for (++p; p < l && str[p] != L'"'; ++p)
     365             :                     {
     366           0 :                         switch (str[p])
     367             :                         {
     368           0 :                         case L'\\':
     369           0 :                             if (++p == l)
     370             :                             {
     371           0 :                                 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
     372           0 :                                 break;
     373             :                             }
     374             :                             // NOTE: We do not support \n in tag parameters
     375             :                             FALLTHROUGH;
     376             :                         default:
     377           0 :                             param.push_back(str[p]);
     378             :                         }
     379             :                     }
     380             : 
     381           0 :                     if (!name.empty())
     382             :                     {
     383           0 :                         TextChunk::Tag::TagAttribute a = {name, param};
     384           0 :                         tag_.m_TagAttributes.push_back(a);
     385             :                     }
     386             :                     else
     387           0 :                         tag_.m_TagValue = param;
     388           0 :                     break;
     389           0 :                 default:
     390           0 :                     tag.push_back(str[p]);
     391           0 :                     break;
     392             :                 }
     393           0 :             }
     394             : 
     395           0 :             if (!tag_.SetTagType(tag))
     396             :             {
     397           0 :                 LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag), p, utf8_from_wstring(str));
     398           0 :                 break;
     399             :             }
     400           0 :             if (!closing)
     401             :             {
     402           0 :                 if (tag_.m_TagType == TextChunk::Tag::TAG_IMGRIGHT
     403           0 :                     || tag_.m_TagType == TextChunk::Tag::TAG_IMGLEFT
     404           0 :                     || tag_.m_TagType == TextChunk::Tag::TAG_ICON)
     405             :                 {
     406           0 :                     TextChunk FreshTextChunk = { rawpos, rawpos };
     407           0 :                     FreshTextChunk.m_Tags.push_back(tag_);
     408           0 :                     m_TextChunks.push_back(FreshTextChunk);
     409             :                 }
     410             :                 else
     411             :                 {
     412           0 :                     tags.push_back(tag);
     413           0 :                     CurrentTextChunk.m_Tags.push_back(tag_);
     414             :                 }
     415             :             }
     416             :             else
     417             :             {
     418           0 :                 if (tag != tags.back())
     419             :                 {
     420           0 :                     LOGERROR("Closing tag '%s' does not match last opened tag '%s' at %d in '%s'", utf8_from_wstring(tag), utf8_from_wstring(tags.back()), p, utf8_from_wstring(str));
     421           0 :                     break;
     422             :                 }
     423             : 
     424           0 :                 tags.pop_back();
     425           0 :                 CurrentTextChunk.m_Tags.pop_back();
     426             :             }
     427           0 :             break;
     428           0 :         case L'\\':
     429           0 :             if (++p == l)
     430             :             {
     431           0 :                 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
     432           0 :                 break;
     433             :             }
     434           0 :             if (str[p] == L'n')
     435             :             {
     436           0 :                 ++rawpos;
     437           0 :                 m_RawString.push_back(L'\n');
     438           0 :                 break;
     439             :             }
     440             :             FALLTHROUGH;
     441             :         default:
     442         304 :             ++rawpos;
     443         304 :             m_RawString.push_back(str[p]);
     444         304 :             break;
     445             :         }
     446             :     }
     447             : 
     448             :     // Add the chunk after the last tag
     449           7 :     if (CurrentTextChunk.m_From != rawpos)
     450             :     {
     451           7 :         CurrentTextChunk.m_To = rawpos;
     452           7 :         m_TextChunks.push_back(CurrentTextChunk);
     453             :     }
     454             : 
     455             : 
     456             :     // Add a delimiter at start and at end, it helps when
     457             :     //  processing later, because we don't have make exceptions for
     458             :     //  those cases.
     459           7 :     m_Words.push_back(0);
     460             : 
     461             :     // Add word boundaries in increasing order
     462         311 :     for (u32 i = 0; i < m_RawString.length(); ++i)
     463             :     {
     464         304 :         wchar_t c = m_RawString[i];
     465         308 :         if (c == '\n')
     466             :         {
     467           4 :             m_Words.push_back((int)i);
     468           4 :             m_Words.push_back((int)i+1);
     469           4 :             continue;
     470             :         }
     471         813 :         for (int n = 0; n < NUM_WORD_DELIMITERS; n += 2)
     472             :         {
     473         813 :             if (c <= WordDelimiters[n+1])
     474             :             {
     475         300 :                 if (c >= WordDelimiters[n])
     476          43 :                     m_Words.push_back((int)i+1);
     477             :                 // assume the WordDelimiters list is stored in increasing order
     478         300 :                 break;
     479             :             }
     480             :         }
     481             :     }
     482             : 
     483           7 :     m_Words.push_back((int)m_RawString.length());
     484             : 
     485             :     // Remove duplicates (only if larger than 2)
     486           7 :     if (m_Words.size() <= 2)
     487           1 :         return;
     488             : 
     489           6 :     m_Words.erase(std::unique(m_Words.begin(), m_Words.end()), m_Words.end());
     490             : }

Generated by: LCOV version 1.13