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

3
.gitignore vendored
View File

@ -43,4 +43,5 @@ desktop.ini
*.tmp *.tmp
*.swp *.swp
*~ *~
nul nul
tmpclaude-*

View File

@ -23,6 +23,14 @@ namespace grove {
BgfxRendererModule::BgfxRendererModule() = default; BgfxRendererModule::BgfxRendererModule() = default;
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) { void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) {
m_io = io; m_io = io;

View File

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

View File

@ -205,9 +205,13 @@ public:
} }
UniformHandle createUniform(const char* name, uint8_t numVec4s) override { 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( bgfx::UniformHandle uniform = bgfx::createUniform(
name, name,
numVec4s == 1 ? bgfx::UniformType::Vec4 : bgfx::UniformType::Mat4 isSampler ? bgfx::UniformType::Sampler :
(numVec4s == 1 ? bgfx::UniformType::Vec4 : bgfx::UniformType::Mat4)
); );
UniformHandle result; UniformHandle result;
@ -557,6 +561,15 @@ public:
if (hasTexture) { if (hasTexture) {
bgfx::TextureHandle tex = { pendingTexture.id }; bgfx::TextureHandle tex = { pendingTexture.id };
bgfx::UniformHandle sampler = { pendingSampler.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::setTexture(pendingTextureSlot, sampler, tex);
} }
bgfx::ProgramHandle program = { cmd.submit.shader.id }; bgfx::ProgramHandle program = { cmd.submit.shader.id };

View File

@ -31,6 +31,10 @@ public:
// Loading (called from main thread) - returns texture ID // Loading (called from main thread) - returns texture ID
uint16_t loadTextureWithId(rhi::IRHIDevice& device, const std::string& path); 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) // Legacy loading (returns handle directly)
rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path); rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path);
rhi::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name, 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.borderRadius = static_cast<float>(styleNode->getDouble("borderRadius", style.borderRadius));
style.textureId = styleNode->getInt("textureId", 0); style.textureId = styleNode->getInt("textureId", 0);
style.useTexture = style.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) // Parse style (const_cast safe for read-only operations)

View File

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

View File

@ -49,6 +49,15 @@ public:
float fontSize = 16.0f; float fontSize = 16.0f;
float spacing = 8.0f; // Space between box and text 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 // State
bool isHovered = false; bool isHovered = false;
bool isPressed = false; bool isPressed = false;

View File

@ -31,7 +31,16 @@ void UIPanel::render(UIRenderer& renderer) {
// Retained mode: only publish if changed // Retained mode: only publish if changed
int layer = renderer.nextLayer(); int layer = renderer.nextLayer();
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, layer);
// 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 // Render children on top
renderChildren(renderer); renderChildren(renderer);

View File

@ -25,6 +25,11 @@ public:
float borderRadius = 0.0f; // For future use float borderRadius = 0.0f; // For future use
float borderWidth = 0.0f; float borderWidth = 0.0f;
uint32_t borderColor = 0x000000FF; 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 } // namespace grove

View File

@ -30,16 +30,28 @@ void UIProgressBar::render(UIRenderer& renderer) {
// Retained mode: only publish if changed // Retained mode: only publish if changed
int bgLayer = renderer.nextLayer(); int bgLayer = renderer.nextLayer();
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, bgLayer); 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 // Render fill based on progress
int fillLayer = renderer.nextLayer(); int fillLayer = renderer.nextLayer();
if (horizontal) { if (horizontal) {
float fillWidth = progress * width; float fillWidth = progress * width;
renderer.updateRect(m_fillRenderId, absX, absY, fillWidth, height, fillColor, fillLayer); 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 { } else {
float fillHeight = progress * height; float fillHeight = progress * height;
renderer.updateRect(m_fillRenderId, absX, absY + height - fillHeight, width, fillHeight, fillColor, fillLayer); 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 // Render percentage text if enabled

View File

@ -42,6 +42,15 @@ public:
uint32_t textColor = 0xFFFFFFFF; uint32_t textColor = 0xFFFFFFFF;
float fontSize = 14.0f; 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: private:
// Retained mode render IDs // Retained mode render IDs
uint32_t m_fillRenderId = 0; // Separate ID for fill bar element uint32_t m_fillRenderId = 0; // Separate ID for fill bar element

View File

@ -72,7 +72,11 @@ void UIScrollPanel::render(UIRenderer& renderer) {
// Render background // Render background
int bgLayer = renderer.nextLayer(); int bgLayer = renderer.nextLayer();
renderer.updateRect(m_renderId, absX, absY, width, height, bgColor, bgLayer); 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 // Render border if needed
if (borderWidth > 0.0f) { if (borderWidth > 0.0f) {
@ -225,7 +229,12 @@ void UIScrollPanel::renderScrollbar(UIRenderer& renderer) {
// Render scrollbar background track // Render scrollbar background track
float trackX = absX + width - scrollbarWidth; float trackX = absX + width - scrollbarWidth;
int trackLayer = renderer.nextLayer(); int trackLayer = renderer.nextLayer();
renderer.updateRect(m_scrollTrackId, trackX, absY, scrollbarWidth, height, scrollbarBgColor, trackLayer);
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 // Render scrollbar thumb
float sbX, sbY, sbW, sbH; 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) // Use hover color if hovered (would need ctx passed to render, simplified for now)
int thumbLayer = renderer.nextLayer(); int thumbLayer = renderer.nextLayer();
renderer.updateRect(m_scrollThumbId, sbX, sbY, sbW, sbH, scrollbarColor, thumbLayer);
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) { void UIScrollPanel::updateScrollInteraction(UIContext& ctx) {

View File

@ -48,6 +48,19 @@ public:
float borderWidth = 1.0f; float borderWidth = 1.0f;
uint32_t borderColor = 0x444444FF; 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 // Interaction state
bool isDraggingContent = false; bool isDraggingContent = false;
bool isDraggingScrollbar = false; bool isDraggingScrollbar = false;

View File

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

View File

@ -61,6 +61,19 @@ public:
uint32_t handleColor = 0xecf0f1FF; uint32_t handleColor = 0xecf0f1FF;
float handleSize = 16.0f; 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 // State
bool isDragging = false; bool isDragging = false;
bool isHovered = false; bool isHovered = false;