2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "GUITextLayout.h"
23 #include "GUIControl.h"
24 #include "GUIColorManager.h"
25 #include "utils/CharsetConverter.h"
26 #include "utils/StringUtils.h"
30 #define WORK_AROUND_NEEDED_FOR_LINE_BREAKS
32 CGUIString::CGUIString(iString start, iString end, bool carriageReturn)
34 m_text.assign(start, end);
35 m_carriageReturn = carriageReturn;
38 CStdString CGUIString::GetAsString() const
41 for (unsigned int i = 0; i < m_text.size(); i++)
42 text += (char)(m_text[i] & 0xff);
46 CGUITextLayout::CGUITextLayout(CGUIFont *font, bool wrap, float fHeight, CGUIFont *borderFont)
49 m_borderFont = borderFont;
52 m_maxHeight = fHeight;
55 m_lastUpdateW = false;
58 void CGUITextLayout::SetWrap(bool bWrap)
63 void CGUITextLayout::Render(float x, float y, float angle, color_t color, color_t shadowColor, uint32_t alignment, float maxWidth, bool solid)
68 // set the main text color
72 // render the text at the required location, angle, and size
75 static const float degrees_to_radians = 0.01745329252f;
76 g_graphicsContext.AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, g_graphicsContext.GetScalingPixelRatio()));
78 // center our text vertically
79 if (alignment & XBFONT_CENTER_Y)
81 y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
82 alignment &= ~XBFONT_CENTER_Y;
85 for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); ++i)
87 const CGUIString &string = *i;
88 uint32_t align = alignment;
89 if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
90 align &= ~XBFONT_JUSTIFIED;
92 m_font->DrawText(x, y, m_colors[0], shadowColor, string.m_text, align, maxWidth);
94 m_font->DrawText(x, y, m_colors, shadowColor, string.m_text, align, maxWidth);
95 y += m_font->GetLineHeight();
99 g_graphicsContext.RemoveTransform();
102 bool CGUITextLayout::UpdateScrollinfo(CScrollInfo &scrollInfo)
109 return m_font->UpdateScrollInfo(m_lines[0].m_text, scrollInfo);
113 void CGUITextLayout::RenderScrolling(float x, float y, float angle, color_t color, color_t shadowColor, uint32_t alignment, float maxWidth, const CScrollInfo &scrollInfo)
118 // set the main text color
122 // render the text at the required location, angle, and size
125 static const float degrees_to_radians = 0.01745329252f;
126 g_graphicsContext.AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, g_graphicsContext.GetScalingPixelRatio()));
128 // center our text vertically
129 if (alignment & XBFONT_CENTER_Y)
131 y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
132 alignment &= ~XBFONT_CENTER_Y;
135 // NOTE: This workaround is needed as otherwise multi-line text that scrolls
136 // will scroll in proportion to the number of lines. Ideally we should
137 // do the DrawScrollingText calculation here. This probably won't make
138 // any difference to the smoothness of scrolling though which will be
139 // jumpy with this sort of thing. It's not exactly a well used situation
140 // though, so this hack is probably OK.
141 for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); ++i)
143 const CGUIString &string = *i;
144 m_font->DrawScrollingText(x, y, m_colors, shadowColor, string.m_text, alignment, maxWidth, scrollInfo);
145 y += m_font->GetLineHeight();
149 g_graphicsContext.RemoveTransform();
152 void CGUITextLayout::RenderOutline(float x, float y, color_t color, color_t outlineColor, uint32_t alignment, float maxWidth)
157 // set the outline color
158 vecColors outlineColors;
160 outlineColors.push_back(outlineColor);
162 // center our text vertically
163 if (alignment & XBFONT_CENTER_Y)
165 y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
166 alignment &= ~XBFONT_CENTER_Y;
170 // adjust so the baselines of the fonts align
171 float by = y + m_font->GetTextBaseLine() - m_borderFont->GetTextBaseLine();
172 m_borderFont->Begin();
173 for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); ++i)
175 const CGUIString &string = *i;
176 uint32_t align = alignment;
177 if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
178 align &= ~XBFONT_JUSTIFIED;
179 // text centered horizontally must be computed using the original font, not the bordered
180 // font, as the bordered font will be wider, and thus will end up uncentered.
181 // TODO: We should really have a better way to handle text extent - at the moment we assume
182 // that text is rendered from a posx, posy, width, and height which isn't enough to
183 // accurately position text. We need a vertical and horizontal offset of the baseline
184 // and cursor as well.
186 if (align & XBFONT_CENTER_X)
188 bx -= m_font->GetTextWidth(string.m_text) * 0.5f;
189 align &= ~XBFONT_CENTER_X;
192 // don't pass maxWidth through to the renderer for the same reason above: it will cause clipping
194 m_borderFont->DrawText(bx, by, outlineColors, 0, string.m_text, align, 0);
195 by += m_borderFont->GetLineHeight();
200 // set the main text color
205 for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); ++i)
207 const CGUIString &string = *i;
208 uint32_t align = alignment;
209 if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
210 align &= ~XBFONT_JUSTIFIED;
212 // don't pass maxWidth through to the renderer for the reason above.
213 m_font->DrawText(x, y, m_colors, 0, string.m_text, align, 0);
214 y += m_font->GetLineHeight();
219 bool CGUITextLayout::Update(const CStdString &text, float maxWidth, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
221 if (text == m_lastUtf8Text && !forceUpdate && !m_lastUpdateW)
224 m_lastUtf8Text = text;
225 m_lastUpdateW = false;
227 utf8ToW(text, utf16);
228 UpdateCommon(utf16, maxWidth, forceLTRReadingOrder);
232 bool CGUITextLayout::UpdateW(const CStdStringW &text, float maxWidth /*= 0*/, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
234 if (text == m_lastText && !forceUpdate && m_lastUpdateW)
238 m_lastUpdateW = true;
239 UpdateCommon(text, maxWidth, forceLTRReadingOrder);
243 void CGUITextLayout::UpdateCommon(const CStdStringW &text, float maxWidth, bool forceLTRReadingOrder)
245 // parse the text for style information
248 ParseText(text, m_font ? m_font->GetStyle() : 0, m_textColor, colors, parsedText);
251 UpdateStyled(parsedText, colors, maxWidth, forceLTRReadingOrder);
254 void CGUITextLayout::UpdateStyled(const vecText &text, const vecColors &colors, float maxWidth, bool forceLTRReadingOrder)
256 // empty out our previous string
260 // if we need to wrap the text, then do so
261 if (m_wrap && maxWidth > 0)
262 WrapText(text, maxWidth);
264 LineBreakText(text, m_lines);
266 // remove any trailing blank lines
267 while (!m_lines.empty() && m_lines.back().m_text.empty())
270 BidiTransform(m_lines, forceLTRReadingOrder);
272 // and cache the width and height for later reading
276 // BidiTransform is used to handle RTL text flipping in the string
277 void CGUITextLayout::BidiTransform(vector<CGUIString> &lines, bool forceLTRReadingOrder)
279 for (unsigned int i=0; i<lines.size(); i++)
281 CGUIString &line = lines[i];
283 // reserve enough space in the flipped text
285 flippedText.reserve(line.m_text.size());
287 character_t sectionStyle = 0xffff0000; // impossible to achieve
288 CStdStringW sectionText;
289 for (vecText::iterator it = line.m_text.begin(); it != line.m_text.end(); ++it)
291 character_t style = *it & 0xffff0000;
292 if (style != sectionStyle)
294 if (!sectionText.empty())
295 { // style has changed, bidi flip text
296 CStdStringW sectionFlipped = BidiFlip(sectionText, forceLTRReadingOrder);
297 for (unsigned int j = 0; j < sectionFlipped.size(); j++)
298 flippedText.push_back(sectionStyle | sectionFlipped[j]);
300 sectionStyle = style;
303 sectionText.push_back( (wchar_t)(*it & 0xffff) );
306 // handle the last section
307 if (!sectionText.empty())
309 CStdStringW sectionFlipped = BidiFlip(sectionText, forceLTRReadingOrder);
310 for (unsigned int j = 0; j < sectionFlipped.size(); j++)
311 flippedText.push_back(sectionStyle | sectionFlipped[j]);
314 // replace the original line with the proccessed one
315 lines[i] = CGUIString(flippedText.begin(), flippedText.end(), line.m_carriageReturn);
319 CStdStringW CGUITextLayout::BidiFlip(const CStdStringW &text, bool forceLTRReadingOrder)
321 CStdStringA utf8text;
322 CStdStringW visualText;
324 // convert to utf8, and back to utf16 with bidi flipping
325 g_charsetConverter.wToUTF8(text, utf8text);
326 g_charsetConverter.utf8ToW(utf8text, visualText, true, forceLTRReadingOrder);
331 void CGUITextLayout::Filter(CStdString &text)
334 utf8ToW(text, utf16);
337 ParseText(utf16, 0, 0xffffffff, colors, parsedText);
339 for (unsigned int i = 0; i < parsedText.size(); i++)
340 utf16 += (wchar_t)(0xffff & parsedText[i]);
341 g_charsetConverter.wToUTF8(utf16, text);
344 void CGUITextLayout::ParseText(const CStdStringW &text, uint32_t defaultStyle, color_t defaultColor, vecColors &colors, vecText &parsedText)
346 // run through the string, searching for:
347 // [B] or [/B] -> toggle bold on and off
348 // [I] or [/I] -> toggle italics on and off
349 // [COLOR ffab007f] or [/COLOR] -> toggle color on and off
350 // [CAPS <option>] or [/CAPS] -> toggle capatilization on and off
352 uint32_t currentStyle = defaultStyle; // start with the default font's style
353 color_t currentColor = 0;
355 colors.push_back(defaultColor);
356 stack<color_t> colorStack;
359 // these aren't independent, but that's probably not too much of an issue
360 // eg [UPPERCASE]Glah[LOWERCASE]FReD[/LOWERCASE]Georeg[/UPPERCASE] will work (lower case >> upper case)
361 // but [LOWERCASE]Glah[UPPERCASE]FReD[/UPPERCASE]Georeg[/LOWERCASE] won't
364 size_t pos = text.find(L'[');
365 while (pos != std::string::npos && pos + 1 < text.size())
367 uint32_t newStyle = 0;
368 color_t newColor = currentColor;
369 bool colorTagChange = false;
370 bool newLine = false;
371 // have a [ - check if it's an ON or OFF switch
373 size_t endPos = pos++; // finish of string
374 if (text[pos] == L'/')
379 // check for each type
380 if (text.compare(pos, 2, L"B]") == 0)
381 { // bold - finish the current text block and assign the bold state
383 if ((on && text.find(L"[/B]",pos) != std::string::npos) || // check for a matching end point
384 (!on && (currentStyle & FONT_STYLE_BOLD))) // or matching start point
385 newStyle = FONT_STYLE_BOLD;
387 else if (text.compare(pos, 2, L"I]") == 0)
390 if ((on && text.find(L"[/I]", pos) != std::string::npos) || // check for a matching end point
391 (!on && (currentStyle & FONT_STYLE_ITALICS))) // or matching start point
392 newStyle = FONT_STYLE_ITALICS;
394 else if (text.compare(pos, 10, L"UPPERCASE]") == 0)
397 if ((on && text.find(L"[/UPPERCASE]", pos) != std::string::npos) || // check for a matching end point
398 (!on && (currentStyle & FONT_STYLE_UPPERCASE))) // or matching start point
399 newStyle = FONT_STYLE_UPPERCASE;
401 else if (text.compare(pos, 10, L"LOWERCASE]") == 0)
404 if ((on && text.find(L"[/LOWERCASE]", pos) != std::string::npos) || // check for a matching end point
405 (!on && (currentStyle & FONT_STYLE_LOWERCASE))) // or matching start point
406 newStyle = FONT_STYLE_LOWERCASE;
408 else if (text.compare(pos, 3, L"CR]") == 0 && on)
413 else if (text.compare(pos,5, L"COLOR") == 0)
415 size_t finish = text.find(L']', pos + 5);
416 if (on && finish != std::string::npos && text.find(L"[/COLOR]",finish) != std::string::npos)
419 g_charsetConverter.wToUTF8(text.substr(pos + 5, finish - pos - 5), t);
420 color_t color = g_colorManager.GetColor(t);
421 vecColors::const_iterator it = std::find(colors.begin(), colors.end(), color);
422 if (it == colors.end())
423 { // create new color
424 if (colors.size() <= 0xFF)
426 newColor = colors.size();
427 colors.push_back(color);
429 else // we have only 8 bits for color index, fallback to first color if reach max.
433 // reuse existing color
434 newColor = it - colors.begin();
435 colorStack.push(newColor);
436 colorTagChange = true;
438 else if (!on && finish == pos + 5 && colorStack.size() > 1)
439 { // revert to previous color
441 newColor = colorStack.top();
442 colorTagChange = true;
444 if (finish != CStdString::npos)
448 if (newStyle || colorTagChange || newLine)
449 { // we have a new style or a new color, so format up the previous segment
450 CStdStringW subText = text.substr(startPos, endPos - startPos);
451 if (currentStyle & FONT_STYLE_UPPERCASE)
452 StringUtils::ToUpper(subText);
453 if (currentStyle & FONT_STYLE_LOWERCASE)
454 StringUtils::ToLower(subText);
455 AppendToUTF32(subText, ((currentStyle & 3) << 24) | (currentColor << 16), parsedText);
457 parsedText.push_back(L'\n');
459 // and switch to the new style
461 currentColor = newColor;
463 currentStyle |= newStyle;
465 currentStyle &= ~newStyle;
467 pos = text.find(L'[', pos);
469 // now grab the remainder of the string
470 CStdStringW subText = text.substr(startPos);
471 if (currentStyle & FONT_STYLE_UPPERCASE)
472 StringUtils::ToUpper(subText);
473 if (currentStyle & FONT_STYLE_LOWERCASE)
474 StringUtils::ToLower(subText);
475 AppendToUTF32(subText, ((currentStyle & 3) << 24) | (currentColor << 16), parsedText);
478 void CGUITextLayout::SetMaxHeight(float fHeight)
480 m_maxHeight = fHeight;
483 void CGUITextLayout::WrapText(const vecText &text, float maxWidth)
488 int nMaxLines = (m_maxHeight > 0 && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
492 vector<CGUIString> lines;
493 LineBreakText(text, lines);
495 for (unsigned int i = 0; i < lines.size(); i++)
497 const CGUIString &line = lines[i];
498 vecText::const_iterator lastSpace = line.m_text.begin();
499 vecText::const_iterator pos = line.m_text.begin();
500 unsigned int lastSpaceInLine = 0;
502 while (pos != line.m_text.end())
504 // Get the current letter in the string
505 character_t letter = *pos;
507 if (CanWrapAtLetter(letter))
509 float width = m_font->GetTextWidth(curLine);
510 if (width > maxWidth)
512 if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
514 CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
515 m_lines.push_back(string);
516 // check for exceeding our number of lines
517 if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
521 while (pos != line.m_text.end() && IsSpace(*pos))
525 lastSpace = line.m_text.begin();
530 lastSpaceInLine = curLine.size();
532 curLine.push_back(letter);
535 // now add whatever we have left to the string
536 float width = m_font->GetTextWidth(curLine);
537 if (width > maxWidth)
539 // too long - put up to the last space on if we can + remove it from what's left.
540 if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
542 CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
543 m_lines.push_back(string);
544 // check for exceeding our number of lines
545 if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
547 curLine.erase(curLine.begin(), curLine.begin() + lastSpaceInLine);
548 while (curLine.size() && IsSpace(curLine.at(0)))
549 curLine.erase(curLine.begin());
552 CGUIString string(curLine.begin(), curLine.end(), true);
553 m_lines.push_back(string);
554 // check for exceeding our number of lines
555 if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
560 void CGUITextLayout::LineBreakText(const vecText &text, vector<CGUIString> &lines)
562 int nMaxLines = (m_maxHeight > 0 && m_font && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
563 vecText::const_iterator lineStart = text.begin();
564 vecText::const_iterator pos = text.begin();
565 while (pos != text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
567 // Get the current letter in the string
568 character_t letter = *pos;
570 // Handle the newline character
571 if ((letter & 0xffff) == L'\n' )
572 { // push back everything up till now
573 CGUIString string(lineStart, pos, true);
574 lines.push_back(string);
579 // handle the last line if non-empty
580 if (lineStart < text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
582 CGUIString string(lineStart, text.end(), true);
583 lines.push_back(string);
587 void CGUITextLayout::GetTextExtent(float &width, float &height) const
590 height = m_textHeight;
593 void CGUITextLayout::CalcTextExtent()
599 for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); ++i)
601 const CGUIString &string = *i;
602 float w = m_font->GetTextWidth(string.m_text);
606 m_textHeight = m_font->GetTextHeight(m_lines.size());
609 unsigned int CGUITextLayout::GetTextLength() const
611 unsigned int length = 0;
612 for (vector<CGUIString>::const_iterator i = m_lines.begin(); i != m_lines.end(); ++i)
613 length += i->m_text.size();
617 void CGUITextLayout::GetFirstText(vecText &text) const
621 text = m_lines[0].m_text;
624 float CGUITextLayout::GetTextWidth(const CStdStringW &text) const
626 // NOTE: Assumes a single line of text
627 if (!m_font) return 0;
629 AppendToUTF32(text, (m_font->GetStyle() & 3) << 24, utf32);
630 return m_font->GetTextWidth(utf32);
633 void CGUITextLayout::DrawText(CGUIFont *font, float x, float y, color_t color, color_t shadowColor, const CStdString &text, uint32_t align)
637 AppendToUTF32(text, 0, utf32);
638 font->DrawText(x, y, color, shadowColor, utf32, align, 0);
641 void CGUITextLayout::AppendToUTF32(const CStdStringW &utf16, character_t colStyle, vecText &utf32)
643 // NOTE: Assumes a single line of text
644 utf32.reserve(utf32.size() + utf16.size());
645 for (unsigned int i = 0; i < utf16.size(); i++)
646 utf32.push_back(utf16[i] | colStyle);
649 void CGUITextLayout::utf8ToW(const CStdString &utf8, CStdStringW &utf16)
651 #ifdef WORK_AROUND_NEEDED_FOR_LINE_BREAKS
652 // NOTE: This appears to strip \n characters from text. This may be a consequence of incorrect
653 // expression of the \n in utf8 (we just use character code 10) or it might be something
654 // more sinister. For now, we use the workaround below.
655 CStdStringArray multiLines;
656 StringUtils::SplitString(utf8, "\n", multiLines);
657 for (unsigned int i = 0; i < multiLines.size(); i++)
660 // no need to bidiflip here - it's done in BidiTransform above
661 g_charsetConverter.utf8ToW(multiLines[i], line, false);
663 if (i < multiLines.size() - 1)
664 utf16.push_back(L'\n');
667 // no need to bidiflip here - it's done in BidiTransform above
668 g_charsetConverter.utf8ToW(utf8, utf16, false);
672 void CGUITextLayout::AppendToUTF32(const CStdString &utf8, character_t colStyle, vecText &utf32)
675 utf8ToW(utf8, utf16);
676 AppendToUTF32(utf16, colStyle, utf32);
679 void CGUITextLayout::Reset()
683 m_lastUtf8Text.clear();
684 m_textWidth = m_textHeight = 0;