diff options
author | Pierre-Yves Rollo <dev@pyrollo.com> | 2019-09-10 15:11:26 +0200 |
---|---|---|
committer | SmallJoker <SmallJoker@users.noreply.github.com> | 2019-11-03 11:45:33 +0100 |
commit | 72416a6a1f75d56abfad0f486e57fd32579b3604 (patch) | |
tree | 2ff2978c1aec4ee6548474b8c65d3c96bab5b05f /src | |
parent | 8697090b3564b508af50d40068a61db280f714a6 (diff) | |
download | minetest-72416a6a1f75d56abfad0f486e57fd32579b3604.tar.gz minetest-72416a6a1f75d56abfad0f486e57fd32579b3604.tar.bz2 minetest-72416a6a1f75d56abfad0f486e57fd32579b3604.zip |
Formspec: add hypertext element
Diffstat (limited to 'src')
-rw-r--r-- | src/client/fontengine.cpp | 70 | ||||
-rw-r--r-- | src/client/fontengine.h | 61 | ||||
-rw-r--r-- | src/client/hud.cpp | 70 | ||||
-rw-r--r-- | src/client/hud.h | 12 | ||||
-rw-r--r-- | src/defaultsettings.cpp | 8 | ||||
-rw-r--r-- | src/gui/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/gui/guiFormSpecMenu.cpp | 91 | ||||
-rw-r--r-- | src/gui/guiFormSpecMenu.h | 2 | ||||
-rw-r--r-- | src/gui/guiHyperText.cpp | 1137 | ||||
-rw-r--r-- | src/gui/guiHyperText.h | 229 | ||||
-rw-r--r-- | src/irrlicht_changes/CGUITTFont.h | 2 | ||||
-rw-r--r-- | src/util/string.cpp | 25 | ||||
-rw-r--r-- | src/util/string.h | 11 |
13 files changed, 1655 insertions, 64 deletions
diff --git a/src/client/fontengine.cpp b/src/client/fontengine.cpp index 858d6780e..8120f82df 100644 --- a/src/client/fontengine.cpp +++ b/src/client/fontengine.cpp @@ -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; } diff --git a/src/client/fontengine.h b/src/client/fontengine.h index 62aa71897..ecffc7660 100644 --- a/src/client/fontengine.h +++ b/src/client/fontengine.h @@ -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; diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 291d03816..304a3ab16 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -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)); +} diff --git a/src/client/hud.h b/src/client/hud.h index 693d2adee..d9b5e0686 100644 --- a/src/client/hud.h +++ b/src/client/hud.h @@ -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 diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 01ee97a33..1ab4a333e 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -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"); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 2307856a4..c9f750b9a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 5def4357e..44fdf7862 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -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; } } } diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 46df0930c..39af1e7c2 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -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 index 000000000..024e4de09 --- /dev/null +++ b/src/gui/guiHyperText.cpp @@ -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 ⪙ + } + } + 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 index 000000000..e3ad0e747 --- /dev/null +++ b/src/gui/guiHyperText.h @@ -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); +}; diff --git a/src/irrlicht_changes/CGUITTFont.h b/src/irrlicht_changes/CGUITTFont.h index 43fc69287..cf64934a2 100644 --- a/src/irrlicht_changes/CGUITTFont.h +++ b/src/irrlicht_changes/CGUITTFont.h @@ -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; diff --git a/src/util/string.cpp b/src/util/string.cpp index 388e8d293..caaef9b30 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -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; +} diff --git a/src/util/string.h b/src/util/string.h index ab9a4a6c8..3aa11080f 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -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); |