GroveEngine/modules/UIModule/Core/UIContext.cpp
StillHammer fd508e4a68 fix: UITextInput focus and keyboard input - hitTest + dispatch handlers
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>
2026-01-14 14:36:49 +07:00

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