]> git.lizzy.rs Git - dragonfireclient.git/blob - src/gui/guiHyperText.cpp
Use "Aux1" key name consistently everywhere
[dragonfireclient.git] / src / gui / guiHyperText.cpp
1 /*
2 Minetest
3 Copyright (C) 2019 EvicenceBKidscode / Pierre-Yves Rollo <dev@pyrollo.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) 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 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "IGUIEnvironment.h"
21 #include "IGUIElement.h"
22 #include "guiScrollBar.h"
23 #include "IGUIFont.h"
24 #include <vector>
25 #include <list>
26 #include <unordered_map>
27 using namespace irr::gui;
28 #include "client/fontengine.h"
29 #include <SColor.h>
30 #include "client/tile.h"
31 #include "IVideoDriver.h"
32 #include "client/client.h"
33 #include "client/renderingengine.h"
34 #include "hud.h"
35 #include "guiHyperText.h"
36 #include "util/string.h"
37
38 bool check_color(const std::string &str)
39 {
40         irr::video::SColor color;
41         return parseColorString(str, color, false);
42 }
43
44 bool check_integer(const std::string &str)
45 {
46         if (str.empty())
47                 return false;
48
49         char *endptr = nullptr;
50         strtol(str.c_str(), &endptr, 10);
51
52         return *endptr == '\0';
53 }
54
55 // -----------------------------------------------------------------------------
56 // ParsedText - A text parser
57
58 void ParsedText::Element::setStyle(StyleList &style)
59 {
60         this->underline = is_yes(style["underline"]);
61
62         video::SColor color;
63
64         if (parseColorString(style["color"], color, false))
65                 this->color = color;
66         if (parseColorString(style["hovercolor"], color, false))
67                 this->hovercolor = color;
68
69         unsigned int font_size = std::atoi(style["fontsize"].c_str());
70         FontMode font_mode = FM_Standard;
71         if (style["fontstyle"] == "mono")
72                 font_mode = FM_Mono;
73
74         FontSpec spec(font_size, font_mode,
75                 is_yes(style["bold"]), is_yes(style["italic"]));
76
77         // TODO: find a way to check font validity
78         // Build a new fontengine ?
79         this->font = g_fontengine->getFont(spec);
80
81         if (!this->font)
82                 printf("No font found ! Size=%d, mode=%d, bold=%s, italic=%s\n",
83                                 font_size, font_mode, style["bold"].c_str(),
84                                 style["italic"].c_str());
85 }
86
87 void ParsedText::Paragraph::setStyle(StyleList &style)
88 {
89         if (style["halign"] == "center")
90                 this->halign = HALIGN_CENTER;
91         else if (style["halign"] == "right")
92                 this->halign = HALIGN_RIGHT;
93         else if (style["halign"] == "justify")
94                 this->halign = HALIGN_JUSTIFY;
95         else
96                 this->halign = HALIGN_LEFT;
97 }
98
99 ParsedText::ParsedText(const wchar_t *text)
100 {
101         // Default style
102         m_root_tag.name = "root";
103         m_root_tag.style["fontsize"] = "16";
104         m_root_tag.style["fontstyle"] = "normal";
105         m_root_tag.style["bold"] = "false";
106         m_root_tag.style["italic"] = "false";
107         m_root_tag.style["underline"] = "false";
108         m_root_tag.style["halign"] = "left";
109         m_root_tag.style["color"] = "#EEEEEE";
110         m_root_tag.style["hovercolor"] = "#FF0000";
111
112         m_active_tags.push_front(&m_root_tag);
113         m_style = m_root_tag.style;
114
115         // Default simple tags definitions
116         StyleList style;
117
118         style["color"] = "#0000FF";
119         style["underline"] = "true";
120         m_elementtags["action"] = style;
121         style.clear();
122
123         style["bold"] = "true";
124         m_elementtags["b"] = style;
125         style.clear();
126
127         style["italic"] = "true";
128         m_elementtags["i"] = style;
129         style.clear();
130
131         style["underline"] = "true";
132         m_elementtags["u"] = style;
133         style.clear();
134
135         style["fontstyle"] = "mono";
136         m_elementtags["mono"] = style;
137         style.clear();
138
139         style["fontsize"] = m_root_tag.style["fontsize"];
140         m_elementtags["normal"] = style;
141         style.clear();
142
143         style["fontsize"] = "24";
144         m_elementtags["big"] = style;
145         style.clear();
146
147         style["fontsize"] = "36";
148         m_elementtags["bigger"] = style;
149         style.clear();
150
151         style["halign"] = "center";
152         m_paragraphtags["center"] = style;
153         style.clear();
154
155         style["halign"] = "justify";
156         m_paragraphtags["justify"] = style;
157         style.clear();
158
159         style["halign"] = "left";
160         m_paragraphtags["left"] = style;
161         style.clear();
162
163         style["halign"] = "right";
164         m_paragraphtags["right"] = style;
165         style.clear();
166
167         m_element = NULL;
168         m_paragraph = NULL;
169         m_end_paragraph_reason = ER_NONE;
170
171         parse(text);
172 }
173
174 ParsedText::~ParsedText()
175 {
176         for (auto &tag : m_not_root_tags)
177                 delete tag;
178 }
179
180 void ParsedText::parse(const wchar_t *text)
181 {
182         wchar_t c;
183         u32 cursor = 0;
184         bool escape = false;
185
186         while ((c = text[cursor]) != L'\0') {
187                 cursor++;
188
189                 if (c == L'\r') { // Mac or Windows breaks
190                         if (text[cursor] == L'\n')
191                                 cursor++;
192                         // If text has begun, don't skip empty line
193                         if (m_paragraph) {
194                                 endParagraph(ER_NEWLINE);
195                                 enterElement(ELEMENT_SEPARATOR);
196                         }
197                         escape = false;
198                         continue;
199                 }
200
201                 if (c == L'\n') { // Unix breaks
202                         // If text has begun, don't skip empty line
203                         if (m_paragraph) {
204                                 endParagraph(ER_NEWLINE);
205                                 enterElement(ELEMENT_SEPARATOR);
206                         }
207                         escape = false;
208                         continue;
209                 }
210
211                 if (escape) {
212                         escape = false;
213                         pushChar(c);
214                         continue;
215                 }
216
217                 if (c == L'\\') {
218                         escape = true;
219                         continue;
220                 }
221
222                 // Tag check
223                 if (c == L'<') {
224                         u32 newcursor = parseTag(text, cursor);
225                         if (newcursor > 0) {
226                                 cursor = newcursor;
227                                 continue;
228                         }
229                 }
230
231                 // Default behavior
232                 pushChar(c);
233         }
234
235         endParagraph(ER_NONE);
236 }
237
238 void ParsedText::endElement()
239 {
240         m_element = NULL;
241 }
242
243 void ParsedText::endParagraph(EndReason reason)
244 {
245         if (!m_paragraph)
246                 return;
247
248         EndReason previous = m_end_paragraph_reason;
249         m_end_paragraph_reason = reason;
250         if (m_empty_paragraph && (reason == ER_TAG ||
251                         (reason == ER_NEWLINE && previous == ER_TAG))) {
252                 // Ignore last empty paragraph
253                 m_paragraph = nullptr;
254                 m_paragraphs.pop_back();
255                 return;
256         }
257         endElement();
258         m_paragraph = NULL;
259 }
260
261 void ParsedText::enterParagraph()
262 {
263         if (!m_paragraph) {
264                 m_paragraphs.emplace_back();
265                 m_paragraph = &m_paragraphs.back();
266                 m_paragraph->setStyle(m_style);
267                 m_empty_paragraph = true;
268         }
269 }
270
271 void ParsedText::enterElement(ElementType type)
272 {
273         enterParagraph();
274
275         if (!m_element || m_element->type != type) {
276                 m_paragraph->elements.emplace_back();
277                 m_element = &m_paragraph->elements.back();
278                 m_element->type = type;
279                 m_element->tags = m_active_tags;
280                 m_element->setStyle(m_style);
281         }
282 }
283
284 void ParsedText::pushChar(wchar_t c)
285 {
286         // New word if needed
287         if (c == L' ' || c == L'\t') {
288                 if (!m_empty_paragraph)
289                         enterElement(ELEMENT_SEPARATOR);
290                 else
291                         return;
292         } else {
293                 m_empty_paragraph = false;
294                 enterElement(ELEMENT_TEXT);
295         }
296         m_element->text += c;
297 }
298
299 ParsedText::Tag *ParsedText::newTag(const std::string &name, const AttrsList &attrs)
300 {
301         endElement();
302         Tag *newtag = new Tag();
303         newtag->name = name;
304         newtag->attrs = attrs;
305         m_not_root_tags.push_back(newtag);
306         return newtag;
307 }
308
309 ParsedText::Tag *ParsedText::openTag(const std::string &name, const AttrsList &attrs)
310 {
311         Tag *newtag = newTag(name, attrs);
312         m_active_tags.push_front(newtag);
313         return newtag;
314 }
315
316 bool ParsedText::closeTag(const std::string &name)
317 {
318         bool found = false;
319         for (auto id = m_active_tags.begin(); id != m_active_tags.end(); ++id)
320                 if ((*id)->name == name) {
321                         m_active_tags.erase(id);
322                         found = true;
323                         break;
324                 }
325         return found;
326 }
327
328 void ParsedText::parseGenericStyleAttr(
329                 const std::string &name, const std::string &value, StyleList &style)
330 {
331         // Color styles
332         if (name == "color" || name == "hovercolor") {
333                 if (check_color(value))
334                         style[name] = value;
335
336                 // Boolean styles
337         } else if (name == "bold" || name == "italic" || name == "underline") {
338                 style[name] = is_yes(value);
339
340         } else if (name == "size") {
341                 if (check_integer(value))
342                         style["fontsize"] = value;
343
344         } else if (name == "font") {
345                 if (value == "mono" || value == "normal")
346                         style["fontstyle"] = value;
347         }
348 }
349
350 void ParsedText::parseStyles(const AttrsList &attrs, StyleList &style)
351 {
352         for (auto const &attr : attrs)
353                 parseGenericStyleAttr(attr.first, attr.second, style);
354 }
355
356 void ParsedText::globalTag(const AttrsList &attrs)
357 {
358         for (const auto &attr : attrs) {
359                 // Only page level style
360                 if (attr.first == "margin") {
361                         if (check_integer(attr.second))
362                                 margin = stoi(attr.second.c_str());
363
364                 } else if (attr.first == "valign") {
365                         if (attr.second == "top")
366                                 valign = ParsedText::VALIGN_TOP;
367                         else if (attr.second == "bottom")
368                                 valign = ParsedText::VALIGN_BOTTOM;
369                         else if (attr.second == "middle")
370                                 valign = ParsedText::VALIGN_MIDDLE;
371                 } else if (attr.first == "background") {
372                         irr::video::SColor color;
373                         if (attr.second == "none") {
374                                 background_type = BACKGROUND_NONE;
375                         } else if (parseColorString(attr.second, color, false)) {
376                                 background_type = BACKGROUND_COLOR;
377                                 background_color = color;
378                         }
379
380                         // Inheriting styles
381
382                 } else if (attr.first == "halign") {
383                         if (attr.second == "left" || attr.second == "center" ||
384                                         attr.second == "right" ||
385                                         attr.second == "justify")
386                                 m_root_tag.style["halign"] = attr.second;
387
388                         // Generic default styles
389
390                 } else {
391                         parseGenericStyleAttr(attr.first, attr.second, m_root_tag.style);
392                 }
393         }
394 }
395
396 u32 ParsedText::parseTag(const wchar_t *text, u32 cursor)
397 {
398         // Tag name
399         bool end = false;
400         std::string name = "";
401         wchar_t c = text[cursor];
402
403         if (c == L'/') {
404                 end = true;
405                 c = text[++cursor];
406                 if (c == L'\0')
407                         return 0;
408         }
409
410         while (c != ' ' && c != '>') {
411                 name += c;
412                 c = text[++cursor];
413                 if (c == L'\0')
414                         return 0;
415         }
416
417         // Tag attributes
418         AttrsList attrs;
419         while (c != L'>') {
420                 std::string attr_name = "";
421                 core::stringw attr_val = L"";
422
423                 while (c == ' ') {
424                         c = text[++cursor];
425                         if (c == L'\0' || c == L'=')
426                                 return 0;
427                 }
428
429                 while (c != L' ' && c != L'=') {
430                         attr_name += (char)c;
431                         c = text[++cursor];
432                         if (c == L'\0' || c == L'>')
433                                 return 0;
434                 }
435
436                 while (c == L' ') {
437                         c = text[++cursor];
438                         if (c == L'\0' || c == L'>')
439                                 return 0;
440                 }
441
442                 if (c != L'=')
443                         return 0;
444
445                 c = text[++cursor];
446
447                 if (c == L'\0')
448                         return 0;
449
450                 while (c != L'>' && c != L' ') {
451                         attr_val += c;
452                         c = text[++cursor];
453                         if (c == L'\0')
454                                 return 0;
455                 }
456
457                 attrs[attr_name] = stringw_to_utf8(attr_val);
458         }
459
460         ++cursor; // Last ">"
461
462         // Tag specific processing
463         StyleList style;
464
465         if (name == "global") {
466                 if (end)
467                         return 0;
468                 globalTag(attrs);
469
470         } else if (name == "style") {
471                 if (end) {
472                         closeTag(name);
473                 } else {
474                         parseStyles(attrs, style);
475                         openTag(name, attrs)->style = style;
476                 }
477                 endElement();
478         } else if (name == "img" || name == "item") {
479                 if (end)
480                         return 0;
481
482                 // Name is a required attribute
483                 if (!attrs.count("name"))
484                         return 0;
485
486                 // Rotate attribute is only for <item>
487                 if (attrs.count("rotate") && name != "item")
488                         return 0;
489
490                 // Angle attribute is only for <item>
491                 if (attrs.count("angle") && name != "item")
492                         return 0;
493
494                 // Ok, element can be created
495                 newTag(name, attrs);
496
497                 if (name == "img")
498                         enterElement(ELEMENT_IMAGE);
499                 else
500                         enterElement(ELEMENT_ITEM);
501
502                 m_element->text = utf8_to_stringw(attrs["name"]);
503
504                 if (attrs.count("float")) {
505                         if (attrs["float"] == "left")
506                                 m_element->floating = FLOAT_LEFT;
507                         if (attrs["float"] == "right")
508                                 m_element->floating = FLOAT_RIGHT;
509                 }
510
511                 if (attrs.count("width")) {
512                         int width = stoi(attrs["width"]);
513                         if (width > 0)
514                                 m_element->dim.Width = width;
515                 }
516
517                 if (attrs.count("height")) {
518                         int height = stoi(attrs["height"]);
519                         if (height > 0)
520                                 m_element->dim.Height = height;
521                 }
522
523                 if (attrs.count("angle")) {
524                         std::string str = attrs["angle"];
525                         std::vector<std::string> parts = split(str, ',');
526                         if (parts.size() == 3) {
527                                 m_element->angle = v3s16(
528                                                 rangelim(stoi(parts[0]), -180, 180),
529                                                 rangelim(stoi(parts[1]), -180, 180),
530                                                 rangelim(stoi(parts[2]), -180, 180));
531                                 m_element->rotation = v3s16(0, 0, 0);
532                         }
533                 }
534
535                 if (attrs.count("rotate")) {
536                         if (attrs["rotate"] == "yes") {
537                                 m_element->rotation = v3s16(0, 100, 0);
538                         } else {
539                                 std::string str = attrs["rotate"];
540                                 std::vector<std::string> parts = split(str, ',');
541                                 if (parts.size() == 3) {
542                                         m_element->rotation = v3s16 (
543                                                         rangelim(stoi(parts[0]), -1000, 1000),
544                                                         rangelim(stoi(parts[1]), -1000, 1000),
545                                                         rangelim(stoi(parts[2]), -1000, 1000));
546                                 }
547                         }
548                 }
549
550                 endElement();
551
552         } else if (name == "tag") {
553                 // Required attributes
554                 if (!attrs.count("name"))
555                         return 0;
556
557                 StyleList tagstyle;
558                 parseStyles(attrs, tagstyle);
559
560                 if (is_yes(attrs["paragraph"]))
561                         m_paragraphtags[attrs["name"]] = tagstyle;
562                 else
563                         m_elementtags[attrs["name"]] = tagstyle;
564
565         } else if (name == "action") {
566                 if (end) {
567                         closeTag(name);
568                 } else {
569                         if (!attrs.count("name"))
570                                 return 0;
571                         openTag(name, attrs)->style = m_elementtags["action"];
572                 }
573
574         } else if (m_elementtags.count(name)) {
575                 if (end) {
576                         closeTag(name);
577                 } else {
578                         openTag(name, attrs)->style = m_elementtags[name];
579                 }
580                 endElement();
581
582         } else if (m_paragraphtags.count(name)) {
583                 if (end) {
584                         closeTag(name);
585                 } else {
586                         openTag(name, attrs)->style = m_paragraphtags[name];
587                 }
588                 endParagraph(ER_TAG);
589
590         } else
591                 return 0; // Unknown tag
592
593         // Update styles accordingly
594         m_style.clear();
595         for (auto tag = m_active_tags.crbegin(); tag != m_active_tags.crend(); ++tag)
596                 for (const auto &prop : (*tag)->style)
597                         m_style[prop.first] = prop.second;
598
599         return cursor;
600 }
601
602 // -----------------------------------------------------------------------------
603 // Text Drawer
604
605 TextDrawer::TextDrawer(const wchar_t *text, Client *client,
606                 gui::IGUIEnvironment *environment, ISimpleTextureSource *tsrc) :
607                 m_text(text),
608                 m_client(client), m_environment(environment)
609 {
610         // Size all elements
611         for (auto &p : m_text.m_paragraphs) {
612                 for (auto &e : p.elements) {
613                         switch (e.type) {
614                         case ParsedText::ELEMENT_SEPARATOR:
615                         case ParsedText::ELEMENT_TEXT:
616                                 if (e.font) {
617                                         e.dim.Width = e.font->getDimension(e.text.c_str()).Width;
618                                         e.dim.Height = e.font->getDimension(L"Yy").Height;
619 #if USE_FREETYPE
620                                         if (e.font->getType() == irr::gui::EGFT_CUSTOM) {
621                                                 e.baseline = e.dim.Height - 1 -
622                                                         ((irr::gui::CGUITTFont *)e.font)->getAscender() / 64;
623                                         }
624 #endif
625                                 } else {
626                                         e.dim = {0, 0};
627                                 }
628                                 break;
629
630                         case ParsedText::ELEMENT_IMAGE:
631                         case ParsedText::ELEMENT_ITEM:
632                                 // Resize only non sized items
633                                 if (e.dim.Height != 0 && e.dim.Width != 0)
634                                         break;
635
636                                 // Default image and item size
637                                 core::dimension2d<u32> dim(80, 80);
638
639                                 if (e.type == ParsedText::ELEMENT_IMAGE) {
640                                         video::ITexture *texture =
641                                                 m_client->getTextureSource()->
642                                                         getTexture(stringw_to_utf8(e.text));
643                                         if (texture)
644                                                 dim = texture->getOriginalSize();
645                                 }
646
647                                 if (e.dim.Height == 0)
648                                         if (e.dim.Width == 0)
649                                                 e.dim = dim;
650                                         else
651                                                 e.dim.Height = dim.Height * e.dim.Width /
652                                                                 dim.Width;
653                                 else
654                                         e.dim.Width = dim.Width * e.dim.Height /
655                                                         dim.Height;
656                                 break;
657                         }
658                 }
659         }
660 }
661
662 // Get element at given coordinates. Coordinates are inner coordinates (starting
663 // at 0,0).
664 ParsedText::Element *TextDrawer::getElementAt(core::position2d<s32> pos)
665 {
666         pos.Y -= m_voffset;
667         for (auto &p : m_text.m_paragraphs) {
668                 for (auto &el : p.elements) {
669                         core::rect<s32> rect(el.pos, el.dim);
670                         if (rect.isPointInside(pos))
671                                 return &el;
672                 }
673         }
674         return 0;
675 }
676
677 /*
678    This function places all elements according to given width. Elements have
679    been previously sized by constructor and will be later drawed by draw.
680    It may be called each time width changes and resulting height can be
681    retrieved using getHeight. See GUIHyperText constructor, it uses it once to
682    test if text fits in window and eventually another time if width is reduced
683    m_floating because of scrollbar added.
684 */
685 void TextDrawer::place(const core::rect<s32> &dest_rect)
686 {
687         m_floating.clear();
688         s32 y = 0;
689         s32 ymargin = m_text.margin;
690
691         // Iterator used :
692         // p - Current paragraph, walked only once
693         // el - Current element, walked only once
694         // e and f - local element and floating operators
695
696         for (auto &p : m_text.m_paragraphs) {
697                 // Find and place floating stuff in paragraph
698                 for (auto e = p.elements.begin(); e != p.elements.end(); ++e) {
699                         if (e->floating != ParsedText::FLOAT_NONE) {
700                                 if (y)
701                                         e->pos.Y = y + std::max(ymargin, e->margin);
702                                 else
703                                         e->pos.Y = ymargin;
704
705                                 if (e->floating == ParsedText::FLOAT_LEFT)
706                                         e->pos.X = m_text.margin;
707                                 if (e->floating == ParsedText::FLOAT_RIGHT)
708                                         e->pos.X = dest_rect.getWidth() - e->dim.Width -
709                                                         m_text.margin;
710
711                                 RectWithMargin floating;
712                                 floating.rect = core::rect<s32>(e->pos, e->dim);
713                                 floating.margin = e->margin;
714
715                                 m_floating.push_back(floating);
716                         }
717                 }
718
719                 if (y)
720                         y = y + std::max(ymargin, p.margin);
721
722                 ymargin = p.margin;
723
724                 // Place non floating stuff
725                 std::vector<ParsedText::Element>::iterator el = p.elements.begin();
726
727                 while (el != p.elements.end()) {
728                         // Determine line width and y pos
729                         s32 left, right;
730                         s32 nexty = y;
731                         do {
732                                 y = nexty;
733                                 nexty = 0;
734
735                                 // Inner left & right
736                                 left = m_text.margin;
737                                 right = dest_rect.getWidth() - m_text.margin;
738
739                                 for (const auto &f : m_floating) {
740                                         // Does floating rect intersect paragraph y line?
741                                         if (f.rect.UpperLeftCorner.Y - f.margin <= y &&
742                                                         f.rect.LowerRightCorner.Y + f.margin >= y) {
743
744                                                 // Next Y to try if no room left
745                                                 if (!nexty || f.rect.LowerRightCorner.Y +
746                                                                 std::max(f.margin, p.margin) < nexty) {
747                                                         nexty = f.rect.LowerRightCorner.Y +
748                                                                         std::max(f.margin, p.margin) + 1;
749                                                 }
750
751                                                 if (f.rect.UpperLeftCorner.X - f.margin <= left &&
752                                                                 f.rect.LowerRightCorner.X + f.margin < right) {
753                                                         // float on left
754                                                         if (f.rect.LowerRightCorner.X +
755                                                                         std::max(f.margin, p.margin) > left) {
756                                                                 left = f.rect.LowerRightCorner.X +
757                                                                                 std::max(f.margin, p.margin);
758                                                         }
759                                                 } else if (f.rect.LowerRightCorner.X + f.margin >= right &&
760                                                                 f.rect.UpperLeftCorner.X - f.margin > left) {
761                                                         // float on right
762                                                         if (f.rect.UpperLeftCorner.X -
763                                                                         std::max(f.margin, p.margin) < right)
764                                                                 right = f.rect.UpperLeftCorner.X -
765                                                                                 std::max(f.margin, p.margin);
766
767                                                 } else if (f.rect.UpperLeftCorner.X - f.margin <= left &&
768                                                                 f.rect.LowerRightCorner.X + f.margin >= right) {
769                                                         // float taking all space
770                                                         left = right;
771                                                 }
772                                                 else
773                                                 { // float in the middle -- should not occure yet, see that later
774                                                 }
775                                         }
776                                 }
777                         } while (nexty && right <= left);
778
779                         u32 linewidth = right - left;
780                         float x = left;
781
782                         u32 charsheight = 0;
783                         u32 charswidth = 0;
784                         u32 wordcount = 0;
785
786                         // Skip begining of line separators but include them in height
787                         // computation.
788                         while (el != p.elements.end() &&
789                                         el->type == ParsedText::ELEMENT_SEPARATOR) {
790                                 if (el->floating == ParsedText::FLOAT_NONE) {
791                                         el->drawwidth = 0;
792                                         if (charsheight < el->dim.Height)
793                                                 charsheight = el->dim.Height;
794                                 }
795                                 el++;
796                         }
797
798                         std::vector<ParsedText::Element>::iterator linestart = el;
799                         std::vector<ParsedText::Element>::iterator lineend = p.elements.end();
800
801                         // First pass, find elements fitting into line
802                         // (or at least one element)
803                         while (el != p.elements.end() && (charswidth == 0 ||
804                                         charswidth + el->dim.Width <= linewidth)) {
805                                 if (el->floating == ParsedText::FLOAT_NONE) {
806                                         if (el->type != ParsedText::ELEMENT_SEPARATOR) {
807                                                 lineend = el;
808                                                 wordcount++;
809                                         }
810                                         charswidth += el->dim.Width;
811                                         if (charsheight < el->dim.Height)
812                                                 charsheight = el->dim.Height;
813                                 }
814                                 el++;
815                         }
816
817                         // Empty line, nothing to place only go down line height
818                         if (lineend == p.elements.end()) {
819                                 y += charsheight;
820                                 continue;
821                         }
822
823                         // Point to the first position outside line (may be end())
824                         lineend++;
825
826                         // Second pass, compute printable line width and adjustments
827                         charswidth = 0;
828                         s32 top = 0;
829                         s32 bottom = 0;
830                         for (auto e = linestart; e != lineend; ++e) {
831                                 if (e->floating == ParsedText::FLOAT_NONE) {
832                                         charswidth += e->dim.Width;
833                                         if (top < (s32)e->dim.Height - e->baseline)
834                                                 top = e->dim.Height - e->baseline;
835                                         if (bottom < e->baseline)
836                                                 bottom = e->baseline;
837                                 }
838                         }
839
840                         float extraspace = 0.f;
841
842                         switch (p.halign) {
843                         case ParsedText::HALIGN_CENTER:
844                                 x += (linewidth - charswidth) / 2.f;
845                                 break;
846                         case ParsedText::HALIGN_JUSTIFY:
847                                 if (wordcount > 1 && // Justification only if at least two words
848                                         !(lineend == p.elements.end())) // Don't justify last line
849                                         extraspace = ((float)(linewidth - charswidth)) / (wordcount - 1);
850                                 break;
851                         case ParsedText::HALIGN_RIGHT:
852                                 x += linewidth - charswidth;
853                                 break;
854                         case ParsedText::HALIGN_LEFT:
855                                 break;
856                         }
857
858                         // Third pass, actually place everything
859                         for (auto e = linestart; e != lineend; ++e) {
860                                 if (e->floating != ParsedText::FLOAT_NONE)
861                                         continue;
862
863                                 e->pos.X = x;
864                                 e->pos.Y = y;
865
866                                 switch (e->type) {
867                                 case ParsedText::ELEMENT_TEXT:
868                                 case ParsedText::ELEMENT_SEPARATOR:
869                                         e->pos.X = x;
870
871                                         // Align char baselines
872                                         e->pos.Y = y + top + e->baseline - e->dim.Height;
873
874                                         x += e->dim.Width;
875                                         if (e->type == ParsedText::ELEMENT_SEPARATOR)
876                                                 x += extraspace;
877                                         break;
878
879                                 case ParsedText::ELEMENT_IMAGE:
880                                 case ParsedText::ELEMENT_ITEM:
881                                         x += e->dim.Width;
882                                         break;
883                                 }
884
885                                 // Draw width for separator can be different than element
886                                 // width. This will be important for char effects like
887                                 // underline.
888                                 e->drawwidth = x - e->pos.X;
889                         }
890                         y += charsheight;
891                 } // Elements (actually lines)
892         } // Paragraph
893
894         // Check if float goes under paragraph
895         for (const auto &f : m_floating) {
896                 if (f.rect.LowerRightCorner.Y >= y)
897                         y = f.rect.LowerRightCorner.Y;
898         }
899
900         m_height = y + m_text.margin;
901         // Compute vertical offset according to vertical alignment
902         if (m_height < dest_rect.getHeight())
903                 switch (m_text.valign) {
904                 case ParsedText::VALIGN_BOTTOM:
905                         m_voffset = dest_rect.getHeight() - m_height;
906                         break;
907                 case ParsedText::VALIGN_MIDDLE:
908                         m_voffset = (dest_rect.getHeight() - m_height) / 2;
909                         break;
910                 case ParsedText::VALIGN_TOP:
911                 default:
912                         m_voffset = 0;
913                 }
914         else
915                 m_voffset = 0;
916 }
917
918 // Draw text in a rectangle with a given offset. Items are actually placed in
919 // relative (to upper left corner) coordinates.
920 void TextDrawer::draw(const core::rect<s32> &clip_rect,
921                 const core::position2d<s32> &dest_offset)
922 {
923         irr::video::IVideoDriver *driver = m_environment->getVideoDriver();
924         core::position2d<s32> offset = dest_offset;
925         offset.Y += m_voffset;
926
927         if (m_text.background_type == ParsedText::BACKGROUND_COLOR)
928                 driver->draw2DRectangle(m_text.background_color, clip_rect);
929
930         for (auto &p : m_text.m_paragraphs) {
931                 for (auto &el : p.elements) {
932                         core::rect<s32> rect(el.pos + offset, el.dim);
933                         if (!rect.isRectCollided(clip_rect))
934                                 continue;
935
936                         switch (el.type) {
937                         case ParsedText::ELEMENT_SEPARATOR:
938                         case ParsedText::ELEMENT_TEXT: {
939                                 irr::video::SColor color = el.color;
940
941                                 for (auto tag : el.tags)
942                                         if (&(*tag) == m_hovertag)
943                                                 color = el.hovercolor;
944
945                                 if (!el.font)
946                                         break;
947
948                                 if (el.type == ParsedText::ELEMENT_TEXT)
949                                         el.font->draw(el.text, rect, color, false, true,
950                                                         &clip_rect);
951
952                                 if (el.underline &&  el.drawwidth) {
953                                         s32 linepos = el.pos.Y + offset.Y +
954                                                         el.dim.Height - (el.baseline >> 1);
955
956                                         core::rect<s32> linerect(el.pos.X + offset.X,
957                                                         linepos - (el.baseline >> 3) - 1,
958                                                         el.pos.X + offset.X + el.drawwidth,
959                                                         linepos + (el.baseline >> 3));
960
961                                         driver->draw2DRectangle(color, linerect, &clip_rect);
962                                 }
963                         } break;
964
965                         case ParsedText::ELEMENT_IMAGE: {
966                                 video::ITexture *texture =
967                                                 m_client->getTextureSource()->getTexture(
968                                                                 stringw_to_utf8(el.text));
969                                 if (texture != 0)
970                                         m_environment->getVideoDriver()->draw2DImage(
971                                                         texture, rect,
972                                                         irr::core::rect<s32>(
973                                                                         core::position2d<s32>(0, 0),
974                                                                         texture->getOriginalSize()),
975                                                         &clip_rect, 0, true);
976                         } break;
977
978                         case ParsedText::ELEMENT_ITEM: {
979                                 IItemDefManager *idef = m_client->idef();
980                                 ItemStack item;
981                                 item.deSerialize(stringw_to_utf8(el.text), idef);
982
983                                 drawItemStack(
984                                                 m_environment->getVideoDriver(),
985                                                 g_fontengine->getFont(), item, rect, &clip_rect,
986                                                 m_client, IT_ROT_OTHER, el.angle, el.rotation
987                                 );
988                         } break;
989                         }
990                 }
991         }
992 }
993
994 // -----------------------------------------------------------------------------
995 // GUIHyperText - The formated text area formspec item
996
997 //! constructor
998 GUIHyperText::GUIHyperText(const wchar_t *text, IGUIEnvironment *environment,
999                 IGUIElement *parent, s32 id, const core::rect<s32> &rectangle,
1000                 Client *client, ISimpleTextureSource *tsrc) :
1001                 IGUIElement(EGUIET_ELEMENT, environment, parent, id, rectangle),
1002                 m_client(client), m_vscrollbar(nullptr),
1003                 m_drawer(text, client, environment, tsrc), m_text_scrollpos(0, 0)
1004 {
1005
1006 #ifdef _DEBUG
1007         setDebugName("GUIHyperText");
1008 #endif
1009
1010         IGUISkin *skin = 0;
1011         if (Environment)
1012                 skin = Environment->getSkin();
1013
1014         m_scrollbar_width = skin ? skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : 16;
1015
1016         core::rect<s32> rect = irr::core::rect<s32>(
1017                         RelativeRect.getWidth() - m_scrollbar_width, 0,
1018                         RelativeRect.getWidth(), RelativeRect.getHeight());
1019
1020         m_vscrollbar = new GUIScrollBar(Environment, this, -1, rect, false, true);
1021         m_vscrollbar->setVisible(false);
1022 }
1023
1024 //! destructor
1025 GUIHyperText::~GUIHyperText()
1026 {
1027         m_vscrollbar->remove();
1028         m_vscrollbar->drop();
1029 }
1030
1031 ParsedText::Element *GUIHyperText::getElementAt(s32 X, s32 Y)
1032 {
1033         core::position2d<s32> pos{X, Y};
1034         pos -= m_display_text_rect.UpperLeftCorner;
1035         pos -= m_text_scrollpos;
1036         return m_drawer.getElementAt(pos);
1037 }
1038
1039 void GUIHyperText::checkHover(s32 X, s32 Y)
1040 {
1041         m_drawer.m_hovertag = nullptr;
1042
1043         if (AbsoluteRect.isPointInside(core::position2d<s32>(X, Y))) {
1044                 ParsedText::Element *element = getElementAt(X, Y);
1045
1046                 if (element) {
1047                         for (auto &tag : element->tags) {
1048                                 if (tag->name == "action") {
1049                                         m_drawer.m_hovertag = tag;
1050                                         break;
1051                                 }
1052                         }
1053                 }
1054         }
1055
1056 #ifndef HAVE_TOUCHSCREENGUI
1057         if (m_drawer.m_hovertag)
1058                 RenderingEngine::get_raw_device()->getCursorControl()->setActiveIcon(
1059                                 gui::ECI_HAND);
1060         else
1061                 RenderingEngine::get_raw_device()->getCursorControl()->setActiveIcon(
1062                                 gui::ECI_NORMAL);
1063 #endif
1064 }
1065
1066 bool GUIHyperText::OnEvent(const SEvent &event)
1067 {
1068         // Scroll bar
1069         if (event.EventType == EET_GUI_EVENT &&
1070                         event.GUIEvent.EventType == EGET_SCROLL_BAR_CHANGED &&
1071                         event.GUIEvent.Caller == m_vscrollbar) {
1072                 m_text_scrollpos.Y = -m_vscrollbar->getPos();
1073         }
1074
1075         // Reset hover if element left
1076         if (event.EventType == EET_GUI_EVENT &&
1077                         event.GUIEvent.EventType == EGET_ELEMENT_LEFT) {
1078                 m_drawer.m_hovertag = nullptr;
1079 #ifndef HAVE_TOUCHSCREENGUI
1080                 gui::ICursorControl *cursor_control =
1081                                 RenderingEngine::get_raw_device()->getCursorControl();
1082                 if (cursor_control->isVisible())
1083                         cursor_control->setActiveIcon(gui::ECI_NORMAL);
1084 #endif
1085         }
1086
1087         if (event.EventType == EET_MOUSE_INPUT_EVENT) {
1088                 if (event.MouseInput.Event == EMIE_MOUSE_MOVED)
1089                         checkHover(event.MouseInput.X, event.MouseInput.Y);
1090
1091                 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL && m_vscrollbar->isVisible()) {
1092                         m_vscrollbar->setPos(m_vscrollbar->getPos() -
1093                                         event.MouseInput.Wheel * m_vscrollbar->getSmallStep());
1094                         m_text_scrollpos.Y = -m_vscrollbar->getPos();
1095                         m_drawer.draw(m_display_text_rect, m_text_scrollpos);
1096                         checkHover(event.MouseInput.X, event.MouseInput.Y);
1097                         return true;
1098
1099                 } else if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
1100                         ParsedText::Element *element = getElementAt(
1101                                         event.MouseInput.X, event.MouseInput.Y);
1102
1103                         if (element) {
1104                                 for (auto &tag : element->tags) {
1105                                         if (tag->name == "action") {
1106                                                 Text = core::stringw(L"action:") +
1107                                                        utf8_to_stringw(tag->attrs["name"]);
1108                                                 if (Parent) {
1109                                                         SEvent newEvent;
1110                                                         newEvent.EventType = EET_GUI_EVENT;
1111                                                         newEvent.GUIEvent.Caller = this;
1112                                                         newEvent.GUIEvent.Element = 0;
1113                                                         newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED;
1114                                                         Parent->OnEvent(newEvent);
1115                                                 }
1116                                                 break;
1117                                         }
1118                                 }
1119                         }
1120                 }
1121         }
1122
1123         return IGUIElement::OnEvent(event);
1124 }
1125
1126 //! draws the element and its children
1127 void GUIHyperText::draw()
1128 {
1129         if (!IsVisible)
1130                 return;
1131
1132         // Text
1133         m_display_text_rect = AbsoluteRect;
1134         m_drawer.place(m_display_text_rect);
1135
1136         // Show scrollbar if text overflow
1137         if (m_drawer.getHeight() > m_display_text_rect.getHeight()) {
1138                 m_vscrollbar->setSmallStep(m_display_text_rect.getHeight() * 0.1f);
1139                 m_vscrollbar->setLargeStep(m_display_text_rect.getHeight() * 0.5f);
1140                 m_vscrollbar->setMax(m_drawer.getHeight() - m_display_text_rect.getHeight());
1141
1142                 m_vscrollbar->setVisible(true);
1143
1144                 m_vscrollbar->setPageSize(s32(m_drawer.getHeight()));
1145
1146                 core::rect<s32> smaller_rect = m_display_text_rect;
1147
1148                 smaller_rect.LowerRightCorner.X -= m_scrollbar_width;
1149                 m_drawer.place(smaller_rect);
1150         } else {
1151                 m_vscrollbar->setMax(0);
1152                 m_vscrollbar->setPos(0);
1153                 m_vscrollbar->setVisible(false);
1154         }
1155         m_drawer.draw(AbsoluteClippingRect,
1156                         m_display_text_rect.UpperLeftCorner + m_text_scrollpos);
1157
1158         // draw children
1159         IGUIElement::draw();
1160 }