This commit implements Phase 7 of the UIModule, adding advanced features that make the UI system production-ready. ## Phase 7.1 - UIScrollPanel New scrollable container widget with: - Vertical and horizontal scrolling (configurable) - Mouse wheel support with smooth scrolling - Drag-to-scroll functionality (drag content or scrollbar) - Interactive scrollbar with proportional thumb - Automatic content size calculation - Visibility culling for performance - Full styling support (colors, borders, scrollbar) Files added: - modules/UIModule/Widgets/UIScrollPanel.h - modules/UIModule/Widgets/UIScrollPanel.cpp - modules/UIModule/Core/UIContext.h (added mouseWheelDelta) - modules/UIModule/UIModule.cpp (mouse wheel event routing) ## Phase 7.2 - Tooltips Smart tooltip system with: - Hover delay (500ms default) - Automatic positioning with edge avoidance - Semi-transparent background with border - Per-widget tooltip text via JSON - Tooltip property on all UIWidget types - Renders on top of all UI elements Files added: - modules/UIModule/Core/UITooltip.h - modules/UIModule/Core/UITooltip.cpp - modules/UIModule/Core/UIWidget.h (added tooltip property) - modules/UIModule/Core/UITree.cpp (tooltip parsing) ## Tests Added comprehensive visual tests: - test_28_ui_scroll.cpp - ScrollPanel with 35+ items - test_29_ui_advanced.cpp - Tooltips on various widgets - assets/ui/test_scroll.json - ScrollPanel layout - assets/ui/test_tooltips.json - Tooltips layout ## Documentation - docs/UI_MODULE_PHASE7_COMPLETE.md - Complete Phase 7 docs - docs/PROMPT_UI_MODULE_PHASE6.md - Phase 6 & 7 prompt - Updated CMakeLists.txt for new files and tests ## UIModule Status UIModule is now feature-complete with: ✅ 9 widget types (Panel, Label, Button, Image, Slider, Checkbox, ProgressBar, TextInput, ScrollPanel) ✅ Flexible layout system (vertical, horizontal, stack, absolute) ✅ Theme and style system ✅ Complete event system ✅ Tooltips with smart positioning ✅ Hot-reload support ✅ Comprehensive tests (Phases 1-7) 🚀 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
480 lines
23 KiB
C++
480 lines
23 KiB
C++
#include "UITree.h"
|
|
#include "UILayout.h"
|
|
#include "../Widgets/UIPanel.h"
|
|
#include "../Widgets/UILabel.h"
|
|
#include "../Widgets/UIButton.h"
|
|
#include "../Widgets/UIImage.h"
|
|
#include "../Widgets/UISlider.h"
|
|
#include "../Widgets/UICheckbox.h"
|
|
#include "../Widgets/UIProgressBar.h"
|
|
#include "../Widgets/UITextInput.h"
|
|
#include "../Widgets/UIScrollPanel.h"
|
|
#include <spdlog/spdlog.h>
|
|
#include <unordered_map>
|
|
#include <string>
|
|
|
|
namespace grove {
|
|
|
|
UITree::UITree() {
|
|
registerDefaultWidgets();
|
|
}
|
|
|
|
void UITree::registerWidget(const std::string& type, WidgetFactory factory) {
|
|
m_factories[type] = std::move(factory);
|
|
}
|
|
|
|
void UITree::registerDefaultWidgets() {
|
|
// Register panel factory
|
|
registerWidget("panel", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto panel = std::make_unique<UIPanel>();
|
|
|
|
// Parse style (const_cast safe for read-only operations)
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string bgColorStr = style->getString("bgColor", "0x333333FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
panel->bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
panel->borderRadius = static_cast<float>(style->getDouble("borderRadius", 0.0));
|
|
}
|
|
|
|
return panel;
|
|
});
|
|
|
|
// Register label factory
|
|
registerWidget("label", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto label = std::make_unique<UILabel>();
|
|
label->text = node.getString("text", "");
|
|
|
|
// Parse style (const_cast safe for read-only operations)
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string colorStr = style->getString("color", "0xFFFFFFFF");
|
|
if (colorStr.size() >= 2 && (colorStr.substr(0, 2) == "0x" || colorStr.substr(0, 2) == "0X")) {
|
|
label->color = static_cast<uint32_t>(std::stoul(colorStr, nullptr, 16));
|
|
}
|
|
label->fontSize = static_cast<float>(style->getDouble("fontSize", 16.0));
|
|
}
|
|
|
|
return label;
|
|
});
|
|
|
|
// Register button factory
|
|
registerWidget("button", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto button = std::make_unique<UIButton>();
|
|
button->text = node.getString("text", "");
|
|
button->onClick = node.getString("onClick", "");
|
|
button->enabled = node.getBool("enabled", true);
|
|
|
|
// Parse style (const_cast safe for read-only operations)
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
// Normal style
|
|
if (auto* normalStyle = style->getChildReadOnly("normal")) {
|
|
std::string bgColorStr = normalStyle->getString("bgColor", "0x444444FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
button->normalStyle.bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = normalStyle->getString("textColor", "0xFFFFFFFF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
button->normalStyle.textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
// Hover style
|
|
if (auto* hoverStyle = style->getChildReadOnly("hover")) {
|
|
std::string bgColorStr = hoverStyle->getString("bgColor", "0x666666FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
button->hoverStyle.bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = hoverStyle->getString("textColor", "0xFFFFFFFF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
button->hoverStyle.textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
// Pressed style
|
|
if (auto* pressedStyle = style->getChildReadOnly("pressed")) {
|
|
std::string bgColorStr = pressedStyle->getString("bgColor", "0x333333FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
button->pressedStyle.bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = pressedStyle->getString("textColor", "0xFFFFFFFF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
button->pressedStyle.textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
// Disabled style
|
|
if (auto* disabledStyle = style->getChildReadOnly("disabled")) {
|
|
std::string bgColorStr = disabledStyle->getString("bgColor", "0x222222FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
button->disabledStyle.bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = disabledStyle->getString("textColor", "0x666666FF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
button->disabledStyle.textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
// Font size from style root
|
|
button->fontSize = static_cast<float>(style->getDouble("fontSize", 16.0));
|
|
}
|
|
|
|
return button;
|
|
});
|
|
|
|
// Register image factory
|
|
registerWidget("image", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto image = std::make_unique<UIImage>();
|
|
image->textureId = node.getInt("textureId", 0);
|
|
image->texturePath = node.getString("texturePath", "");
|
|
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string tintStr = style->getString("tintColor", "0xFFFFFFFF");
|
|
if (tintStr.size() >= 2 && (tintStr.substr(0, 2) == "0x" || tintStr.substr(0, 2) == "0X")) {
|
|
image->tintColor = static_cast<uint32_t>(std::stoul(tintStr, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
return image;
|
|
});
|
|
|
|
// Register slider factory
|
|
registerWidget("slider", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto slider = std::make_unique<UISlider>();
|
|
slider->minValue = static_cast<float>(node.getDouble("min", 0.0));
|
|
slider->maxValue = static_cast<float>(node.getDouble("max", 100.0));
|
|
slider->value = static_cast<float>(node.getDouble("value", 50.0));
|
|
slider->step = static_cast<float>(node.getDouble("step", 0.0));
|
|
slider->horizontal = node.getBool("horizontal", true);
|
|
slider->onChange = node.getString("onChange", "");
|
|
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string trackColorStr = style->getString("trackColor", "0x34495eFF");
|
|
if (trackColorStr.size() >= 2 && (trackColorStr.substr(0, 2) == "0x" || trackColorStr.substr(0, 2) == "0X")) {
|
|
slider->trackColor = static_cast<uint32_t>(std::stoul(trackColorStr, nullptr, 16));
|
|
}
|
|
std::string fillColorStr = style->getString("fillColor", "0x3498dbFF");
|
|
if (fillColorStr.size() >= 2 && (fillColorStr.substr(0, 2) == "0x" || fillColorStr.substr(0, 2) == "0X")) {
|
|
slider->fillColor = static_cast<uint32_t>(std::stoul(fillColorStr, nullptr, 16));
|
|
}
|
|
std::string handleColorStr = style->getString("handleColor", "0xecf0f1FF");
|
|
if (handleColorStr.size() >= 2 && (handleColorStr.substr(0, 2) == "0x" || handleColorStr.substr(0, 2) == "0X")) {
|
|
slider->handleColor = static_cast<uint32_t>(std::stoul(handleColorStr, nullptr, 16));
|
|
}
|
|
slider->handleSize = static_cast<float>(style->getDouble("handleSize", 16.0));
|
|
}
|
|
|
|
return slider;
|
|
});
|
|
|
|
// Register checkbox factory
|
|
registerWidget("checkbox", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto checkbox = std::make_unique<UICheckbox>();
|
|
checkbox->checked = node.getBool("checked", false);
|
|
checkbox->text = node.getString("text", "");
|
|
checkbox->onChange = node.getString("onChange", "");
|
|
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string boxColorStr = style->getString("boxColor", "0x34495eFF");
|
|
if (boxColorStr.size() >= 2 && (boxColorStr.substr(0, 2) == "0x" || boxColorStr.substr(0, 2) == "0X")) {
|
|
checkbox->boxColor = static_cast<uint32_t>(std::stoul(boxColorStr, nullptr, 16));
|
|
}
|
|
std::string checkColorStr = style->getString("checkColor", "0x2ecc71FF");
|
|
if (checkColorStr.size() >= 2 && (checkColorStr.substr(0, 2) == "0x" || checkColorStr.substr(0, 2) == "0X")) {
|
|
checkbox->checkColor = static_cast<uint32_t>(std::stoul(checkColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = style->getString("textColor", "0xecf0f1FF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
checkbox->textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
checkbox->boxSize = static_cast<float>(style->getDouble("boxSize", 24.0));
|
|
checkbox->fontSize = static_cast<float>(style->getDouble("fontSize", 16.0));
|
|
checkbox->spacing = static_cast<float>(style->getDouble("spacing", 8.0));
|
|
}
|
|
|
|
return checkbox;
|
|
});
|
|
|
|
// Register progressbar factory
|
|
registerWidget("progressbar", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto progressBar = std::make_unique<UIProgressBar>();
|
|
progressBar->setProgress(static_cast<float>(node.getDouble("progress", 0.5)));
|
|
progressBar->horizontal = node.getBool("horizontal", true);
|
|
progressBar->showText = node.getBool("showText", false);
|
|
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string bgColorStr = style->getString("bgColor", "0x34495eFF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
progressBar->bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string fillColorStr = style->getString("fillColor", "0x2ecc71FF");
|
|
if (fillColorStr.size() >= 2 && (fillColorStr.substr(0, 2) == "0x" || fillColorStr.substr(0, 2) == "0X")) {
|
|
progressBar->fillColor = static_cast<uint32_t>(std::stoul(fillColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = style->getString("textColor", "0xFFFFFFFF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
progressBar->textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
progressBar->fontSize = static_cast<float>(style->getDouble("fontSize", 14.0));
|
|
}
|
|
|
|
return progressBar;
|
|
});
|
|
|
|
// Register textinput factory
|
|
registerWidget("textinput", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto textInput = std::make_unique<UITextInput>();
|
|
textInput->text = node.getString("text", "");
|
|
textInput->placeholder = node.getString("placeholder", "Enter text...");
|
|
textInput->maxLength = node.getInt("maxLength", 256);
|
|
textInput->passwordMode = node.getBool("passwordMode", false);
|
|
textInput->onSubmit = node.getString("onSubmit", "");
|
|
|
|
// Parse filter type
|
|
std::string filterStr = node.getString("filter", "none");
|
|
if (filterStr == "alphanumeric") {
|
|
textInput->filter = TextInputFilter::Alphanumeric;
|
|
} else if (filterStr == "numeric") {
|
|
textInput->filter = TextInputFilter::Numeric;
|
|
} else if (filterStr == "float") {
|
|
textInput->filter = TextInputFilter::Float;
|
|
} else if (filterStr == "nospaces") {
|
|
textInput->filter = TextInputFilter::NoSpaces;
|
|
} else {
|
|
textInput->filter = TextInputFilter::None;
|
|
}
|
|
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
// Normal style
|
|
std::string bgColorStr = style->getString("bgColor", "0x222222FF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
textInput->normalStyle.bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
std::string textColorStr = style->getString("textColor", "0xFFFFFFFF");
|
|
if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) {
|
|
textInput->normalStyle.textColor = static_cast<uint32_t>(std::stoul(textColorStr, nullptr, 16));
|
|
}
|
|
std::string borderColorStr = style->getString("borderColor", "0x666666FF");
|
|
if (borderColorStr.size() >= 2 && (borderColorStr.substr(0, 2) == "0x" || borderColorStr.substr(0, 2) == "0X")) {
|
|
textInput->normalStyle.borderColor = static_cast<uint32_t>(std::stoul(borderColorStr, nullptr, 16));
|
|
}
|
|
std::string focusBorderColorStr = style->getString("focusBorderColor", "0x4488FFFF");
|
|
if (focusBorderColorStr.size() >= 2 && (focusBorderColorStr.substr(0, 2) == "0x" || focusBorderColorStr.substr(0, 2) == "0X")) {
|
|
textInput->normalStyle.focusBorderColor = static_cast<uint32_t>(std::stoul(focusBorderColorStr, nullptr, 16));
|
|
}
|
|
|
|
// Copy normal style to focused and disabled
|
|
textInput->focusedStyle = textInput->normalStyle;
|
|
textInput->disabledStyle = textInput->normalStyle;
|
|
textInput->disabledStyle.bgColor = 0x111111FF;
|
|
textInput->disabledStyle.textColor = 0x666666FF;
|
|
|
|
textInput->fontSize = static_cast<float>(style->getDouble("fontSize", 16.0));
|
|
}
|
|
|
|
return textInput;
|
|
});
|
|
|
|
// Register scrollpanel factory
|
|
registerWidget("scrollpanel", [](const IDataNode& node) -> std::unique_ptr<UIWidget> {
|
|
auto scrollPanel = std::make_unique<UIScrollPanel>();
|
|
|
|
scrollPanel->scrollVertical = node.getBool("scrollVertical", true);
|
|
scrollPanel->scrollHorizontal = node.getBool("scrollHorizontal", false);
|
|
scrollPanel->showScrollbar = node.getBool("showScrollbar", true);
|
|
scrollPanel->dragToScroll = node.getBool("dragToScroll", true);
|
|
|
|
// Parse style
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* style = mutableNode.getChildReadOnly("style")) {
|
|
std::string bgColorStr = style->getString("bgColor", "0x2a2a2aFF");
|
|
if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) {
|
|
scrollPanel->bgColor = static_cast<uint32_t>(std::stoul(bgColorStr, nullptr, 16));
|
|
}
|
|
|
|
std::string borderColorStr = style->getString("borderColor", "0x444444FF");
|
|
if (borderColorStr.size() >= 2 && (borderColorStr.substr(0, 2) == "0x" || borderColorStr.substr(0, 2) == "0X")) {
|
|
scrollPanel->borderColor = static_cast<uint32_t>(std::stoul(borderColorStr, nullptr, 16));
|
|
}
|
|
|
|
std::string scrollbarColorStr = style->getString("scrollbarColor", "0x666666FF");
|
|
if (scrollbarColorStr.size() >= 2 && (scrollbarColorStr.substr(0, 2) == "0x" || scrollbarColorStr.substr(0, 2) == "0X")) {
|
|
scrollPanel->scrollbarColor = static_cast<uint32_t>(std::stoul(scrollbarColorStr, nullptr, 16));
|
|
}
|
|
|
|
scrollPanel->borderWidth = static_cast<float>(style->getDouble("borderWidth", 1.0));
|
|
scrollPanel->scrollbarWidth = static_cast<float>(style->getDouble("scrollbarWidth", 8.0));
|
|
}
|
|
|
|
return scrollPanel;
|
|
});
|
|
}
|
|
|
|
std::unique_ptr<UIWidget> UITree::loadFromJson(const IDataNode& layoutData) {
|
|
m_root = parseWidget(layoutData);
|
|
if (m_root) {
|
|
m_root->computeAbsolutePosition();
|
|
}
|
|
return std::move(m_root);
|
|
}
|
|
|
|
UIWidget* UITree::findById(const std::string& id) {
|
|
if (!m_root) return nullptr;
|
|
return m_root->findById(id);
|
|
}
|
|
|
|
std::unique_ptr<UIWidget> UITree::parseWidget(const IDataNode& node) {
|
|
std::string type = node.getString("type", "");
|
|
if (type.empty()) {
|
|
spdlog::warn("UITree: Widget missing 'type' property");
|
|
return nullptr;
|
|
}
|
|
|
|
auto it = m_factories.find(type);
|
|
if (it == m_factories.end()) {
|
|
spdlog::warn("UITree: Unknown widget type '{}'", type);
|
|
return nullptr;
|
|
}
|
|
|
|
// Create widget via factory
|
|
auto widget = it->second(node);
|
|
if (!widget) {
|
|
spdlog::warn("UITree: Factory failed for type '{}'", type);
|
|
return nullptr;
|
|
}
|
|
|
|
// Parse common properties
|
|
parseCommonProperties(widget.get(), node);
|
|
|
|
// Parse children recursively (const_cast safe for read-only operations)
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* children = mutableNode.getChildReadOnly("children")) {
|
|
auto childNames = children->getChildNames();
|
|
for (const auto& childName : childNames) {
|
|
if (auto* childNode = children->getChildReadOnly(childName)) {
|
|
if (auto child = parseWidget(*childNode)) {
|
|
widget->addChild(std::move(child));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check for array-style children (indexed by number)
|
|
// JsonDataNode stores array elements as children with numeric keys
|
|
int childIndex = 0;
|
|
while (true) {
|
|
std::string childKey = std::to_string(childIndex);
|
|
// Check if there's a child with this numeric key inside "children"
|
|
if (auto* childrenNode = mutableNode.getChildReadOnly("children")) {
|
|
if (auto* childNode = childrenNode->getChildReadOnly(childKey)) {
|
|
if (auto child = parseWidget(*childNode)) {
|
|
widget->addChild(std::move(child));
|
|
}
|
|
childIndex++;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return widget;
|
|
}
|
|
|
|
void UITree::parseCommonProperties(UIWidget* widget, const IDataNode& node) {
|
|
widget->id = node.getString("id", "");
|
|
widget->tooltip = node.getString("tooltip", "");
|
|
widget->x = static_cast<float>(node.getDouble("x", 0.0));
|
|
widget->y = static_cast<float>(node.getDouble("y", 0.0));
|
|
widget->width = static_cast<float>(node.getDouble("width", 0.0));
|
|
widget->height = static_cast<float>(node.getDouble("height", 0.0));
|
|
widget->visible = node.getBool("visible", true);
|
|
|
|
// Parse layout properties (Phase 2)
|
|
auto& mutableNode = const_cast<IDataNode&>(node);
|
|
if (auto* layout = mutableNode.getChildReadOnly("layout")) {
|
|
parseLayoutProperties(widget, *layout);
|
|
}
|
|
|
|
// Parse flex property (can be at root level)
|
|
if (node.hasChild("flex")) {
|
|
widget->layoutProps.flex = static_cast<float>(node.getDouble("flex", 0.0));
|
|
}
|
|
}
|
|
|
|
void UITree::parseLayoutProperties(UIWidget* widget, const IDataNode& layoutNode) {
|
|
// Layout mode
|
|
std::string modeStr = layoutNode.getString("type", "absolute");
|
|
static const std::unordered_map<std::string, LayoutMode> modeMap = {
|
|
{"vertical", LayoutMode::Vertical},
|
|
{"horizontal", LayoutMode::Horizontal},
|
|
{"stack", LayoutMode::Stack},
|
|
{"absolute", LayoutMode::Absolute}
|
|
};
|
|
auto modeIt = modeMap.find(modeStr);
|
|
if (modeIt != modeMap.end()) {
|
|
widget->layoutProps.mode = modeIt->second;
|
|
}
|
|
|
|
// Padding
|
|
widget->layoutProps.padding = static_cast<float>(layoutNode.getDouble("padding", 0.0));
|
|
widget->layoutProps.paddingTop = static_cast<float>(layoutNode.getDouble("paddingTop", 0.0));
|
|
widget->layoutProps.paddingRight = static_cast<float>(layoutNode.getDouble("paddingRight", 0.0));
|
|
widget->layoutProps.paddingBottom = static_cast<float>(layoutNode.getDouble("paddingBottom", 0.0));
|
|
widget->layoutProps.paddingLeft = static_cast<float>(layoutNode.getDouble("paddingLeft", 0.0));
|
|
|
|
// Margin
|
|
widget->layoutProps.margin = static_cast<float>(layoutNode.getDouble("margin", 0.0));
|
|
widget->layoutProps.marginTop = static_cast<float>(layoutNode.getDouble("marginTop", 0.0));
|
|
widget->layoutProps.marginRight = static_cast<float>(layoutNode.getDouble("marginRight", 0.0));
|
|
widget->layoutProps.marginBottom = static_cast<float>(layoutNode.getDouble("marginBottom", 0.0));
|
|
widget->layoutProps.marginLeft = static_cast<float>(layoutNode.getDouble("marginLeft", 0.0));
|
|
|
|
// Spacing
|
|
widget->layoutProps.spacing = static_cast<float>(layoutNode.getDouble("spacing", 0.0));
|
|
|
|
// Alignment
|
|
std::string alignStr = layoutNode.getString("align", "start");
|
|
static const std::unordered_map<std::string, Alignment> alignMap = {
|
|
{"start", Alignment::Start},
|
|
{"center", Alignment::Center},
|
|
{"end", Alignment::End},
|
|
{"stretch", Alignment::Stretch}
|
|
};
|
|
auto alignIt = alignMap.find(alignStr);
|
|
if (alignIt != alignMap.end()) {
|
|
widget->layoutProps.align = alignIt->second;
|
|
}
|
|
|
|
// Justification
|
|
std::string justifyStr = layoutNode.getString("justify", "start");
|
|
static const std::unordered_map<std::string, Justification> justifyMap = {
|
|
{"start", Justification::Start},
|
|
{"center", Justification::Center},
|
|
{"end", Justification::End},
|
|
{"spaceBetween", Justification::SpaceBetween},
|
|
{"spaceAround", Justification::SpaceAround}
|
|
};
|
|
auto justifyIt = justifyMap.find(justifyStr);
|
|
if (justifyIt != justifyMap.end()) {
|
|
widget->layoutProps.justify = justifyIt->second;
|
|
}
|
|
|
|
// Size constraints
|
|
widget->layoutProps.minWidth = static_cast<float>(layoutNode.getDouble("minWidth", 0.0));
|
|
widget->layoutProps.minHeight = static_cast<float>(layoutNode.getDouble("minHeight", 0.0));
|
|
widget->layoutProps.maxWidth = static_cast<float>(layoutNode.getDouble("maxWidth", -1.0));
|
|
widget->layoutProps.maxHeight = static_cast<float>(layoutNode.getDouble("maxHeight", -1.0));
|
|
|
|
// Flex
|
|
widget->layoutProps.flex = static_cast<float>(layoutNode.getDouble("flex", 0.0));
|
|
}
|
|
|
|
} // namespace grove
|