1 /* Copyright (C) 2017 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
18 #include "precompiled.h"
24 #include "graphics/FontMetrics.h"
25 #include "ps/CLogger.h"
28 // List of word delimiter bounds
29 // The list contains ranges of word delimiters. The odd indexed chars are the start
30 // of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
31 static const int NUM_WORD_DELIMITERS
= 4*2;
32 static const u16 WordDelimiters
[NUM_WORD_DELIMITERS
] = {
35 0x3000, 0x31FF, // ideographic symbols
37 // TODO add unicode blocks of other languages that don't use spaces
40 void CGUIString::SFeedback::Reset()
42 m_Images
[Left
].clear();
43 m_Images
[Right
].clear();
45 m_SpriteCalls
.clear();
50 void CGUIString::GenerateTextCall(const CGUI
* pGUI
, SFeedback
& Feedback
, CStrIntern DefaultFont
, const int& from
, const int& to
, const bool FirstLine
, const IGUIObject
* pObject
) const
52 // Reset width and height, because they will be determined with incrementation
56 // Check out which text chunk this is within.
57 for (const TextChunk
& textChunk
: m_TextChunks
)
59 // Get the area that is overlapped by both the TextChunk and
60 // by the from/to inputted.
61 int _from
= std::max(from
, textChunk
.m_From
);
62 int _to
= std::min(to
, textChunk
.m_To
);
64 // If from is larger than to, then they are not overlapping
65 if (_to
== _from
&& textChunk
.m_From
== textChunk
.m_To
)
67 // These should never be able to have more than one tag.
68 ENSURE(textChunk
.m_Tags
.size() == 1);
70 // Icons and images are placed on exactly one position
71 // in the words-list, and they can be counted twice if placed
72 // on an edge. But there is always only one logical preference
73 // that we want. This check filters the unwanted.
75 // it's in the end of one word, and the icon
76 // should really belong to the beginning of the next one
77 if (_to
== to
&& to
>= 1 && to
< (int)m_RawString
.length())
79 if (m_RawString
[to
-1] == ' ' ||
80 m_RawString
[to
-1] == '-' ||
81 m_RawString
[to
-1] == '\n')
84 // This std::string is just a break
85 if (_from
== from
&& from
>= 1)
87 if (m_RawString
[from
] == '\n' &&
88 m_RawString
[from
-1] != '\n' &&
89 m_RawString
[from
-1] != ' ' &&
90 m_RawString
[from
-1] != '-')
94 const TextChunk::Tag
& tag
= textChunk
.m_Tags
[0];
95 ENSURE(tag
.m_TagType
== TextChunk::Tag::TAG_IMGLEFT
||
96 tag
.m_TagType
== TextChunk::Tag::TAG_IMGRIGHT
||
97 tag
.m_TagType
== TextChunk::Tag::TAG_ICON
);
99 const std::string
& path
= utf8_from_wstring(tag
.m_TagValue
);
100 if (!pGUI
->IconExists(path
))
103 LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path
.c_str());
107 switch (tag
.m_TagType
)
109 case TextChunk::Tag::TAG_IMGLEFT
:
110 Feedback
.m_Images
[SFeedback::Left
].push_back(path
);
112 case TextChunk::Tag::TAG_IMGRIGHT
:
113 Feedback
.m_Images
[SFeedback::Right
].push_back(path
);
115 case TextChunk::Tag::TAG_ICON
:
117 // We'll need to setup a text-call that will point
118 // to the icon, this is to be able to iterate
119 // through the text-calls without having to
120 // complex the structure virtually for nothing more.
121 SGUIText::STextCall TextCall
;
123 // Also add it to the sprites being rendered.
124 SGUIText::SSpriteCall SpriteCall
;
126 // Get Icon from icon database in pGUI
127 SGUIIcon icon
= pGUI
->GetIcon(path
);
129 CSize size
= icon
.m_Size
;
131 // append width, and make maximum height the height.
132 Feedback
.m_Size
.cx
+= size
.cx
;
133 Feedback
.m_Size
.cy
= std::max(Feedback
.m_Size
.cy
, size
.cy
);
135 // These are also needed later
136 TextCall
.m_Size
= size
;
137 SpriteCall
.m_Area
= size
;
139 // Handle additional attributes
140 for (const TextChunk::Tag::TagAttribute
& tagAttrib
: tag
.m_TagAttributes
)
142 if (tagAttrib
.attrib
== L
"displace" && !tagAttrib
.value
.empty())
144 // Displace the sprite
147 if (!GUI
<CSize
>::ParseString(tagAttrib
.value
, displacement
))
148 LOGERROR("Error parsing 'displace' value for tag [ICON]");
150 SpriteCall
.m_Area
+= displacement
;
152 else if (tagAttrib
.attrib
== L
"tooltip")
153 SpriteCall
.m_Tooltip
= tagAttrib
.value
;
154 else if (tagAttrib
.attrib
== L
"tooltip_style")
155 SpriteCall
.m_TooltipStyle
= tagAttrib
.value
;
158 SpriteCall
.m_Sprite
= icon
.m_SpriteName
;
159 SpriteCall
.m_CellID
= icon
.m_CellID
;
162 Feedback
.m_SpriteCalls
.push_back(SpriteCall
);
164 // Finalize text call
165 TextCall
.m_pSpriteCall
= &Feedback
.m_SpriteCalls
.back();
168 Feedback
.m_TextCalls
.push_back(TextCall
);
175 else if (_to
> _from
&& !Feedback
.m_NewLine
)
177 SGUIText::STextCall TextCall
;
180 TextCall
.m_Font
= DefaultFont
;
181 TextCall
.m_UseCustomColor
= false;
183 TextCall
.m_String
= m_RawString
.substr(_from
, _to
-_from
);
185 // Go through tags and apply changes.
186 for (const TextChunk::Tag
& tag
: textChunk
.m_Tags
)
188 switch (tag
.m_TagType
)
190 case TextChunk::Tag::TAG_COLOR
:
191 TextCall
.m_UseCustomColor
= true;
193 if (!GUI
<CColor
>::ParseString(tag
.m_TagValue
, TextCall
.m_Color
) && pObject
)
194 LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject
->GetPresentableName().c_str());
196 case TextChunk::Tag::TAG_FONT
:
197 // TODO Gee: (2004-08-15) Check if Font exists?
198 TextCall
.m_Font
= CStrIntern(utf8_from_wstring(tag
.m_TagValue
));
201 LOGERROR("Encountered unexpected tag applied to text");
206 // Calculate the size of the font
209 CFontMetrics
font (TextCall
.m_Font
);
210 font
.CalculateStringSize(TextCall
.m_String
.c_str(), cx
, cy
);
211 // For anything other than the first line, the line spacing
212 // needs to be considered rather than just the height of the text
214 cy
= font
.GetLineSpacing();
219 // Append width, and make maximum height the height.
220 Feedback
.m_Size
.cx
+= size
.cx
;
221 Feedback
.m_Size
.cy
= std::max(Feedback
.m_Size
.cy
, size
.cy
);
223 // These are also needed later
224 TextCall
.m_Size
= size
;
226 if (!TextCall
.m_String
.empty() && TextCall
.m_String
[0] == '\n')
227 Feedback
.m_NewLine
= true;
230 Feedback
.m_TextCalls
.push_back(TextCall
);
235 bool CGUIString::TextChunk::Tag::SetTagType(const CStrW
& tagtype
)
237 TagType t
= GetTagType(tagtype
);
238 if (t
== TAG_INVALID
)
245 CGUIString::TextChunk::Tag::TagType
CGUIString::TextChunk::Tag::GetTagType(const CStrW
& tagtype
) const
247 if (tagtype
== L
"color")
249 if (tagtype
== L
"font")
251 if (tagtype
== L
"icon")
253 if (tagtype
== L
"imgleft")
255 if (tagtype
== L
"imgright")
261 void CGUIString::SetValue(const CStrW
& str
)
263 m_OriginalString
= str
;
265 m_TextChunks
.clear();
269 // Current Text Chunk
270 CGUIString::TextChunk CurrentTextChunk
;
271 CurrentTextChunk
.m_From
= 0;
273 int l
= str
.length();
276 std::vector
<CStrW
> tags
;
277 bool closing
= false;
278 for (int p
= 0; p
< l
; ++p
)
284 CurrentTextChunk
.m_To
= rawpos
;
285 // Add the current chunks if it is not empty
286 if (CurrentTextChunk
.m_From
!= rawpos
)
287 m_TextChunks
.push_back(CurrentTextChunk
);
288 CurrentTextChunk
.m_From
= rawpos
;
293 LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str
));
301 LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p
, utf8_from_wstring(str
));
306 LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str
));
312 for (; p
< l
&& str
[p
] != L
']'; ++p
)
318 if (closing
) // We still parse them to make error handling cleaner
319 LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p
, utf8_from_wstring(str
));
321 // parse something="something else"
322 for (++p
; p
< l
&& str
[p
] != L
'='; ++p
)
323 name
.push_back(str
[p
]);
327 LOGERROR("Parameter without value at pos %d '%s'", p
, utf8_from_wstring(str
));
332 // parse a quoted parameter
333 if (closing
) // We still parse them to make error handling cleaner
334 LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p
, utf8_from_wstring(str
));
338 LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str
));
343 LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p
, utf8_from_wstring(str
));
346 for (++p
; p
< l
&& str
[p
] != L
'"'; ++p
)
353 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str
));
356 // NOTE: We do not support \n in tag parameters
359 param
.push_back(str
[p
]);
365 TextChunk::Tag::TagAttribute a
= {name
, param
};
366 tag_
.m_TagAttributes
.push_back(a
);
369 tag_
.m_TagValue
= param
;
372 tag
.push_back(str
[p
]);
377 if (!tag_
.SetTagType(tag
))
379 LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag
), p
, utf8_from_wstring(str
));
384 if (tag_
.m_TagType
== TextChunk::Tag::TAG_IMGRIGHT
385 || tag_
.m_TagType
== TextChunk::Tag::TAG_IMGLEFT
386 || tag_
.m_TagType
== TextChunk::Tag::TAG_ICON
)
388 TextChunk FreshTextChunk
= { rawpos
, rawpos
};
389 FreshTextChunk
.m_Tags
.push_back(tag_
);
390 m_TextChunks
.push_back(FreshTextChunk
);
395 CurrentTextChunk
.m_Tags
.push_back(tag_
);
400 if (tag
!= tags
.back())
402 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
));
407 CurrentTextChunk
.m_Tags
.pop_back();
413 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str
));
419 m_RawString
.push_back(L
'\n');
425 m_RawString
.push_back(str
[p
]);
430 // Add the chunk after the last tag
431 if (CurrentTextChunk
.m_From
!= rawpos
)
433 CurrentTextChunk
.m_To
= rawpos
;
434 m_TextChunks
.push_back(CurrentTextChunk
);
438 // Add a delimiter at start and at end, it helps when
439 // processing later, because we don't have make exceptions for
441 m_Words
.push_back(0);
443 // Add word boundaries in increasing order
444 for (u32 i
= 0; i
< m_RawString
.length(); ++i
)
446 wchar_t c
= m_RawString
[i
];
449 m_Words
.push_back((int)i
);
450 m_Words
.push_back((int)i
+1);
453 for (int n
= 0; n
< NUM_WORD_DELIMITERS
; n
+= 2)
455 if (c
<= WordDelimiters
[n
+1])
457 if (c
>= WordDelimiters
[n
])
458 m_Words
.push_back((int)i
+1);
459 // assume the WordDelimiters list is stored in increasing order
465 m_Words
.push_back((int)m_RawString
.length());
467 // Remove duplicates (only if larger than 2)
468 if (m_Words
.size() <= 2)
471 m_Words
.erase(std::unique(m_Words
.begin(), m_Words
.end()), m_Words
.end());