Fixed three critical bugs preventing UITextInput from working: 1. **hitTest() missing textinput handler**: The hit test function only checked for button, slider, and checkbox types. Clicks on text input fields were never detected. FIX: Added textinput case to hitTest() in UIContext.cpp 2. **dispatchMouseButton() missing textinput handler**: Even if hit test worked, mouse button events were not dispatched to text input widgets. FIX: Added textinput case to dispatchMouseButton() in UIContext.cpp 3. **Keyboard event collision**: SDL_KEYDOWN was publishing events for printable characters with char=0, which were rejected by UITextInput. Printable chars should only come from SDL_TEXTINPUT. FIX: Only publish SDL_KEYDOWN for special keys (Backspace, Delete, arrows, etc.) Printable characters come exclusively from SDL_TEXTINPUT events. Changes: - UIContext.cpp: Added textinput handlers to hitTest() and dispatchMouseButton() - UITextInput.cpp: Added debug logging for gainFocus() and render() - UIModule.cpp: Added debug logging for widget clicks - test_ui_showcase.cpp: Fixed keyboard event handling (KEYDOWN vs TEXTINPUT) Tested: Text input now gains focus (border turns blue), accepts keyboard input, and displays typed text correctly. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
158 lines
4.8 KiB
C++
158 lines
4.8 KiB
C++
#include "UIContext.h"
|
|
#include "UIWidget.h"
|
|
#include "../Widgets/UIButton.h"
|
|
#include "../Widgets/UISlider.h"
|
|
#include "../Widgets/UICheckbox.h"
|
|
#include "../Widgets/UITextInput.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;
|
|
}
|
|
}
|
|
else if (type == "textinput") {
|
|
UITextInput* textInput = static_cast<UITextInput*>(widget);
|
|
if (textInput->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
|
|
}
|
|
}
|
|
else if (type == "textinput") {
|
|
UITextInput* textInput = static_cast<UITextInput*>(target);
|
|
handled = textInput->onMouseButton(button, pressed, ctx.mouseX, ctx.mouseY);
|
|
|
|
if (handled) {
|
|
return target; // Return for focus handling in UIModule
|
|
}
|
|
}
|
|
|
|
return handled ? target : nullptr;
|
|
}
|
|
|
|
} // namespace grove
|