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