2eb676cd3b089c236348ad301bf4610b61f0be5e
[vuplus_xbmc] / guilib / GUITextLayout.cpp
1 /*
2  *      Copyright (C) 2005-2008 Team XBMC
3  *      http://www.xbmc.org
4  *
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)
8  *  any later version.
9  *
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.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "GUITextLayout.h"
23 #include "GUIFont.h"
24 #include "GUIControl.h"
25 #include "GUIColorManager.h"
26 #include "utils/CharsetConverter.h"
27 #include "StringUtils.h"
28
29 using namespace std;
30
31 #define WORK_AROUND_NEEDED_FOR_LINE_BREAKS
32
33 CGUIString::CGUIString(iString start, iString end, bool carriageReturn)
34 {
35   m_text.assign(start, end);
36   m_carriageReturn = carriageReturn;
37 }
38
39 CStdString CGUIString::GetAsString() const
40 {
41   CStdString text;
42   for (unsigned int i = 0; i < m_text.size(); i++)
43     text += (char)(m_text[i] & 0xff);
44   return text;
45 }
46
47 CGUITextLayout::CGUITextLayout(CGUIFont *font, bool wrap, float fHeight, CGUIFont *borderFont)
48 {
49   m_font = font;
50   m_borderFont = borderFont;
51   m_textColor = 0;
52   m_wrap = wrap;
53   m_maxHeight = fHeight;
54   m_textWidth = 0;
55   m_textHeight = 0;
56 }
57
58 void CGUITextLayout::SetWrap(bool bWrap)
59 {
60   m_wrap = bWrap;
61 }
62
63 void CGUITextLayout::Render(float x, float y, float angle, color_t color, color_t shadowColor, uint32_t alignment, float maxWidth, bool solid)
64 {
65   if (!m_font)
66     return;
67
68   // set the main text color
69   if (m_colors.size())
70     m_colors[0] = color;
71
72   // render the text at the required location, angle, and size
73   if (angle)
74   {
75     static const float degrees_to_radians = 0.01745329252f;
76     g_graphicsContext.AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, g_graphicsContext.GetScalingPixelRatio()));
77   }
78   // center our text vertically
79   if (alignment & XBFONT_CENTER_Y)
80   {
81     y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
82     alignment &= ~XBFONT_CENTER_Y;
83   }
84   m_font->Begin();
85   for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); i++)
86   {
87     const CGUIString &string = *i;
88     uint32_t align = alignment;
89     if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
90       align &= ~XBFONT_JUSTIFIED;
91     if (solid)
92       m_font->DrawText(x, y, m_colors[0], shadowColor, string.m_text, align, maxWidth);
93     else
94       m_font->DrawText(x, y, m_colors, shadowColor, string.m_text, align, maxWidth);
95     y += m_font->GetLineHeight();
96   }
97   m_font->End();
98   if (angle)
99     g_graphicsContext.RemoveTransform();
100 }
101
102
103 void CGUITextLayout::RenderScrolling(float x, float y, float angle, color_t color, color_t shadowColor, uint32_t alignment, float maxWidth, CScrollInfo &scrollInfo)
104 {
105   if (!m_font)
106     return;
107
108   // set the main text color
109   if (m_colors.size())
110     m_colors[0] = color;
111
112   // render the text at the required location, angle, and size
113   if (angle)
114   {
115     static const float degrees_to_radians = 0.01745329252f;
116     g_graphicsContext.AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, g_graphicsContext.GetScalingPixelRatio()));
117   }
118   // center our text vertically
119   if (alignment & XBFONT_CENTER_Y)
120   {
121     y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
122     alignment &= ~XBFONT_CENTER_Y;
123   }
124   m_font->Begin();
125   // NOTE: This workaround is needed as otherwise multi-line text that scrolls
126   //       will scroll in proportion to the number of lines.  Ideally we should
127   //       do the DrawScrollingText calculation here.  This probably won't make
128   //       any difference to the smoothness of scrolling though which will be
129   //       jumpy with this sort of thing.  It's not exactly a well used situation
130   //       though, so this hack is probably OK.
131   float speed = scrollInfo.pixelSpeed;
132   for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); i++)
133   {
134     const CGUIString &string = *i;
135     m_font->DrawScrollingText(x, y, m_colors, shadowColor, string.m_text, alignment, maxWidth, scrollInfo);
136     y += m_font->GetLineHeight();
137     scrollInfo.pixelSpeed = 0;
138   }
139   scrollInfo.pixelSpeed = speed;
140   m_font->End();
141   if (angle)
142     g_graphicsContext.RemoveTransform();
143 }
144
145 void CGUITextLayout::RenderOutline(float x, float y, color_t color, color_t outlineColor, uint32_t alignment, float maxWidth)
146 {
147   if (!m_font)
148     return;
149
150   // set the outline color
151   vecColors outlineColors;
152   if (m_colors.size())
153     outlineColors.push_back(outlineColor);
154
155   // center our text vertically
156   if (alignment & XBFONT_CENTER_Y)
157   {
158     y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;;
159     alignment &= ~XBFONT_CENTER_Y;
160   }
161   if (m_borderFont)
162   {
163     float by = y;
164     m_borderFont->Begin();
165     for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); i++)
166     {
167       const CGUIString &string = *i;
168       uint32_t align = alignment;
169       if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
170         align &= ~XBFONT_JUSTIFIED;
171
172       m_borderFont->DrawText(x, by, outlineColors, 0, string.m_text, align, maxWidth);
173       by += m_borderFont->GetLineHeight();
174     }
175     m_borderFont->End();
176   }
177
178   // set the main text color
179   if (m_colors.size())
180     m_colors[0] = color;
181
182   m_font->Begin();
183   for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); i++)
184   {
185     const CGUIString &string = *i;
186     uint32_t align = alignment;
187     if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
188       align &= ~XBFONT_JUSTIFIED;
189
190     m_font->DrawText(x, y, m_colors, 0, string.m_text, align, maxWidth);
191     y += m_font->GetLineHeight();
192   }
193   m_font->End();
194 }
195
196 bool CGUITextLayout::Update(const CStdString &text, float maxWidth, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
197 {
198   if (text == m_lastText && !forceUpdate)
199     return false;
200
201   // convert to utf16
202   CStdStringW utf16;
203   utf8ToW(text, utf16);
204
205   // update
206   SetText(utf16, maxWidth, forceLTRReadingOrder);
207
208   // and set our parameters to indicate no further update is required
209   m_lastText = text;
210   return true;
211 }
212
213 void CGUITextLayout::SetText(const CStdStringW &text, float maxWidth, bool forceLTRReadingOrder /*= false*/)
214 {
215   vecText parsedText;
216
217   // empty out our previous string
218   m_lines.clear();
219   m_colors.clear();
220   m_colors.push_back(m_textColor);
221
222   // parse the text into our string objects
223   ParseText(text, parsedText);
224
225   // add \n to the end of the string
226   parsedText.push_back(L'\n');
227
228   // if we need to wrap the text, then do so
229   if (m_wrap && maxWidth > 0)
230     WrapText(parsedText, maxWidth);
231   else
232     LineBreakText(parsedText, m_lines);
233
234   // remove any trailing blank lines
235   while (!m_lines.empty() && m_lines.back().m_text.empty())
236     m_lines.pop_back();
237
238   BidiTransform(m_lines, forceLTRReadingOrder);
239
240   // and cache the width and height for later reading
241   CalcTextExtent();
242 }
243
244 // BidiTransform is used to handle RTL text flipping in the string
245 void CGUITextLayout::BidiTransform(vector<CGUIString> &lines, bool forceLTRReadingOrder)
246 {
247   for (unsigned int i=0; i<lines.size(); i++)
248   {
249     CGUIString &line = lines[i];
250
251     // reserve enough space in the flipped text
252     vecText flippedText;
253     flippedText.reserve(line.m_text.size());
254
255     character_t sectionStyle = 0xffff0000; // impossible to achieve
256     CStdStringW sectionText;
257     for (vecText::iterator it = line.m_text.begin(); it != line.m_text.end(); ++it)
258     {
259       character_t style = *it & 0xffff0000;
260       if (style != sectionStyle)
261       {
262         if (!sectionText.IsEmpty())
263         { // style has changed, bidi flip text
264           CStdStringW sectionFlipped = BidiFlip(sectionText, forceLTRReadingOrder);
265           for (unsigned int j = 0; j < sectionFlipped.size(); j++)
266             flippedText.push_back(sectionStyle | sectionFlipped[j]);
267         }
268         sectionStyle = style;
269         sectionText.clear();
270       }
271       sectionText.push_back( (wchar_t)(*it & 0xffff) );
272     }
273
274     // handle the last section
275     if (!sectionText.IsEmpty())
276     {
277       CStdStringW sectionFlipped = BidiFlip(sectionText, forceLTRReadingOrder);
278       for (unsigned int j = 0; j < sectionFlipped.size(); j++)
279         flippedText.push_back(sectionStyle | sectionFlipped[j]);
280     }
281
282     // replace the original line with the proccessed one
283     lines[i] = CGUIString(flippedText.begin(), flippedText.end(), line.m_carriageReturn);
284   }
285 }
286
287 CStdStringW CGUITextLayout::BidiFlip(const CStdStringW &text, bool forceLTRReadingOrder)
288 {
289   CStdStringA utf8text;
290   CStdStringW visualText;
291
292   // convert to utf8, and back to utf16 with bidi flipping
293   g_charsetConverter.wToUTF8(text, utf8text);
294   g_charsetConverter.utf8ToW(utf8text, visualText, true, forceLTRReadingOrder);
295
296   return visualText;
297 }
298
299 void CGUITextLayout::Filter(CStdString &text)
300 {
301   CStdStringW utf16;
302   utf8ToW(text, utf16);
303   vecColors colors;
304   vecText parsedText;
305   ParseText(utf16, 0, colors, parsedText);
306   utf16.Empty();
307   for (unsigned int i = 0; i < parsedText.size(); i++)
308     utf16 += (wchar_t)(0xffff & parsedText[i]);
309   g_charsetConverter.wToUTF8(utf16, text);
310 }
311
312 void CGUITextLayout::ParseText(const CStdStringW &text, vecText &parsedText)
313 {
314   if (!m_font)
315     return;
316   ParseText(text, m_font->GetStyle(), m_colors, parsedText);
317 }
318
319 void CGUITextLayout::ParseText(const CStdStringW &text, uint32_t defaultStyle, vecColors &colors, vecText &parsedText)
320 {
321   // run through the string, searching for:
322   // [B] or [/B] -> toggle bold on and off
323   // [I] or [/I] -> toggle italics on and off
324   // [COLOR ffab007f] or [/COLOR] -> toggle color on and off
325   // [CAPS <option>] or [/CAPS] -> toggle capatilization on and off
326
327   uint32_t currentStyle = defaultStyle; // start with the default font's style
328   color_t currentColor = 0;
329
330   stack<color_t> colorStack;
331   colorStack.push(0);
332
333   // these aren't independent, but that's probably not too much of an issue
334   // eg [UPPERCASE]Glah[LOWERCASE]FReD[/LOWERCASE]Georeg[/UPPERCASE] will work (lower case >> upper case)
335   // but [LOWERCASE]Glah[UPPERCASE]FReD[/UPPERCASE]Georeg[/LOWERCASE] won't
336 #define FONT_STYLE_UPPERCASE 4
337 #define FONT_STYLE_LOWERCASE 8
338
339   int startPos = 0;
340   size_t pos = text.Find(L'[');
341   while (pos != CStdString::npos && pos + 1 < text.size())
342   {
343     uint32_t newStyle = 0;
344     color_t newColor = currentColor;
345     bool newLine = false;
346     // have a [ - check if it's an ON or OFF switch
347     bool on(true);
348     int endPos = pos++; // finish of string
349     if (text[pos] == L'/')
350     {
351       on = false;
352       pos++;
353     }
354     // check for each type
355     if (text.Mid(pos,2) == L"B]")
356     { // bold - finish the current text block and assign the bold state
357       pos += 2;
358       if ((on && text.Find(L"[/B]",pos) >= 0) ||          // check for a matching end point
359          (!on && (currentStyle & FONT_STYLE_BOLD)))       // or matching start point
360         newStyle = FONT_STYLE_BOLD;
361     }
362     else if (text.Mid(pos,2) == L"I]")
363     { // italics
364       pos += 2;
365       if ((on && text.Find(L"[/I]",pos) >= 0) ||          // check for a matching end point
366          (!on && (currentStyle & FONT_STYLE_ITALICS)))    // or matching start point
367         newStyle = FONT_STYLE_ITALICS;
368     }
369     else if (text.Mid(pos,10) == L"UPPERCASE]")
370     {
371       pos += 10;
372       if ((on && text.Find(L"[/UPPERCASE]",pos) >= 0) ||  // check for a matching end point
373          (!on && (currentStyle & FONT_STYLE_UPPERCASE)))  // or matching start point
374         newStyle = FONT_STYLE_UPPERCASE;
375     }
376     else if (text.Mid(pos,10) == L"LOWERCASE]")
377     {
378       pos += 10;
379       if ((on && text.Find(L"[/LOWERCASE]",pos) >= 0) ||  // check for a matching end point
380          (!on && (currentStyle & FONT_STYLE_LOWERCASE)))  // or matching start point
381         newStyle = FONT_STYLE_LOWERCASE;
382     }
383     else if (text.Mid(pos,3) == L"CR]" && on)
384     {
385       newLine = true;
386       pos += 3;
387     }
388     else if (text.Mid(pos,5) == L"COLOR")
389     { // color
390       size_t finish = text.Find(L']', pos + 5);
391       if (on && finish != CStdString::npos && (size_t)text.Find(L"[/COLOR]",finish) != CStdString::npos)
392       { // create new color
393         newColor = colors.size();
394         colors.push_back(g_colorManager.GetColor(text.Mid(pos + 5, finish - pos - 5)));
395         colorStack.push(newColor);
396       }
397       else if (!on && finish == pos + 5 && colorStack.size() > 1)
398       { // revert to previous color
399         colorStack.pop();
400         newColor = colorStack.top();
401       }
402       pos = finish + 1;
403     }
404
405     if (newStyle || newColor != currentColor || newLine)
406     { // we have a new style or a new color, so format up the previous segment
407       CStdStringW subText = text.Mid(startPos, endPos - startPos);
408       if (currentStyle & FONT_STYLE_UPPERCASE)
409         subText.ToUpper();
410       if (currentStyle & FONT_STYLE_LOWERCASE)
411         subText.ToLower();
412       AppendToUTF32(subText, ((currentStyle & 3) << 24) | (currentColor << 16), parsedText);
413       if (newLine)
414         parsedText.push_back(L'\n');
415
416       // and switch to the new style
417       startPos = pos;
418       currentColor = newColor;
419       if (on)
420         currentStyle |= newStyle;
421       else
422         currentStyle &= ~newStyle;
423     }
424     pos = text.Find(L'[',pos);
425   }
426   // now grab the remainder of the string
427   CStdStringW subText = text.Mid(startPos, text.GetLength() - startPos);
428   if (currentStyle & FONT_STYLE_UPPERCASE)
429     subText.ToUpper();
430   if (currentStyle & FONT_STYLE_LOWERCASE)
431     subText.ToLower();
432   AppendToUTF32(subText, ((currentStyle & 3) << 24) | (currentColor << 16), parsedText);
433 }
434
435 void CGUITextLayout::SetMaxHeight(float fHeight)
436 {
437   m_maxHeight = fHeight;
438 }
439
440 void CGUITextLayout::WrapText(const vecText &text, float maxWidth)
441 {
442   if (!m_font)
443     return;
444
445   int nMaxLines = (m_maxHeight > 0 && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
446
447   m_lines.clear();
448
449   vector<CGUIString> lines;
450   LineBreakText(text, lines);
451
452   for (unsigned int i = 0; i < lines.size(); i++)
453   {
454     const CGUIString &line = lines[i];
455     vecText::const_iterator lastSpace = line.m_text.begin();
456     vecText::const_iterator pos = line.m_text.begin();
457     unsigned int lastSpaceInLine = 0;
458     vecText curLine;
459     while (pos != line.m_text.end() && (nMaxLines <= 0 || m_lines.size() < (size_t)nMaxLines))
460     {
461       // Get the current letter in the string
462       character_t letter = *pos;
463       // check for a space
464       if (CanWrapAtLetter(letter))
465       {
466         float width = m_font->GetTextWidth(curLine);
467         if (width > maxWidth)
468         {
469           if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
470           {
471             CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
472             m_lines.push_back(string);
473             // skip over spaces
474             pos = lastSpace;
475             while (pos != line.m_text.end() && IsSpace(*pos))
476               pos++;
477             curLine.clear();
478             lastSpaceInLine = 0;
479             lastSpace = line.m_text.begin();
480             continue;
481           }
482         }
483         lastSpace = pos;
484         lastSpaceInLine = curLine.size();
485       }
486       curLine.push_back(letter);
487       pos++;
488     }
489     // now add whatever we have left to the string
490     float width = m_font->GetTextWidth(curLine);
491     if (width > maxWidth)
492     {
493       // too long - put up to the last space on if we can + remove it from what's left.
494       if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
495       {
496         CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
497         m_lines.push_back(string);
498         curLine.erase(curLine.begin(), curLine.begin() + lastSpaceInLine);
499         while (curLine.size() && IsSpace(curLine.at(0)))
500           curLine.erase(curLine.begin());
501       }
502     }
503     CGUIString string(curLine.begin(), curLine.end(), true);
504     m_lines.push_back(string);
505   }
506 }
507
508 void CGUITextLayout::LineBreakText(const vecText &text, vector<CGUIString> &lines)
509 {
510   int nMaxLines = (m_maxHeight > 0 && m_font && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
511   vecText::const_iterator lineStart = text.begin();
512   vecText::const_iterator pos = text.begin();
513   while (pos != text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
514   {
515     // Get the current letter in the string
516     character_t letter = *pos;
517
518     // Handle the newline character
519     if ((letter & 0xffff) == L'\n' )
520     { // push back everything up till now
521       CGUIString string(lineStart, pos, true);
522       lines.push_back(string);
523       lineStart = pos + 1;
524     }
525     pos++;
526   }
527 }
528
529 void CGUITextLayout::GetTextExtent(float &width, float &height) const
530 {
531   width = m_textWidth;
532   height = m_textHeight;
533 }
534
535 void CGUITextLayout::CalcTextExtent()
536 {
537   m_textWidth = 0;
538   m_textHeight = 0;
539   if (!m_font) return;
540
541   for (vector<CGUIString>::iterator i = m_lines.begin(); i != m_lines.end(); i++)
542   {
543     const CGUIString &string = *i;
544     float w = m_font->GetTextWidth(string.m_text);
545     if (w > m_textWidth)
546       m_textWidth = w;
547   }
548   m_textHeight = m_font->GetTextHeight(m_lines.size());
549 }
550
551 unsigned int CGUITextLayout::GetTextLength() const
552 {
553   unsigned int length = 0;
554   for (vector<CGUIString>::const_iterator i = m_lines.begin(); i != m_lines.end(); i++)
555     length += i->m_text.size();
556   return length;
557 }
558
559 void CGUITextLayout::GetFirstText(vecText &text) const
560 {
561   text.clear();
562   if (m_lines.size())
563     text = m_lines[0].m_text;
564 }
565
566 float CGUITextLayout::GetTextWidth(const CStdStringW &text) const
567 {
568   // NOTE: Assumes a single line of text
569   if (!m_font) return 0;
570   vecText utf32;
571   AppendToUTF32(text, (m_font->GetStyle() & 3) << 24, utf32);
572   return m_font->GetTextWidth(utf32);
573 }
574
575 void CGUITextLayout::DrawText(CGUIFont *font, float x, float y, color_t color, color_t shadowColor, const CStdString &text, uint32_t align)
576 {
577   if (!font) return;
578   vecText utf32;
579   AppendToUTF32(text, 0, utf32);
580   font->DrawText(x, y, color, shadowColor, utf32, align, 0);
581 }
582
583 void CGUITextLayout::AppendToUTF32(const CStdStringW &utf16, character_t colStyle, vecText &utf32)
584 {
585   // NOTE: Assumes a single line of text
586   utf32.reserve(utf32.size() + utf16.size());
587   for (unsigned int i = 0; i < utf16.size(); i++)
588     utf32.push_back(utf16[i] | colStyle);
589 }
590
591 void CGUITextLayout::utf8ToW(const CStdString &utf8, CStdStringW &utf16)
592 {
593 #ifdef WORK_AROUND_NEEDED_FOR_LINE_BREAKS
594   // NOTE: This appears to strip \n characters from text.  This may be a consequence of incorrect
595   //       expression of the \n in utf8 (we just use character code 10) or it might be something
596   //       more sinister.  For now, we use the workaround below.
597   CStdStringArray multiLines;
598   StringUtils::SplitString(utf8, "\n", multiLines);
599   for (unsigned int i = 0; i < multiLines.size(); i++)
600   {
601     CStdStringW line;
602     // no need to bidiflip here - it's done in BidiTransform above
603     g_charsetConverter.utf8ToW(multiLines[i], line, false);
604     utf16 += line;
605     if (i < multiLines.size() - 1)
606       utf16.push_back(L'\n');
607   }
608 #else
609   // no need to bidiflip here - it's done in BidiTransform above
610   g_charsetConverter.utf8ToW(utf8, utf16, false);
611 #endif
612 }
613
614 void CGUITextLayout::AppendToUTF32(const CStdString &utf8, character_t colStyle, vecText &utf32)
615 {
616   CStdStringW utf16;
617   utf8ToW(utf8, utf16);
618   AppendToUTF32(utf16, colStyle, utf32);
619 }
620
621 void CGUITextLayout::Reset()
622 {
623   m_lines.clear();
624   m_lastText.Empty();
625   m_textWidth = m_textHeight = 0;
626 }
627
628