1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-08-11 17:51:04 +00:00

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.
This commit is contained in:
SmallJoker 2025-05-20 21:49:41 +02:00
parent ebef65e905
commit 810bbc5472
10 changed files with 134 additions and 51 deletions

View file

@ -3632,9 +3632,9 @@ Some types may inherit styles from parent types.
* bgimg - standard background image. Defaults to none. * bgimg - standard background image. Defaults to none.
* bgimg_hovered - background image when hovered. Defaults to bgimg when not provided. * bgimg_hovered - background image when hovered. Defaults to bgimg when not provided.
* This is deprecated, use states instead. * 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 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. * bgimg_pressed - background image when pressed. Defaults to bgimg when not provided.
* This is deprecated, use states instead. * This is deprecated, use states instead.
* font - Sets font type. This is a comma separated list of options. Valid options: * font - Sets font type. This is a comma separated list of options. Valid options:

View file

@ -38,6 +38,10 @@ A key-value config file with the following keys:
* `textdomain`: Textdomain used to translate title and description. * `textdomain`: Textdomain used to translate title and description.
Defaults to the texture pack name. Defaults to the texture pack name.
See [Translating content meta](lua_api.md#translating-content-meta). 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` ### `description.txt`
**Deprecated**, you should use texture_pack.conf instead. **Deprecated**, you should use texture_pack.conf instead.

View file

@ -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] 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] 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: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: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] style[one_btn16:focused;bgimg=testformspec_bg_9slice_focused.png;fgimg=testformspec_bg_focused.png]

View file

@ -380,12 +380,24 @@ public:
StyleSpec &operator|=(const StyleSpec &other) 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++) { for (size_t i = 0; i < NUM_PROPERTIES; i++) {
auto prop = (Property)i; auto prop = (Property)i;
if (other.hasProperty(prop)) { if (other.hasProperty(prop)) {
props_set |= (1 << i);
set(prop, other.get(prop, "")); 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; return *this;
} }

View file

@ -255,9 +255,10 @@ void GUIButton::draw()
video::IVideoDriver* driver = Environment->getVideoDriver(); video::IVideoDriver* driver = Environment->getVideoDriver();
IGUISkin *skin = Environment->getSkin(); IGUISkin *skin = Environment->getSkin();
const bool is_9_slice_border = BgMiddle.getArea() > 0;
// END PATCH // END PATCH
if (DrawBorder) if (DrawBorder && !is_9_slice_border)
{ {
if (!Pressed) if (!Pressed)
{ {
@ -303,13 +304,17 @@ void GUIButton::draw()
// PATCH // PATCH
video::ITexture* texture = ButtonImages[(u32)imageState].Texture; 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 }; video::SColor image_colors[] = { BgColor, BgColor, BgColor, BgColor };
if (BgMiddle.getArea() == 0) { if (!is_9_slice_border) {
// Regular image button
driver->draw2DImage(texture, driver->draw2DImage(texture,
ScaleImage? AbsoluteRect : core::rect<s32>(pos, sourceRect.getSize()), ScaleImage? AbsoluteRect : core::rect<s32>(pos, sourceRect.getSize()),
sourceRect, &AbsoluteClippingRect, sourceRect, &AbsoluteClippingRect,
image_colors, UseAlphaChannel); image_colors, UseAlphaChannel);
} else { } else if (DrawBorder) {
// The background image is 9-slice --> use as new border style
draw2DImage9Slice(driver, texture, draw2DImage9Slice(driver, texture,
ScaleImage ? AbsoluteRect : core::rect<s32>(pos, sourceRect.getSize()), ScaleImage ? AbsoluteRect : core::rect<s32>(pos, sourceRect.getSize()),
sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors); sourceRect, BgMiddle, &AbsoluteClippingRect, image_colors);

View file

@ -26,16 +26,9 @@ GUIButtonImage::GUIButtonImage(gui::IGUIEnvironment *environment,
void GUIButtonImage::draw() 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(); GUIButton::draw();
} else {
IGUIElement::draw();
}
} }
void GUIButtonImage::setForegroundImage(irr_ptr<video::ITexture> image, void GUIButtonImage::setForegroundImage(irr_ptr<video::ITexture> image,
const core::rect<s32> &middle) const core::rect<s32> &middle)
{ {

View file

@ -8,6 +8,7 @@
#include "client/guiscalingfilter.h" #include "client/guiscalingfilter.h"
#include "client/renderingengine.h" #include "client/renderingengine.h"
#include "client/shader.h" #include "client/shader.h"
#include "client/texturepaths.h"
#include "client/tile.h" #include "client/tile.h"
#include "clientdynamicinfo.h" #include "clientdynamicinfo.h"
#include "config.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() MenuTextureSource::~MenuTextureSource()
{ {
u32 before = m_driver->getTextureCount(); g_settings->deregisterAllChangedCallbacks(this);
for (const auto &it: m_to_delete) { u32 before = m_driver->getTextureCount();
m_driver->removeTexture(it); cleanupTextures();
}
m_to_delete.clear();
infostream << "~MenuTextureSource() before cleanup: "<< before infostream << "~MenuTextureSource() before cleanup: "<< before
<< " after: " << m_driver->getTextureCount() << std::endl; << " after: " << m_driver->getTextureCount() << std::endl;
@ -68,8 +73,14 @@ video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id)
if (retval) if (retval)
return retval; return retval;
verbosestream << "MenuTextureSource: loading " << name << std::endl; // Try to find the texture in the active texture pack
video::IImage *image = m_driver->createImageFromFile(name.c_str()); 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) if (!image)
return NULL; return NULL;
@ -81,6 +92,20 @@ video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id)
return retval; 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 */ /** MenuMusicFetcher */
/******************************************************************************/ /******************************************************************************/

View file

@ -74,7 +74,7 @@ public:
* default constructor * default constructor
* @param driver the video driver to load textures from * @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 * destructor, removes all loaded textures
@ -89,6 +89,12 @@ public:
video::ITexture *getTexture(const std::string &name, u32 *id = NULL); video::ITexture *getTexture(const std::string &name, u32 *id = NULL);
private: 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 */ /** driver to get textures from */
video::IVideoDriver *m_driver = nullptr; video::IVideoDriver *m_driver = nullptr;
/** set of textures to delete */ /** set of textures to delete */

View file

@ -114,10 +114,15 @@ GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick,
m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay"); m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay");
m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname"); m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname");
g_settings->registerChangedCallback("texture_path", onTxpSettingChanged, this);
setThemeFromSettings();
} }
GUIFormSpecMenu::~GUIFormSpecMenu() GUIFormSpecMenu::~GUIFormSpecMenu()
{ {
g_settings->deregisterAllChangedCallbacks(this);
removeAll(); removeAll();
delete m_selected_item; delete m_selected_item;
@ -2618,15 +2623,9 @@ void GUIFormSpecMenu::parsePadding(parserData *data, const std::string &element)
<< "'" << std::endl; << "'" << 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<std::string> *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<std::string> parts = split(element, ';'); std::vector<std::string> parts = split(element, ';');
if (parts.size() < 2) { if (parts.size() < 2) {
@ -2653,11 +2652,11 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element)
StyleSpec::Property prop = StyleSpec::GetPropertyByName(propname); StyleSpec::Property prop = StyleSpec::GetPropertyByName(propname);
if (prop == StyleSpec::NONE) { 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 << "): '" warningstream << "Invalid style element (Unknown property " << propname << "): '"
<< element << element
<< "'" << std::endl; << "'" << std::endl;
property_warned.insert(propname); prop_warned->insert(propname);
} }
continue; continue;
} }
@ -2705,11 +2704,7 @@ void GUIFormSpecMenu::parseStyle(parserData *data, const std::string &element)
continue; continue;
} }
if (style_type) { out[selector].push_back(selector_spec);
theme_by_type[selector].push_back(selector_spec);
} else {
theme_by_name[selector].push_back(selector_spec);
}
// Backwards-compatibility for existing _hovered/_pressed properties // Backwards-compatibility for existing _hovered/_pressed properties
if (selector_spec.hasProperty(StyleSpec::BGCOLOR_HOVERED) 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, "")); hover_spec.set(StyleSpec::FGIMG, selector_spec.get(StyleSpec::FGIMG_HOVERED, ""));
} }
if (style_type) { out[selector].push_back(hover_spec);
theme_by_type[selector].push_back(hover_spec);
} else {
theme_by_name[selector].push_back(hover_spec);
}
} }
if (selector_spec.hasProperty(StyleSpec::BGCOLOR_PRESSED) if (selector_spec.hasProperty(StyleSpec::BGCOLOR_PRESSED)
|| selector_spec.hasProperty(StyleSpec::BGIMG_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, "")); press_spec.set(StyleSpec::FGIMG, selector_spec.get(StyleSpec::FGIMG_PRESSED, ""));
} }
if (style_type) { out[selector].push_back(press_spec);
theme_by_type[selector].push_back(press_spec);
} else {
theme_by_name[selector].push_back(press_spec);
} }
} }
}
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;
} }
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) void GUIFormSpecMenu::parseSetFocus(parserData*, const std::string &element)
@ -3041,7 +3039,7 @@ void GUIFormSpecMenu::regenerateGui(v2u32 screensize)
m_dropdowns.clear(); m_dropdowns.clear();
m_scroll_containers.clear(); m_scroll_containers.clear();
theme_by_name.clear(); theme_by_name.clear();
theme_by_type.clear(); theme_by_type = theme_by_type_default;
m_clickthrough_elements.clear(); m_clickthrough_elements.clear();
field_enter_after_edit.clear(); field_enter_after_edit.clear();
field_close_on_enter.clear(); field_close_on_enter.clear();
@ -5028,6 +5026,40 @@ std::wstring GUIFormSpecMenu::getLabelByID(s32 id)
return L""; 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<std::string> *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, StyleSpec GUIFormSpecMenu::getDefaultStyleForElement(const std::string &type,
const std::string &name, const std::string &parent_type) { const std::string &name, const std::string &parent_type) {
return getStyleForElement(type, name, parent_type)[StyleSpec::STATE_DEFAULT]; return getStyleForElement(type, name, parent_type)[StyleSpec::STATE_DEFAULT];

View file

@ -302,10 +302,14 @@ protected:
bool precheckElement(const std::string &name, const std::string &element, bool precheckElement(const std::string &name, const std::string &element,
size_t args_min, size_t args_max, std::vector<std::string> &parts); size_t args_min, size_t args_max, std::vector<std::string> &parts);
std::unordered_map<std::string, std::vector<StyleSpec>> theme_by_type; using StyleSpecMap = std::unordered_map<std::string, std::vector<StyleSpec>>;
std::unordered_map<std::string, std::vector<StyleSpec>> theme_by_name; StyleSpecMap theme_by_type, theme_by_name,
theme_by_type_default;
std::unordered_set<std::string> property_warned; std::unordered_set<std::string> property_warned;
void setThemeFromSettings();
static void onTxpSettingChanged(const std::string &name, void *data);
StyleSpec getDefaultStyleForElement(const std::string &type, StyleSpec getDefaultStyleForElement(const std::string &type,
const std::string &name="", const std::string &parent_type=""); const std::string &name="", const std::string &parent_type="");
std::array<StyleSpec, StyleSpec::NUM_STATES> getStyleForElement(const std::string &type, std::array<StyleSpec, StyleSpec::NUM_STATES> getStyleForElement(const std::string &type,
@ -483,6 +487,8 @@ private:
void parseAnchor(parserData *data, const std::string &element); void parseAnchor(parserData *data, const std::string &element);
bool parsePaddingDirect(parserData *data, const std::string &element); bool parsePaddingDirect(parserData *data, const std::string &element);
void parsePadding(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<std::string> *prop_warned);
void parseStyle(parserData *data, const std::string &element); void parseStyle(parserData *data, const std::string &element);
void parseSetFocus(parserData *, const std::string &element); void parseSetFocus(parserData *, const std::string &element);
void parseModel(parserData *data, const std::string &element); void parseModel(parserData *data, const std::string &element);