GroveEngine/modules/UIModule/Widgets/UIButton.cpp
StillHammer 63751d6f91 fix: Multi-texture sprite rendering - setState per batch + transient buffers
Fixed two critical bugs preventing multiple textured sprites from rendering correctly:

1. **setState consumed by submit**: Render state was set once at the beginning,
   but bgfx consumes state at each submit(). Batches 2+ had no state → invisible.

   FIX: Call setState() before EACH batch, not once globally.

2. **Buffer overwrite race condition**: updateBuffer() is immediate but submit()
   is deferred. When batch 2 called updateBuffer(), it overwrote batch 1's data
   BEFORE bgfx executed the draw calls. All batches used the last batch's data
   → all sprites rendered at the same position (superimposed).

   FIX: Use transient buffers (one per batch, frame-local) instead of reusing
   the same dynamic buffer. Each batch gets its own isolated memory.

Changes:
- SpritePass: setState before each batch + transient buffer allocation per batch
- UIRenderer: Retained mode rendering (render:sprite:add/update/remove)
- test_ui_showcase: Added 3 textured buttons demo section
- test_3buttons_minimal: Minimal test case for multi-texture debugging

Tested: 3 textured buttons now render at correct positions with correct textures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 14:05:56 +07:00

168 lines
5.5 KiB
C++

#include "UIButton.h"
#include "../Core/UIContext.h"
#include "../Rendering/UIRenderer.h"
#include <algorithm>
#include <cmath>
#include <spdlog/spdlog.h>
namespace grove {
void UIButton::update(UIContext& ctx, float deltaTime) {
// Update state based on enabled flag
if (!enabled) {
state = ButtonState::Disabled;
isHovered = false;
isPressed = false;
} else {
// State is managed by UIContext during hit testing
// We just update our visual state enum here
if (isPressed) {
state = ButtonState::Pressed;
} else if (isHovered) {
state = ButtonState::Hover;
} else {
state = ButtonState::Normal;
}
}
// Update children (buttons typically don't have children, but support it)
updateChildren(ctx, deltaTime);
}
void UIButton::render(UIRenderer& renderer) {
// Register with renderer on first render (need 2 entries: bg + text)
if (!m_registered) {
m_renderId = renderer.registerEntry(); // Background
m_textRenderId = renderer.registerEntry(); // Text
m_registered = true;
// Set destroy callback to unregister both
setDestroyCallback([&renderer, textId = m_textRenderId](uint32_t id) {
renderer.unregisterEntry(id);
renderer.unregisterEntry(textId);
});
}
const ButtonStyle& style = getCurrentStyle();
static int logCount = 0;
if (logCount < 10) { // Log first 10 buttons to see all textured ones
spdlog::info("UIButton[{}]::render() id='{}', state={}, normalStyle.textureId={}, useTexture={}",
logCount, id, (int)state, normalStyle.textureId, normalStyle.useTexture);
spdlog::info(" current style: textureId={}, useTexture={}", style.textureId, style.useTexture);
logCount++;
}
// Retained mode: only publish if changed
int bgLayer = renderer.nextLayer();
// Render background (texture or solid color)
if (style.useTexture && style.textureId > 0) {
spdlog::info("🎨 [UIButton '{}'] Rendering SPRITE: renderId={}, pos=({},{}), size={}x{}, textureId={}, color=0x{:08X}, layer={}",
id, m_renderId, absX, absY, width, height, style.textureId, style.bgColor, bgLayer);
renderer.updateSprite(m_renderId, absX, absY, width, height, style.textureId, style.bgColor, bgLayer);
} else {
renderer.updateRect(m_renderId, absX, absY, width, height, style.bgColor, bgLayer);
}
// Render text centered
if (!text.empty()) {
int textLayer = renderer.nextLayer();
float textX = absX + width * 0.5f;
float textY = absY + height * 0.5f;
renderer.updateText(m_textRenderId, textX, textY, text, fontSize, style.textColor, textLayer);
}
// Render children on top
renderChildren(renderer);
}
void UIButton::generateDefaultStyles() {
// If hover style wasn't explicitly set, lighten normal color
if (!hoverStyleSet) {
hoverStyle = normalStyle;
hoverStyle.bgColor = adjustBrightness(normalStyle.bgColor, 1.2f);
}
// If pressed style wasn't explicitly set, darken normal color
if (!pressedStyleSet) {
pressedStyle = normalStyle;
pressedStyle.bgColor = adjustBrightness(normalStyle.bgColor, 0.7f);
}
// Disabled style: desaturate and dim
disabledStyle = normalStyle;
disabledStyle.bgColor = adjustBrightness(normalStyle.bgColor, 0.5f);
disabledStyle.textColor = 0x888888FF;
}
uint32_t UIButton::adjustBrightness(uint32_t color, float factor) {
uint8_t r = (color >> 24) & 0xFF;
uint8_t g = (color >> 16) & 0xFF;
uint8_t b = (color >> 8) & 0xFF;
uint8_t a = color & 0xFF;
// Adjust RGB, clamp to 0-255
r = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, r * factor)));
g = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, g * factor)));
b = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, b * factor)));
return (r << 24) | (g << 16) | (b << 8) | a;
}
bool UIButton::containsPoint(float px, float py) const {
return px >= absX && px < absX + width &&
py >= absY && py < absY + height;
}
bool UIButton::onMouseButton(int button, bool pressed, float x, float y) {
if (!enabled) return false;
if (button == 0) { // Left mouse button
if (pressed) {
// Mouse down
if (containsPoint(x, y)) {
isPressed = true;
return true;
}
} else {
// Mouse up - only trigger click if still hovering
if (isPressed && containsPoint(x, y)) {
// Button clicked! Event will be published by UIModule
isPressed = false;
return true;
}
isPressed = false;
}
}
return false;
}
void UIButton::onMouseEnter() {
if (enabled) {
isHovered = true;
}
}
void UIButton::onMouseLeave() {
isHovered = false;
isPressed = false; // Cancel press if mouse leaves
}
const ButtonStyle& UIButton::getCurrentStyle() const {
switch (state) {
case ButtonState::Hover:
return hoverStyle;
case ButtonState::Pressed:
return pressedStyle;
case ButtonState::Disabled:
return disabledStyle;
case ButtonState::Normal:
default:
return normalStyle;
}
}
} // namespace grove