diff --git a/doc/lua_api.md b/doc/lua_api.md index 438769085..614421dbb 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -3653,6 +3653,8 @@ Some types may inherit styles from parent types. * `+`/`-`: Offsets default font size by `number` points. * `*`: Multiplies default font size by `number`, similar to CSS `em`. * border - boolean, draw border. Set to false to hide the bevelled button pane. Default true. + * border_img - string, a texture that overrides the border style. + * border_img_middle - rect, to render `border_img` in 9-sliced mode. Refer to `background9[...]`. * content_offset - 2d vector, shifts the position of the button's content without resizing it. * noclip - boolean, set to true to allow the element to exceed formspec bounds. * padding - rect, adds space between the edges of the button and the content. This value is diff --git a/doc/texture_packs.md b/doc/texture_packs.md index bde5eecfd..7036f3508 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. Formspec elements that define the defaut style. + * Allowed elements: `bgcolor`, `style_type` +* `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/irr/src/CGUIEnvironment.cpp b/irr/src/CGUIEnvironment.cpp index abfa0aab9..1a4560eab 100644 --- a/irr/src/CGUIEnvironment.cpp +++ b/irr/src/CGUIEnvironment.cpp @@ -28,6 +28,8 @@ #endif #include "os.h" +irr::gui::IGUISkin *impl_create_irr_guiskin(irr::video::IVideoDriver *driver); + namespace irr { namespace gui @@ -590,7 +592,7 @@ If you no longer need the skin, you should call IGUISkin::drop(). See IReferenceCounted::drop() for more information. */ IGUISkin *CGUIEnvironment::createSkin() { - IGUISkin *skin = new CGUISkin(Driver); + IGUISkin *skin = impl_create_irr_guiskin(Driver); IGUIFont *builtinfont = getBuiltInFont(); IGUIFontBitmap *bitfont = 0; diff --git a/irr/src/CGUISkin.h b/irr/src/CGUISkin.h index 704a09745..e3bb6f380 100644 --- a/irr/src/CGUISkin.h +++ b/irr/src/CGUISkin.h @@ -291,7 +291,7 @@ namespace gui //! gets the colors virtual void getColors(video::SColor* colors); // ::PATCH: - private: + protected: float Scale = 1.0f; video::SColor Colors[EGDC_COUNT]; diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index 4c8eaf060..35961532f 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -14,6 +14,7 @@ #include "inputhandler.h" #include "profiler.h" #include "gui/guiEngine.h" +#include "gui/guiSkin.h" #include "fontengine.h" #include "clientlauncher.h" #include "version.h" @@ -338,6 +339,11 @@ static video::ITexture *loadTexture(video::IVideoDriver *driver, const char *pat return texture; } +gui::IGUISkin *impl_create_irr_guiskin(video::IVideoDriver *driver) +{ + return new GUISkin(driver); +} + void ClientLauncher::config_guienv() { gui::IGUISkin *skin = guienv->getSkin(); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0c1db2ae0..9b9216be1 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -19,6 +19,7 @@ set(gui_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/guiScene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiScrollBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiScrollContainer.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 diff --git a/src/gui/StyleSpec.h b/src/gui/StyleSpec.h index f78a038c2..a7d389cea 100644 --- a/src/gui/StyleSpec.h +++ b/src/gui/StyleSpec.h @@ -13,6 +13,9 @@ #include #include +class StyleSpec; + +using StyleSpecMap = std::unordered_map>; class StyleSpec { @@ -59,6 +62,8 @@ public: STATE_INVALID = 1 << 4, }; + using StateMap = std::array; + private: std::array property_set{}; std::array properties; @@ -380,12 +385,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..f4ded8b18 100644 --- a/src/gui/guiButton.cpp +++ b/src/gui/guiButton.cpp @@ -303,13 +303,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) { + // Regular image button driver->draw2DImage(texture, ScaleImage? AbsoluteRect : core::rect(pos, sourceRect.getSize()), sourceRect, &AbsoluteClippingRect, image_colors, UseAlphaChannel); } else { + // This is generally used to replace the default border style draw2DImage9Slice(driver, texture, ScaleImage ? AbsoluteRect : core::rect(pos, sourceRect.getSize()), sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors); diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index e843b5b38..2e57b9a66 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" @@ -43,14 +44,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; @@ -70,8 +75,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; @@ -83,6 +94,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 4fde215fb..c95a21441 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -76,7 +76,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 @@ -91,6 +91,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 29ff83af4..919efcb18 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -56,6 +56,7 @@ #include "guiScrollContainer.h" #include "guiHyperText.h" #include "guiScene.h" +#include "guiSkin.h" #define MY_CHECKPOS(a,b) \ if (v_pos.size() != 2) { \ @@ -114,10 +115,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 +2624,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 +2653,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 +2705,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 +2724,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 +2742,31 @@ 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"); + bool do_warn = m_formspec_version <= FORMSPEC_API_VERSION; + + StyleSpecMap *map = style_type ? &theme_by_type : &theme_by_name; + if (data->reading_theme) { + GUISkin *skin = (GUISkin *)Environment->getSkin(); + map = &skin->getThemeRef(); + } + parse_style_to_map( + *map, + element, + do_warn ? &property_warned : nullptr + ); } void GUIFormSpecMenu::parseSetFocus(parserData*, const std::string &element) @@ -2898,8 +2906,8 @@ void GUIFormSpecMenu::removeAll() scroll_container_it.second->drop(); } -const std::unordered_map> GUIFormSpecMenu::element_parsers = { +const std::unordered_map + GUIFormSpecMenu::element_parsers = { {"container", &GUIFormSpecMenu::parseContainer}, {"container_end", &GUIFormSpecMenu::parseContainerEnd}, {"list", &GUIFormSpecMenu::parseList}, @@ -2948,14 +2956,20 @@ const std::unordered_map + GUIFormSpecMenu::element_parsers_theme = { + {"bgcolor", &GUIFormSpecMenu::parseBackgroundColor}, + {"style_type", &GUIFormSpecMenu::parseStyle}, +}; -void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) +void GUIFormSpecMenu::parseElement(parserData *data, const std::string &element, bool is_theme) { //some prechecks if (element.empty()) return; - if (parseVersionDirect(element)) + if (!is_theme && parseVersionDirect(element)) return; size_t pos = element.find('['); @@ -2968,8 +2982,9 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) // They remain here due to bool flags, for now data->type = type; - auto it = element_parsers.find(type); - if (it != element_parsers.end()) { + auto &parser_lut = is_theme ? element_parsers_theme : element_parsers; + auto it = parser_lut.find(type); + if (it != parser_lut.end()) { it->second(this, data, description); return; } @@ -5054,6 +5069,40 @@ std::wstring GUIFormSpecMenu::getLabelByID(s32 id) return L""; } + +void GUIFormSpecMenu::setThemeFromSettings() +{ + GUISkin *skin = (GUISkin *)Environment->getSkin(); + skin->getThemeRef().clear(); + skin->setTextureSource(m_tsrc); + + 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; + + settings.getU16NoEx("formspec_version_theme", m_theme_formspec_version); + auto theme_elements = split(settings.get("formspec_theme"), ']'); + + parserData mydata; + mydata.reading_theme = true; + + const u16 version_backup = m_formspec_version; + m_formspec_version = m_theme_formspec_version; + for (const std::string &element : theme_elements) + parseElement(&mydata, element, true); + m_formspec_version = version_backup; +} + +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..d7a18cf4b 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; + StyleSpecMap theme_by_type, theme_by_name; std::unordered_set property_warned; + // Texturepack-definied formspec theming support + u16 m_theme_formspec_version; + 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, @@ -387,8 +391,9 @@ private: bool m_show_debug = false; struct parserData { - bool explicit_size; - bool real_coordinates; + bool explicit_size = false; + bool real_coordinates = false; + bool reading_theme = false; u8 simple_field_count; v2f invsize; v2s32 size; @@ -419,7 +424,9 @@ private: std::string type; }; - static const std::unordered_map> element_parsers; + using parser_function_t = std::function; + static const std::unordered_map + element_parsers, element_parsers_theme; struct fs_key_pending { bool key_up; @@ -433,7 +440,7 @@ private: void removeAll(); - void parseElement(parserData* data, const std::string &element); + void parseElement(parserData* data, const std::string &element, bool is_theme = false); void parseSize(parserData* data, const std::string &element); void parseContainer(parserData* data, const std::string &element); @@ -483,6 +490,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); diff --git a/src/gui/guiSkin.cpp b/src/gui/guiSkin.cpp new file mode 100644 index 000000000..b4aef156b --- /dev/null +++ b/src/gui/guiSkin.cpp @@ -0,0 +1,76 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Krock/SmallJoker + +#include "guiSkin.h" +#include "client/guiscalingfilter.h" + + +GUISkin::GUISkin(video::IVideoDriver *driver) : gui::CGUISkin(driver) +{ +} + + +GUISkin::~GUISkin() +{ +} + +void GUISkin::drawColored3DButtonPanePressed(gui::IGUIElement *element, + const core::rect &rect, + const core::rect *clip, + const video::SColor *colors) +{ + if (!Driver) + return; + + if (tryDrawPane("_skin_button", StyleSpec::STATE_PRESSED, rect, clip)) + return; + + gui::CGUISkin::drawColored3DButtonPanePressed(element, rect, clip, colors); +} + +void GUISkin::drawColored3DButtonPaneStandard(gui::IGUIElement *element, + const core::rect &rect, + const core::rect *clip, + const video::SColor *colors) +{ + if (!Driver) + return; + + if (tryDrawPane("_skin_button", StyleSpec::STATE_DEFAULT, rect, clip)) + return; + + gui::CGUISkin::drawColored3DButtonPaneStandard(element, rect, clip, colors); +} + +bool GUISkin::tryDrawPane(const char *type, StyleSpec::State state, + const core::rect &rect, + const core::rect *clip) +{ + auto it = m_theme.find(type); + if (it == m_theme.end()) + return false; + + video::SColor c = 0xFFFFFFFF; + video::SColor image_colors[] = { c, c, c, c }; + + // Similar to GUIFormSpecMenu::getStyleForElement + StyleSpec::StateMap states; + for (const StyleSpec &spec : it->second) + states[(u32)spec.getState()] |= spec; + + StyleSpec style = StyleSpec::getStyleFromStatePropagation(states, state); + video::ITexture *texture = style.getTexture(StyleSpec::BGIMG, m_texture_source); + core::recti source_rect = core::rect(core::position2di(0,0), texture->getOriginalSize()); + + core::recti bg_middle = style.getRect(StyleSpec::BGIMG_MIDDLE, core::recti()); + if (bg_middle.getArea() == 0) { + Driver->draw2DImage(texture, rect, source_rect, clip, image_colors, true); + } else { + draw2DImage9Slice(Driver, texture, rect, source_rect, bg_middle, clip, image_colors); + } + + return true; +} + + diff --git a/src/gui/guiSkin.h b/src/gui/guiSkin.h new file mode 100644 index 000000000..4c46a79d5 --- /dev/null +++ b/src/gui/guiSkin.h @@ -0,0 +1,37 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Krock/SmallJoker + +#pragma once + +#include "StyleSpec.h" // StyleSpecMap +#include "../../irr/src/CGUISkin.h" + +class GUISkin : public gui::CGUISkin { +public: + GUISkin(video::IVideoDriver *driver); + virtual ~GUISkin(); + + void setTextureSource(ISimpleTextureSource *src) { m_texture_source = src; } + + virtual void drawColored3DButtonPaneStandard(gui::IGUIElement *element, + const core::rect &rect, + const core::rect *clip = 0, + const video::SColor *colors = 0) override; + + virtual void drawColored3DButtonPanePressed(gui::IGUIElement *element, + const core::rect &rect, + const core::rect *clip = 0, + const video::SColor *colors = 0) override; + + StyleSpecMap &getThemeRef() { return m_theme; } + +private: + bool tryDrawPane(const char *type, StyleSpec::State state, + const core::rect &rect, + const core::rect *clip = 0); + + ISimpleTextureSource *m_texture_source = nullptr; + StyleSpecMap m_theme; +}; +