diff --git a/changelog.md b/changelog.md index 493c1cbbf..1af250df9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,12 @@ TGUI 1.13 (TBD) ---------------- +- Implemented support for themes in forms ([Issue #325](https://github.com/texus/TGUI/issues/325)): + - `loadWidgetsFromStream` now clears and restores the default theme during load, matching `loadWidgetsFromFile` + - `FormLoadOptions::applyDefaultTheme` keeps the global default theme active during form load so widgets without a `Renderer` property match programmatic construction + - Form files may declare top-level `Theme.` sections with per-section renderer fallbacks (`Button = &1;`, etc.) + - `Renderer = @Alias` / `@Alias.Section` bind to runtime themes (`FormLoadOptions::themesByAlias`) or form fallbacks + - Form loading calls non-virtual `Widget::load(node, WidgetLoadResources)`, which sets a short-lived load context then dispatches to virtual `load(node, LoadingRenderersMap)` so custom widget subclasses keep a single override; `load(map)` builds a full `WidgetLoadResources` (including themes) and calls `Widget::loadUsingResources` - Added Emscripten support - Each tab in Tabs and VerticalTabs widgets can now be assigned a unique id - Position and size layout expressions weren't saved when the result equaled (0,0) diff --git a/include/TGUI/Backend/Window/BackendGui.hpp b/include/TGUI/Backend/Window/BackendGui.hpp index 990fa8c55..b6a489929 100644 --- a/include/TGUI/Backend/Window/BackendGui.hpp +++ b/include/TGUI/Backend/Window/BackendGui.hpp @@ -470,6 +470,11 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void loadWidgetsFromFile(const String& filename, bool replaceExisting = true); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a text file with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromFile(const String& filename, bool replaceExisting, const FormLoadOptions& options); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Saves the child widgets to a text file /// @@ -495,6 +500,16 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting = true); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a string stream with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromStream(std::stringstream& stream, bool replaceExisting, const FormLoadOptions& options); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a string stream with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting, const FormLoadOptions& options); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Saves this the child widgets to a text file /// diff --git a/include/TGUI/Container.hpp b/include/TGUI/Container.hpp index 9e15cd606..2d00bee09 100644 --- a/include/TGUI/Container.hpp +++ b/include/TGUI/Container.hpp @@ -25,6 +25,7 @@ #ifndef TGUI_CONTAINER_HPP #define TGUI_CONTAINER_HPP +#include #include ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -196,6 +197,11 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void loadWidgetsFromFile(const String& filename, bool replaceExisting = true); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a text file with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromFile(const String& filename, bool replaceExisting, const FormLoadOptions& options); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Saves the child widgets to a text file /// @@ -221,6 +227,16 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting = true); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a string stream with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromStream(std::stringstream& stream, bool replaceExisting, const FormLoadOptions& options); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the child widgets from a string stream with load options + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting, const FormLoadOptions& options); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Saves the child widgets to a text file /// @@ -615,7 +631,22 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Mutual code in loadWidgetsFromFile and loadWidgetsFromStream ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - void loadWidgetsImpl(const std::unique_ptr& rootNode, bool replaceExisting); + void loadWidgetsImpl(const std::unique_ptr& rootNode, bool replaceExisting, const FormLoadOptions& options); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads child widgets from a parsed form root using renderer and theme data from the node tree and options + /// + /// Scans @a rootNode for Renderer and Theme sections, builds renderer lookup maps, then instantiates each widget + /// section and loads it. Theme aliases in Renderer values (e.g. \@Name or \@Name.Section) are resolved from + /// @a options.themesByAlias, with Theme.Name blocks in the file as fallback. When @a replaceExisting is true, + /// existing child widgets are removed first. Clearing or preserving the global default theme during load is done + /// in loadWidgetsImpl before this function runs. + /// + /// @param rootNode Root node produced by DataIO::parse (widget form) + /// @param replaceExisting If true, remove all widgets before loading + /// @param options Theme bindings and other load options (see FormLoadOptions) + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadWidgetsFromNodeTree(const std::unique_ptr& rootNode, bool replaceExisting, const FormLoadOptions& options); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -635,6 +666,11 @@ namespace tgui friend class SubwidgetContainer; // Needs access to save and load functions + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @internal + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadContainedWidgetsFromNodes(const std::unique_ptr& node, const WidgetLoadResources& resources); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }; diff --git a/include/TGUI/FormLoadOptions.hpp b/include/TGUI/FormLoadOptions.hpp new file mode 100644 index 000000000..d96c6173b --- /dev/null +++ b/include/TGUI/FormLoadOptions.hpp @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TGUI - Texus' Graphical User Interface +// Copyright (C) 2012-2026 Bruno Van de Velde (vdv_b@tgui.eu) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef TGUI_FORM_LOAD_OPTIONS_HPP +#define TGUI_FORM_LOAD_OPTIONS_HPP + +#include +#include + +#include +#include + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace tgui +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Options for loadWidgetsFromFile / loadWidgetsFromStream overloads that take FormLoadOptions + /// + /// Controls runtime theme binding and default-theme behavior while a .txt form is loaded. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + struct TGUI_API FormLoadOptions + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Runtime themes keyed by alias name used in the form file + /// + /// When a widget line sets Renderer to \@Alias or \@Alias.Section, the renderer is taken from + /// themesByAlias[Alias] (Section defaults to the widget type if omitted). If the alias is missing from this map, + /// loading falls back to a matching Theme.Alias block in the same form file when present. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + std::map themesByAlias; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Whether Theme::getDefault() stays active for the duration of the load + /// + /// If false (default), the implementation temporarily clears the stored default theme while parsing and constructing + /// widgets (matching historical loadWidgetsFromFile behavior and aligning stream loading with file loading). + /// If true, the current default theme remains set so widgets without an explicit Renderer line keep the same + /// renderer they would get from normal construction with Theme::getDefault(). + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + bool applyDefaultTheme = false; + }; +} // namespace tgui + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // TGUI_FORM_LOAD_OPTIONS_HPP diff --git a/include/TGUI/ScopeExit.hpp b/include/TGUI/ScopeExit.hpp new file mode 100644 index 000000000..c55a37f41 --- /dev/null +++ b/include/TGUI/ScopeExit.hpp @@ -0,0 +1,90 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TGUI - Texus' Graphical User Interface +// Copyright (C) 2012-2026 Bruno Van de Velde (vdv_b@tgui.eu) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef TGUI_SCOPE_EXIT_HPP +#define TGUI_SCOPE_EXIT_HPP + +#include + +#include +#include + +namespace tgui +{ + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Invokes a function when leaving scope (return or exception). Not included from TGUI.hpp; include explicitly if needed. + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + template + class ScopeExit + { + public: + template ::type, ScopeExit>::value>::type> + explicit ScopeExit(G&& func) : + m_func(std::forward(func)), + m_active(true) + { + } + + ScopeExit(const ScopeExit&) = delete; + + ScopeExit& operator=(const ScopeExit&) = delete; + ScopeExit& operator=(ScopeExit&&) = delete; + + ScopeExit(ScopeExit&& other) noexcept(std::is_nothrow_move_constructible::value) : + m_func(std::move(other.m_func)), + m_active(other.m_active) + { + other.m_active = false; + } + + ~ScopeExit() + { + if (m_active) + m_func(); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Skip invoking the function when the guard is destroyed + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void release() noexcept + { + m_active = false; + } + + private: + F m_func; + bool m_active; + }; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Helper to create a ScopeExit without naming the lambda's type + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + template + TGUI_NODISCARD ScopeExit::type> makeScopeExit(F&& func) + { + return ScopeExit::type>(std::forward(func)); + } +} // namespace tgui + +#endif // TGUI_SCOPE_EXIT_HPP diff --git a/include/TGUI/Widget.hpp b/include/TGUI/Widget.hpp index 40a614124..0afde926c 100644 --- a/include/TGUI/Widget.hpp +++ b/include/TGUI/Widget.hpp @@ -45,6 +45,7 @@ #include #include #include +#include #include @@ -111,6 +112,11 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// virtual ~Widget(); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads the widget from a form node (with theme resources) + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void load(const std::unique_ptr& node, const WidgetLoadResources& resources); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Overload of copy assignment operator ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1159,6 +1165,11 @@ namespace tgui using SavingRenderersMap = std::map, String>>; using LoadingRenderersMap = std::map>; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Loads widget fields from a form node using the given resources + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void loadUsingResources(const std::unique_ptr& node, const WidgetLoadResources& resources); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Function called when one of the properties of the renderer is changed /// @@ -1313,6 +1324,9 @@ namespace tgui bool m_transparentTextureCached = false; unsigned int m_textSizeCached = 0; + const std::map>* m_loadRuntimeThemesByAlias = nullptr; + const ThemeFallbackMap* m_loadThemeFallbacks = nullptr; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// friend class Container; // Container accesses save and load functions diff --git a/include/TGUI/WidgetLoadResources.hpp b/include/TGUI/WidgetLoadResources.hpp new file mode 100644 index 000000000..b25e48cc5 --- /dev/null +++ b/include/TGUI/WidgetLoadResources.hpp @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TGUI - Texus' Graphical User Interface +// Copyright (C) 2012-2026 Bruno Van de Velde (vdv_b@tgui.eu) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef TGUI_WIDGET_LOAD_RESOURCES_HPP +#define TGUI_WIDGET_LOAD_RESOURCES_HPP + +#include +#include + +#include +#include + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace tgui +{ + class Theme; + + /// Fallback renderers from Theme.\ sections in the form file + using ThemeFallbackMap = std::map>>; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @internal + /// @brief Resources used when loading widgets from a form file + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + struct TGUI_API WidgetLoadResources + { + const std::map>& renderers; + const std::map>* runtimeThemesByAlias = nullptr; + const ThemeFallbackMap* themeFallbacks = nullptr; + + explicit WidgetLoadResources(const std::map>& r) : + renderers(r) + { + } + }; +} // namespace tgui + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // TGUI_WIDGET_LOAD_RESOURCES_HPP diff --git a/src/Backend/Window/BackendGui.cpp b/src/Backend/Window/BackendGui.cpp index 1cf72728b..a659e465f 100644 --- a/src/Backend/Window/BackendGui.cpp +++ b/src/Backend/Window/BackendGui.cpp @@ -537,6 +537,13 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::loadWidgetsFromFile(const String& filename, bool replaceExisting, const FormLoadOptions& options) + { + m_container->loadWidgetsFromFile(filename, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::saveWidgetsToFile(const String& filename) const { m_container->saveWidgetsToFile(filename); @@ -551,6 +558,13 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::loadWidgetsFromStream(std::stringstream& stream, bool replaceExisting, const FormLoadOptions& options) + { + m_container->loadWidgetsFromStream(stream, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting) { loadWidgetsFromStream(stream, replaceExisting); @@ -558,6 +572,13 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting, const FormLoadOptions& options) + { + loadWidgetsFromStream(stream, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BackendGui::saveWidgetsToStream(std::stringstream& stream) const { m_container->saveWidgetsToStream(stream); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a98bc94ff..adf1dc445 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -227,6 +227,7 @@ set(TGUI_HEADERS "${PROJECT_SOURCE_DIR}/include/TGUI/Exception.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/FileDialogIconLoader.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Filesystem.hpp" + "${PROJECT_SOURCE_DIR}/include/TGUI/FormLoadOptions.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Font.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Global.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Keyboard.hpp" @@ -280,6 +281,7 @@ set(TGUI_HEADERS "${PROJECT_SOURCE_DIR}/include/TGUI/Renderers/TextBoxRenderer.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Renderers/TreeViewRenderer.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Renderers/WidgetRenderer.hpp" + "${PROJECT_SOURCE_DIR}/include/TGUI/ScopeExit.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Signal.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/SignalManager.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Sprite.hpp" @@ -302,6 +304,7 @@ set(TGUI_HEADERS "${PROJECT_SOURCE_DIR}/include/TGUI/Vector2.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Vertex.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Widget.hpp" + "${PROJECT_SOURCE_DIR}/include/TGUI/WidgetLoadResources.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Widgets/BitmapButton.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Widgets/BoxLayout.hpp" "${PROJECT_SOURCE_DIR}/include/TGUI/Widgets/BoxLayoutRatios.hpp" diff --git a/src/Container.cpp b/src/Container.cpp index 7f8ee1c22..e9699a00c 100644 --- a/src/Container.cpp +++ b/src/Container.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -472,9 +473,13 @@ namespace tgui void Container::loadWidgetsFromFile(const String& filename, bool replaceExisting) { - auto oldTheme = Theme::getDefault(); - Theme::setDefault(nullptr); + loadWidgetsFromFile(filename, replaceExisting, FormLoadOptions{}); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void Container::loadWidgetsFromFile(const String& filename, bool replaceExisting, const FormLoadOptions& options) + { // If a resource path is set then place it in front of the filename (unless the filename is an absolute path) String filenameInResources = filename; if (!getResourcePath().isEmpty()) @@ -497,9 +502,7 @@ namespace tgui injectFormFilePath(rootNode, parentPath.asString(), checkedFilenames); } - loadWidgetsFromNodeTree(rootNode, replaceExisting); - - Theme::setDefault(oldTheme); + loadWidgetsImpl(rootNode, replaceExisting, options); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -523,9 +526,40 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Container::loadWidgetsFromStream(std::stringstream& stream, bool replaceExisting) + { + loadWidgetsFromStream(stream, replaceExisting, FormLoadOptions{}); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void Container::loadWidgetsFromStream(std::stringstream& stream, bool replaceExisting, const FormLoadOptions& options) { const auto rootNode = DataIO::parse(stream); - loadWidgetsFromNodeTree(rootNode, replaceExisting); + loadWidgetsImpl(rootNode, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void Container::loadWidgetsImpl(const std::unique_ptr& rootNode, bool replaceExisting, const FormLoadOptions& options) + { + const Theme::Ptr oldTheme = Theme::getDefault(); + if (!options.applyDefaultTheme) + Theme::setDefault(nullptr); + const auto restoreTheme = makeScopeExit( + [&oldTheme, &options] + { + if (!options.applyDefaultTheme) + Theme::setDefault(oldTheme); + }); + + loadWidgetsFromNodeTree(rootNode, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void Container::loadWidgetsFromStream(std::stringstream&& stream, bool replaceExisting, const FormLoadOptions& options) + { + loadWidgetsFromStream(stream, replaceExisting, options); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -546,20 +580,26 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void Container::loadWidgetsFromNodeTree(const std::unique_ptr& rootNode, bool replaceExisting) + { + FormLoadOptions options; + loadWidgetsFromNodeTree(rootNode, replaceExisting, options); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void Container::loadWidgetsFromNodeTree(const std::unique_ptr& rootNode, bool replaceExisting, const FormLoadOptions& options) { // Replace the existing widgets by the ones that will be loaded if requested if (replaceExisting) removeAllWidgets(); - if (!rootNode->propertyValuePairs.empty()) - Widget::load(rootNode, {}); + LoadingRenderersMap availableRenderers; + ThemeFallbackMap themeFallbacks; - std::vector>>> widgetsToLoad; - std::map> availableRenderers; for (const auto& node : rootNode->children) { - auto nameSeparator = node->name.find('.'); - auto widgetType = node->name.substr(0, nameSeparator); + const auto nameSeparator = node->name.find('.'); + const auto widgetType = node->name.substr(0, nameSeparator); String objectName; if (nameSeparator != String::npos) @@ -570,29 +610,91 @@ namespace tgui if (!objectName.empty()) availableRenderers[objectName] = RendererData::createFromDataIONode(node.get()); } - else // Section describes a widget + } + + for (const auto& node : rootNode->children) + { + const auto nameSeparator = node->name.find('.'); + const auto widgetType = node->name.substr(0, nameSeparator); + + String objectName; + if (nameSeparator != String::npos) + objectName = Deserializer::deserialize(ObjectConverter::Type::String, node->name.substr(nameSeparator + 1)).getString(); + + if (widgetType != U"Theme") + continue; + + if (objectName.empty()) + throw Exception{U"Theme section in widget file requires a name (e.g. Theme.MyTheme)."}; + for (const auto& pair : node->propertyValuePairs) { - const auto& constructor = WidgetFactory::getConstructFunction(widgetType); - if (constructor) + if (!pair.second) + continue; + + const String ref = pair.second->value.trim(); + if (!ref.starts_with(U'&')) + throw Exception{U"Invalid value for theme section '" + objectName + U"." + pair.first + + U"'. Expected renderer reference (e.g. &1)."}; + + const String id = ref.substr(1); + const auto rendererIt = availableRenderers.find(id); + if (rendererIt == availableRenderers.end()) + throw Exception{U"Theme '" + objectName + U"' references unknown renderer '" + id + U"'."}; + + themeFallbacks[objectName][pair.first] = rendererIt->second; + } + } + + WidgetLoadResources widgetResources(availableRenderers); + widgetResources.runtimeThemesByAlias = &options.themesByAlias; + widgetResources.themeFallbacks = &themeFallbacks; + + if (!rootNode->propertyValuePairs.empty()) + { + m_loadRuntimeThemesByAlias = widgetResources.runtimeThemesByAlias; + m_loadThemeFallbacks = widgetResources.themeFallbacks; + const auto clearLoadContext = makeScopeExit( + [this] { - const Widget::Ptr widget = constructor(); - add(widget, objectName); + m_loadRuntimeThemesByAlias = nullptr; + m_loadThemeFallbacks = nullptr; + }); + Widget::load(rootNode, availableRenderers); + } - // We delay loading of widgets until they have all been added to the container. - // Otherwise there would be issues if their position and size layouts refer to - // widgets that have not yet been loaded. - widgetsToLoad.emplace_back(widget, std::cref(node)); - } - else - throw Exception{U"No construct function exists for widget type '" + widgetType + U"'."}; + std::vector>>> widgetsToLoad; + for (const auto& node : rootNode->children) + { + const auto nameSeparator = node->name.find('.'); + const auto widgetType = node->name.substr(0, nameSeparator); + + String objectName; + if (nameSeparator != String::npos) + objectName = Deserializer::deserialize(ObjectConverter::Type::String, node->name.substr(nameSeparator + 1)).getString(); + + if ((widgetType == U"Renderer") || (widgetType == U"Theme")) + continue; + + const auto& constructor = WidgetFactory::getConstructFunction(widgetType); + if (constructor) + { + const Widget::Ptr widget = constructor(); + add(widget, objectName); + + // We delay loading of widgets until they have all been added to the container. + // Otherwise there would be issues if their position and size layouts refer to + // widgets that have not yet been loaded. + widgetsToLoad.emplace_back(widget, std::cref(node)); } + else + throw Exception{U"No construct function exists for widget type '" + widgetType + U"'."}; } for (auto& pair : widgetsToLoad) { const Widget::Ptr& widget = pair.first; const auto& node = pair.second.get(); - widget->load(node, availableRenderers); + widget->load(node, widgetResources); } } @@ -1118,10 +1220,8 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - void Container::load(const std::unique_ptr& node, const LoadingRenderersMap& renderers) + void Container::loadContainedWidgetsFromNodes(const std::unique_ptr& node, const WidgetLoadResources& resources) { - Widget::load(node, renderers); - std::vector>>> widgetsToLoad; for (const auto& childNode : node->children) { @@ -1151,12 +1251,23 @@ namespace tgui { const Widget::Ptr& childWidget = pair.first; const auto& childNode = pair.second.get(); - childWidget->load(childNode, renderers); + childWidget->load(childNode, resources); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void Container::load(const std::unique_ptr& node, const LoadingRenderersMap& renderers) + { + WidgetLoadResources wlr(renderers); + wlr.runtimeThemesByAlias = m_loadRuntimeThemesByAlias; + wlr.themeFallbacks = m_loadThemeFallbacks; + Widget::loadUsingResources(node, wlr); + loadContainedWidgetsFromNodes(node, wlr); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + bool Container::processMouseMoveEvent(Vector2f mousePos) { // Some widgets should always receive mouse move events while dragging them, even if the mouse is no longer on top of them diff --git a/src/SubwidgetContainer.cpp b/src/SubwidgetContainer.cpp index be47f0f9f..2776eb4cc 100644 --- a/src/SubwidgetContainer.cpp +++ b/src/SubwidgetContainer.cpp @@ -320,7 +320,10 @@ namespace tgui Widget::load(node, renderers); if (node->children.size() == 1) { - m_container->load(node->children[0], renderers); + WidgetLoadResources childWlr(renderers); + childWlr.runtimeThemesByAlias = m_loadRuntimeThemesByAlias; + childWlr.themeFallbacks = m_loadThemeFallbacks; + m_container->Widget::load(node->children[0], childWlr); m_container->setSize(getSize()); } } diff --git a/src/Widget.cpp b/src/Widget.cpp index cb1087a05..40a7013ab 100644 --- a/src/Widget.cpp +++ b/src/Widget.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +129,77 @@ namespace tgui return {str.substr(0, commaPos).trim().toFloat(), str.substr(commaPos + 1).trim().toFloat()}; } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void loadRendererFromFormValue(Widget* widget, const String& value, const WidgetLoadResources& resources) + { + const String trimmed = value.trim(); + if (trimmed.empty()) + throw Exception{U"Renderer property has empty value."}; + + if (trimmed[0] == U'&') + { + const auto it = resources.renderers.find(trimmed.substr(1)); + if (it == resources.renderers.end()) + throw Exception{ + U"Widget refers to renderer with name '" + trimmed.substr(1) + U"', but no such renderer was found"}; + + widget->setRenderer(it->second); + return; + } + + if (trimmed[0] == U'@') + { + const String body = trimmed.substr(1).trim(); + if (body.empty()) + throw Exception{U"Invalid renderer theme binding '@' with empty name."}; + + String alias; + String section; + + const auto dotPos = body.find('.'); + if (dotPos == String::npos) + { + alias = body; + section = widget->getWidgetType(); + } + else + { + alias = body.substr(0, dotPos); + section = body.substr(dotPos + 1); + } + + if (alias.empty() || section.empty()) + throw Exception{U"Invalid renderer theme binding '" + trimmed + U"'."}; + + if (resources.runtimeThemesByAlias) + { + const auto runtimeIt = resources.runtimeThemesByAlias->find(alias); + if (runtimeIt != resources.runtimeThemesByAlias->end() && runtimeIt->second) + { + widget->setRenderer(runtimeIt->second->getRenderer(section)); + return; + } + } + + if (!resources.themeFallbacks) + throw Exception{U"No runtime theme provided for alias '" + alias + U"' and no theme fallbacks are available."}; + + const auto fbThemeIt = resources.themeFallbacks->find(alias); + if (fbThemeIt == resources.themeFallbacks->end()) + throw Exception{U"No runtime theme or Theme section for alias '" + alias + U"'."}; + + const auto fbSectionIt = fbThemeIt->second.find(section); + if (fbSectionIt == fbThemeIt->second.end()) + throw Exception{U"Theme '" + alias + U"' has no fallback for section '" + section + U"'."}; + + widget->setRenderer(fbSectionIt->second); + return; + } + + throw Exception{U"Expected renderer reference '&' or theme binding '@' in Renderer property, got '" + trimmed + U"'."}; + } } // anonymous namespace ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1846,7 +1918,7 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - void Widget::load(const std::unique_ptr& node, const LoadingRenderersMap& renderers) + void Widget::loadUsingResources(const std::unique_ptr& node, const WidgetLoadResources& resources) { if (node->propertyValuePairs[U"Visible"]) setVisible(Deserializer::deserialize(ObjectConverter::Type::Bool, node->propertyValuePairs[U"Visible"]->value).getBool()); @@ -1948,17 +2020,7 @@ namespace tgui } if (node->propertyValuePairs[U"Renderer"]) - { - const String value = node->propertyValuePairs[U"Renderer"]->value; - if (value.empty() || (value[0] != '&')) - throw Exception{U"Expected reference to renderer, did not find '&' character"}; - - const auto it = renderers.find(value.substr(1)); - if (it == renderers.end()) - throw Exception{U"Widget refers to renderer with name '" + value.substr(1) + U"', but no such renderer was found"}; - - setRenderer(it->second); - } + loadRendererFromFormValue(this, node->propertyValuePairs[U"Renderer"]->value, resources); for (const auto& childNode : node->children) { @@ -1983,7 +2045,7 @@ namespace tgui if (constructor) { const Widget::Ptr toolTip = constructor(); - toolTip->load(toolTipWidgetNode, renderers); + toolTip->load(toolTipWidgetNode, resources); setToolTip(toolTip); } else @@ -2004,6 +2066,31 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void Widget::load(const std::unique_ptr& node, const WidgetLoadResources& resources) + { + const auto clearLoadContext = makeScopeExit( + [this] + { + m_loadRuntimeThemesByAlias = nullptr; + m_loadThemeFallbacks = nullptr; + }); + m_loadRuntimeThemesByAlias = resources.runtimeThemesByAlias; + m_loadThemeFallbacks = resources.themeFallbacks; + load(node, resources.renderers); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + void Widget::load(const std::unique_ptr& node, const LoadingRenderersMap& renderers) + { + WidgetLoadResources wlr(renderers); + wlr.runtimeThemesByAlias = m_loadRuntimeThemesByAlias; + wlr.themeFallbacks = m_loadThemeFallbacks; + loadUsingResources(node, wlr); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void Widget::updateTextSize() { } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ed985cd26..20766c36b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(TEST_SOURCES Global.cpp Layouts.cpp Loading/DataIO.cpp + Loading/FormThemeLoad.cpp Loading/Deserializer.cpp Loading/Serializer.cpp Loading/Theme.cpp diff --git a/tests/Loading/FormThemeLoad.cpp b/tests/Loading/FormThemeLoad.cpp new file mode 100644 index 000000000..bbe6edace --- /dev/null +++ b/tests/Loading/FormThemeLoad.cpp @@ -0,0 +1,779 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TGUI - Texus' Graphical User Interface +// Copyright (C) 2012-2026 Bruno Van de Velde (vdv_b@tgui.eu) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + /// Calls Widget::load(WidgetLoadResources) explicitly (ButtonBase::load(map) hides the base overload name). + class TestButton : public tgui::Button + { + public: + using Ptr = std::shared_ptr; + + static Ptr create() + { + return std::make_shared(); + } + + void loadExposed(const std::unique_ptr& node, const tgui::WidgetLoadResources& resources) + { + Widget::load(node, resources); + } + }; + + /// Custom Button that calls Widget::loadUsingResources with a WidgetLoadResources built only from the renderer map, + /// so it never sees FormLoadOptions runtime themes or Theme.* fallbacks. Matches legacy two-parameter form load when + /// those features are unused; other widgets on the same form still receive full options. + class ThemeBlindButton : public tgui::Button + { + public: + ThemeBlindButton() : + tgui::Button("ThemeBlindButton") + { + } + + static std::shared_ptr create() + { + return std::make_shared(); + } + + protected: + void load(const std::unique_ptr& node, const LoadingRenderersMap& renderers) override + { + tgui::WidgetLoadResources wlr(renderers); + Widget::loadUsingResources(node, wlr); + + if (node->propertyValuePairs[U"Text"]) + setText(tgui::Deserializer::deserialize(tgui::ObjectConverter::Type::String, node->propertyValuePairs[U"Text"]->value) + .getString()); + } + }; + + /// Custom Button with no load override: uses ButtonBase::load and the normal theme-aware path (same as stock Button). + class ThemeAwareCustomButton : public tgui::Button + { + public: + ThemeAwareCustomButton() : + tgui::Button("ThemeAwareCustomButton") + { + } + + static std::shared_ptr create() + { + return std::make_shared(); + } + }; + + void registerFormThemeLoadCustomWidgetTypes() + { + static const int once = [] + { + tgui::WidgetFactory::setConstructFunction(U"ThemeBlindButton", [] { return tgui::Widget::Ptr(ThemeBlindButton::create()); }); + tgui::WidgetFactory::setConstructFunction(U"ThemeAwareCustomButton", + [] { return tgui::Widget::Ptr(ThemeAwareCustomButton::create()); }); + return 0; + }(); + (void)once; + } + + void requireFormLoadThrows(const std::string& form) + { + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + REQUIRE_THROWS_AS(panel->loadWidgetsFromStream(stream), tgui::Exception); + } + + /// Property names from TGUI_RENDERER_PROPERTY_* macros in src/Renderers/ButtonRenderer.cpp (keep in sync). + constexpr std::array buttonRendererPropertyNames = { + "Borders", + "TextColor", + "TextColorDown", + "TextColorHover", + "TextColorDownHover", + "TextColorDisabled", + "TextColorDownDisabled", + "TextColorFocused", + "TextColorDownFocused", + "BackgroundColor", + "BackgroundColorDown", + "BackgroundColorHover", + "BackgroundColorDownHover", + "BackgroundColorDisabled", + "BackgroundColorDownDisabled", + "BackgroundColorFocused", + "BackgroundColorDownFocused", + "BorderColor", + "BorderColorDown", + "BorderColorHover", + "BorderColorDownHover", + "BorderColorDisabled", + "BorderColorDownDisabled", + "BorderColorFocused", + "BorderColorDownFocused", + "Texture", + "TextureDown", + "TextureHover", + "TextureDownHover", + "TextureDisabled", + "TextureDownDisabled", + "TextureFocused", + "TextureDownFocused", + "TextStyle", + "TextStyleDown", + "TextStyleHover", + "TextStyleDownHover", + "TextStyleDisabled", + "TextStyleDownDisabled", + "TextStyleFocused", + "TextStyleDownFocused", + "TextOutlineThickness", + "TextOutlineColor", + "RoundedBorderRadius", + }; +} // namespace + +TEST_CASE("[FormThemeLoad]") +{ + SECTION("Theme.MyTheme block with Renderer = &1 still loads") + { + const std::string form = + "Renderer.1 { TextColor = rgb(10, 20, 30); }\n" + "Theme.MyTheme { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + REQUIRE_NOTHROW(panel->loadWidgetsFromStream(stream)); + REQUIRE(panel->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color(10, 20, 30)); + } + + SECTION("Theme section without widget construct error is skipped") + { + const std::string form = + "Renderer.1 {}\n" + "Theme.X { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + REQUIRE_NOTHROW(panel->loadWidgetsFromStream(stream)); + } + + SECTION("Renderer = @Main uses Theme.Main fallback when no runtime theme") + { + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n"; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream); + REQUIRE(panel->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color(200, 100, 50)); + } + + SECTION("Runtime theme map overrides Theme section fallback") + { + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Cyan; + runtime->addRenderer(U"Button", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + REQUIRE(panel->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Cyan); + } + + SECTION("Runtime theme renderer data is shared with loaded widget") + { + const std::string form = + "Renderer.1 { TextColor = rgb(1, 2, 3); }\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Magenta; + runtime->addRenderer(U"Button", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + REQUIRE(panel->get("B1")->getSharedRenderer()->getData() == runtime->getRenderer(U"Button")); + } + + SECTION("Updating runtime theme after form load refreshes bound widget renderer") + { + const std::string form = + "Renderer.1 { TextColor = rgb(1, 2, 3); }\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n"; + + auto runtime = std::make_shared(); + auto rdBefore = std::make_shared(); + rdBefore->propertyValuePairs[U"TextColor"] = tgui::Color(11, 22, 33); + runtime->addRenderer(U"Button", rdBefore); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + // Theme renderer data is shared (setRenderer marks it shared). getRenderer() would clone and unsubscribe + // from the theme, so Theme::replace would no longer refresh this widget — use getSharedRenderer() to read. + REQUIRE(panel->get("B1")->getSharedRenderer()->getProperty("TextColor").getColor() == tgui::Color(11, 22, 33)); + + tgui::Theme replacement; + auto rdAfter = std::make_shared(); + rdAfter->propertyValuePairs[U"TextColor"] = tgui::Color(44, 55, 66); + replacement.addRenderer(U"Button", rdAfter); + + REQUIRE_NOTHROW(runtime->replace(replacement)); + + REQUIRE(panel->get("B1")->getSharedRenderer()->getProperty("TextColor").getColor() == tgui::Color(44, 55, 66)); + } + + SECTION("Theme references missing renderer id") + { + const std::string form = + "Theme.Main { Button = &missing; }\n" + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer refers to missing renderer id") + { + const std::string form = + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &missing; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer whitespace-only value throws") + { + const std::string form = + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = ; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer @ with no alias throws") + { + const std::string form = + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer @ alias without Theme or runtime theme throws") + { + const std::string form = + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @NoSuchTheme; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer @Alias.Section missing in Theme section throws") + { + const std::string form = + "Renderer.1 { TextColor = rgb(1, 2, 3); }\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main.Label; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Widget Renderer malformed theme binding throws") + { + requireFormLoadThrows( + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @.Button; }\n"); + + requireFormLoadThrows( + "Renderer.1 {}\n" + "Theme.Main { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main.; }\n"); + } + + SECTION("Widget Renderer value must start with & or @") + { + const std::string form = + "Renderer.1 {}\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = invalid; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Theme section without alias name throws") + { + const std::string form = + "Renderer.1 {}\n" + "Theme { Button = &1; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Theme section property must use renderer reference") + { + const std::string form = + "Renderer.1 {}\n" + "Theme.Main { Button = foo; }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + requireFormLoadThrows(form); + } + + SECTION("Panel forwards Theme.* fallbacks to nested buttons (two-parameter load)") + { + const std::string form = + "Renderer.1 { TextColor = rgb(55, 66, 77); }\n" + "Theme.Main { Button = &1; }\n" + "Panel.P1 {\n" + " Position = (0, 0);\n" + " Size = (400, 300);\n" + " Button.B1 { Position = (10, 10); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n" + "}\n"; + + tgui::Panel::Ptr root = tgui::Panel::create(); + std::stringstream stream(form); + root->loadWidgetsFromStream(stream); + + const auto p1 = std::dynamic_pointer_cast(root->get("P1")); + REQUIRE(p1 != nullptr); + REQUIRE(p1->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color(55, 66, 77)); + } + + SECTION("Panel forwards themesByAlias to nested buttons") + { + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; }\n" + "Panel.P1 {\n" + " Position = (0, 0);\n" + " Size = (400, 300);\n" + " Button.B1 { Position = (10, 10); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n" + "}\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Cyan; + runtime->addRenderer(U"Button", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr root = tgui::Panel::create(); + std::stringstream stream(form); + root->loadWidgetsFromStream(stream, true, opt); + + const auto p1 = std::dynamic_pointer_cast(root->get("P1")); + REQUIRE(p1 != nullptr); + REQUIRE(p1->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Cyan); + } + + SECTION("Nested Panel forwards themesByAlias to deeply nested button") + { + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; }\n" + "Panel.Outer {\n" + " Position = (0, 0);\n" + " Size = (500, 400);\n" + " Panel.Inner {\n" + " Position = (5, 5);\n" + " Size = (200, 150);\n" + " Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n" + " }\n" + "}\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Green; + runtime->addRenderer(U"Button", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr root = tgui::Panel::create(); + std::stringstream stream(form); + root->loadWidgetsFromStream(stream, true, opt); + + const auto outer = std::dynamic_pointer_cast(root->get("Outer")); + REQUIRE(outer != nullptr); + const auto inner = std::dynamic_pointer_cast(outer->get("Inner")); + REQUIRE(inner != nullptr); + REQUIRE(inner->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Green); + } + + SECTION("ChildWindow forwards themesByAlias to nested buttons") + { + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; }\n" + "ChildWindow.CW {\n" + " Position = (0, 0);\n" + " Size = (400, 300);\n" + " Title = \"T\";\n" + " Button.B1 { Position = (10, 10); Size = (50, 30); Text = \"X\"; Renderer = @Main; }\n" + "}\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Magenta; + runtime->addRenderer(U"Button", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr root = tgui::Panel::create(); + std::stringstream stream(form); + root->loadWidgetsFromStream(stream, true, opt); + + const auto cw = std::dynamic_pointer_cast(root->get("CW")); + REQUIRE(cw != nullptr); + REQUIRE(cw->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Magenta); + } + + SECTION("Theme-blind custom widget matches two-parameter load when FormLoadOptions are default") + { + registerFormThemeLoadCustomWidgetTypes(); + + const std::string form = + "Renderer.1 { TextColor = rgb(81, 82, 83); }\n" + "ThemeBlindButton.TB { Position = (10, 20); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::Panel::Ptr pOld = tgui::Panel::create(); + std::stringstream sOld(form); + pOld->loadWidgetsFromStream(sOld); + + tgui::Panel::Ptr pNew = tgui::Panel::create(); + std::stringstream sNew(form); + pNew->loadWidgetsFromStream(sNew, true, tgui::FormLoadOptions{}); + + REQUIRE(pOld->get("TB")->getPosition() == pNew->get("TB")->getPosition()); + REQUIRE(pOld->get("TB")->getSize() == pNew->get("TB")->getSize()); + REQUIRE(pOld->get("TB")->getRenderer()->getProperty("TextColor").getColor() + == pNew->get("TB")->getRenderer()->getProperty("TextColor").getColor()); + } + + SECTION("Theme-blind custom widget ignores themesByAlias; other widgets on the same form still use it") + { + registerFormThemeLoadCustomWidgetTypes(); + + // TB uses &1: a theme-blind load has no runtime/fallback theme context, so Renderer = @Main would throw. + const std::string form = + "Renderer.1 { TextColor = rgb(200, 100, 50); }\n" + "Theme.Main { Button = &1; ThemeAwareCustomButton = &1; }\n" + "ThemeBlindButton.TB { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n" + "ThemeAwareCustomButton.TAC { Position = (0, 40); Size = (50, 30); Text = \"Z\"; Renderer = @Main; }\n" + "Button.B1 { Position = (60, 0); Size = (50, 30); Text = \"Y\"; Renderer = @Main; }\n"; + + auto runtime = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Cyan; + runtime->addRenderer(U"Button", rd); + runtime->addRenderer(U"ThemeAwareCustomButton", rd); + + tgui::FormLoadOptions opt; + opt.themesByAlias[U"Main"] = runtime; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + REQUIRE(panel->get("TB")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color(200, 100, 50)); + REQUIRE(panel->get("TAC")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Cyan); + REQUIRE(panel->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Cyan); + } + + SECTION("Theme-blind custom widget ignores applyDefaultTheme; stock Button on the same form still uses default theme") + { + registerFormThemeLoadCustomWidgetTypes(); + + const tgui::Theme::Ptr oldDefault = tgui::Theme::getDefault(); + auto theme = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color::Yellow; + theme->addRenderer(U"Button", rd); + tgui::Theme::setDefault(theme); + + // Default theme has no ThemeBlindButton section; without Renderer in the form, TB has no TextColor (getColor asserts). + const std::string form = + "Renderer.1 { TextColor = rgb(90, 91, 92); }\n" + "ThemeBlindButton.TB { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n" + "Button.B1 { Position = (60, 0); Size = (50, 30); Text = \"Y\"; }\n"; + + tgui::FormLoadOptions optOn; + optOn.applyDefaultTheme = true; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, optOn); + + REQUIRE(panel->get("B1")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color::Yellow); + REQUIRE(panel->get("TB")->getRenderer()->getProperty("TextColor").getColor() == tgui::Color(90, 91, 92)); + + tgui::Theme::setDefault(oldDefault); + } + + SECTION("applyDefaultTheme true: sparse form renderer does not keep default theme texture image") + { + const tgui::Theme::Ptr oldDefault = tgui::Theme::getDefault(); + auto theme = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"Texture"] = tgui::Texture{"resources/Texture1.png"}; + theme->addRenderer(U"Button", rd); + tgui::Theme::setDefault(theme); + REQUIRE(rd->propertyValuePairs[U"Texture"].getTexture().getData() != nullptr); + + const std::string form = + "Renderer.1 { TextColor = rgb(11, 22, 33); }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::FormLoadOptions opt; + opt.applyDefaultTheme = true; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + const tgui::Button::Ptr btn = panel->get(U"B1"); + REQUIRE(btn->getSharedRenderer()->getProperty(U"TextColor").getColor() == tgui::Color(11, 22, 33)); + // Inline renderer has no Texture; setRenderer notifies removal of old Texture and ButtonBase::rendererChanged + // calls getTexture(), whose generated getter inserts Texture{} when the key is missing (RendererDefines.hpp). + REQUIRE(btn->getSharedRenderer()->getProperty(U"Texture").getType() == tgui::ObjectConverter::Type::Texture); + REQUIRE(btn->getSharedRenderer()->getProperty(U"Texture").getTexture().getData() == nullptr); + + tgui::Theme::setDefault(oldDefault); + } + + SECTION("applyDefaultTheme false: sparse inline renderer has no Texture key (built-in default Button has no Texture)") + { + const tgui::Theme::Ptr oldDefault = tgui::Theme::getDefault(); + auto theme = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"Texture"] = tgui::Texture{"resources/Texture1.png"}; + theme->addRenderer(U"Button", rd); + tgui::Theme::setDefault(theme); + + const std::string form = + "Renderer.1 { TextColor = rgb(11, 22, 33); }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::FormLoadOptions opt; + opt.applyDefaultTheme = false; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + const tgui::Button::Ptr btn = panel->get(U"B1"); + REQUIRE(btn->getSharedRenderer()->getProperty(U"TextColor").getColor() == tgui::Color(11, 22, 33)); + REQUIRE(btn->getSharedRenderer()->getPropertyValuePairs().count(U"Texture") == 0); + REQUIRE(btn->getSharedRenderer()->getProperty(U"Texture").getType() == tgui::ObjectConverter::Type::None); + + tgui::Theme::setDefault(oldDefault); + } + + SECTION("applyDefaultTheme true: button without Renderer keeps default theme Texture") + { + const tgui::Theme::Ptr oldDefault = tgui::Theme::getDefault(); + auto theme = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"Texture"] = tgui::Texture{"resources/Texture1.png"}; + theme->addRenderer(U"Button", rd); + tgui::Theme::setDefault(theme); + + const std::string form = "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; }\n"; + + tgui::FormLoadOptions opt; + opt.applyDefaultTheme = true; + + tgui::Panel::Ptr panel = tgui::Panel::create(); + std::stringstream stream(form); + panel->loadWidgetsFromStream(stream, true, opt); + + const tgui::Button::Ptr btn = panel->get(U"B1"); + REQUIRE(btn->getSharedRenderer()->getProperty(U"Texture").getType() != tgui::ObjectConverter::Type::None); + REQUIRE(btn->getSharedRenderer()->getProperty(U"Texture").getTexture().getData() != nullptr); + + tgui::Theme::setDefault(oldDefault); + } + + SECTION("applyDefaultTheme true vs false: sparse load matches on form fields; Texture key differs when default Button had Texture") + { + const tgui::Theme::Ptr oldDefault = tgui::Theme::getDefault(); + auto theme = std::make_shared(); + auto rd = std::make_shared(); + rd->propertyValuePairs[U"Texture"] = tgui::Texture{"resources/Texture1.png"}; + theme->addRenderer(U"Button", rd); + tgui::Theme::setDefault(theme); + + const std::string form = + "Renderer.1 { TextColor = rgb(44, 55, 66); }\n" + "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + + tgui::Panel::Ptr panelTrue = tgui::Panel::create(); + { + tgui::FormLoadOptions opt; + opt.applyDefaultTheme = true; + std::stringstream stream(form); + panelTrue->loadWidgetsFromStream(stream, true, opt); + } + + tgui::Theme::setDefault(theme); + + tgui::Panel::Ptr panelFalse = tgui::Panel::create(); + { + tgui::FormLoadOptions opt; + opt.applyDefaultTheme = false; + std::stringstream stream(form); + panelFalse->loadWidgetsFromStream(stream, true, opt); + } + + const tgui::Button::Ptr btnTrue = panelTrue->get(U"B1"); + const tgui::Button::Ptr btnFalse = panelFalse->get(U"B1"); + + REQUIRE(btnTrue->getSharedRenderer()->getProperty(U"TextColor") == btnFalse->getSharedRenderer()->getProperty(U"TextColor")); + REQUIRE(btnTrue->getSharedRenderer()->getPropertyValuePairs().count(U"Texture") == 1); + REQUIRE(btnFalse->getSharedRenderer()->getPropertyValuePairs().count(U"Texture") == 0); + + for (const char* propName : buttonRendererPropertyNames) + { + const tgui::String name(propName); + auto pTrue = btnTrue->getSharedRenderer()->getProperty(name); + auto pFalse = btnFalse->getSharedRenderer()->getProperty(name); + if (name == U"Texture") + { + REQUIRE(pFalse.getType() == tgui::ObjectConverter::Type::None); + REQUIRE(pTrue.getType() == tgui::ObjectConverter::Type::Texture); + REQUIRE(pTrue.getTexture().getData() == nullptr); + } + else + REQUIRE(pTrue == pFalse); + } + + tgui::Theme::setDefault(oldDefault); + } + + SECTION("ButtonRenderer::getTexture const getter can insert empty Texture when map key is missing") + { + tgui::Button::Ptr btn = tgui::Button::create(); + auto data = tgui::RendererData::create(); + data->propertyValuePairs[U"TextColor"] = tgui::Color::Red; + btn->setRenderer(data); + + REQUIRE(btn->getSharedRenderer()->getPropertyValuePairs().count(U"Texture") == 0); + (void)btn->getSharedRenderer()->getTexture(); + REQUIRE(btn->getSharedRenderer()->getPropertyValuePairs().count(U"Texture") == 1); + auto texProp = btn->getSharedRenderer()->getProperty(U"Texture"); + REQUIRE(texProp.getTexture().getData() == nullptr); + } + + SECTION("Widget::load with WidgetLoadResources matches former renderers-map-only behavior") + { + const std::string fragment = "Button.B1 { Position = (0, 0); Size = (50, 30); Text = \"X\"; Renderer = &1; }\n"; + auto rd = std::make_shared(); + rd->propertyValuePairs[U"TextColor"] = tgui::Color(201, 202, 203); + + auto parseNode = [&fragment] + { + std::stringstream ss(fragment); + auto root = tgui::DataIO::parse(ss); + REQUIRE(root->children.size() == 1); + return root; + }; + + std::map> renderersMap; + renderersMap[U"1"] = rd; + + auto root1 = parseNode(); + TestButton::Ptr wLocal = TestButton::create(); + tgui::WidgetLoadResources resLocal(renderersMap); + REQUIRE_NOTHROW(wLocal->loadExposed(root1->children[0], resLocal)); + + auto root2 = parseNode(); + TestButton::Ptr wTempRes = TestButton::create(); + REQUIRE_NOTHROW(wTempRes->loadExposed(root2->children[0], tgui::WidgetLoadResources(renderersMap))); + + auto root3 = parseNode(); + TestButton::Ptr wTempMap = TestButton::create(); + REQUIRE_NOTHROW( + wTempMap->loadExposed(root3->children[0], + tgui::WidgetLoadResources(std::map>{{U"1", rd}}))); + + const tgui::Color expected = tgui::Color(201, 202, 203); + REQUIRE(wLocal->getSharedRenderer()->getProperty("TextColor").getColor() == expected); + REQUIRE(wTempRes->getSharedRenderer()->getProperty("TextColor").getColor() == expected); + REQUIRE(wTempMap->getSharedRenderer()->getProperty("TextColor").getColor() == expected); + } + + SECTION("WidgetLoadResources can be constructed explicitly from renderers map") + { + std::map> map; + auto data = tgui::RendererData::create(); + map[U"1"] = data; + tgui::WidgetLoadResources res(map); + REQUIRE(&res.renderers == &map); + REQUIRE(res.renderers.find(U"1")->second == data); + } +}