GroveEngine/modules/UIModule/Core/UITree.cpp
StillHammer 579cadeae8 feat: Complete UIModule Phase 7 - ScrollPanel & Tooltips
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>
2025-11-29 07:13:13 +08:00

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