feat: Add texture support to UI widgets and update gitignore

UI Widget Enhancements:
- Add texture support to UICheckbox (box and checkmark textures)
- Add texture support to UISlider (track and handle textures)
- Add texture support to UIPanel (background texture)
- Add texture support to UIProgressBar (background and fill textures)
- Add texture support to UIScrollPanel (background and scrollbar textures)
- All widgets now support textureId with tint color for flexible styling

BgfxRenderer:
- Add texture loading helpers for widget texturing
- Update RHI device for texture management
- Add ResourceCache texture ID support

Maintenance:
- Add tmpclaude-* to .gitignore (temporary Claude Code directories)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-14 23:15:13 +07:00
parent 0441a9d648
commit b39854cf2c
16 changed files with 198 additions and 32 deletions

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ desktop.ini
*.swp
*~
nul
tmpclaude-*

View File

@ -23,6 +23,14 @@ namespace grove {
BgfxRendererModule::BgfxRendererModule() = default;
BgfxRendererModule::~BgfxRendererModule() = default;
ResourceCache* BgfxRendererModule::getResourceCache() const {
return m_resourceCache.get();
}
rhi::IRHIDevice* BgfxRendererModule::getDevice() const {
return m_device.get();
}
void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) {
m_io = io;

View File

@ -45,6 +45,13 @@ public:
std::string getType() const override { return "bgfx_renderer"; }
bool isIdle() const override { return true; }
// ========================================
// Public API for external access
// ========================================
ResourceCache* getResourceCache() const;
rhi::IRHIDevice* getDevice() const;
private:
// Logger
std::shared_ptr<spdlog::logger> m_logger;

View File

@ -205,9 +205,13 @@ public:
}
UniformHandle createUniform(const char* name, uint8_t numVec4s) override {
// Detect sampler uniforms by name prefix (bgfx convention: s_*)
bool isSampler = (name[0] == 's' && name[1] == '_');
bgfx::UniformHandle uniform = bgfx::createUniform(
name,
numVec4s == 1 ? bgfx::UniformType::Vec4 : bgfx::UniformType::Mat4
isSampler ? bgfx::UniformType::Sampler :
(numVec4s == 1 ? bgfx::UniformType::Vec4 : bgfx::UniformType::Mat4)
);
UniformHandle result;
@ -557,6 +561,15 @@ public:
if (hasTexture) {
bgfx::TextureHandle tex = { pendingTexture.id };
bgfx::UniformHandle sampler = { pendingSampler.id };
static int submitCount = 0;
if (submitCount < 10) {
spdlog::info("[Submit #{}] BgfxDevice::submit() - pendingTexture.id={}, tex.idx={}, sampler.idx={}, slot={}",
submitCount, pendingTexture.id, tex.idx, sampler.idx, pendingTextureSlot);
spdlog::info(" bgfx::isValid(tex): {}", bgfx::isValid(tex));
submitCount++;
}
bgfx::setTexture(pendingTextureSlot, sampler, tex);
}
bgfx::ProgramHandle program = { cmd.submit.shader.id };

View File

@ -31,6 +31,10 @@ public:
// Loading (called from main thread) - returns texture ID
uint16_t loadTextureWithId(rhi::IRHIDevice& device, const std::string& path);
// Register an already-created texture (for runtime/procedural textures)
// Returns the assigned texture ID
uint16_t registerTexture(rhi::TextureHandle handle, const std::string& name = "");
// Legacy loading (returns handle directly)
rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path);
rhi::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name,

View File

@ -86,6 +86,9 @@ void UITree::registerDefaultWidgets() {
style.borderRadius = static_cast<float>(styleNode->getDouble("borderRadius", style.borderRadius));
style.textureId = styleNode->getInt("textureId", 0);
style.useTexture = style.textureId > 0;
if (style.textureId > 0) {
spdlog::info("UIButton style parsed: textureId={}, useTexture={}", style.textureId, style.useTexture);
}
};
// Parse style (const_cast safe for read-only operations)

View File

@ -34,13 +34,32 @@ void UICheckbox::render(UIRenderer& renderer) {
// Box background (retained mode)
int boxLayer = renderer.nextLayer();
uint32_t currentBoxColor = isHovered ? 0x475569FF : boxColor;
if (useBoxTexture && boxTextureId > 0) {
renderer.updateSprite(m_renderId, boxX, boxY, boxSize, boxSize, boxTextureId, boxTintColor, boxLayer);
} else {
renderer.updateRect(m_renderId, boxX, boxY, boxSize, boxSize, currentBoxColor, boxLayer);
}
// Check mark if checked (retained mode)
int checkLayer = renderer.nextLayer();
if (checked) {
// Draw a smaller filled rect as checkmark
float checkPadding = boxSize * 0.25f;
if (useCheckTexture && checkTextureId > 0) {
// Draw checkmark texture
renderer.updateSprite(
m_checkRenderId,
boxX + checkPadding,
boxY + checkPadding,
boxSize - checkPadding * 2,
boxSize - checkPadding * 2,
checkTextureId,
checkTintColor,
checkLayer
);
} else {
// Draw a smaller filled rect as checkmark
renderer.updateRect(
m_checkRenderId,
boxX + checkPadding,
@ -50,6 +69,7 @@ void UICheckbox::render(UIRenderer& renderer) {
checkColor,
checkLayer
);
}
} else {
// Hide checkmark when unchecked (zero-size rect)
renderer.updateRect(m_checkRenderId, 0, 0, 0, 0, 0x00000000, checkLayer);

View File

@ -49,6 +49,15 @@ public:
float fontSize = 16.0f;
float spacing = 8.0f; // Space between box and text
// Texture support
int boxTextureId = 0; // Box texture ID (0 = solid color)
bool useBoxTexture = false; // Use texture for box
uint32_t boxTintColor = 0xFFFFFFFF; // Tint for box texture
int checkTextureId = 0; // Checkmark texture ID (0 = solid rect)
bool useCheckTexture = false; // Use texture for checkmark
uint32_t checkTintColor = 0xFFFFFFFF; // Tint for checkmark texture
// State
bool isHovered = false;
bool isPressed = false;

View File

@ -31,7 +31,16 @@ void UIPanel::render(UIRenderer& renderer) {
// Retained mode: only publish if changed
int layer = renderer.nextLayer();
// Check if fully transparent (alpha channel = 0)
bool isFullyTransparent = (bgColor & 0xFF) == 0;
// Render background (texture or solid color) - skip if fully transparent
if (useTexture && textureId > 0) {
renderer.updateSprite(m_renderId, absX, absY, width, height, textureId, tintColor, layer);
} else if (!isFullyTransparent) {
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, layer);
}
// Render children on top
renderChildren(renderer);

View File

@ -25,6 +25,11 @@ public:
float borderRadius = 0.0f; // For future use
float borderWidth = 0.0f;
uint32_t borderColor = 0x000000FF;
// Texture support
int textureId = 0; // Texture ID (0 = solid color)
bool useTexture = false; // Use texture instead of solid color
uint32_t tintColor = 0xFFFFFFFF; // RGBA tint for texture (white = no tint)
};
} // namespace grove

View File

@ -30,17 +30,29 @@ void UIProgressBar::render(UIRenderer& renderer) {
// Retained mode: only publish if changed
int bgLayer = renderer.nextLayer();
if (useBgTexture && bgTextureId > 0) {
renderer.updateSprite(m_renderId, absX, absY, width, height, bgTextureId, bgTintColor, bgLayer);
} else {
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, bgLayer);
}
// Render fill based on progress
int fillLayer = renderer.nextLayer();
if (horizontal) {
float fillWidth = progress * width;
if (useFillTexture && fillTextureId > 0) {
renderer.updateSprite(m_fillRenderId, absX, absY, fillWidth, height, fillTextureId, fillTintColor, fillLayer);
} else {
renderer.updateRect(m_fillRenderId, absX, absY, fillWidth, height, fillColor, fillLayer);
}
} else {
float fillHeight = progress * height;
if (useFillTexture && fillTextureId > 0) {
renderer.updateSprite(m_fillRenderId, absX, absY + height - fillHeight, width, fillHeight, fillTextureId, fillTintColor, fillLayer);
} else {
renderer.updateRect(m_fillRenderId, absX, absY + height - fillHeight, width, fillHeight, fillColor, fillLayer);
}
}
// Render percentage text if enabled
if (showText) {

View File

@ -42,6 +42,15 @@ public:
uint32_t textColor = 0xFFFFFFFF;
float fontSize = 14.0f;
// Texture support
int bgTextureId = 0; // Background texture ID (0 = solid color)
bool useBgTexture = false; // Use texture for background
uint32_t bgTintColor = 0xFFFFFFFF; // Tint for background texture
int fillTextureId = 0; // Fill texture ID (0 = solid color)
bool useFillTexture = false; // Use texture for fill
uint32_t fillTintColor = 0xFFFFFFFF; // Tint for fill texture
private:
// Retained mode render IDs
uint32_t m_fillRenderId = 0; // Separate ID for fill bar element

View File

@ -72,7 +72,11 @@ void UIScrollPanel::render(UIRenderer& renderer) {
// Render background
int bgLayer = renderer.nextLayer();
if (useBgTexture && bgTextureId > 0) {
renderer.updateSprite(m_renderId, absX, absY, width, height, bgTextureId, bgTintColor, bgLayer);
} else {
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, bgLayer);
}
// Render border if needed
if (borderWidth > 0.0f) {
@ -225,7 +229,12 @@ void UIScrollPanel::renderScrollbar(UIRenderer& renderer) {
// Render scrollbar background track
float trackX = absX + width - scrollbarWidth;
int trackLayer = renderer.nextLayer();
if (useScrollbarTrackTexture && scrollbarTrackTextureId > 0) {
renderer.updateSprite(m_scrollTrackId, trackX, absY, scrollbarWidth, height, scrollbarTrackTextureId, scrollbarTrackTintColor, trackLayer);
} else {
renderer.updateRect(m_scrollTrackId, trackX, absY, scrollbarWidth, height, scrollbarBgColor, trackLayer);
}
// Render scrollbar thumb
float sbX, sbY, sbW, sbH;
@ -233,7 +242,12 @@ void UIScrollPanel::renderScrollbar(UIRenderer& renderer) {
// Use hover color if hovered (would need ctx passed to render, simplified for now)
int thumbLayer = renderer.nextLayer();
if (useScrollbarThumbTexture && scrollbarThumbTextureId > 0) {
renderer.updateSprite(m_scrollThumbId, sbX, sbY, sbW, sbH, scrollbarThumbTextureId, scrollbarThumbTintColor, thumbLayer);
} else {
renderer.updateRect(m_scrollThumbId, sbX, sbY, sbW, sbH, scrollbarColor, thumbLayer);
}
}
void UIScrollPanel::updateScrollInteraction(UIContext& ctx) {

View File

@ -48,6 +48,19 @@ public:
float borderWidth = 1.0f;
uint32_t borderColor = 0x444444FF;
// Texture support
int bgTextureId = 0; // Background texture ID (0 = solid color)
bool useBgTexture = false; // Use texture for background
uint32_t bgTintColor = 0xFFFFFFFF; // Tint for background texture
int scrollbarTrackTextureId = 0; // Scrollbar track texture ID (0 = solid color)
bool useScrollbarTrackTexture = false; // Use texture for scrollbar track
uint32_t scrollbarTrackTintColor = 0xFFFFFFFF; // Tint for scrollbar track texture
int scrollbarThumbTextureId = 0; // Scrollbar thumb texture ID (0 = solid color)
bool useScrollbarThumbTexture = false; // Use texture for scrollbar thumb
uint32_t scrollbarThumbTintColor = 0xFFFFFFFF; // Tint for scrollbar thumb texture
// Interaction state
bool isDraggingContent = false;
bool isDraggingScrollbar = false;

View File

@ -38,17 +38,29 @@ void UISlider::render(UIRenderer& renderer) {
// Render track (background)
int trackLayer = renderer.nextLayer();
if (useTrackTexture && trackTextureId > 0) {
renderer.updateSprite(m_renderId, absX, absY, width, height, trackTextureId, trackTintColor, trackLayer);
} else {
renderer.updateRect(m_renderId, absX, absY, width, height, trackColor, trackLayer);
}
// Render fill (progress)
int fillLayer = renderer.nextLayer();
if (horizontal) {
float fillWidth = (value - minValue) / (maxValue - minValue) * width;
if (useFillTexture && fillTextureId > 0) {
renderer.updateSprite(m_fillRenderId, absX, absY, fillWidth, height, fillTextureId, fillTintColor, fillLayer);
} else {
renderer.updateRect(m_fillRenderId, absX, absY, fillWidth, height, fillColor, fillLayer);
}
} else {
float fillHeight = (value - minValue) / (maxValue - minValue) * height;
if (useFillTexture && fillTextureId > 0) {
renderer.updateSprite(m_fillRenderId, absX, absY + height - fillHeight, width, fillHeight, fillTextureId, fillTintColor, fillLayer);
} else {
renderer.updateRect(m_fillRenderId, absX, absY + height - fillHeight, width, fillHeight, fillColor, fillLayer);
}
}
// Render handle
float handleX, handleY;
@ -57,6 +69,19 @@ void UISlider::render(UIRenderer& renderer) {
// Handle is a small square
int handleLayer = renderer.nextLayer();
float halfHandle = handleSize * 0.5f;
if (useHandleTexture && handleTextureId > 0) {
renderer.updateSprite(
m_handleRenderId,
handleX - halfHandle,
handleY - halfHandle,
handleSize,
handleSize,
handleTextureId,
handleTintColor,
handleLayer
);
} else {
renderer.updateRect(
m_handleRenderId,
handleX - halfHandle,
@ -66,6 +91,7 @@ void UISlider::render(UIRenderer& renderer) {
handleColor,
handleLayer
);
}
// Render children on top
renderChildren(renderer);

View File

@ -61,6 +61,19 @@ public:
uint32_t handleColor = 0xecf0f1FF;
float handleSize = 16.0f;
// Texture support
int trackTextureId = 0; // Track texture ID (0 = solid color)
bool useTrackTexture = false; // Use texture for track
uint32_t trackTintColor = 0xFFFFFFFF; // Tint for track texture
int fillTextureId = 0; // Fill texture ID (0 = solid color)
bool useFillTexture = false; // Use texture for fill
uint32_t fillTintColor = 0xFFFFFFFF; // Tint for fill texture
int handleTextureId = 0; // Handle texture ID (0 = solid color)
bool useHandleTexture = false; // Use texture for handle
uint32_t handleTintColor = 0xFFFFFFFF; // Tint for handle texture
// State
bool isDragging = false;
bool isHovered = false;