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>
143 lines
4.3 KiB
C++
143 lines
4.3 KiB
C++
#include "UIContext.h"
|
|
#include "UIWidget.h"
|
|
#include "../Widgets/UIButton.h"
|
|
#include "../Widgets/UISlider.h"
|
|
#include "../Widgets/UICheckbox.h"
|
|
#include <spdlog/spdlog.h>
|
|
|
|
namespace grove {
|
|
|
|
/**
|
|
* @brief Perform hit testing to find the topmost widget at a point
|
|
*
|
|
* Recursively searches the widget tree from front to back (reverse order)
|
|
* to find the topmost visible widget containing the point.
|
|
*
|
|
* @param widget Root widget to search from
|
|
* @param x Point X coordinate
|
|
* @param y Point Y coordinate
|
|
* @return Topmost widget at point, or nullptr
|
|
*/
|
|
UIWidget* hitTest(UIWidget* widget, float x, float y) {
|
|
if (!widget || !widget->visible) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Check children first (front to back = reverse order for hit testing)
|
|
for (auto it = widget->children.rbegin(); it != widget->children.rend(); ++it) {
|
|
UIWidget* hit = hitTest(it->get(), x, y);
|
|
if (hit) {
|
|
return hit;
|
|
}
|
|
}
|
|
|
|
// Check this widget if it's interactive
|
|
std::string type = widget->getType();
|
|
|
|
if (type == "button") {
|
|
UIButton* button = static_cast<UIButton*>(widget);
|
|
if (button->containsPoint(x, y)) {
|
|
return widget;
|
|
}
|
|
}
|
|
else if (type == "slider") {
|
|
UISlider* slider = static_cast<UISlider*>(widget);
|
|
if (slider->containsPoint(x, y)) {
|
|
return widget;
|
|
}
|
|
}
|
|
else if (type == "checkbox") {
|
|
UICheckbox* checkbox = static_cast<UICheckbox*>(widget);
|
|
if (checkbox->containsPoint(x, y)) {
|
|
return widget;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Update hover state for all widgets in tree
|
|
*
|
|
* Calls onMouseEnter/onMouseLeave for buttons based on hover state.
|
|
*
|
|
* @param widget Root widget
|
|
* @param ctx UI context with hover state
|
|
* @param prevHoveredId Previous frame's hovered widget ID
|
|
*/
|
|
void updateHoverState(UIWidget* widget, UIContext& ctx, const std::string& prevHoveredId) {
|
|
if (!widget) return;
|
|
|
|
// Check if this widget's hover state changed
|
|
if (widget->getType() == "button") {
|
|
UIButton* button = static_cast<UIButton*>(widget);
|
|
|
|
bool wasHovered = (widget->id == prevHoveredId);
|
|
bool isHovered = (widget->id == ctx.hoveredWidgetId);
|
|
|
|
if (isHovered && !wasHovered) {
|
|
button->onMouseEnter();
|
|
} else if (!isHovered && wasHovered) {
|
|
button->onMouseLeave();
|
|
}
|
|
}
|
|
|
|
// Recurse to children
|
|
for (auto& child : widget->children) {
|
|
updateHoverState(child.get(), ctx, prevHoveredId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Dispatch mouse button event to widget tree
|
|
*
|
|
* Finds the widget under the mouse and delivers the event.
|
|
*
|
|
* @param widget Root widget
|
|
* @param ctx UI context
|
|
* @param button Mouse button (0 = left, 1 = right, 2 = middle)
|
|
* @param pressed true if button pressed, false if released
|
|
* @return Widget that handled the event (for action publishing), or nullptr
|
|
*/
|
|
UIWidget* dispatchMouseButton(UIWidget* widget, UIContext& ctx, int button, bool pressed) {
|
|
// Hit test to find target widget
|
|
UIWidget* target = hitTest(widget, ctx.mouseX, ctx.mouseY);
|
|
|
|
if (!target) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Dispatch to appropriate widget type
|
|
std::string type = target->getType();
|
|
bool handled = false;
|
|
|
|
if (type == "button") {
|
|
UIButton* btn = static_cast<UIButton*>(target);
|
|
handled = btn->onMouseButton(button, pressed, ctx.mouseX, ctx.mouseY);
|
|
|
|
if (handled && !pressed && !btn->onClick.empty()) {
|
|
return target; // Return for action publishing
|
|
}
|
|
}
|
|
else if (type == "slider") {
|
|
UISlider* slider = static_cast<UISlider*>(target);
|
|
handled = slider->onMouseButton(button, pressed, ctx.mouseX, ctx.mouseY);
|
|
|
|
if (handled) {
|
|
return target; // Return for value_changed publishing
|
|
}
|
|
}
|
|
else if (type == "checkbox") {
|
|
UICheckbox* checkbox = static_cast<UICheckbox*>(target);
|
|
handled = checkbox->onMouseButton(button, pressed, ctx.mouseX, ctx.mouseY);
|
|
|
|
if (handled) {
|
|
return target; // Return for value_changed publishing
|
|
}
|
|
}
|
|
|
|
return handled ? target : nullptr;
|
|
}
|
|
|
|
} // namespace grove
|