From 810bbc54727ec07b29656956b70e2f4c1a519dda Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Tue, 20 May 2025 21:49:41 +0200 Subject: [PATCH] Formspec: Introduce default element styling/theming This introduces a new setting to customize the default appearance of formspecs. Server-sent 'formspec prepends' will internally overwrite this setting. --- doc/lua_api.md | 4 +- doc/texture_packs.md | 4 + games/devtest/mods/testformspec/formspec.lua | 2 +- src/gui/StyleSpec.h | 12 +++ src/gui/guiButton.cpp | 11 ++- src/gui/guiButtonImage.cpp | 9 +- src/gui/guiEngine.cpp | 39 +++++++-- src/gui/guiEngine.h | 8 +- src/gui/guiFormSpecMenu.cpp | 86 ++++++++++++++------ src/gui/guiFormSpecMenu.h | 10 ++- 10 files changed, 134 insertions(+), 51 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index 3a6da1bdb..8b839514b 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -3632,9 +3632,9 @@ Some types may inherit styles from parent types. * bgimg - standard background image. Defaults to none. * bgimg_hovered - background image when hovered. Defaults to bgimg when not provided. * This is deprecated, use states instead. - * bgimg_middle - Makes the bgimg textures render in 9-sliced mode and defines the middle rect. + * bgimg_middle - Replaces the default border. Defines the middle rect for 9-sliced mode. See background9[] documentation for more details. This property also pads the - button's content when set. + button's content when set. When `border` is set to false * bgimg_pressed - background image when pressed. Defaults to bgimg when not provided. * This is deprecated, use states instead. * font - Sets font type. This is a comma separated list of options. Valid options: diff --git a/doc/texture_packs.md b/doc/texture_packs.md index bde5eecfd..c5abea79b 100644 --- a/doc/texture_packs.md +++ b/doc/texture_packs.md @@ -38,6 +38,10 @@ A key-value config file with the following keys: * `textdomain`: Textdomain used to translate title and description. Defaults to the texture pack name. See [Translating content meta](lua_api.md#translating-content-meta). +* `formspec_theme`: optional. Default formspec styling. + This is a newline separated list of `style_type[...]` values. +* `formspec_version_theme`: optional. Indicates the minimal [formspec version](lua_api.md) + needed to properly display `formspec_theme`. On older clients, this suppresses warnings. ### `description.txt` **Deprecated**, you should use texture_pack.conf instead. diff --git a/games/devtest/mods/testformspec/formspec.lua b/games/devtest/mods/testformspec/formspec.lua index f2f632fa0..94c2ceba6 100644 --- a/games/devtest/mods/testformspec/formspec.lua +++ b/games/devtest/mods/testformspec/formspec.lua @@ -235,7 +235,7 @@ local style_fs = [[ style[one_btn15;border=false;bgcolor=#1cc;bgimg=testformspec_bg.png;bgimg_hovered=testformspec_bg_hovered.png;bgimg_pressed=testformspec_bg_pressed.png] item_image_button[1.25,9.6;1,1;testformspec:item;one_btn15;Bg] - style[one_btn16;border=false;bgimg=testformspec_bg_9slice.png;bgimg_middle=4,6;padding=5,7;fgimg=testformspec_bg.png;fgimg_middle=1] + style[one_btn16;bgimg=testformspec_bg_9slice.png;bgimg_middle=4,6;padding=5,7;fgimg=testformspec_bg.png;fgimg_middle=1] style[one_btn16:hovered;bgimg=testformspec_bg_9slice_hovered.png;fgimg=testformspec_bg_hovered.png] style[one_btn16:pressed;bgimg=testformspec_bg_9slice_pressed.png;fgimg=testformspec_bg_pressed.png] style[one_btn16:focused;bgimg=testformspec_bg_9slice_focused.png;fgimg=testformspec_bg_focused.png] diff --git a/src/gui/StyleSpec.h b/src/gui/StyleSpec.h index f78a038c2..a952e855a 100644 --- a/src/gui/StyleSpec.h +++ b/src/gui/StyleSpec.h @@ -380,12 +380,24 @@ public: StyleSpec &operator|=(const StyleSpec &other) { + u32 props_set = 0; + static_assert(sizeof(props_set) * 8 > NUM_PROPERTIES); + for (size_t i = 0; i < NUM_PROPERTIES; i++) { auto prop = (Property)i; if (other.hasProperty(prop)) { + props_set |= (1 << i); set(prop, other.get(prop, "")); } } + if ((props_set & (1 << FGIMG | 1 << FGIMG_MIDDLE)) == (1 << FGIMG)) { + // Image was specified without 9-slice. Reset to non-9-slice. + set(FGIMG_MIDDLE, ""); + } + if ((props_set & (1 << BGIMG | 1 << BGIMG_MIDDLE)) == (1 << BGIMG)) { + // Image was specified without 9-slice. Reset to non-9-slice. + set(BGIMG_MIDDLE, ""); + } return *this; } diff --git a/src/gui/guiButton.cpp b/src/gui/guiButton.cpp index 9975549fe..a8cd2cafa 100644 --- a/src/gui/guiButton.cpp +++ b/src/gui/guiButton.cpp @@ -255,9 +255,10 @@ void GUIButton::draw() video::IVideoDriver* driver = Environment->getVideoDriver(); IGUISkin *skin = Environment->getSkin(); + const bool is_9_slice_border = BgMiddle.getArea() > 0; // END PATCH - if (DrawBorder) + if (DrawBorder && !is_9_slice_border) { if (!Pressed) { @@ -303,13 +304,17 @@ void GUIButton::draw() // PATCH video::ITexture* texture = ButtonImages[(u32)imageState].Texture; + // FIXME: Vertices can only be darkened because [0, 255] is normalized to [0, 1] + // For reference: irr/src/OpenGL/Driver.cpp -> `vt2DImage` video::SColor image_colors[] = { BgColor, BgColor, BgColor, BgColor }; - if (BgMiddle.getArea() == 0) { + if (!is_9_slice_border) { + // Regular image button driver->draw2DImage(texture, ScaleImage? AbsoluteRect : core::rect(pos, sourceRect.getSize()), sourceRect, &AbsoluteClippingRect, image_colors, UseAlphaChannel); - } else { + } else if (DrawBorder) { + // The background image is 9-slice --> use as new border style draw2DImage9Slice(driver, texture, ScaleImage ? AbsoluteRect : core::rect(pos, sourceRect.getSize()), sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors); diff --git a/src/gui/guiButtonImage.cpp b/src/gui/guiButtonImage.cpp index f085d4383..61849be22 100644 --- a/src/gui/guiButtonImage.cpp +++ b/src/gui/guiButtonImage.cpp @@ -26,16 +26,9 @@ GUIButtonImage::GUIButtonImage(gui::IGUIEnvironment *environment, void GUIButtonImage::draw() { - if (isDrawingBorder()) { - // `GUIButton` also allows drawing different textures depending on - // `EGUI_BUTTON_STATE` --> Skip everything if the border is disabled (else case). - GUIButton::draw(); - } else { - IGUIElement::draw(); - } + GUIButton::draw(); } - void GUIButtonImage::setForegroundImage(irr_ptr image, const core::rect &middle) { diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index 0ec0d7a76..465274c67 100644 --- a/src/gui/guiEngine.cpp +++ b/src/gui/guiEngine.cpp @@ -8,6 +8,7 @@ #include "client/guiscalingfilter.h" #include "client/renderingengine.h" #include "client/shader.h" +#include "client/texturepaths.h" #include "client/tile.h" #include "clientdynamicinfo.h" #include "config.h" @@ -41,14 +42,18 @@ void TextDestGuiEngine::gotText(const StringMap &fields) } /******************************************************************************/ +MenuTextureSource::MenuTextureSource(video::IVideoDriver* driver) : + m_driver(driver) +{ + g_settings->registerChangedCallback("texture_path", onTxpSettingChanged, this); +} + MenuTextureSource::~MenuTextureSource() { - u32 before = m_driver->getTextureCount(); + g_settings->deregisterAllChangedCallbacks(this); - for (const auto &it: m_to_delete) { - m_driver->removeTexture(it); - } - m_to_delete.clear(); + u32 before = m_driver->getTextureCount(); + cleanupTextures(); infostream << "~MenuTextureSource() before cleanup: "<< before << " after: " << m_driver->getTextureCount() << std::endl; @@ -68,8 +73,14 @@ video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id) if (retval) return retval; - verbosestream << "MenuTextureSource: loading " << name << std::endl; - video::IImage *image = m_driver->createImageFromFile(name.c_str()); + // Try to find the texture in the active texture pack + std::string path; + if (!fs::IsPathAbsolute(name)) + path = getTexturePath(name, nullptr); + + const char *filepath = path.empty() ? name.c_str() : path.c_str(); + verbosestream << "MenuTextureSource: loading " << filepath << std::endl; + video::IImage *image = m_driver->createImageFromFile(filepath); if (!image) return NULL; @@ -81,6 +92,20 @@ video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id) return retval; } +void MenuTextureSource::cleanupTextures() +{ + for (const auto &it: m_to_delete) { + m_driver->removeTexture(it); + } + m_to_delete.clear(); +} + +void MenuTextureSource::onTxpSettingChanged(const std::string &name, void *data) +{ + ((MenuTextureSource *)data)->cleanupTextures(); + clearTextureNameCache(); +} + /******************************************************************************/ /** MenuMusicFetcher */ /******************************************************************************/ diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h index f4b22f3c2..9fd5e869c 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -74,7 +74,7 @@ public: * default constructor * @param driver the video driver to load textures from */ - MenuTextureSource(video::IVideoDriver *driver) : m_driver(driver) {}; + MenuTextureSource(video::IVideoDriver *driver); /** * destructor, removes all loaded textures @@ -89,6 +89,12 @@ public: video::ITexture *getTexture(const std::string &name, u32 *id = NULL); private: + /** Unloads all textures in `m_to_delete` */ + void cleanupTextures(); + + /** Update the texture cache */ + static void onTxpSettingChanged(const std::string &name, void *data); + /** driver to get textures from */ video::IVideoDriver *m_driver = nullptr; /** set of textures to delete */ diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 97d86b05f..50187a2a7 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -114,10 +114,15 @@ GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick, m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay"); m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname"); + + g_settings->registerChangedCallback("texture_path", onTxpSettingChanged, this); + setThemeFromSettings(); } GUIFormSpecMenu::~GUIFormSpecMenu() { + g_settings->deregisterAllChangedCallbacks(this); + removeAll(); delete m_selected_item; @@ -2618,15 +2623,9 @@ void GUIFormSpecMenu::parsePadding(parserData *data, const std::string &element) << "'" << std::endl; } -void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) +void GUIFormSpecMenu::parse_style_to_map(StyleSpecMap &out, const std::string &element, + std::unordered_set *prop_warned) { - if (data->type != "style" && data->type != "style_type") { - errorstream << "Invalid style element type: '" << data->type << "'" << std::endl; - return; - } - - bool style_type = (data->type == "style_type"); - std::vector parts = split(element, ';'); if (parts.size() < 2) { @@ -2653,11 +2652,11 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) StyleSpec::Property prop = StyleSpec::GetPropertyByName(propname); if (prop == StyleSpec::NONE) { - if (property_warned.find(propname) != property_warned.end()) { + if (prop_warned && prop_warned->find(propname) != prop_warned->end()) { warningstream << "Invalid style element (Unknown property " << propname << "): '" << element << "'" << std::endl; - property_warned.insert(propname); + prop_warned->insert(propname); } continue; } @@ -2705,11 +2704,7 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) continue; } - if (style_type) { - theme_by_type[selector].push_back(selector_spec); - } else { - theme_by_name[selector].push_back(selector_spec); - } + out[selector].push_back(selector_spec); // Backwards-compatibility for existing _hovered/_pressed properties if (selector_spec.hasProperty(StyleSpec::BGCOLOR_HOVERED) @@ -2728,11 +2723,7 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) hover_spec.set(StyleSpec::FGIMG, selector_spec.get(StyleSpec::FGIMG_HOVERED, "")); } - if (style_type) { - theme_by_type[selector].push_back(hover_spec); - } else { - theme_by_name[selector].push_back(hover_spec); - } + out[selector].push_back(hover_spec); } if (selector_spec.hasProperty(StyleSpec::BGCOLOR_PRESSED) || selector_spec.hasProperty(StyleSpec::BGIMG_PRESSED) @@ -2750,15 +2741,22 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) press_spec.set(StyleSpec::FGIMG, selector_spec.get(StyleSpec::FGIMG_PRESSED, "")); } - if (style_type) { - theme_by_type[selector].push_back(press_spec); - } else { - theme_by_name[selector].push_back(press_spec); - } + out[selector].push_back(press_spec); } } +} - return; +void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element) +{ + if (data->type != "style" && data->type != "style_type") { + errorstream << "Invalid style element type: '" << data->type << "'" << std::endl; + return; + } + + bool style_type = (data->type == "style_type"); + + parse_style_to_map(style_type ? theme_by_type : theme_by_name, + element, &property_warned); } void GUIFormSpecMenu::parseSetFocus(parserData*, const std::string &element) @@ -3041,7 +3039,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize) m_dropdowns.clear(); m_scroll_containers.clear(); theme_by_name.clear(); - theme_by_type.clear(); + theme_by_type = theme_by_type_default; m_clickthrough_elements.clear(); field_enter_after_edit.clear(); field_close_on_enter.clear(); @@ -5028,6 +5026,40 @@ std::wstring GUIFormSpecMenu::getLabelByID(s32 id) return L""; } + +void GUIFormSpecMenu::setThemeFromSettings() +{ + theme_by_type_default.clear(); + + const std::string settingspath = g_settings->get("texture_path") + DIR_DELIM + "texture_pack.conf"; + Settings settings; + if (!settings.readConfigFile(settingspath.c_str())) + return; + if (!settings.exists("formspec_theme")) + return; + + std::unordered_set *prop_warned = nullptr; + { + u16 fs_ver = FORMSPEC_API_VERSION; + settings.getU16NoEx("formspec_version_theme", fs_ver); + if (fs_ver <= FORMSPEC_API_VERSION) + prop_warned = &property_warned; + // else: silence + } + + auto splits = split(settings.get("formspec_theme"), '\n'); + for (const std::string &s : splits) { + parse_style_to_map(theme_by_type_default, s, prop_warned); + } +} + +void GUIFormSpecMenu::onTxpSettingChanged(const std::string &name, void *data) +{ + GUIFormSpecMenu *me = (GUIFormSpecMenu *)data; + me->setThemeFromSettings(); + me->regenerateGui(me->m_screensize_old); +} + StyleSpec GUIFormSpecMenu::getDefaultStyleForElement(const std::string &type, const std::string &name, const std::string &parent_type) { return getStyleForElement(type, name, parent_type)[StyleSpec::STATE_DEFAULT]; diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index f279bbb29..72b7a2ad5 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -302,10 +302,14 @@ protected: bool precheckElement(const std::string &name, const std::string &element, size_t args_min, size_t args_max, std::vector &parts); - std::unordered_map> theme_by_type; - std::unordered_map> theme_by_name; + using StyleSpecMap = std::unordered_map>; + StyleSpecMap theme_by_type, theme_by_name, + theme_by_type_default; std::unordered_set property_warned; + void setThemeFromSettings(); + static void onTxpSettingChanged(const std::string &name, void *data); + StyleSpec getDefaultStyleForElement(const std::string &type, const std::string &name="", const std::string &parent_type=""); std::array getStyleForElement(const std::string &type, @@ -483,6 +487,8 @@ private: void parseAnchor(parserData *data, const std::string &element); bool parsePaddingDirect(parserData *data, const std::string &element); void parsePadding(parserData *data, const std::string &element); + static void parse_style_to_map(StyleSpecMap &out, const std::string &element, + std::unordered_set *prop_warned); void parseStyle(parserData *data, const std::string &element); void parseSetFocus(parserData *, const std::string &element); void parseModel(parserData *data, const std::string &element);