Added a null texture slot to FontGlyphAtlas

This commit is contained in:
OmniaX-Dev 2026-04-19 22:15:51 +02:00
parent a20e5849f5
commit 0cf7478c24
8 changed files with 268 additions and 227 deletions

View file

@ -24,6 +24,7 @@ Add theme caching
Implement cursors in Stylesheet Implement cursors in Stylesheet
FIX: Window getting exponentially bigger when Desktop scale changes FIX: Window getting exponentially bigger when Desktop scale changes
FIX: Refreshing scroll when content extent changes FIX: Refreshing scroll when content extent changes
Add Dark Mode Default theme

View file

@ -133,6 +133,111 @@ namespace ogfx
default: break; default: break;
} }
} }
TabPanel& TabPanel::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::widgets::TabPanel");
disableDrawBox();
disableFocus();
enableStopEvents();
validate();
return *this;
}
void TabPanel::applyTheme(const ostd::Stylesheet& theme)
{
}
void TabPanel::onMouseReleased(const Event& event)
{
}
void TabPanel::onDraw(ogfx::BasicRenderer2D& gfx)
{
}
Panel& TabPanel::addTab(const String& title)
{
m_tabs.push_back(std::make_unique<Panel>(getWindow()));
auto& tab = *m_tabs.back();
// Initialization code here
return tab;
}
bool TabPanel::removeTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
if (m_currentTab == it->get())
prepare_for_current_tab_removal();
m_tabs.erase(it);
return true;
}
bool TabPanel::removeTab(i32 index)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (std::distance(m_tabs.begin(), it) == index)
prepare_for_current_tab_removal();
m_tabs.erase(m_tabs.begin() + index);
return true;
}
bool TabPanel::removeTab(const String& title)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p->getTitle() == title; });
if (it == m_tabs.end())
return false;
m_tabs.erase(it);
return true;
}
bool TabPanel::setCurrentTab(Panel& tab)
{
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == &tab; });
if (it == m_tabs.end())
return false;
m_currentTab = it->get();
return true;
}
bool TabPanel::setCurrentTab(i32 index)
{
if (index < 0 || index >= (i32)m_tabs.size())
return false;
m_currentTab = m_tabs[index].get();
return true;
}
void TabPanel::prepare_for_current_tab_removal(void)
{
if (m_currentTab == nullptr)
return;
if (m_tabs.size() < 2)
{
m_currentTab = nullptr;
return;
}
auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&](const std::unique_ptr<Panel>& p) { return p.get() == m_currentTab; });
if (it == m_tabs.end())
return; // shouldn't happen but defensive
if (it != m_tabs.begin())
m_currentTab = (it - 1)->get(); // tab to the left
else
m_currentTab = (it + 1)->get(); // first tab being removed, go right
}
} }
} }
} }

View file

@ -70,6 +70,30 @@ namespace ogfx
Color m_titlebarBorderColor { Colors::Black }; Color m_titlebarBorderColor { Colors::Black };
i32 m_titleTextAlign { 0 }; i32 m_titleTextAlign { 0 };
}; };
class TabPanel : public Widget
{
public:
inline TabPanel(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
TabPanel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onMouseReleased(const Event& event) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
Panel& addTab(const String& title);
bool removeTab(Panel& tab);
bool removeTab(i32 index);
bool removeTab(const String& title);
bool setCurrentTab(Panel& tab);
bool setCurrentTab(i32 index);
private:
void prepare_for_current_tab_removal(void);
private:
stdvec<std::unique_ptr<Panel>> m_tabs;
Panel* m_currentTab { nullptr };
f32 m_tabBarHeight { 30 };
};
} }
} }
} }

View file

@ -66,6 +66,9 @@ namespace ogfx
loadDefaultFont(20); loadDefaultFont(20);
m_initialized = false; m_initialized = false;
m_fontGlyphAtlas.init(*this); m_fontGlyphAtlas.init(*this);
auto atlas = m_fontGlyphAtlas.getAtlas(0);
if (atlas) //TODO: Error
m_lastUsedGlyphAtlasTex = atlas;
m_initialized = true; m_initialized = true;
return set_error_state(tErrors::NoError); return set_error_state(tErrors::NoError);
} }
@ -89,6 +92,7 @@ namespace ogfx
void BasicRenderer2D::endFrame(void) void BasicRenderer2D::endFrame(void)
{ {
flushBatch(); flushBatch();
m_lastFrameDrawCallCount = m_drawCallCount;
m_drawCallCount = 0; m_drawCallCount = 0;
} }
@ -309,7 +313,7 @@ namespace ogfx
// 5. Push quad // 5. Push quad
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, uvs, 4, inds, 6, Colors::White, tex); push_polygon(verts, uvs, 4, inds, 6, Colors::White, tex, false);
} }
void BasicRenderer2D::drawAnimation(const Animation& anim, const Vec2& position, const Vec2& size) void BasicRenderer2D::drawAnimation(const Animation& anim, const Vec2& position, const Vec2& size)
@ -416,8 +420,8 @@ namespace ogfx
{ x, y + g->size.y * scale } { x, y + g->size.y * scale }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, g->uvs, 4, inds, 6, color, g->atlas); push_polygon(verts, g->uvs, 4, inds, 6, color, g->atlas, false);
m_lastUsedGlyphAtlasTex = g->atlas;
x += (g->advance * scale); x += (g->advance * scale);
} }
setFontSize(oldFontSize); setFontSize(oldFontSize);
@ -462,14 +466,14 @@ namespace ogfx
}}; }};
std::array<u32, 6> inds = QUAD_INDICES_ARR; std::array<u32, 6> inds = QUAD_INDICES_ARR;
push_polygon(verts.data(), nullptr, 4, inds.data(), 6, color, nullptr); push_polygon(verts.data(), nullptr, 4, inds.data(), 6, color, nullptr, true);
if (!rounded || thickness < 4) if (!rounded || thickness < 4)
return; return;
i32 segments = std::max(16, i32(thickness * 1.5f)); i32 segments = std::max(16, i32(thickness * 1.5f));
generate_half_circle(p1, -dir, half, segments, color); generate_half_circle(p1, -dir, half, segments, color, true);
generate_half_circle(p2, dir, half, segments, color); generate_half_circle(p2, dir, half, segments, color, true);
} }
void BasicRenderer2D::drawRect(const Rectangle& rect, const Color& color, i32 thickness) void BasicRenderer2D::drawRect(const Rectangle& rect, const Color& color, i32 thickness)
@ -546,10 +550,10 @@ namespace ogfx
// Corner arcs // Corner arcs
auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); }; auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); };
if (rTL > 0) generate_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, thickness, M_PI, M_PI * 1.5f, color, segments(rTL)); if (rTL > 0) generate_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, thickness, M_PI, M_PI * 1.5f, color, segments(rTL), true);
if (rTR > 0) generate_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, thickness, M_PI * 1.5f, M_PI * 2.0f, color, segments(rTR)); if (rTR > 0) generate_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, thickness, M_PI * 1.5f, M_PI * 2.0f, color, segments(rTR), true);
if (rBR > 0) generate_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, thickness, 0.0f, M_PI * 0.5f, color, segments(rBR)); if (rBR > 0) generate_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, thickness, 0.0f, M_PI * 0.5f, color, segments(rBR), true);
if (rBL > 0) generate_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, thickness, M_PI * 0.5f, M_PI, color, segments(rBL)); if (rBL > 0) generate_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, thickness, M_PI * 0.5f, M_PI, color, segments(rBL), true);
} }
void BasicRenderer2D::drawCircle(const Vec2& center, f32 radius, const Color& color, i32 thickness) void BasicRenderer2D::drawCircle(const Vec2& center, f32 radius, const Color& color, i32 thickness)
@ -557,7 +561,7 @@ namespace ogfx
if (!m_initialized || thickness <= 0) if (!m_initialized || thickness <= 0)
return; return;
i32 segments = std::max(16, i32(radius * 1.5f)); i32 segments = std::max(16, i32(radius * 1.5f));
generate_ellipse_stroke(center, radius, radius, thickness, 0.0f, 2.0f * M_PI, color, segments); generate_ellipse_stroke(center, radius, radius, thickness, 0.0f, 2.0f * M_PI, color, segments, true);
} }
void BasicRenderer2D::drawCircle(const Rectangle& rect, const Color& color, i32 thickness) void BasicRenderer2D::drawCircle(const Rectangle& rect, const Color& color, i32 thickness)
@ -583,7 +587,7 @@ namespace ogfx
f32 rx = rect.w * 0.5f; f32 rx = rect.w * 0.5f;
f32 ry = rect.h * 0.5f; f32 ry = rect.h * 0.5f;
i32 segments = std::max(16, i32(std::max(rx, ry) * 1.5f)); i32 segments = std::max(16, i32(std::max(rx, ry) * 1.5f));
generate_ellipse_stroke(center, rx, ry, thickness, 0.0f, 2.0f * M_PI, color, segments); generate_ellipse_stroke(center, rx, ry, thickness, 0.0f, 2.0f * M_PI, color, segments, true);
} }
void BasicRenderer2D::drawTriangle(const Triangle& tri, const Color& color, i32 thickness) void BasicRenderer2D::drawTriangle(const Triangle& tri, const Color& color, i32 thickness)
@ -622,7 +626,7 @@ namespace ogfx
{ x1, y2 } { x1, y2 }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
void BasicRenderer2D::fillRect(const Vec2& center, const Vec2& size, const Color& color) void BasicRenderer2D::fillRect(const Vec2& center, const Vec2& size, const Color& color)
@ -676,7 +680,7 @@ namespace ogfx
{ x1 + leftInset, y2 - bottomInset } { x1 + leftInset, y2 - bottomInset }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Top strip // Top strip
@ -688,7 +692,7 @@ namespace ogfx
{ x1 + rTL, y1 + topInset } { x1 + rTL, y1 + topInset }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Bottom strip // Bottom strip
@ -700,7 +704,7 @@ namespace ogfx
{ x1 + rBL, y2 } { x1 + rBL, y2 }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Left strip // Left strip
@ -712,7 +716,7 @@ namespace ogfx
{ x1, y2 - rBL } { x1, y2 - rBL }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Right strip // Right strip
@ -724,7 +728,7 @@ namespace ogfx
{ x2 - rightInset, y2 - rBR } { x2 - rightInset, y2 - rBR }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Fill gaps between corners and strips when radii differ on the same side // Fill gaps between corners and strips when radii differ on the same side
@ -738,7 +742,7 @@ namespace ogfx
{ x1, y1 + topInset } { x1, y1 + topInset }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Top-right gap // Top-right gap
@ -751,7 +755,7 @@ namespace ogfx
{ x2 - rightInset, y1 + topInset } { x2 - rightInset, y1 + topInset }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Bottom-left gap // Bottom-left gap
@ -764,7 +768,7 @@ namespace ogfx
{ x1, y2 - rBL } { x1, y2 - rBL }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Bottom-right gap // Bottom-right gap
@ -777,7 +781,7 @@ namespace ogfx
{ x2 - rightInset, y2 - rBR } { x2 - rightInset, y2 - rBR }
}; };
u32 inds[6] = QUAD_INDICES_ARR; u32 inds[6] = QUAD_INDICES_ARR;
push_polygon(verts, nullptr, 4, inds, 6, color, nullptr); push_polygon(verts, nullptr, 4, inds, 6, color, nullptr, true);
} }
// Left-side gap (if rTL != rBL, leftInset is the max, smaller one needs a gap) // Left-side gap (if rTL != rBL, leftInset is the max, smaller one needs a gap)
@ -793,10 +797,10 @@ namespace ogfx
// Corner fills // Corner fills
auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); }; auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); };
if (rTL > 0) generate_filled_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, M_PI, color, segments(rTL)); if (rTL > 0) generate_filled_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, M_PI, color, segments(rTL), true);
if (rTR > 0) generate_filled_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, M_PI * 1.5f, color, segments(rTR)); if (rTR > 0) generate_filled_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, M_PI * 1.5f, color, segments(rTR), true);
if (rBR > 0) generate_filled_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, 0.0f, color, segments(rBR)); if (rBR > 0) generate_filled_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, 0.0f, color, segments(rBR), true);
if (rBL > 0) generate_filled_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, M_PI * 0.5f, color, segments(rBL)); if (rBL > 0) generate_filled_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, M_PI * 0.5f, color, segments(rBL), true);
} }
void BasicRenderer2D::fillCircle(const Vec2& center, f32 radius, const Color& color) void BasicRenderer2D::fillCircle(const Vec2& center, f32 radius, const Color& color)
@ -805,7 +809,7 @@ namespace ogfx
return; return;
i32 segments = std::max(16, i32(radius * 1.5f)); i32 segments = std::max(16, i32(radius * 1.5f));
generate_filled_ellipse(center, radius, radius, color, segments); generate_filled_ellipse(center, radius, radius, color, segments, true);
} }
void BasicRenderer2D::fillCircle(const Rectangle& rect, const Color& color) void BasicRenderer2D::fillCircle(const Rectangle& rect, const Color& color)
@ -836,7 +840,7 @@ namespace ogfx
f32 radiusY = rect.h * 0.5f; f32 radiusY = rect.h * 0.5f;
i32 segments = std::max(16, i32(std::max(radiusX, radiusY) * 1.5f)); i32 segments = std::max(16, i32(std::max(radiusX, radiusY) * 1.5f));
generate_filled_ellipse(center, radiusX, radiusY, color, segments); generate_filled_ellipse(center, radiusX, radiusY, color, segments, true);
} }
void BasicRenderer2D::fillTriangle(const Triangle& tri, const Color& color) void BasicRenderer2D::fillTriangle(const Triangle& tri, const Color& color)
@ -968,7 +972,7 @@ namespace ogfx
i = 0; i = 0;
} }
void BasicRenderer2D::generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color) void BasicRenderer2D::generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color, bool use_null_tex)
{ {
// Ensure we have room // Ensure we have room
if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices) if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices)
@ -993,6 +997,10 @@ namespace ogfx
f32 startAngle = baseAngle - M_PI * 0.5f; f32 startAngle = baseAngle - M_PI * 0.5f;
f32 endAngle = baseAngle + M_PI * 0.5f; f32 endAngle = baseAngle + M_PI * 0.5f;
Vec2 uvs = { 0.0f, 0.0f };
if (use_null_tex)
uvs = m_fontGlyphAtlas.getNullTextureSlotUVs();
// Generate arc vertices // Generate arc vertices
for (i32 i = 0; i <= segments; i++) for (i32 i = 0; i <= segments; i++)
{ {
@ -1005,7 +1013,7 @@ namespace ogfx
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ x, y }, { x, y },
col, col,
{ 0.0f, 0.0f } { uvs.x, uvs.y }
}; };
} }
@ -1018,132 +1026,7 @@ namespace ogfx
} }
} }
void BasicRenderer2D::generate_quarter_circle(const Vec2& center, f32 radius, f32 thickness, f32 startAngle, const Color& color, i32 segments) void BasicRenderer2D::generate_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 thickness, f32 startAngle, f32 endAngle, const Color& color, i32 segments, bool use_null_tex)
{
f32 half = thickness * 0.5f;
f32 outerR = radius + half;
f32 innerR = radius - half;
if (innerR < 0.0f)
innerR = 0.0f;
// Ensure capacity
if (m_vertexCount + (segments + 1) * 2 >= MaxVertices || m_indexCount + segments * 6 >= MaxIndices)
flushBatch();
SDL_FColor col = COLOR_CAST(color);
i32 base = m_vertexCount;
f32 endAngle = startAngle + M_PI * 0.5f;
// Generate vertices: outer arc + inner arc
for (i32 i = 0; i <= segments; i++)
{
f32 t = f32(i) / f32(segments);
f32 angle = startAngle + t * (endAngle - startAngle);
f32 cs = std::cos(angle);
f32 sn = std::sin(angle);
// Outer arc
m_vertices[m_vertexCount++] = {
{ center.x + cs * outerR, center.y + sn * outerR },
col,
{ 0, 0 }
};
// Inner arc
m_vertices[m_vertexCount++] = {
{ center.x + cs * innerR, center.y + sn * innerR },
col,
{ 0, 0 }
};
}
// Generate indices (triangle strip converted to triangles)
for (i32 i = 0; i < segments; i++)
{
i32 o0 = base + i * 2;
i32 i0 = o0 + 1;
i32 o1 = o0 + 2;
i32 i1 = o0 + 3;
// Triangle 1
m_indices[m_indexCount++] = o0;
m_indices[m_indexCount++] = o1;
m_indices[m_indexCount++] = i0;
// Triangle 2
m_indices[m_indexCount++] = o1;
m_indices[m_indexCount++] = i1;
m_indices[m_indexCount++] = i0;
}
}
void BasicRenderer2D::generate_circle_stroke(const Vec2& center, f32 radius, f32 thickness, const Color& color, i32 segments)
{
f32 half = thickness * 0.5f;
f32 outerR = radius + half;
f32 innerR = radius - half;
if (innerR < 0.0f)
innerR = 0.0f;
// Ensure capacity
if (m_vertexCount + (segments + 1) * 2 >= MaxVertices || m_indexCount + segments * 6 >= MaxIndices)
flushBatch();
SDL_FColor col = COLOR_CAST(color);
i32 base = m_vertexCount;
// Generate vertices: outer arc + inner arc
for (i32 i = 0; i <= segments; i++)
{
f32 t = f32(i) / f32(segments);
f32 angle = t * (2.0f * M_PI);
f32 cs = std::cos(angle);
f32 sn = std::sin(angle);
// Outer arc
m_vertices[m_vertexCount++] = {
{ center.x + cs * outerR, center.y + sn * outerR },
col,
{ 0, 0 }
};
// Inner arc
m_vertices[m_vertexCount++] = {
{ center.x + cs * innerR, center.y + sn * innerR },
col,
{ 0, 0 }
};
}
// Generate indices (triangle strip → triangles)
for (i32 i = 0; i < segments; i++)
{
i32 o0 = base + i * 2;
i32 i0 = o0 + 1;
i32 o1 = o0 + 2;
i32 i1 = o0 + 3;
// Triangle 1
m_indices[m_indexCount++] = o0;
m_indices[m_indexCount++] = o1;
m_indices[m_indexCount++] = i0;
// Triangle 2
m_indices[m_indexCount++] = o1;
m_indices[m_indexCount++] = i1;
m_indices[m_indexCount++] = i0;
}
}
void BasicRenderer2D::generate_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 thickness, f32 startAngle, f32 endAngle, const Color& color, i32 segments)
{ {
f32 half = thickness * 0.5f; f32 half = thickness * 0.5f;
@ -1165,6 +1048,10 @@ namespace ogfx
f32 angleRange = endAngle - startAngle; f32 angleRange = endAngle - startAngle;
Vec2 uvs = { 0.0f, 0.0f };
if (use_null_tex)
uvs = m_fontGlyphAtlas.getNullTextureSlotUVs();
// Generate vertices: outer arc + inner arc // Generate vertices: outer arc + inner arc
for (i32 i = 0; i <= segments; i++) for (i32 i = 0; i <= segments; i++)
{ {
@ -1178,14 +1065,14 @@ namespace ogfx
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ center.x + cs * outerRX, center.y + sn * outerRY }, { center.x + cs * outerRX, center.y + sn * outerRY },
col, col,
{ 0, 0 } { uvs.x, uvs.y }
}; };
// Inner arc // Inner arc
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ center.x + cs * innerRX, center.y + sn * innerRY }, { center.x + cs * innerRX, center.y + sn * innerRY },
col, col,
{ 0, 0 } { uvs.x, uvs.y }
}; };
} }
@ -1209,7 +1096,7 @@ namespace ogfx
} }
} }
void BasicRenderer2D::generate_filled_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 startAngle, const Color& color, i32 segments) void BasicRenderer2D::generate_filled_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 startAngle, const Color& color, i32 segments, bool use_null_tex)
{ {
// Ensure capacity // Ensure capacity
if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices) if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices)
@ -1228,6 +1115,10 @@ namespace ogfx
f32 endAngle = startAngle + M_PI * 0.5f; f32 endAngle = startAngle + M_PI * 0.5f;
Vec2 uvs = { 0.0f, 0.0f };
if (use_null_tex)
uvs = m_fontGlyphAtlas.getNullTextureSlotUVs();
for (i32 i = 0; i <= segments; i++) for (i32 i = 0; i <= segments; i++)
{ {
f32 t = f32(i) / f32(segments); f32 t = f32(i) / f32(segments);
@ -1239,7 +1130,7 @@ namespace ogfx
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ x, y }, { x, y },
col, col,
{ 0, 0 } { uvs.x, uvs.y }
}; };
} }
@ -1252,7 +1143,7 @@ namespace ogfx
} }
} }
void BasicRenderer2D::generate_filled_ellipse(const Vec2& center, f32 radiusX, f32 radiusY, const Color& color, i32 segments) void BasicRenderer2D::generate_filled_ellipse(const Vec2& center, f32 radiusX, f32 radiusY, const Color& color, i32 segments, bool use_null_tex)
{ {
// Ensure capacity // Ensure capacity
if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices) if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices)
@ -1269,6 +1160,10 @@ namespace ogfx
{ 0, 0 } { 0, 0 }
}; };
Vec2 uvs = { 0.0f, 0.0f };
if (use_null_tex)
uvs = m_fontGlyphAtlas.getNullTextureSlotUVs();
// Outer ring vertices // Outer ring vertices
for (i32 i = 0; i <= segments; i++) for (i32 i = 0; i <= segments; i++)
{ {
@ -1281,7 +1176,7 @@ namespace ogfx
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ x, y }, { x, y },
col, col,
{ 0, 0 } { uvs.x, uvs.y }
}; };
} }
@ -1294,7 +1189,7 @@ namespace ogfx
} }
} }
void BasicRenderer2D::push_polygon(const Vec2* verts, const Vec2* texCoords, u32 vertCount, const u32* inds, u32 indexCount, const Color& color, SDL_Texture* texture) void BasicRenderer2D::push_polygon(const Vec2* verts, const Vec2* texCoords, u32 vertCount, const u32* inds, u32 indexCount, const Color& color, SDL_Texture* texture, bool use_null_tex)
{ {
if (!m_initialized || vertCount <= 0 || indexCount <= 0) if (!m_initialized || vertCount <= 0 || indexCount <= 0)
return; return;
@ -1304,20 +1199,24 @@ namespace ogfx
OX_ERROR("Maximum number of vertices exceeded in single draw call."); OX_ERROR("Maximum number of vertices exceeded in single draw call.");
return; return;
} }
if (texture != m_texture) use_null_tex = use_null_tex || !texCoords;
auto tmpTex = (use_null_tex ? m_lastUsedGlyphAtlasTex : texture);
if (tmpTex != m_texture)
flushBatch(); flushBatch();
m_texture = texture; m_texture = tmpTex;
if (m_vertexCount + vertCount >= MaxVertices || m_indexCount + indexCount >= MaxIndices) if (m_vertexCount + vertCount >= MaxVertices || m_indexCount + indexCount >= MaxIndices)
flushBatch(); flushBatch();
SDL_FColor col = COLOR_CAST(color); SDL_FColor col = COLOR_CAST(color);
i32 base = m_vertexCount; i32 base = m_vertexCount;
bool hasTexCoords = texCoords != nullptr; Vec2 whiteUV = m_fontGlyphAtlas.getNullTextureSlotUVs();
for (i32 i = 0; i < vertCount; i++) for (i32 i = 0; i < vertCount; i++)
{ {
Vec2 tc { 0.0f, 0.0f }; Vec2 tc { 0.0f, 0.0f };
if (hasTexCoords) if (use_null_tex)
tc = whiteUV;
else
tc = texCoords[i]; tc = texCoords[i];
m_vertices[m_vertexCount++] = { m_vertices[m_vertexCount++] = {
{ verts[i].x, verts[i].y }, { verts[i].x, verts[i].y },

View file

@ -70,7 +70,7 @@ namespace ogfx
i32 setFontSize(i32 fontSize); i32 setFontSize(i32 fontSize);
Vec2 getStringDimensions(const String& message, i32 fontSize = 0, TTF_Font* font = nullptr); Vec2 getStringDimensions(const String& message, i32 fontSize = 0, TTF_Font* font = nullptr);
inline u32 getDrawCallCount(void) { return m_drawCallCount; } inline u32 getDrawCallCount(void) { return m_lastFrameDrawCallCount; }
inline bool hasOpenFont(void) { return m_fontOpen; } inline bool hasOpenFont(void) { return m_fontOpen; }
inline TTF_Font* getSDLFont(void) { return m_font; } inline TTF_Font* getSDLFont(void) { return m_font; }
inline bool isValid(void) const { return m_initialized && m_fontOpen && (m_font != nullptr || m_fontFromMemory); } inline bool isValid(void) const { return m_initialized && m_fontOpen && (m_font != nullptr || m_fontFromMemory); }
@ -126,13 +126,11 @@ namespace ogfx
private: private:
void init_arrays(void); void init_arrays(void);
void generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color); void generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color, bool use_null_tex = false);
void generate_quarter_circle(const Vec2& center, f32 radius, f32 thickness, f32 startAngle, const Color& color, i32 segments); void generate_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 thickness, f32 startAngle, f32 endAngle, const Color& color, i32 segments, bool use_null_tex = false);
void generate_circle_stroke(const Vec2& center, f32 radius, f32 thickness, const Color& color, i32 segments); void generate_filled_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 startAngle, const Color& color, i32 segments, bool use_null_tex = false);
void generate_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 thickness, f32 startAngle, f32 endAngle, const Color& color, i32 segments); void generate_filled_ellipse(const Vec2& center, f32 radiusX, f32 radiusY, const Color& color, i32 segments, bool use_null_tex = false);
void generate_filled_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 startAngle, const Color& color, i32 segments); void push_polygon(const Vec2* verts, const Vec2* texCoords, u32 vertCount, const u32* inds, u32 indexCount, const Color& color, SDL_Texture* texture, bool use_null_tex = false);
void generate_filled_ellipse(const Vec2& center, f32 radiusX, f32 radiusY, const Color& color, i32 segments);
void push_polygon(const Vec2* verts, const Vec2* texCoords, u32 vertCount, const u32* inds, u32 indexCount, const Color& color, SDL_Texture* texture);
void print_ttf_error(const String& funcName); void print_ttf_error(const String& funcName);
inline i32 set_error_state(i32 err) { m_errorState = err; return m_errorState; } inline i32 set_error_state(i32 err) { m_errorState = err; return m_errorState; }
@ -152,6 +150,7 @@ namespace ogfx
SDL_Texture* m_texture { nullptr }; SDL_Texture* m_texture { nullptr };
u32 m_drawCallCount { 0 }; u32 m_drawCallCount { 0 };
u32 m_lastFrameDrawCallCount { 0 };
bool m_fontOpen { false }; bool m_fontOpen { false };
TTF_Font* m_font { nullptr }; TTF_Font* m_font { nullptr };
@ -159,6 +158,7 @@ namespace ogfx
i32 m_fontSize { DefaultFontSize }; i32 m_fontSize { DefaultFontSize };
bool m_fontFromMemory { false }; bool m_fontFromMemory { false };
FontGlyphAtlas m_fontGlyphAtlas; FontGlyphAtlas m_fontGlyphAtlas;
SDL_Texture* m_lastUsedGlyphAtlasTex { nullptr };
SignalHandler m_sigHandler { *this }; SignalHandler m_sigHandler { *this };
inline static constexpr i32 DefaultFontSize { 16 }; inline static constexpr i32 DefaultFontSize { 16 };

View file

@ -41,27 +41,27 @@ namespace ogfx
const stdvec<const FontGlyphAtlas::GlyphInfo*> FontGlyphAtlas::processString(const String& str, TTF_Font* font, u32 fontSize) const stdvec<const FontGlyphAtlas::GlyphInfo*> FontGlyphAtlas::processString(const String& str, TTF_Font* font, u32 fontSize)
{ {
if (m_currentAtlasCount <= 0) if (m_currentAtlasCount <= 0)
return {}; return {};
// Pass 1: ensure all glyphs are rasterized (map may rehash here) // Pass 1: ensure all glyphs are rasterized (map may rehash here)
for (auto& c : str) for (auto& c : str)
{ {
const GlyphInfo* dummy; const GlyphInfo* dummy;
if (!rasterize_glyph(String("").addChar(c), font, fontSize, &dummy)) if (!rasterize_glyph(String("").addChar(c), font, fontSize, &dummy))
return {}; return {};
} }
// Pass 2: collect stable pointers (no more insertions, no rehash risk) // Pass 2: collect stable pointers (no more insertions, no rehash risk)
stdvec<const GlyphInfo*> glyphs; stdvec<const GlyphInfo*> glyphs;
glyphs.reserve(str.len()); glyphs.reserve(str.len());
for (auto& c : str) for (auto& c : str)
{ {
auto cps = String("").addChar(c).getUTF8Codepoints(); auto cps = String("").addChar(c).getUTF8Codepoints();
if (cps.size() != 1) return {}; if (cps.size() != 1) return {};
GlyphKey key { cps[0], u64(font), fontSize }; GlyphKey key { cps[0], u64(font), fontSize };
glyphs.push_back(&m_uvs[key]); glyphs.push_back(&m_uvs[key]);
} }
return glyphs; return glyphs;
} }
bool FontGlyphAtlas::rasterize_glyph(const String& glyphStr, TTF_Font* font, u32 fontSize, const GlyphInfo** outGlyph) bool FontGlyphAtlas::rasterize_glyph(const String& glyphStr, TTF_Font* font, u32 fontSize, const GlyphInfo** outGlyph)
@ -80,7 +80,7 @@ namespace ogfx
if (it != m_uvs.end()) if (it != m_uvs.end())
{ {
*outGlyph = &(it->second); *outGlyph = &(it->second);
return true; return true;
} }
SDL_Surface* surf = TTF_RenderText_Blended(font, glyphStr.c_str(), glyphStr.len(), SDL_Color{255, 255, 255, 255}); SDL_Surface* surf = TTF_RenderText_Blended(font, glyphStr.c_str(), glyphStr.len(), SDL_Color{255, 255, 255, 255});
@ -169,9 +169,17 @@ namespace ogfx
{ // Clear the texture to transparent { // Clear the texture to transparent
void* pixels = nullptr; void* pixels = nullptr;
i32 pitch = 0; i32 pitch = 0;
if (SDL_LockTexture(tex, nullptr, &pixels, &pitch) == 0) if (SDL_LockTexture(tex, nullptr, &pixels, &pitch))
{ {
memset(pixels, 0x00, pitch * AtlasTextureDimension); memset(pixels, 0x00, pitch * AtlasTextureDimension);
auto u8_pixels = cast<u8*>(pixels);
for (i32 y = 0; y < 4; y++)
{
for (i32 x = 0; x < 4 * 4; x++)
{
u8_pixels[x + (y * pitch)] = 0xFF;
}
}
SDL_UnlockTexture(tex); SDL_UnlockTexture(tex);
} }
} }
@ -179,7 +187,7 @@ namespace ogfx
m_atlases[m_currentAtlasCount] = tex; m_atlases[m_currentAtlasCount] = tex;
m_currentAtlasCount++; m_currentAtlasCount++;
m_penX = 0; m_penX = 4;
m_penY = 0; m_penY = 0;
m_rowHeight = 0; m_rowHeight = 0;

View file

@ -1,21 +1,21 @@
/* /*
OmniaFramework - A collection of useful functionality OmniaFramework - A collection of useful functionality
Copyright (C) 2025 OmniaX-Dev Copyright (C) 2025 OmniaX-Dev
This file is part of OmniaFramework. This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful, OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>. along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/ */
#pragma once #pragma once
@ -39,25 +39,25 @@ namespace ogfx
}; };
struct GlyphKey struct GlyphKey
{ {
u32 codepoint; u32 codepoint;
u64 fontID; u64 fontID;
u32 pixelSize; u32 pixelSize;
bool operator==(const GlyphKey& other) const noexcept bool operator==(const GlyphKey& other) const noexcept
{ {
return codepoint == other.codepoint && return codepoint == other.codepoint &&
fontID == other.fontID && fontID == other.fontID &&
pixelSize == other.pixelSize; pixelSize == other.pixelSize;
} }
}; };
struct GlyphKeyHasher struct GlyphKeyHasher
{ {
size_t operator()(const GlyphKey& k) const noexcept size_t operator()(const GlyphKey& k) const noexcept
{ {
size_t h = std::hash<u32>()(k.codepoint); size_t h = std::hash<u32>()(k.codepoint);
h ^= std::hash<u64>()(k.fontID) + 0x9e3779b9 + (h << 6) + (h >> 2); h ^= std::hash<u64>()(k.fontID) + 0x9e3779b9 + (h << 6) + (h >> 2);
h ^= std::hash<u32>()(k.pixelSize) + 0x9e3779b9 + (h << 6) + (h >> 2); h ^= std::hash<u32>()(k.pixelSize) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h; return h;
} }
}; };
public: public:
@ -65,6 +65,8 @@ namespace ogfx
inline FontGlyphAtlas(BasicRenderer2D& renderer) { init(renderer); } inline FontGlyphAtlas(BasicRenderer2D& renderer) { init(renderer); }
FontGlyphAtlas init(BasicRenderer2D& renderer); FontGlyphAtlas init(BasicRenderer2D& renderer);
const stdvec<const GlyphInfo*> processString(const String& str, TTF_Font* font, u32 fontSize); const stdvec<const GlyphInfo*> processString(const String& str, TTF_Font* font, u32 fontSize);
inline Vec2 getNullTextureSlotUVs(void) const { return m_nullTexUVs; }
inline SDL_Texture* getAtlas(u32 index) const { return (index >= m_currentAtlasCount ? nullptr : m_atlases[index]); }
private: private:
bool rasterize_glyph(const String& glyphStr, TTF_Font* font, u32 fontSize, const GlyphInfo** outGlyph); bool rasterize_glyph(const String& glyphStr, TTF_Font* font, u32 fontSize, const GlyphInfo** outGlyph);
@ -74,13 +76,14 @@ namespace ogfx
inline static constexpr u32 AtlasTextureDimension { 8192 }; inline static constexpr u32 AtlasTextureDimension { 8192 };
inline static constexpr u32 MaxAtlasCount { 16 }; inline static constexpr u32 MaxAtlasCount { 16 };
public: private:
std::unordered_map<GlyphKey, GlyphInfo, GlyphKeyHasher> m_uvs; std::unordered_map<GlyphKey, GlyphInfo, GlyphKeyHasher> m_uvs;
SDL_Texture* m_atlases[FontGlyphAtlas::MaxAtlasCount]; SDL_Texture* m_atlases[FontGlyphAtlas::MaxAtlasCount];
i32 m_currentAtlasCount { 0 }; i32 m_currentAtlasCount { 0 };
BasicRenderer2D* m_renderer { nullptr }; BasicRenderer2D* m_renderer { nullptr };
i32 m_penX { 0 }; i32 m_penX { 0 };
i32 m_penY { 0 }; i32 m_penY { 0 };
i32 m_rowHeight { 0 }; i32 m_rowHeight { 0 };
Vec2 m_nullTexUVs { 2.0f / cast<f32>(AtlasTextureDimension), 2.0f / cast<f32>(AtlasTextureDimension) };
}; };
} }

View file

@ -74,7 +74,7 @@ class Window : public ogfx::gui::Window
m_label4.setText("Label4"); m_label4.setText("Label4");
m_label5.setText("Label5"); m_label5.setText("Label5");
m_panel1.setSize(300, 300); m_panel1.setSize(700, 300);
m_panel1.allowVScroll(false); m_panel1.allowVScroll(false);
m_panel1.allowHScroll(false); m_panel1.allowHScroll(false);
m_panel1.setTitle("Panel 1"); m_panel1.setTitle("Panel 1");
@ -99,7 +99,7 @@ class Window : public ogfx::gui::Window
m_panel2.addWidget(m_btn1, { 0, 300 }); m_panel2.addWidget(m_btn1, { 0, 300 });
addWidget(m_check1, { 30, 30 }); addWidget(m_check1, { 30, 30 });
addWidget(m_panel2, { 30, 100 }); addWidget(m_panel2, { 500, 100 });
m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList()); m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList());
setTheme(m_theme); setTheme(m_theme);
@ -118,6 +118,7 @@ class Window : public ogfx::gui::Window
void onRedraw(ogfx::BasicRenderer2D& gfx) override void onRedraw(ogfx::BasicRenderer2D& gfx) override
{ {
gfx.drawAnimation(m_anim, { 200, 200 }); gfx.drawAnimation(m_anim, { 200, 200 });
// std::cout << (i32)gfx.getDrawCallCount() << "\n";
// gfx.fillRect(m_panel2.getGlobalPureContentBounds(), { 0, 255, 0, 120 }); // gfx.fillRect(m_panel2.getGlobalPureContentBounds(), { 0, 255, 0, 120 });
} }