Formspec: add hypertext element
authorPierre-Yves Rollo <dev@pyrollo.com>
Tue, 10 Sep 2019 13:11:26 +0000 (15:11 +0200)
committerSmallJoker <SmallJoker@users.noreply.github.com>
Sun, 3 Nov 2019 10:45:33 +0000 (11:45 +0100)
22 files changed:
builtin/settingtypes.txt
doc/lua_api.txt
fonts/Arimo-Bold.ttf [new file with mode: 0644]
fonts/Arimo-BoldItalic.ttf [new file with mode: 0644]
fonts/Arimo-Italic.ttf [new file with mode: 0644]
fonts/Cousine-Bold.ttf [new file with mode: 0644]
fonts/Cousine-BoldItalic.ttf [new file with mode: 0644]
fonts/Cousine-Italic.ttf [new file with mode: 0644]
src/client/fontengine.cpp
src/client/fontengine.h
src/client/hud.cpp
src/client/hud.h
src/defaultsettings.cpp
src/gui/CMakeLists.txt
src/gui/guiFormSpecMenu.cpp
src/gui/guiFormSpecMenu.h
src/gui/guiHyperText.cpp [new file with mode: 0644]
src/gui/guiHyperText.h [new file with mode: 0644]
src/irrlicht_changes/CGUITTFont.h
src/util/string.cpp
src/util/string.h
util/travis/clang-format-whitelist.txt

index 5669519dea95c116bda19af658efabd6268fc102..e74b8ea8b84afdb4920c24d93623876ffedf2f35 100644 (file)
@@ -851,14 +851,9 @@ tooltip_append_itemname (Append item name) bool false
 #    If disabled, bitmap and XML vectors fonts are used instead.
 freetype (FreeType fonts) bool true
 
-#    Path to the default font.
-#    If “freetype” setting is enabled: Must be a TrueType font.
-#    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
-#    The fallback font will be used if the font cannot be loaded.
-font_path (Font path) filepath fonts/liberationsans.ttf
+font_bold (Font bold by default) bool false
 
-#    Font size of the default font in point (pt).
-font_size (Font size) int 16 1
+font_italic (Font italic by default) bool false
 
 #    Shadow offset (in pixels) of the default font. If 0, then shadow will not be drawn.
 font_shadow (Font shadow) int 1
@@ -866,20 +861,31 @@ font_shadow (Font shadow) int 1
 #    Opaqueness (alpha) of the shadow behind the default font, between 0 and 255.
 font_shadow_alpha (Font shadow alpha) int 127 0 255
 
-#    Path to the monospace font.
+#    Font size of the default font in point (pt).
+font_size (Font size) int 16 1
+
+#    Path to the default font.
 #    If “freetype” setting is enabled: Must be a TrueType font.
 #    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
-#    This font is used for e.g. the console and profiler screen.
-mono_font_path (Monospace font path) filepath fonts/liberationmono.ttf
+#    The fallback font will be used if the font cannot be loaded.
+font_path (Regular font path) filepath fonts/Arimo-Regular.ttf
+
+font_path_bold (Bold font path) filepath fonts/Arimo-Bold.ttf
+font_path_italic (Italic font path) filepath fonts/Arimo-Italic.ttf
+font_path_bolditalic (Bold and italic font path) filepath fonts/Arimo-BoldItalic.ttf
 
 #    Font size of the monospace font in point (pt).
 mono_font_size (Monospace font size) int 15 1
 
-#    Path of the fallback font.
+#    Path to the monospace font.
 #    If “freetype” setting is enabled: Must be a TrueType font.
 #    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
-#    This font will be used for certain languages or if the default font is unavailable.
-fallback_font_path (Fallback font path) filepath fonts/DroidSansFallbackFull.ttf
+#    This font is used for e.g. the console and profiler screen.
+mono_font_path (Monospace font path) filepath fonts/Cousine-Regular.ttf
+
+mono_font_path_bold (Bold monospace font path) filepath fonts/Cousine-Bold.ttf
+mono_font_path_italic (Italic monospace font path) filepath fonts/Cousine-Italic.ttf
+mono_font_path_bolditalic (Bold and italic monospace font path) filepath fonts/Cousine-BoldItalic.ttf
 
 #    Font size of the fallback font in point (pt).
 fallback_font_size (Fallback font size) int 15 1
@@ -890,6 +896,12 @@ fallback_font_shadow (Fallback font shadow) int 1
 #    Opaqueness (alpha) of the shadow behind the fallback font, between 0 and 255.
 fallback_font_shadow_alpha (Fallback font shadow alpha) int 128 0 255
 
+#    Path of the fallback font.
+#    If “freetype” setting is enabled: Must be a TrueType font.
+#    If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+#    This font will be used for certain languages or if the default font is unavailable.
+fallback_font_path (Fallback font path) filepath fonts/DroidSansFallbackFull.ttf
+
 #    Path to save screenshots at.
 screenshot_path (Screenshot folder) path
 
index 5640be73c37d329e5bbb84a60e61d8bc8bfbdd46..1905eff4455551120fc31ed5ce4477bdd78f90fa 100644 (file)
@@ -2189,8 +2189,13 @@ Elements
   half a coordinate.  With the old system, newlines are spaced 2/5 of
   an inventory slot.
 
-### `vertlabel[<X>,<Y>;<label>]`
+### `hypertext[<X>,<Y>;<W>,<H>;<name>;<text>]`
+* Displays a static formated text with hyperlinks.
+* `x`, `y`, `w` and `h` work as per field
+* `name` is the name of the field as returned in fields to `on_receive_fields` in case of action in text.
+* `text` is the formatted text using `markup language` described below.
 
+### `vertlabel[<X>,<Y>;<label>]`
 * Textual label drawn vertically
 * `label` is the text on the label
 * **Note**: If the new coordinate system is enabled, vertlabels are
@@ -2534,6 +2539,110 @@ Some types may inherit styles from parent types.
     * noclip - boolean, set to true to allow the element to exceed formspec bounds.
     * textcolor - color. Default white.
 
+Markup language
+---------------
+
+Markup language used in `hypertext[]` elements uses tag that look like HTML tags. Some
+tags can enclose text, they open with `<tagname>` and close with `</tagname>`.
+Tags can have attributes, in that case, attributes are in the opening tag in
+form of a key/value separated with equal signs. Attribute values should not be quoted.
+
+These are the technically basic tags but see below for usual tags. Base tags are:
+
+`<style color=... font=... size=...>...</style>`
+
+Changes the style of the text.
+
+* `color`: Text color. Given color is a `colorspec`.
+* `size`: Text size.
+* `font`: Text font (`mono` or `normal`).
+
+`<global background=... margin=... valign=... color=... hovercolor=... size=... font=... halign=... >`
+
+Sets global style.
+
+Global only styles:
+* `background`: Text background, a `colorspec` or `none`.
+* `margin`: Page margins in pixel.
+* `valign`: Text vertical alignment (`top`, `middle`, `bottom`).
+
+Inheriting styles (affects child elements):
+* `color`: Default text color. Given color is a `colorspec`.
+* `hovercolor`: Color of <action> tags when mouse is over.
+* `size`: Default text size.
+* `font`: Default text font (`mono` or `normal`).
+* `halign`: Default text horizontal alignment (`left`, `right`, `center`, `justify`).
+
+This tag needs to be placed only once as it changes the global settings of the
+text. Anyway, if several tags are placed, each changed will be made in the order
+tags appear.
+
+`<tag name=... color=... hovercolor=... font=... size=...>`
+
+Defines or redefines tag style. This can be used to define new tags.
+* `name`: Name of the tag to define or change.
+* `color`: Text color. Given color is a `colorspec`.
+* `hovercolor`: Text color when element hovered (only for `action` tags). Given color is a `colorspec`.
+* `size`: Text size.
+* `font`: Text font (`mono` or `normal`).
+
+Following tags are the usual tags for text layout. They are defined by default.
+Other tags can be added using `<tag ...>` tag.
+
+`<normal>...</normal>`: Normal size text
+
+`<big>...</big>`: Big text
+
+`<bigger>...</bigger>`: Bigger text
+
+`<center>...</center>`: Centered text
+
+`<left>...</left>`: Left-aligned text
+
+`<right>...</right>`: Right-aligned text
+
+`<justify>...</justify>`: Justified text
+
+`<mono>...</mono>`: Monospaced font
+
+`<b>...</b>`, `<i>...</i>`, `<u>...</u>`: Bold, italic, underline styles.
+
+`<action name=...>...</action>`
+
+Make that text a clickable text triggering an action.
+
+* `name`: Name of the action (mandatory).
+
+When clicked, the formspec is send to the server. The value of the text field
+sent to `on_player_receive_fields` will be "action:" concatenated to the action
+name.
+
+`<img name=... float=... width=... height=...>`
+
+Draws an image which is present in the client media cache.
+
+* `name`: Name of the texture (mandatory).
+* `float`: If present, makes the image floating (`left` or `right`).
+* `width`: Force image width instead of taking texture width.
+* `height`: Force image height instead of taking texture height.
+
+If only width or height given, texture aspect is kept.
+
+`<item name=... float=... width=... height=... rotate=...>`
+
+Draws an item image.
+
+* `name`: Item string of the item to draw (mandatory).
+* `float`: If present, makes the image floating (`left` or `right`).
+* `width`: Item image width.
+* `height`: Item image height.
+* `rotate`: Rotate item image if set to `yes` or `X,Y,Z`. X, Y and Z being
+rotation speeds in percent of standard speed (-1000 to 1000). Works only if
+`inventory_items_animations` is set to true.
+* `angle`: Angle in which the item image is shown. Value has `X,Y,Z` form.
+X, Y and Z being angles around each three axes. Works only if
+`inventory_items_animations` is set to true.
+
 Inventory
 =========
 
@@ -2557,7 +2666,6 @@ Player Inventory lists
     * Is not created automatically, use `InvRef:set_size`
     * Is only used to enhance the empty hand's tool capabilities
 
-
 Colors
 ======
 
diff --git a/fonts/Arimo-Bold.ttf b/fonts/Arimo-Bold.ttf
new file mode 100644 (file)
index 0000000..0b05b44
Binary files /dev/null and b/fonts/Arimo-Bold.ttf differ
diff --git a/fonts/Arimo-BoldItalic.ttf b/fonts/Arimo-BoldItalic.ttf
new file mode 100644 (file)
index 0000000..3ca5311
Binary files /dev/null and b/fonts/Arimo-BoldItalic.ttf differ
diff --git a/fonts/Arimo-Italic.ttf b/fonts/Arimo-Italic.ttf
new file mode 100644 (file)
index 0000000..fe31082
Binary files /dev/null and b/fonts/Arimo-Italic.ttf differ
diff --git a/fonts/Cousine-Bold.ttf b/fonts/Cousine-Bold.ttf
new file mode 100644 (file)
index 0000000..997817a
Binary files /dev/null and b/fonts/Cousine-Bold.ttf differ
diff --git a/fonts/Cousine-BoldItalic.ttf b/fonts/Cousine-BoldItalic.ttf
new file mode 100644 (file)
index 0000000..3d91878
Binary files /dev/null and b/fonts/Cousine-BoldItalic.ttf differ
diff --git a/fonts/Cousine-Italic.ttf b/fonts/Cousine-Italic.ttf
new file mode 100644 (file)
index 0000000..2ec2c34
Binary files /dev/null and b/fonts/Cousine-Italic.ttf differ
index 858d6780e9773849a80c16ccb20a2ebebb346964..8120f82df4d926c59c7e4f8b5c5b0779fdea3cf3 100644 (file)
@@ -41,6 +41,11 @@ static void font_setting_changed(const std::string &name, void *userdata)
        g_fontengine->readSettings();
 }
 
+unsigned int get_font_cache_index(FontMode mode, bool bold = false, bool italic = false)
+{
+       return (mode << 2) | (bold << 1) | italic;
+}
+
 /******************************************************************************/
 FontEngine::FontEngine(Settings* main_settings, gui::IGUIEnvironment* env) :
        m_settings(main_settings),
@@ -59,7 +64,12 @@ FontEngine::FontEngine(Settings* main_settings, gui::IGUIEnvironment* env) :
 
        if (m_currentMode == FM_Standard) {
                m_settings->registerChangedCallback("font_size", font_setting_changed, NULL);
+               m_settings->registerChangedCallback("font_bold", font_setting_changed, NULL);
+               m_settings->registerChangedCallback("font_italic", font_setting_changed, NULL);
                m_settings->registerChangedCallback("font_path", font_setting_changed, NULL);
+               m_settings->registerChangedCallback("font_path_bold", font_setting_changed, NULL);
+               m_settings->registerChangedCallback("font_path_italic", font_setting_changed, NULL);
+               m_settings->registerChangedCallback("font_path_bolditalic", font_setting_changed, NULL);
                m_settings->registerChangedCallback("font_shadow", font_setting_changed, NULL);
                m_settings->registerChangedCallback("font_shadow_alpha", font_setting_changed, NULL);
        }
@@ -96,7 +106,8 @@ void FontEngine::cleanCache()
 }
 
 /******************************************************************************/
-irr::gui::IGUIFont* FontEngine::getFont(unsigned int font_size, FontMode mode)
+irr::gui::IGUIFont *FontEngine::getFont(unsigned int font_size, FontMode mode,
+       bool bold, bool italic)
 {
        if (mode == FM_Unspecified) {
                mode = m_currentMode;
@@ -110,22 +121,30 @@ irr::gui::IGUIFont* FontEngine::getFont(unsigned int font_size, FontMode mode)
        if (font_size == FONT_SIZE_UNSPECIFIED)
                font_size = m_default_size[mode];
 
-       const auto &cache = m_font_cache[mode];
+       unsigned int cache_index = get_font_cache_index(mode, bold, italic);
+
+       const auto &cache = m_font_cache[cache_index];
+
        if (cache.find(font_size) == cache.end()) {
                if (mode == FM_Simple || mode == FM_SimpleMono)
                        initSimpleFont(font_size, mode);
                else
-                       initFont(font_size, mode);
+                       initFont(font_size, mode, bold, italic);
        }
 
+       if (m_font_cache[cache_index].find(font_size) ==
+                       m_font_cache[cache_index].end())
+               initFont(font_size, mode, bold, italic);
+
        const auto &font = cache.find(font_size);
        return font != cache.end() ? font->second : nullptr;
 }
 
 /******************************************************************************/
-unsigned int FontEngine::getTextHeight(unsigned int font_size, FontMode mode)
+unsigned int FontEngine::getTextHeight(unsigned int font_size, FontMode mode,
+       bool bold, bool italic)
 {
-       irr::gui::IGUIFont* font = getFont(font_size, mode);
+       irr::gui::IGUIFont *font = getFont(font_size, mode, bold, italic);
 
        // use current skin font as fallback
        if (font == NULL) {
@@ -138,9 +157,9 @@ unsigned int FontEngine::getTextHeight(unsigned int font_size, FontMode mode)
 
 /******************************************************************************/
 unsigned int FontEngine::getTextWidth(const std::wstring& text,
-               unsigned int font_size, FontMode mode)
+               unsigned int font_size, FontMode mode, bool bold, bool italic)
 {
-       irr::gui::IGUIFont* font = getFont(font_size, mode);
+       irr::gui::IGUIFont *font = getFont(font_size, mode, bold, italic);
 
        // use current skin font as fallback
        if (font == NULL) {
@@ -153,9 +172,10 @@ unsigned int FontEngine::getTextWidth(const std::wstring& text,
 
 
 /** get line height for a specific font (including empty room between lines) */
-unsigned int FontEngine::getLineHeight(unsigned int font_size, FontMode mode)
+unsigned int FontEngine::getLineHeight(unsigned int font_size, FontMode mode,
+       bool bold, bool italic)
 {
-       irr::gui::IGUIFont* font = getFont(font_size, mode);
+       irr::gui::IGUIFont *font = getFont(font_size, mode, bold, italic);
 
        // use current skin font as fallback
        if (font == NULL) {
@@ -183,6 +203,10 @@ void FontEngine::readSettings()
 
                m_currentMode = is_yes(gettext("needs_fallback_font")) ?
                                FM_Fallback : FM_Standard;
+
+               m_default_bold = m_settings->getBool("font_bold");
+               m_default_italic = m_settings->getBool("font_italic");
+
        } else {
                m_currentMode = FM_Simple;
        }
@@ -226,14 +250,17 @@ void FontEngine::updateFontCache()
 }
 
 /******************************************************************************/
-void FontEngine::initFont(unsigned int basesize, FontMode mode)
+void FontEngine::initFont(unsigned int basesize, FontMode mode,
+       bool bold, bool italic)
 {
        assert(mode != FM_Unspecified);
        assert(basesize != FONT_SIZE_UNSPECIFIED);
 
-       if (m_font_cache[mode].find(basesize) != m_font_cache[mode].end())
-               return;
+       int cache_index = get_font_cache_index(mode, bold, italic);
 
+       if (m_font_cache[cache_index].find(basesize) !=
+                       m_font_cache[cache_index].end())
+               return;
 
        std::string setting_prefix = "";
 
@@ -249,8 +276,13 @@ void FontEngine::initFont(unsigned int basesize, FontMode mode)
                        break;
        }
 
+       std::string setting_suffix = (bold) ?
+                       ((italic) ? "_bold_italic" : "_bold") :
+                       ((italic) ? "_italic" : "");
+
        u32 size = std::floor(RenderingEngine::getDisplayDensity() *
                        m_settings->getFloat("gui_scaling") * basesize);
+
        if (size == 0) {
                errorstream << "FontEngine: attempt to use font size 0" << std::endl;
                errorstream << "  display density: " << RenderingEngine::getDisplayDensity() << std::endl;
@@ -260,10 +292,14 @@ void FontEngine::initFont(unsigned int basesize, FontMode mode)
        u16 font_shadow       = 0;
        u16 font_shadow_alpha = 0;
        g_settings->getU16NoEx(setting_prefix + "font_shadow", font_shadow);
-       g_settings->getU16NoEx(setting_prefix + "font_shadow_alpha", font_shadow_alpha);
+       g_settings->getU16NoEx(setting_prefix + "font_shadow_alpha",
+                       font_shadow_alpha);
+
+       std::string wanted_font_path;
+       wanted_font_path = g_settings->get(setting_prefix + "font_path" + setting_suffix);
 
        std::string fallback_settings[] = {
-               m_settings->get(setting_prefix + "font_path"),
+               wanted_font_path,
                m_settings->get("fallback_font_path"),
                m_settings->getDefault(setting_prefix + "font_path")
        };
@@ -275,7 +311,7 @@ void FontEngine::initFont(unsigned int basesize, FontMode mode)
                                font_shadow_alpha);
 
                if (font) {
-                       m_font_cache[mode][basesize] = font;
+                       m_font_cache[cache_index][basesize] = font;
                        return;
                }
 
@@ -340,7 +376,7 @@ void FontEngine::initSimpleFont(unsigned int basesize, FontMode mode)
                        path.str(""); // Clear
                        path << basename << "_" << (size + offset * sign) << ext;
 
-                       if (!fs::PathExists(path.str())) 
+                       if (!fs::PathExists(path.str()))
                                continue;
 
                        font = m_env->getFont(path.str().c_str());
@@ -365,5 +401,5 @@ void FontEngine::initSimpleFont(unsigned int basesize, FontMode mode)
        }
 
        if (font)
-               m_font_cache[mode][basesize] = font;
+               m_font_cache[get_font_cache_index(mode)][basesize] = font;
 }
index 62aa718973e43eb7b352115ed7b2e552fcb5f738..ecffc7660854e881fc16df60d03839fd5cee8d53 100644 (file)
@@ -48,29 +48,62 @@ public:
        ~FontEngine();
 
        /** get Font */
-       irr::gui::IGUIFont* getFont(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
-                       FontMode mode=FM_Unspecified);
+       irr::gui::IGUIFont *getFont(unsigned int font_size, FontMode mode,
+                       bool bold, bool italic);
+
+       irr::gui::IGUIFont *getFont(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+                       FontMode mode=FM_Unspecified)
+       {
+               return getFont(font_size, mode, m_default_bold, m_default_italic);
+       }
 
        /** get text height for a specific font */
-       unsigned int getTextHeight(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
-                       FontMode mode=FM_Unspecified);
+       unsigned int getTextHeight(unsigned int font_size, FontMode mode,
+                       bool bold, bool italic);
 
        /** get text width if a text for a specific font */
-       unsigned int getTextWidth(const std::string& text,
+       unsigned int getTextHeight(
                        unsigned int font_size=FONT_SIZE_UNSPECIFIED,
                        FontMode mode=FM_Unspecified)
        {
-               return getTextWidth(utf8_to_wide(text));
+               return getTextHeight(font_size, mode, m_default_bold, m_default_italic);
        }
 
+       unsigned int getTextWidth(const std::wstring& text,
+                       unsigned int font_size, FontMode mode, bool bold, bool italic);
+
        /** get text width if a text for a specific font */
        unsigned int getTextWidth(const std::wstring& text,
                        unsigned int font_size=FONT_SIZE_UNSPECIFIED,
-                       FontMode mode=FM_Unspecified);
+                       FontMode mode=FM_Unspecified)
+       {
+               return getTextWidth(text, font_size, mode, m_default_bold,
+                               m_default_italic);
+       }
+
+       unsigned int getTextWidth(const std::string& text,
+                       unsigned int font_size, FontMode mode, bool bold, bool italic)
+       {
+               return getTextWidth(utf8_to_wide(text), font_size, mode, bold, italic);
+       }
+
+       unsigned int getTextWidth(const std::string& text,
+                       unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+                       FontMode mode=FM_Unspecified)
+       {
+               return getTextWidth(utf8_to_wide(text), font_size, mode, m_default_bold,
+                               m_default_italic);
+       }
 
        /** get line height for a specific font (including empty room between lines) */
+       unsigned int getLineHeight(unsigned int font_size, FontMode mode, bool bold,
+                       bool italic);
+
        unsigned int getLineHeight(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
-                       FontMode mode=FM_Unspecified);
+                       FontMode mode=FM_Unspecified)
+       {
+               return getLineHeight(font_size, mode, m_default_bold, m_default_italic);
+       }
 
        /** get default font size */
        unsigned int getDefaultFontSize();
@@ -86,7 +119,11 @@ private:
        void updateFontCache();
 
        /** initialize a new font */
-       void initFont(unsigned int basesize, FontMode mode=FM_Unspecified);
+       void initFont(
+               unsigned int basesize,
+               FontMode mode,
+               bool bold,
+               bool italic);
 
        /** initialize a font without freetype */
        void initSimpleFont(unsigned int basesize, FontMode mode);
@@ -104,11 +141,15 @@ private:
        gui::IGUIEnvironment* m_env = nullptr;
 
        /** internal storage for caching fonts of different size */
-       std::map<unsigned int, irr::gui::IGUIFont*> m_font_cache[FM_MaxMode];
+       std::map<unsigned int, irr::gui::IGUIFont*> m_font_cache[FM_MaxMode << 2];
 
        /** default font size to use */
        unsigned int m_default_size[FM_MaxMode];
 
+       /** default bold and italic */
+       bool m_default_bold;
+       bool m_default_italic;
+
        /** current font engine mode */
        FontMode m_currentMode = FM_Standard;
 
index 291d038167a40851996213ff39c291780e56e43d..304a3ab16194272bd18b6cf2204ee21aae848c40 100644 (file)
@@ -608,23 +608,24 @@ void Hud::resizeHotbar() {
 
 struct MeshTimeInfo {
        u64 time;
-       scene::IMesh *mesh;
+       scene::IMesh *mesh = nullptr;
 };
 
-void drawItemStack(video::IVideoDriver *driver,
+void drawItemStack(
+               video::IVideoDriver *driver,
                gui::IGUIFont *font,
                const ItemStack &item,
                const core::rect<s32> &rect,
                const core::rect<s32> *clip,
                Client *client,
-               ItemRotationKind rotation_kind)
+               ItemRotationKind rotation_kind,
+               const v3s16 &angle,
+               const v3s16 &rotation_speed)
 {
        static MeshTimeInfo rotation_time_infos[IT_ROT_NONE];
-       static thread_local bool enable_animations =
-               g_settings->getBool("inventory_items_animations");
 
        if (item.empty()) {
-               if (rotation_kind < IT_ROT_NONE) {
+               if (rotation_kind < IT_ROT_NONE && rotation_kind != IT_ROT_OTHER) {
                        rotation_time_infos[rotation_kind].mesh = NULL;
                }
                return;
@@ -639,7 +640,7 @@ void drawItemStack(video::IVideoDriver *driver,
                s32 delta = 0;
                if (rotation_kind < IT_ROT_NONE) {
                        MeshTimeInfo &ti = rotation_time_infos[rotation_kind];
-                       if (mesh != ti.mesh) {
+                       if (mesh != ti.mesh && rotation_kind != IT_ROT_OTHER) {
                                ti.mesh = mesh;
                                ti.time = porting::getTimeMs();
                        } else {
@@ -677,9 +678,16 @@ void drawItemStack(video::IVideoDriver *driver,
                core::matrix4 matrix;
                matrix.makeIdentity();
 
+               static thread_local bool enable_animations =
+                       g_settings->getBool("inventory_items_animations");
+
                if (enable_animations) {
-                       float timer_f = (float) delta / 5000.0;
-                       matrix.setRotationDegrees(core::vector3df(0, 360 * timer_f, 0));
+                       float timer_f = (float) delta / 5000.f;
+                       matrix.setRotationDegrees(v3f(
+                               angle.X + rotation_speed.X * 3.60f * timer_f,
+                               angle.Y + rotation_speed.Y * 3.60f * timer_f,
+                               angle.Z + rotation_speed.Z * 3.60f * timer_f)
+                       );
                }
 
                driver->setTransform(video::ETS_WORLD, matrix);
@@ -695,15 +703,18 @@ void drawItemStack(video::IVideoDriver *driver,
                        // because these meshes are not buffered.
                        assert(buf->getHardwareMappingHint_Vertex() == scene::EHM_NEVER);
                        video::SColor c = basecolor;
+
                        if (imesh->buffer_colors.size() > j) {
                                ItemPartColor *p = &imesh->buffer_colors[j];
                                if (p->override_base)
                                        c = p->color;
                        }
+
                        if (imesh->needs_shading)
                                colorizeMeshBuffer(buf, &c);
                        else
                                setMeshBufferColor(buf, c);
+
                        video::SMaterial &material = buf->getMaterial();
                        material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
                        material.Lighting = false;
@@ -726,12 +737,12 @@ void drawItemStack(video::IVideoDriver *driver,
                }
        }
 
-       if(def.type == ITEM_TOOL && item.wear != 0)
-       {
+       if (def.type == ITEM_TOOL && item.wear != 0) {
                // Draw a progressbar
-               float barheight = rect.getHeight()/16;
-               float barpad_x = rect.getWidth()/16;
-               float barpad_y = rect.getHeight()/16;
+               float barheight = rect.getHeight() / 16;
+               float barpad_x = rect.getWidth() / 16;
+               float barpad_y = rect.getHeight() / 16;
+
                core::rect<s32> progressrect(
                        rect.UpperLeftCorner.X + barpad_x,
                        rect.LowerRightCorner.Y - barpad_y - barheight,
@@ -739,18 +750,19 @@ void drawItemStack(video::IVideoDriver *driver,
                        rect.LowerRightCorner.Y - barpad_y);
 
                // Shrink progressrect by amount of tool damage
-               float wear = item.wear / 65535.0;
+               float wear = item.wear / 65535.0f;
                int progressmid =
                        wear * progressrect.UpperLeftCorner.X +
-                       (1-wear) * progressrect.LowerRightCorner.X;
+                       (1 - wear) * progressrect.LowerRightCorner.X;
 
                // Compute progressbar color
                //   wear = 0.0: green
                //   wear = 0.5: yellow
                //   wear = 1.0: red
-               video::SColor color(255,255,255,255);
+               video::SColor color(255, 255, 255, 255);
                int wear_i = MYMIN(std::floor(wear * 600), 511);
                wear_i = MYMIN(wear_i + 10, 511);
+
                if (wear_i <= 255)
                        color.set(255, wear_i, 255, 0);
                else
@@ -760,18 +772,17 @@ void drawItemStack(video::IVideoDriver *driver,
                progressrect2.LowerRightCorner.X = progressmid;
                driver->draw2DRectangle(color, progressrect2, clip);
 
-               color = video::SColor(255,0,0,0);
+               color = video::SColor(255, 0, 0, 0);
                progressrect2 = progressrect;
                progressrect2.UpperLeftCorner.X = progressmid;
                driver->draw2DRectangle(color, progressrect2, clip);
        }
 
-       if(font != NULL && item.count >= 2)
-       {
+       if (font != NULL && item.count >= 2) {
                // Get the item count as a string
                std::string text = itos(item.count);
                v2u32 dim = font->getDimension(utf8_to_wide(text).c_str());
-               v2s32 sdim(dim.X,dim.Y);
+               v2s32 sdim(dim.X, dim.Y);
 
                core::rect<s32> rect2(
                        /*rect.UpperLeftCorner,
@@ -780,10 +791,23 @@ void drawItemStack(video::IVideoDriver *driver,
                        sdim
                );
 
-               video::SColor bgcolor(128,0,0,0);
+               video::SColor bgcolor(128, 0, 0, 0);
                driver->draw2DRectangle(bgcolor, rect2, clip);
 
-               video::SColor color(255,255,255,255);
+               video::SColor color(255, 255, 255, 255);
                font->draw(text.c_str(), rect2, color, false, false, clip);
        }
 }
+
+void drawItemStack(
+               video::IVideoDriver *driver,
+               gui::IGUIFont *font,
+               const ItemStack &item,
+               const core::rect<s32> &rect,
+               const core::rect<s32> *clip,
+               Client *client,
+               ItemRotationKind rotation_kind)
+{
+       drawItemStack(driver, font, item, rect, clip, client, rotation_kind,
+               v3s16(0, 0, 0), v3s16(0, 100, 0));
+}
index 693d2adee18aad258804fbff691d1100ab46979e..d9b5e0686c3b8682f6de49b39eebfbf64a48adac 100644 (file)
@@ -122,6 +122,7 @@ enum ItemRotationKind
        IT_ROT_SELECTED,
        IT_ROT_HOVERED,
        IT_ROT_DRAGGED,
+       IT_ROT_OTHER,
        IT_ROT_NONE, // Must be last, also serves as number
 };
 
@@ -133,4 +134,15 @@ void drawItemStack(video::IVideoDriver *driver,
                Client *client,
                ItemRotationKind rotation_kind);
 
+void drawItemStack(
+               video::IVideoDriver *driver,
+               gui::IGUIFont *font,
+               const ItemStack &item,
+               const core::rect<s32> &rect,
+               const core::rect<s32> *clip,
+               Client *client,
+               ItemRotationKind rotation_kind,
+               const v3s16 &angle,
+               const v3s16 &rotation_speed);
+
 #endif
index 01ee97a3347437f90a8294b9482889992569de59..1ab4a333ee22fff50a45851a21e0c322730ba7f1 100644 (file)
@@ -292,9 +292,17 @@ void set_default_settings(Settings *settings)
 #if USE_FREETYPE
        settings->setDefault("freetype", "true");
        settings->setDefault("font_path", porting::getDataPath("fonts" DIR_DELIM "Arimo-Regular.ttf"));
+       settings->setDefault("font_path_italic", porting::getDataPath("fonts" DIR_DELIM "Arimo-Italic.ttf"));
+       settings->setDefault("font_path_bold", porting::getDataPath("fonts" DIR_DELIM "Arimo-Bold.ttf"));
+       settings->setDefault("font_path_bold_italic", porting::getDataPath("fonts" DIR_DELIM "Arimo-BoldItalic.ttf"));
+       settings->setDefault("font_bold", "false");
+       settings->setDefault("font_italic", "false");
        settings->setDefault("font_shadow", "1");
        settings->setDefault("font_shadow_alpha", "127");
        settings->setDefault("mono_font_path", porting::getDataPath("fonts" DIR_DELIM "Cousine-Regular.ttf"));
+       settings->setDefault("mono_font_path_italic", porting::getDataPath("fonts" DIR_DELIM "Cousine-Italic.ttf"));
+       settings->setDefault("mono_font_path_bold", porting::getDataPath("fonts" DIR_DELIM "Cousine-Bold.ttf"));
+       settings->setDefault("mono_font_path_bold_italic", porting::getDataPath("fonts" DIR_DELIM "Cousine-BoldItalic.ttf"));
        settings->setDefault("fallback_font_path", porting::getDataPath("fonts" DIR_DELIM "DroidSansFallbackFull.ttf"));
 
        settings->setDefault("fallback_font_shadow", "1");
index 2307856a4c41c3ee50c808fa56ff4d6cf2a14e2e..c9f750b9ab4548fd84011ca68a1225de41a71b0e 100644 (file)
@@ -11,6 +11,7 @@ set(gui_SRCS
        ${CMAKE_CURRENT_SOURCE_DIR}/guiScrollBar.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiSkin.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp
+       ${CMAKE_CURRENT_SOURCE_DIR}/guiHyperText.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/intlGUIEditBox.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp
index 5def4357e2fbe14a06532ba1ca89f902731eee97..44fdf78621b9bdfd2bce70cb4980d9547ece8b93 100644 (file)
@@ -57,6 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "client/guiscalingfilter.h"
 #include "guiEditBoxWithScrollbar.h"
 #include "intlGUIEditBox.h"
+#include "guiHyperText.h"
 
 #define MY_CHECKPOS(a,b)                                                                                                       \
        if (v_pos.size() != 2) {                                                                                                \
@@ -155,16 +156,15 @@ void GUIFormSpecMenu::removeChildren()
 {
        const core::list<gui::IGUIElement*> &children = getChildren();
 
-       while(!children.empty()) {
+       while (!children.empty()) {
                (*children.getLast())->remove();
        }
 
-       if(m_tooltip_element) {
+       if (m_tooltip_element) {
                m_tooltip_element->remove();
                m_tooltip_element->drop();
-               m_tooltip_element = NULL;
+               m_tooltip_element = nullptr;
        }
-
 }
 
 void GUIFormSpecMenu::setInitialFocus()
@@ -1318,7 +1318,6 @@ void GUIFormSpecMenu::parseSimpleField(parserData* data,
 void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector<std::string>& parts,
                const std::string &type)
 {
-
        std::vector<std::string> v_pos = split(parts[0],',');
        std::vector<std::string> v_geom = split(parts[1],',');
        std::string name = parts[2];
@@ -1402,6 +1401,59 @@ void GUIFormSpecMenu::parseField(parserData* data, const std::string &element,
        errorstream<< "Invalid field element(" << parts.size() << "): '" << element << "'"  << std::endl;
 }
 
+void GUIFormSpecMenu::parseHyperText(parserData *data, const std::string &element)
+{
+       std::vector<std::string> parts = split(element, ';');
+
+       if (parts.size() != 4 && m_formspec_version < FORMSPEC_API_VERSION) {
+               errorstream << "Invalid text element(" << parts.size() << "): '" << element << "'"  << std::endl;
+               return;
+       }
+
+       std::vector<std::string> v_pos = split(parts[0], ',');
+       std::vector<std::string> v_geom = split(parts[1], ',');
+       std::string name = parts[2];
+       std::string text = parts[3];
+
+       MY_CHECKPOS("hypertext", 0);
+       MY_CHECKGEOM("hypertext", 1);
+
+       v2s32 pos;
+       v2s32 geom;
+
+       if (data->real_coordinates) {
+               pos = getRealCoordinateBasePos(false, v_pos);
+               geom = getRealCoordinateGeometry(v_geom);
+       } else {
+               pos = getElementBasePos(false, &v_pos);
+               pos -= padding;
+
+               pos.X += stof(v_pos[0]) * spacing.X;
+               pos.Y += stof(v_pos[1]) * spacing.Y + (m_btn_height * 2);
+
+               geom.X = (stof(v_geom[0]) * spacing.X) - (spacing.X - imgsize.X);
+               geom.Y = (stof(v_geom[1]) * imgsize.Y) - (spacing.Y - imgsize.Y);
+       }
+
+       core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X + geom.X, pos.Y + geom.Y);
+
+       if(m_form_src)
+               text = m_form_src->resolveText(text);
+
+       FieldSpec spec(
+               name,
+               utf8_to_wide(unescape_string(text)),
+               L"",
+               258 + m_fields.size()
+       );
+
+       spec.ftype = f_Unknown;
+       new GUIHyperText(
+               spec.flabel.c_str(), Environment, this, spec.fid, rect, m_client, m_tsrc);
+
+       m_fields.push_back(spec);
+}
+
 void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element)
 {
        std::vector<std::string> parts = split(element,';');
@@ -2293,6 +2345,11 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element)
                return;
        }
 
+       if (type == "hypertext") {
+               parseHyperText(data,description);
+               return;
+       }
+
        if (type == "label") {
                parseLabel(data,description);
                return;
@@ -2879,8 +2936,8 @@ void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int layer,
                        if (!item.empty()) {
                                // Draw item stack
                                drawItemStack(driver, m_font, item,
-                                       rect, &AbsoluteClippingRect, m_client,
-                                       rotation_kind);
+                                       rect, &AbsoluteClippingRect, m_client, rotation_kind);
+
                                // Draw tooltip
                                if (hovering && !m_selected_item) {
                                        std::string tooltip = item.getDescription(m_client->idef());
@@ -2900,8 +2957,8 @@ void GUIFormSpecMenu::drawSelectedItem()
 
        if (!m_selected_item) {
                drawItemStack(driver, m_font, ItemStack(),
-                       core::rect<s32>(v2s32(0, 0), v2s32(0, 0)),
-                       NULL, m_client, IT_ROT_DRAGGED);
+                               core::rect<s32>(v2s32(0, 0), v2s32(0, 0)), NULL,
+                               m_client, IT_ROT_DRAGGED);
                return;
        }
 
@@ -3482,9 +3539,10 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
                        }
                }
        }
-       // Mouse wheel events: send to hovered element instead of focused
-       if(event.EventType==EET_MOUSE_INPUT_EVENT
-                       && event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
+       // Mouse wheel and move events: send to hovered element instead of focused
+       if (event.EventType == EET_MOUSE_INPUT_EVENT &&
+                       (event.MouseInput.Event == EMIE_MOUSE_WHEEL ||
+                        event.MouseInput.Event == EMIE_MOUSE_MOVED)) {
                s32 x = event.MouseInput.X;
                s32 y = event.MouseInput.Y;
                gui::IGUIElement *hovered =
@@ -3492,7 +3550,7 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event)
                                core::position2d<s32>(x, y));
                if (hovered && isMyChild(hovered)) {
                        hovered->OnEvent(event);
-                       return true;
+                       return event.MouseInput.Event == EMIE_MOUSE_WHEEL;
                }
        }
 
@@ -4041,8 +4099,8 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                }
                m_old_pointer = m_pointer;
        }
-       if (event.EventType == EET_GUI_EVENT) {
 
+       if (event.EventType == EET_GUI_EVENT) {
                if (event.GUIEvent.EventType == gui::EGET_TAB_CHANGED
                                && isVisible()) {
                        // find the element that was clicked
@@ -4128,6 +4186,11 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
                                        s.fdefault = L"Changed";
                                        acceptInput(quit_mode_no);
                                        s.fdefault = L"";
+                               } else if ((s.ftype == f_Unknown) &&
+                                               (s.fid == event.GUIEvent.Caller->getID())) {
+                                       s.send = true;
+                                       acceptInput();
+                                       s.send = false;
                                }
                        }
                }
index 46df0930cc1b9d0993f9e2f26ca8ef429f6e586c..39af1e7c223a7202d552230ef1e675642deb88d5 100644 (file)
@@ -469,6 +469,7 @@ protected:
        video::SColor m_default_tooltip_bgcolor;
        video::SColor m_default_tooltip_color;
 
+       
 private:
        IFormSource        *m_form_src;
        TextDest           *m_text_dst;
@@ -529,6 +530,7 @@ private:
        void parseSimpleField(parserData* data,std::vector<std::string> &parts);
        void parseTextArea(parserData* data,std::vector<std::string>& parts,
                        const std::string &type);
+       void parseHyperText(parserData *data, const std::string &element);
        void parseLabel(parserData* data, const std::string &element);
        void parseVertLabel(parserData* data, const std::string &element);
        void parseImageButton(parserData* data, const std::string &element,
diff --git a/src/gui/guiHyperText.cpp b/src/gui/guiHyperText.cpp
new file mode 100644 (file)
index 0000000..024e4de
--- /dev/null
@@ -0,0 +1,1137 @@
+/*
+Minetest
+Copyright (C) 2019 EvicenceBKidscode / Pierre-Yves Rollo <dev@pyrollo.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "IGUIEnvironment.h"
+#include "IGUIElement.h"
+#include "guiScrollBar.h"
+#include "IGUIFont.h"
+#include <vector>
+#include <list>
+#include <unordered_map>
+using namespace irr::gui;
+#include "client/fontengine.h"
+#include <SColor.h>
+#include "client/tile.h"
+#include "IVideoDriver.h"
+#include "client/client.h"
+#include "client/renderingengine.h"
+#include "hud.h"
+#include "guiHyperText.h"
+#include "util/string.h"
+
+bool check_color(const std::string &str)
+{
+       irr::video::SColor color;
+       return parseColorString(str, color, false);
+}
+
+bool check_integer(const std::string &str)
+{
+       if (str.empty())
+               return false;
+
+       char *endptr = nullptr;
+       strtol(str.c_str(), &endptr, 10);
+
+       return *endptr == '\0';
+}
+
+// -----------------------------------------------------------------------------
+// ParsedText - A text parser
+
+void ParsedText::Element::setStyle(StyleList &style)
+{
+       this->underline = is_yes(style["underline"]);
+
+       video::SColor color;
+
+       if (parseColorString(style["color"], color, false))
+               this->color = color;
+       if (parseColorString(style["hovercolor"], color, false))
+               this->hovercolor = color;
+
+       unsigned int font_size = std::atoi(style["fontsize"].c_str());
+       FontMode font_mode = FM_Standard;
+       if (style["fontstyle"] == "mono")
+               font_mode = FM_Mono;
+
+       // TODO: find a way to check font validity
+       // Build a new fontengine ?
+       this->font =
+#if USE_FREETYPE
+                       (gui::CGUITTFont *)
+#endif
+                                       g_fontengine->getFont(font_size, font_mode,
+                                                       is_yes(style["bold"]), is_yes(style["italic"]));
+
+       if (!this->font)
+               printf("No font found ! Size=%d, mode=%d, bold=%s, italic=%s\n",
+                               font_size, font_mode, style["bold"].c_str(),
+                               style["italic"].c_str());
+}
+
+void ParsedText::Paragraph::setStyle(StyleList &style)
+{
+       if (style["halign"] == "center")
+               this->halign = HALIGN_CENTER;
+       else if (style["halign"] == "right")
+               this->halign = HALIGN_RIGHT;
+       else if (style["halign"] == "justify")
+               this->halign = HALIGN_JUSTIFY;
+       else
+               this->halign = HALIGN_LEFT;
+}
+
+ParsedText::ParsedText(const wchar_t *text)
+{
+       // Default style
+       m_root_tag.name = "root";
+       m_root_tag.style["fontsize"] = "16";
+       m_root_tag.style["fontstyle"] = "normal";
+       m_root_tag.style["bold"] = "false";
+       m_root_tag.style["italic"] = "false";
+       m_root_tag.style["underline"] = "false";
+       m_root_tag.style["halign"] = "left";
+       m_root_tag.style["color"] = "#EEEEEE";
+       m_root_tag.style["hovercolor"] = m_root_tag.style["color"];
+
+       m_tags.push_back(&m_root_tag);
+       m_active_tags.push_front(&m_root_tag);
+       m_style = m_root_tag.style;
+
+       // Default simple tags definitions
+       StyleList style;
+
+       style["hovercolor"] = "#FF0000";
+       style["color"] = "#0000FF";
+       style["underline"] = "true";
+       m_elementtags["action"] = style;
+       style.clear();
+
+       style["bold"] = "true";
+       m_elementtags["b"] = style;
+       style.clear();
+
+       style["italic"] = "true";
+       m_elementtags["i"] = style;
+       style.clear();
+
+       style["underline"] = "true";
+       m_elementtags["u"] = style;
+       style.clear();
+
+       style["fontstyle"] = "mono";
+       m_elementtags["mono"] = style;
+       style.clear();
+
+       style["fontsize"] = m_root_tag.style["fontsize"];
+       m_elementtags["normal"] = style;
+       style.clear();
+
+       style["fontsize"] = "24";
+       m_elementtags["big"] = style;
+       style.clear();
+
+       style["fontsize"] = "36";
+       m_elementtags["bigger"] = style;
+       style.clear();
+
+       style["halign"] = "center";
+       m_paragraphtags["center"] = style;
+       style.clear();
+
+       style["halign"] = "justify";
+       m_paragraphtags["justify"] = style;
+       style.clear();
+
+       style["halign"] = "left";
+       m_paragraphtags["left"] = style;
+       style.clear();
+
+       style["halign"] = "right";
+       m_paragraphtags["right"] = style;
+       style.clear();
+
+       m_element = NULL;
+       m_paragraph = NULL;
+
+       parse(text);
+}
+
+ParsedText::~ParsedText()
+{
+       for (auto &tag : m_tags)
+               delete tag;
+}
+
+void ParsedText::parse(const wchar_t *text)
+{
+       wchar_t c;
+       u32 cursor = 0;
+       bool escape = false;
+
+       while ((c = text[cursor]) != L'\0') {
+               cursor++;
+
+               if (c == L'\r') { // Mac or Windows breaks
+                       if (text[cursor] == L'\n')
+                               cursor++;
+                       // If text has begun, don't skip empty line
+                       if (m_paragraph) {
+                               endParagraph();
+                               enterElement(ELEMENT_SEPARATOR);
+                       }
+                       escape = false;
+                       continue;
+               }
+
+               if (c == L'\n') { // Unix breaks
+                       // If text has begun, don't skip empty line
+                       if (m_paragraph) {
+                               endParagraph();
+                               enterElement(ELEMENT_SEPARATOR);
+                       }
+                       escape = false;
+                       continue;
+               }
+
+               if (escape) {
+                       escape = false;
+                       pushChar(c);
+                       continue;
+               }
+
+               if (c == L'\\') {
+                       escape = true;
+                       continue;
+               }
+
+               // Tag check
+               if (c == L'<') {
+                       u32 newcursor = parseTag(text, cursor);
+                       if (newcursor > 0) {
+                               cursor = newcursor;
+                               continue;
+                       }
+               }
+
+               // Default behavior
+               pushChar(c);
+       }
+
+       endParagraph();
+}
+
+void ParsedText::endElement()
+{
+       m_element = NULL;
+}
+
+void ParsedText::endParagraph()
+{
+       if (!m_paragraph)
+               return;
+
+       endElement();
+       m_paragraph = NULL;
+}
+
+void ParsedText::enterParagraph()
+{
+       if (!m_paragraph) {
+               m_paragraphs.emplace_back();
+               m_paragraph = &m_paragraphs.back();
+               m_paragraph->setStyle(m_style);
+       }
+}
+
+void ParsedText::enterElement(ElementType type)
+{
+       enterParagraph();
+
+       if (!m_element || m_element->type != type) {
+               m_paragraph->elements.emplace_back();
+               m_element = &m_paragraph->elements.back();
+               m_element->type = type;
+               m_element->tags = m_active_tags;
+               m_element->setStyle(m_style);
+       }
+}
+
+void ParsedText::pushChar(wchar_t c)
+{
+       // New word if needed
+       if (c == L' ' || c == L'\t')
+               enterElement(ELEMENT_SEPARATOR);
+       else
+               enterElement(ELEMENT_TEXT);
+
+       m_element->text += c;
+}
+
+ParsedText::Tag *ParsedText::newTag(const std::string &name, const AttrsList &attrs)
+{
+       endElement();
+       Tag *newtag = new Tag();
+       newtag->name = name;
+       newtag->attrs = attrs;
+       m_tags.push_back(newtag);
+       return newtag;
+}
+
+ParsedText::Tag *ParsedText::openTag(const std::string &name, const AttrsList &attrs)
+{
+       Tag *newtag = newTag(name, attrs);
+       m_active_tags.push_front(newtag);
+       return newtag;
+}
+
+bool ParsedText::closeTag(const std::string &name)
+{
+       bool found = false;
+       for (auto id = m_active_tags.begin(); id != m_active_tags.end(); ++id)
+               if ((*id)->name == name) {
+                       m_active_tags.erase(id);
+                       found = true;
+                       break;
+               }
+       return found;
+}
+
+void ParsedText::parseGenericStyleAttr(
+               const std::string &name, const std::string &value, StyleList &style)
+{
+       // Color styles
+       if (name == "color" || name == "hovercolor") {
+               if (check_color(value))
+                       style[name] = value;
+
+               // Boolean styles
+       } else if (name == "bold" || name == "italic" || name == "underline") {
+               style[name] = is_yes(value);
+
+       } else if (name == "size") {
+               if (check_integer(value))
+                       style["fontsize"] = value;
+
+       } else if (name == "font") {
+               if (value == "mono" || value == "normal")
+                       style["fontstyle"] = value;
+       }
+}
+
+void ParsedText::parseStyles(const AttrsList &attrs, StyleList &style)
+{
+       for (auto const &attr : attrs)
+               parseGenericStyleAttr(attr.first, attr.second, style);
+}
+
+void ParsedText::globalTag(const AttrsList &attrs)
+{
+       for (const auto &attr : attrs) {
+               // Only page level style
+               if (attr.first == "margin") {
+                       if (check_integer(attr.second))
+                               margin = stoi(attr.second.c_str());
+
+               } else if (attr.first == "valign") {
+                       if (attr.second == "top")
+                               valign = ParsedText::VALIGN_TOP;
+                       else if (attr.second == "bottom")
+                               valign = ParsedText::VALIGN_BOTTOM;
+                       else if (attr.second == "middle")
+                               valign = ParsedText::VALIGN_MIDDLE;
+               } else if (attr.first == "background") {
+                       irr::video::SColor color;
+                       if (attr.second == "none") {
+                               background_type = BACKGROUND_NONE;
+                       } else if (parseColorString(attr.second, color, false)) {
+                               background_type = BACKGROUND_COLOR;
+                               background_color = color;
+                       }
+
+                       // Inheriting styles
+
+               } else if (attr.first == "halign") {
+                       if (attr.second == "left" || attr.second == "center" ||
+                                       attr.second == "right" ||
+                                       attr.second == "justify")
+                               m_root_tag.style["halign"] = attr.second;
+
+                       // Generic default styles
+
+               } else {
+                       parseGenericStyleAttr(attr.first, attr.second, m_root_tag.style);
+               }
+       }
+}
+
+u32 ParsedText::parseTag(const wchar_t *text, u32 cursor)
+{
+       // Tag name
+       bool end = false;
+       std::string name = "";
+       wchar_t c = text[cursor];
+
+       if (c == L'/') {
+               end = true;
+               c = text[++cursor];
+               if (c == L'\0')
+                       return 0;
+       }
+
+       while (c != ' ' && c != '>') {
+               name += c;
+               c = text[++cursor];
+               if (c == L'\0')
+                       return 0;
+       }
+
+       // Tag attributes
+       AttrsList attrs;
+       while (c != L'>') {
+               std::string attr_name = "";
+               std::string attr_val = "";
+
+               while (c == ' ') {
+                       c = text[++cursor];
+                       if (c == L'\0' || c == L'=')
+                               return 0;
+               }
+
+               while (c != L' ' && c != L'=') {
+                       attr_name += (char)c;
+                       c = text[++cursor];
+                       if (c == L'\0' || c == L'>')
+                               return 0;
+               }
+
+               while (c == L' ') {
+                       c = text[++cursor];
+                       if (c == L'\0' || c == L'>')
+                               return 0;
+               }
+
+               if (c != L'=')
+                       return 0;
+
+               c = text[++cursor];
+
+               if (c == L'\0')
+                       return 0;
+
+               while (c != L'>' && c != L' ') {
+                       attr_val += (char)c;
+                       c = text[++cursor];
+                       if (c == L'\0')
+                               return 0;
+               }
+
+               attrs[attr_name] = attr_val;
+       }
+
+       ++cursor; // Last ">"
+
+       // Tag specific processing
+       StyleList style;
+
+       if (name == "global") {
+               if (end)
+                       return 0;
+               globalTag(attrs);
+
+       } else if (name == "style") {
+               if (end) {
+                       closeTag(name);
+               } else {
+                       parseStyles(attrs, style);
+                       openTag(name, attrs)->style = style;
+               }
+               endElement();
+       } else if (name == "img" || name == "item") {
+               if (end)
+                       return 0;
+
+               // Name is a required attribute
+               if (!attrs.count("name"))
+                       return 0;
+
+               // Rotate attribute is only for <item>
+               if (attrs.count("rotate") && name != "item")
+                       return 0;
+
+               // Angle attribute is only for <item>
+               if (attrs.count("angle") && name != "item")
+                       return 0;
+
+               // Ok, element can be created
+               newTag(name, attrs);
+
+               if (name == "img")
+                       enterElement(ELEMENT_IMAGE);
+               else
+                       enterElement(ELEMENT_ITEM);
+
+               m_element->text = strtostrw(attrs["name"]);
+
+               if (attrs.count("float")) {
+                       if (attrs["float"] == "left")
+                               m_element->floating = FLOAT_LEFT;
+                       if (attrs["float"] == "right")
+                               m_element->floating = FLOAT_RIGHT;
+               }
+
+               if (attrs.count("width")) {
+                       int width = stoi(attrs["width"]);
+                       if (width > 0)
+                               m_element->dim.Width = width;
+               }
+
+               if (attrs.count("height")) {
+                       int height = stoi(attrs["height"]);
+                       if (height > 0)
+                               m_element->dim.Height = height;
+               }
+
+               if (attrs.count("angle")) {
+                       std::string str = attrs["angle"];
+                       std::vector<std::string> parts = split(str, ',');
+                       if (parts.size() == 3) {
+                               m_element->angle = v3s16(
+                                               rangelim(stoi(parts[0]), -180, 180),
+                                               rangelim(stoi(parts[1]), -180, 180),
+                                               rangelim(stoi(parts[2]), -180, 180));
+                               m_element->rotation = v3s16(0, 0, 0);
+                       }
+               }
+
+               if (attrs.count("rotate")) {
+                       if (attrs["rotate"] == "yes") {
+                               m_element->rotation = v3s16(0, 100, 0);
+                       } else {
+                               std::string str = attrs["rotate"];
+                               std::vector<std::string> parts = split(str, ',');
+                               if (parts.size() == 3) {
+                                       m_element->rotation = v3s16 (
+                                                       rangelim(stoi(parts[0]), -1000, 1000),
+                                                       rangelim(stoi(parts[1]), -1000, 1000),
+                                                       rangelim(stoi(parts[2]), -1000, 1000));
+                               }
+                       }
+               }
+
+               endElement();
+
+       } else if (name == "tag") {
+               // Required attributes
+               if (!attrs.count("name"))
+                       return 0;
+
+               StyleList tagstyle;
+               parseStyles(attrs, tagstyle);
+
+               if (is_yes(attrs["paragraph"]))
+                       m_paragraphtags[attrs["name"]] = tagstyle;
+               else
+                       m_elementtags[attrs["name"]] = tagstyle;
+
+       } else if (name == "action") {
+               if (end) {
+                       closeTag(name);
+               } else {
+                       if (!attrs.count("name"))
+                               return 0;
+                       openTag(name, attrs)->style = m_elementtags["action"];
+               }
+
+       } else if (m_elementtags.count(name)) {
+               if (end) {
+                       closeTag(name);
+               } else {
+                       openTag(name, attrs)->style = m_elementtags[name];
+               }
+               endElement();
+
+       } else if (m_paragraphtags.count(name)) {
+               if (end) {
+                       closeTag(name);
+               } else {
+                       openTag(name, attrs)->style = m_paragraphtags[name];
+               }
+               endParagraph();
+
+       } else
+               return 0; // Unknown tag
+
+       // Update styles accordingly
+       m_style.clear();
+       for (auto tag = m_active_tags.crbegin(); tag != m_active_tags.crend(); ++tag)
+               for (const auto &prop : (*tag)->style)
+                       m_style[prop.first] = prop.second;
+
+       return cursor;
+}
+
+// -----------------------------------------------------------------------------
+// Text Drawer
+
+TextDrawer::TextDrawer(const wchar_t *text, Client *client,
+               gui::IGUIEnvironment *environment, ISimpleTextureSource *tsrc) :
+               m_text(text),
+               m_client(client), m_environment(environment)
+{
+       // Size all elements
+       for (auto &p : m_text.m_paragraphs) {
+               for (auto &e : p.elements) {
+                       switch (e.type) {
+                       case ParsedText::ELEMENT_SEPARATOR:
+                       case ParsedText::ELEMENT_TEXT:
+                               if (e.font) {
+                                       e.dim.Width = e.font->getDimension(e.text.c_str()).Width;
+                                       e.dim.Height = e.font->getDimension(L"Yy").Height;
+#if USE_FREETYPE
+                                       e.baseline = e.dim.Height - 1 - e.font->getAscender()/64;
+#endif
+                               } else {
+                                       e.dim = {0, 0};
+                               }
+                               break;
+
+                       case ParsedText::ELEMENT_IMAGE:
+                       case ParsedText::ELEMENT_ITEM:
+                               // Resize only non sized items
+                               if (e.dim.Height != 0 && e.dim.Width != 0)
+                                       break;
+
+                               // Default image and item size
+                               core::dimension2d<u32> dim(80, 80);
+
+                               if (e.type == ParsedText::ELEMENT_IMAGE) {
+                                       video::ITexture *texture =
+                                               m_client->getTextureSource()->
+                                                       getTexture(strwtostr(e.text));
+                                       if (texture)
+                                               dim = texture->getOriginalSize();
+                               }
+
+                               if (e.dim.Height == 0)
+                                       if (e.dim.Width == 0)
+                                               e.dim = dim;
+                                       else
+                                               e.dim.Height = dim.Height * e.dim.Width /
+                                                               dim.Width;
+                               else
+                                       e.dim.Width = dim.Width * e.dim.Height /
+                                                       dim.Height;
+                               break;
+                       }
+               }
+       }
+}
+
+// Get element at given coordinates. Coordinates are inner coordinates (starting
+// at 0,0).
+ParsedText::Element *TextDrawer::getElementAt(core::position2d<s32> pos)
+{
+       pos.Y -= m_voffset;
+       for (auto &p : m_text.m_paragraphs) {
+               for (auto &el : p.elements) {
+                       core::rect<s32> rect(el.pos, el.dim);
+                       if (rect.isPointInside(pos))
+                               return &el;
+               }
+       }
+       return 0;
+}
+
+/*
+   This function places all elements according to given width. Elements have
+   been previously sized by constructor and will be later drawed by draw.
+   It may be called each time width changes and resulting height can be
+   retrieved using getHeight. See GUIHyperText constructor, it uses it once to
+   test if text fits in window and eventually another time if width is reduced
+   m_floatingbecause of scrollbar added.
+*/
+void TextDrawer::place(const core::rect<s32> &dest_rect)
+{
+       m_floating.clear();
+       s32 y = 0;
+       s32 ymargin = m_text.margin;
+
+       // Iterator used :
+       // p - Current paragraph, walked only once
+       // el - Current element, walked only once
+       // e and f - local element and floating operators
+
+       for (auto &p : m_text.m_paragraphs) {
+               // Find and place floating stuff in paragraph
+               for (auto e = p.elements.begin(); e != p.elements.end(); ++e) {
+                       if (e->floating != ParsedText::FLOAT_NONE) {
+                               if (y)
+                                       e->pos.Y = y + std::max(ymargin, e->margin);
+                               else
+                                       e->pos.Y = ymargin;
+
+                               if (e->floating == ParsedText::FLOAT_LEFT)
+                                       e->pos.X = m_text.margin;
+                               if (e->floating == ParsedText::FLOAT_RIGHT)
+                                       e->pos.X = dest_rect.getWidth() - e->dim.Width -
+                                                       m_text.margin;
+
+                               RectWithMargin floating;
+                               floating.rect = core::rect<s32>(e->pos, e->dim);
+                               floating.margin = e->margin;
+
+                               m_floating.push_back(floating);
+                       }
+               }
+
+               if (y)
+                       y = y + std::max(ymargin, p.margin);
+
+               ymargin = p.margin;
+
+               // Place non floating stuff
+               std::vector<ParsedText::Element>::iterator el = p.elements.begin();
+
+               while (el != p.elements.end()) {
+                       // Determine line width and y pos
+                       s32 left, right;
+                       s32 nexty = y;
+                       do {
+                               y = nexty;
+                               nexty = 0;
+
+                               // Inner left & right
+                               left = m_text.margin;
+                               right = dest_rect.getWidth() - m_text.margin;
+
+                               for (const auto &f : m_floating) {
+                                       // Does floating rect intersect paragraph y line?
+                                       if (f.rect.UpperLeftCorner.Y - f.margin <= y &&
+                                                       f.rect.LowerRightCorner.Y + f.margin >= y) {
+
+                                               // Next Y to try if no room left
+                                               if (!nexty || f.rect.LowerRightCorner.Y +
+                                                               std::max(f.margin, p.margin) < nexty) {
+                                                       nexty = f.rect.LowerRightCorner.Y +
+                                                                       std::max(f.margin, p.margin) + 1;
+                                               }
+
+                                               if (f.rect.UpperLeftCorner.X - f.margin <= left &&
+                                                               f.rect.LowerRightCorner.X + f.margin < right) {
+                                                       // float on left
+                                                       if (f.rect.LowerRightCorner.X +
+                                                                       std::max(f.margin, p.margin) > left) {
+                                                               left = f.rect.LowerRightCorner.X +
+                                                                               std::max(f.margin, p.margin);
+                                                       }
+                                               } else if (f.rect.LowerRightCorner.X + f.margin >= right &&
+                                                               f.rect.UpperLeftCorner.X - f.margin > left) {
+                                                       // float on right
+                                                       if (f.rect.UpperLeftCorner.X -
+                                                                       std::max(f.margin, p.margin) < right)
+                                                               right = f.rect.UpperLeftCorner.X -
+                                                                               std::max(f.margin, p.margin);
+
+                                               } else if (f.rect.UpperLeftCorner.X - f.margin <= left &&
+                                                               f.rect.LowerRightCorner.X + f.margin >= right) {
+                                                       // float taking all space
+                                                       left = right;
+                                               }
+                                               else
+                                               { // float in the middle -- should not occure yet, see that later
+                                               }
+                                       }
+                               }
+                       } while (nexty && right <= left);
+
+                       u32 linewidth = right - left;
+                       float x = left;
+
+                       u32 charsheight = 0;
+                       u32 charswidth = 0;
+                       u32 wordcount = 0;
+
+                       // Skip begining of line separators but include them in height
+                       // computation.
+                       while (el != p.elements.end() &&
+                                       el->type == ParsedText::ELEMENT_SEPARATOR) {
+                               if (el->floating == ParsedText::FLOAT_NONE) {
+                                       el->drawwidth = 0;
+                                       if (charsheight < el->dim.Height)
+                                               charsheight = el->dim.Height;
+                               }
+                               el++;
+                       }
+
+                       std::vector<ParsedText::Element>::iterator linestart = el;
+                       std::vector<ParsedText::Element>::iterator lineend = p.elements.end();
+
+                       // First pass, find elements fitting into line
+                       // (or at least one element)
+                       while (el != p.elements.end() && (charswidth == 0 ||
+                                       charswidth + el->dim.Width <= linewidth)) {
+                               if (el->floating == ParsedText::FLOAT_NONE) {
+                                       if (el->type != ParsedText::ELEMENT_SEPARATOR) {
+                                               lineend = el;
+                                               wordcount++;
+                                       }
+                                       charswidth += el->dim.Width;
+                                       if (charsheight < el->dim.Height)
+                                               charsheight = el->dim.Height;
+                               }
+                               el++;
+                       }
+
+                       // Empty line, nothing to place only go down line height
+                       if (lineend == p.elements.end()) {
+                               y += charsheight;
+                               continue;
+                       }
+
+                       // Point to the first position outside line (may be end())
+                       lineend++;
+
+                       // Second pass, compute printable line width and adjustments
+                       charswidth = 0;
+                       s32 top = 0;
+                       s32 bottom = 0;
+                       for (auto e = linestart; e != lineend; ++e) {
+                               if (e->floating == ParsedText::FLOAT_NONE) {
+                                       charswidth += e->dim.Width;
+                                       if (top < (s32)e->dim.Height - e->baseline)
+                                               top = e->dim.Height - e->baseline;
+                                       if (bottom < e->baseline)
+                                               bottom = e->baseline;
+                               }
+                       }
+
+                       float extraspace = 0.f;
+
+                       switch (p.halign) {
+                       case ParsedText::HALIGN_CENTER:
+                               x += (linewidth - charswidth) / 2.f;
+                               break;
+                       case ParsedText::HALIGN_JUSTIFY:
+                               if (wordcount > 1 && // Justification only if at least two words
+                                       !(lineend == p.elements.end())) // Don't justify last line
+                                       extraspace = ((float)(linewidth - charswidth)) / (wordcount - 1);
+                               break;
+                       case ParsedText::HALIGN_RIGHT:
+                               x += linewidth - charswidth;
+                               break;
+                       case ParsedText::HALIGN_LEFT:
+                               break;
+                       }
+
+                       // Third pass, actually place everything
+                       for (auto e = linestart; e != lineend; ++e) {
+                               if (e->floating != ParsedText::FLOAT_NONE)
+                                       continue;
+
+                               e->pos.X = x;
+                               e->pos.Y = y;
+
+                               switch (e->type) {
+                               case ParsedText::ELEMENT_TEXT:
+                               case ParsedText::ELEMENT_SEPARATOR:
+                                       e->pos.X = x;
+
+                                       // Align char baselines
+                                       e->pos.Y = y + top + e->baseline - e->dim.Height;
+
+                                       x += e->dim.Width;
+                                       if (e->type == ParsedText::ELEMENT_SEPARATOR)
+                                               x += extraspace;
+                                       break;
+
+                               case ParsedText::ELEMENT_IMAGE:
+                               case ParsedText::ELEMENT_ITEM:
+                                       x += e->dim.Width;
+                                       break;
+                               }
+
+                               // Draw width for separator can be different than element
+                               // width. This will be important for char effects like
+                               // underline.
+                               e->drawwidth = x - e->pos.X;
+                       }
+                       y += charsheight;
+               } // Elements (actually lines)
+       } // Paragraph
+
+       // Check if float goes under paragraph
+       for (const auto &f : m_floating) {
+               if (f.rect.LowerRightCorner.Y >= y)
+                       y = f.rect.LowerRightCorner.Y;
+       }
+
+       m_height = y + m_text.margin;
+       // Compute vertical offset according to vertical alignment
+       if (m_height < dest_rect.getHeight())
+               switch (m_text.valign) {
+               case ParsedText::VALIGN_BOTTOM:
+                       m_voffset = dest_rect.getHeight() - m_height;
+                       break;
+               case ParsedText::VALIGN_MIDDLE:
+                       m_voffset = (dest_rect.getHeight() - m_height) / 2;
+                       break;
+               case ParsedText::VALIGN_TOP:
+               default:
+                       m_voffset = 0;
+               }
+       else
+               m_voffset = 0;
+}
+
+// Draw text in a rectangle with a given offset. Items are actually placed in
+// relative (to upper left corner) coordinates.
+void TextDrawer::draw(const core::rect<s32> &dest_rect,
+               const core::position2d<s32> &dest_offset)
+{
+       irr::video::IVideoDriver *driver = m_environment->getVideoDriver();
+       core::position2d<s32> offset = dest_rect.UpperLeftCorner + dest_offset;
+       offset.Y += m_voffset;
+
+       if (m_text.background_type == ParsedText::BACKGROUND_COLOR)
+               driver->draw2DRectangle(m_text.background_color, dest_rect);
+
+       for (auto &p : m_text.m_paragraphs) {
+               for (auto &el : p.elements) {
+                       core::rect<s32> rect(el.pos + offset, el.dim);
+                       if (!rect.isRectCollided(dest_rect))
+                               continue;
+
+                       switch (el.type) {
+                       case ParsedText::ELEMENT_SEPARATOR:
+                       case ParsedText::ELEMENT_TEXT: {
+                               irr::video::SColor color = el.color;
+
+                               for (auto tag : el.tags)
+                                       if (&(*tag) == m_hovertag)
+                                               color = el.hovercolor;
+
+                               if (!el.font)
+                                       break;
+
+                               if (el.type == ParsedText::ELEMENT_TEXT)
+                                       el.font->draw(el.text, rect, color, false, true,
+                                                       &dest_rect);
+
+                               if (el.underline &&  el.drawwidth) {
+                                       s32 linepos = el.pos.Y + offset.Y +
+                                                       el.dim.Height - (el.baseline >> 1);
+
+                                       core::rect<s32> linerect(el.pos.X + offset.X,
+                                                       linepos - (el.baseline >> 3) - 1,
+                                                       el.pos.X + offset.X + el.drawwidth,
+                                                       linepos + (el.baseline >> 3));
+
+                                       driver->draw2DRectangle(color, linerect, &dest_rect);
+                               }
+                       } break;
+
+                       case ParsedText::ELEMENT_IMAGE: {
+                               video::ITexture *texture =
+                                               m_client->getTextureSource()->getTexture(
+                                                               strwtostr(el.text));
+                               if (texture != 0)
+                                       m_environment->getVideoDriver()->draw2DImage(
+                                                       texture, rect,
+                                                       irr::core::rect<s32>(
+                                                                       core::position2d<s32>(0, 0),
+                                                                       texture->getOriginalSize()),
+                                                       &dest_rect, 0, true);
+                       } break;
+
+                       case ParsedText::ELEMENT_ITEM: {
+                               IItemDefManager *idef = m_client->idef();
+                               ItemStack item;
+                               item.deSerialize(strwtostr(el.text), idef);
+
+                               drawItemStack(
+                                               m_environment->getVideoDriver(),
+                                               g_fontengine->getFont(), item, rect, &dest_rect,
+                                               m_client, IT_ROT_OTHER, el.angle, el.rotation
+                               );
+                       } break;
+                       }
+               }
+       }
+}
+
+// -----------------------------------------------------------------------------
+// GUIHyperText - The formated text area formspec item
+
+//! constructor
+GUIHyperText::GUIHyperText(const wchar_t *text, IGUIEnvironment *environment,
+               IGUIElement *parent, s32 id, const core::rect<s32> &rectangle,
+               Client *client, ISimpleTextureSource *tsrc) :
+               IGUIElement(EGUIET_ELEMENT, environment, parent, id, rectangle),
+               m_client(client), m_vscrollbar(nullptr),
+               m_drawer(text, client, environment, tsrc), m_text_scrollpos(0, 0)
+{
+
+#ifdef _DEBUG
+       setDebugName("GUIHyperText");
+#endif
+
+       IGUISkin *skin = 0;
+       if (Environment)
+               skin = Environment->getSkin();
+
+       m_scrollbar_width = skin ? skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : 16;
+
+       core::rect<s32> rect = irr::core::rect<s32>(
+                       RelativeRect.getWidth() - m_scrollbar_width, 0,
+                       RelativeRect.getWidth(), RelativeRect.getHeight());
+
+       m_vscrollbar = new GUIScrollBar(Environment, this, -1, rect, false, true);
+       m_vscrollbar->setVisible(false);
+}
+
+//! destructor
+GUIHyperText::~GUIHyperText()
+{
+       m_vscrollbar->remove();
+}
+
+ParsedText::Element *GUIHyperText::getElementAt(s32 X, s32 Y)
+{
+       core::position2d<s32> pos{X, Y};
+       pos -= m_display_text_rect.UpperLeftCorner;
+       pos -= m_text_scrollpos;
+       return m_drawer.getElementAt(pos);
+}
+
+void GUIHyperText::checkHover(s32 X, s32 Y)
+{
+       m_drawer.m_hovertag = nullptr;
+
+       if (AbsoluteRect.isPointInside(core::position2d<s32>(X, Y))) {
+               ParsedText::Element *element = getElementAt(X, Y);
+
+               if (element) {
+                       for (auto &tag : element->tags) {
+                               if (tag->name == "action") {
+                                       m_drawer.m_hovertag = tag;
+                                       break;
+                               }
+                       }
+               }
+       }
+
+       if (m_drawer.m_hovertag)
+               RenderingEngine::get_raw_device()->getCursorControl()->setActiveIcon(
+                               gui::ECI_HAND);
+       else
+               RenderingEngine::get_raw_device()->getCursorControl()->setActiveIcon(
+                               gui::ECI_NORMAL);
+}
+
+bool GUIHyperText::OnEvent(const SEvent &event)
+{
+       // Scroll bar
+       if (event.EventType == EET_GUI_EVENT &&
+                       event.GUIEvent.EventType == EGET_SCROLL_BAR_CHANGED &&
+                       event.GUIEvent.Caller == m_vscrollbar) {
+               m_text_scrollpos.Y = -m_vscrollbar->getPos();
+       }
+
+       // Reset hover if element left
+       if (event.EventType == EET_GUI_EVENT &&
+                       event.GUIEvent.EventType == EGET_ELEMENT_LEFT) {
+               m_drawer.m_hovertag = nullptr;
+               RenderingEngine::get_raw_device()->getCursorControl()->setActiveIcon(
+                               gui::ECI_NORMAL);
+       }
+
+       if (event.EventType == EET_MOUSE_INPUT_EVENT) {
+               if (event.MouseInput.Event == EMIE_MOUSE_MOVED)
+                       checkHover(event.MouseInput.X, event.MouseInput.Y);
+
+               if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) {
+                       m_vscrollbar->setPos(m_vscrollbar->getPos() -
+                                       event.MouseInput.Wheel * m_vscrollbar->getSmallStep());
+                       m_text_scrollpos.Y = -m_vscrollbar->getPos();
+                       m_drawer.draw(m_display_text_rect, m_text_scrollpos);
+                       checkHover(event.MouseInput.X, event.MouseInput.Y);
+
+               } else if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
+                       ParsedText::Element *element = getElementAt(
+                                       event.MouseInput.X, event.MouseInput.Y);
+
+                       if (element) {
+                               for (auto &tag : element->tags) {
+                                       if (tag->name == "action") {
+                                               Text = core::stringw(L"action:") +
+                                                      strtostrw(tag->attrs["name"]);
+                                               if (Parent) {
+                                                       SEvent newEvent;
+                                                       newEvent.EventType = EET_GUI_EVENT;
+                                                       newEvent.GUIEvent.Caller = this;
+                                                       newEvent.GUIEvent.Element = 0;
+                                                       newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED;
+                                                       Parent->OnEvent(newEvent);
+                                               }
+                                               break;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return IGUIElement::OnEvent(event);
+}
+
+//! draws the element and its children
+void GUIHyperText::draw()
+{
+       if (!IsVisible)
+               return;
+
+       // Text
+       m_display_text_rect = AbsoluteRect;
+       m_drawer.place(m_display_text_rect);
+
+       // Show scrollbar if text overflow
+       if (m_drawer.getHeight() > m_display_text_rect.getHeight()) {
+               m_vscrollbar->setSmallStep(m_display_text_rect.getHeight() * 0.1f);
+               m_vscrollbar->setLargeStep(m_display_text_rect.getHeight() * 0.5f);
+               m_vscrollbar->setMax(m_drawer.getHeight() - m_display_text_rect.getHeight());
+
+               m_vscrollbar->setVisible(true);
+
+               m_vscrollbar->setPageSize(s32(m_drawer.getHeight()));
+
+               core::rect<s32> smaller_rect = m_display_text_rect;
+
+               smaller_rect.LowerRightCorner.X -= m_scrollbar_width;
+               m_drawer.place(smaller_rect);
+       } else {
+               m_vscrollbar->setMax(0);
+               m_vscrollbar->setPos(0);
+               m_vscrollbar->setVisible(false);
+       }
+       m_drawer.draw(m_display_text_rect, m_text_scrollpos);
+
+       // draw children
+       IGUIElement::draw();
+}
diff --git a/src/gui/guiHyperText.h b/src/gui/guiHyperText.h
new file mode 100644 (file)
index 0000000..e3ad0e7
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+Minetest
+Copyright (C) 2019 EvicenceBKidscode / Pierre-Yves Rollo <dev@pyrollo.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "config.h" // for USE_FREETYPE
+
+using namespace irr;
+
+class ISimpleTextureSource;
+class Client;
+
+#if USE_FREETYPE
+#include "irrlicht_changes/CGUITTFont.h"
+#endif
+
+class ParsedText
+{
+public:
+       ParsedText(const wchar_t *text);
+       ~ParsedText();
+
+       enum ElementType
+       {
+               ELEMENT_TEXT,
+               ELEMENT_SEPARATOR,
+               ELEMENT_IMAGE,
+               ELEMENT_ITEM
+       };
+
+       enum BackgroundType
+       {
+               BACKGROUND_NONE,
+               BACKGROUND_COLOR
+       };
+
+       enum FloatType
+       {
+               FLOAT_NONE,
+               FLOAT_RIGHT,
+               FLOAT_LEFT
+       };
+
+       enum HalignType
+       {
+               HALIGN_CENTER,
+               HALIGN_LEFT,
+               HALIGN_RIGHT,
+               HALIGN_JUSTIFY
+       };
+
+       enum ValignType
+       {
+               VALIGN_MIDDLE,
+               VALIGN_TOP,
+               VALIGN_BOTTOM
+       };
+
+       typedef std::unordered_map<std::string, std::string> StyleList;
+       typedef std::unordered_map<std::string, std::string> AttrsList;
+
+       struct Tag
+       {
+               std::string name;
+               AttrsList attrs;
+               StyleList style;
+       };
+
+       struct Element
+       {
+               std::list<Tag *> tags;
+               ElementType type;
+               core::stringw text = "";
+
+               core::dimension2d<u32> dim;
+               core::position2d<s32> pos;
+               s32 drawwidth;
+
+               FloatType floating = FLOAT_NONE;
+
+               ValignType valign;
+
+#if USE_FREETYPE
+               gui::CGUITTFont *font;
+#else
+               gui::IGUIFont *font;
+#endif
+
+               irr::video::SColor color;
+               irr::video::SColor hovercolor;
+               bool underline;
+
+               s32 baseline = 0;
+
+               // img & item specific attributes
+               std::string name;
+               v3s16 angle{0, 0, 0};
+               v3s16 rotation{0, 0, 0};
+
+               s32 margin = 10;
+
+               void setStyle(StyleList &style);
+       };
+
+       struct Paragraph
+       {
+               std::vector<Element> elements;
+               HalignType halign;
+               s32 margin = 10;
+
+               void setStyle(StyleList &style);
+       };
+
+       std::vector<Paragraph> m_paragraphs;
+
+       // Element style
+       s32 margin = 3;
+       ValignType valign = VALIGN_TOP;
+       BackgroundType background_type = BACKGROUND_NONE;
+       irr::video::SColor background_color;
+
+       Tag m_root_tag;
+
+protected:
+       // Parser functions
+       void enterElement(ElementType type);
+       void endElement();
+       void enterParagraph();
+       void endParagraph();
+       void pushChar(wchar_t c);
+       ParsedText::Tag *newTag(const std::string &name, const AttrsList &attrs);
+       ParsedText::Tag *openTag(const std::string &name, const AttrsList &attrs);
+       bool closeTag(const std::string &name);
+       void parseGenericStyleAttr(const std::string &name, const std::string &value,
+                       StyleList &style);
+       void parseStyles(const AttrsList &attrs, StyleList &style);
+       void globalTag(const ParsedText::AttrsList &attrs);
+       u32 parseTag(const wchar_t *text, u32 cursor);
+       void parse(const wchar_t *text);
+
+       std::unordered_map<std::string, StyleList> m_elementtags;
+       std::unordered_map<std::string, StyleList> m_paragraphtags;
+
+       std::vector<Tag *> m_tags;
+       std::list<Tag *> m_active_tags;
+
+       // Current values
+       StyleList m_style;
+       Element *m_element;
+       Paragraph *m_paragraph;
+};
+
+class TextDrawer
+{
+public:
+       TextDrawer(const wchar_t *text, Client *client, gui::IGUIEnvironment *environment,
+                       ISimpleTextureSource *tsrc);
+
+       void place(const core::rect<s32> &dest_rect);
+       inline s32 getHeight() { return m_height; };
+       void draw(const core::rect<s32> &dest_rect,
+                       const core::position2d<s32> &dest_offset);
+       ParsedText::Element *getElementAt(core::position2d<s32> pos);
+       ParsedText::Tag *m_hovertag;
+
+protected:
+       struct RectWithMargin
+       {
+               core::rect<s32> rect;
+               s32 margin;
+       };
+
+       ParsedText m_text;
+       Client *m_client;
+       gui::IGUIEnvironment *m_environment;
+       s32 m_height;
+       s32 m_voffset;
+       std::vector<RectWithMargin> m_floating;
+};
+
+class GUIHyperText : public gui::IGUIElement
+{
+public:
+       //! constructor
+       GUIHyperText(const wchar_t *text, gui::IGUIEnvironment *environment,
+                       gui::IGUIElement *parent, s32 id,
+                       const core::rect<s32> &rectangle, Client *client,
+                       ISimpleTextureSource *tsrc);
+
+       //! destructor
+       virtual ~GUIHyperText();
+
+       //! draws the element and its children
+       virtual void draw();
+
+       core::dimension2du getTextDimension();
+
+       bool OnEvent(const SEvent &event);
+
+protected:
+       // GUI members
+       Client *m_client;
+       GUIScrollBar *m_vscrollbar;
+       TextDrawer m_drawer;
+
+       // Positioning
+       u32 m_scrollbar_width;
+       core::rect<s32> m_display_text_rect;
+       core::position2d<s32> m_text_scrollpos;
+
+       ParsedText::Element *getElementAt(s32 X, s32 Y);
+       void checkHover(s32 X, s32 Y);
+};
index 43fc692878ac021e903df4b2dcd035a95f43698d..cf64934a2562c480a41d621348eba517347385c0 100644 (file)
@@ -327,6 +327,8 @@ namespace gui
                                (const wchar_t* text, scene::ISceneManager* smgr, scene::ISceneNode* parent = 0,
                                 const video::SColor& color = video::SColor(255, 0, 0, 0), bool center = false );
 
+                       inline s32 getAscender() const { return font_metrics.ascender; }
+
                protected:
                        bool use_monochrome;
                        bool use_transparency;
index 388e8d293c0b5feb993b9c1cc86f93195505409f..caaef9b30a7fcaa8091b357f6b6e1b888e1d8dad 100644 (file)
@@ -947,3 +947,28 @@ std::wstring translate_string(const std::wstring &s) {
        translate_all(s, i, res);
        return res;
 }
+
+/**
+ * Create a std::string from a irr::core:stringw.
+ */
+std::string strwtostr(const irr::core::stringw &str)
+{
+       std::string text = core::stringc(str.c_str()).c_str();
+       return text;
+}
+
+/**
+ * Create a irr::core:stringw from a std::string.
+ */
+irr::core::stringw strtostrw(const std::string &str)
+{
+       size_t size = str.size();
+       // s.size() doesn't include NULL terminator
+       wchar_t *text = new wchar_t[size + sizeof(wchar_t)];
+       const char *data = &str[0];
+
+       mbsrtowcs(text, &data, size, NULL);
+
+       text[size] = L'\0';
+       return text;
+}
index ab9a4a6c8563fdde3a6d08404bdf39eb1b73b179..3aa11080f1aeccc155c66f31b2c54386b1ffd489 100644 (file)
@@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #pragma once
 
 #include "irrlichttypes_bloated.h"
+#include "irrString.h"
 #include <cstdlib>
 #include <string>
 #include <cstring>
@@ -723,3 +724,13 @@ inline std::string str_join(const std::vector<std::string> &list,
        }
        return oss.str();
 }
+
+/**
+ * Create a std::string from a irr::core::stringw.
+ */
+std::string strwtostr(const irr::core::stringw &str);
+
+/**
+ * Create a irr::core:stringw from a std::string.
+ */
+irr::core::stringw strtostrw(const std::string &str);
index 0b36dcd5754a38c912c3afe1555b37b67e7cbc9b..27e780a65f3bfe14fa41b5eeb2affb3aca6fb969 100644 (file)
@@ -167,6 +167,8 @@ src/gui/guiEngine.h
 src/gui/guiFormSpecMenu.cpp
 src/gui/guiFormSpecMenu.h
 src/gui/guiKeyChangeMenu.cpp
+src/gui/guiHyperText.cpp
+src/gui/guiHyperText.h
 src/gui/guiMainMenu.h
 src/gui/guiPasswordChange.cpp
 src/gui/guiPathSelectMenu.cpp