From 0cf7478c246a7a072c481e0a88428d01214ae00d Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Sun, 19 Apr 2026 22:15:51 +0200 Subject: [PATCH] Added a null texture slot to FontGlyphAtlas --- other/TODO.txt | 1 + src/ogfx/gui/widgets/Containers.cpp | 105 +++++++++++++ src/ogfx/gui/widgets/Containers.hpp | 24 +++ src/ogfx/render/BasicRenderer.cpp | 233 ++++++++-------------------- src/ogfx/render/BasicRenderer.hpp | 16 +- src/ogfx/render/FontGlyphAtlas.cpp | 52 ++++--- src/ogfx/render/FontGlyphAtlas.hpp | 59 +++---- src/test/GuiTest.cpp | 5 +- 8 files changed, 268 insertions(+), 227 deletions(-) diff --git a/other/TODO.txt b/other/TODO.txt index 0878795..136f63d 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -24,6 +24,7 @@ Add theme caching Implement cursors in Stylesheet FIX: Window getting exponentially bigger when Desktop scale changes FIX: Refreshing scroll when content extent changes +Add Dark Mode Default theme diff --git a/src/ogfx/gui/widgets/Containers.cpp b/src/ogfx/gui/widgets/Containers.cpp index 911b3cb..acf38ac 100644 --- a/src/ogfx/gui/widgets/Containers.cpp +++ b/src/ogfx/gui/widgets/Containers.cpp @@ -133,6 +133,111 @@ namespace ogfx 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(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& 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& 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& 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& 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& 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 + } } } } diff --git a/src/ogfx/gui/widgets/Containers.hpp b/src/ogfx/gui/widgets/Containers.hpp index 7cfb25b..27e31d0 100644 --- a/src/ogfx/gui/widgets/Containers.hpp +++ b/src/ogfx/gui/widgets/Containers.hpp @@ -70,6 +70,30 @@ namespace ogfx Color m_titlebarBorderColor { Colors::Black }; 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> m_tabs; + Panel* m_currentTab { nullptr }; + + f32 m_tabBarHeight { 30 }; + }; } } } diff --git a/src/ogfx/render/BasicRenderer.cpp b/src/ogfx/render/BasicRenderer.cpp index 584133d..4829aa1 100644 --- a/src/ogfx/render/BasicRenderer.cpp +++ b/src/ogfx/render/BasicRenderer.cpp @@ -66,6 +66,9 @@ namespace ogfx loadDefaultFont(20); m_initialized = false; m_fontGlyphAtlas.init(*this); + auto atlas = m_fontGlyphAtlas.getAtlas(0); + if (atlas) //TODO: Error + m_lastUsedGlyphAtlasTex = atlas; m_initialized = true; return set_error_state(tErrors::NoError); } @@ -89,6 +92,7 @@ namespace ogfx void BasicRenderer2D::endFrame(void) { flushBatch(); + m_lastFrameDrawCallCount = m_drawCallCount; m_drawCallCount = 0; } @@ -309,7 +313,7 @@ namespace ogfx // 5. Push quad 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) @@ -416,8 +420,8 @@ namespace ogfx { x, y + g->size.y * scale } }; 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); } setFontSize(oldFontSize); @@ -462,14 +466,14 @@ namespace ogfx }}; std::array 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) return; i32 segments = std::max(16, i32(thickness * 1.5f)); - generate_half_circle(p1, -dir, half, segments, color); - generate_half_circle(p2, dir, half, segments, color); + generate_half_circle(p1, -dir, half, segments, color, true); + generate_half_circle(p2, dir, half, segments, color, true); } void BasicRenderer2D::drawRect(const Rectangle& rect, const Color& color, i32 thickness) @@ -546,10 +550,10 @@ namespace ogfx // Corner arcs 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 (rTR > 0) generate_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, thickness, M_PI * 1.5f, M_PI * 2.0f, color, segments(rTR)); - if (rBR > 0) generate_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, thickness, 0.0f, M_PI * 0.5f, color, segments(rBR)); - if (rBL > 0) generate_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, thickness, M_PI * 0.5f, M_PI, color, segments(rBL)); + 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), true); + 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), true); } void BasicRenderer2D::drawCircle(const Vec2& center, f32 radius, const Color& color, i32 thickness) @@ -557,7 +561,7 @@ namespace ogfx if (!m_initialized || thickness <= 0) return; 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) @@ -583,7 +587,7 @@ namespace ogfx f32 rx = rect.w * 0.5f; f32 ry = rect.h * 0.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) @@ -622,7 +626,7 @@ namespace ogfx { x1, y2 } }; 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) @@ -676,7 +680,7 @@ namespace ogfx { x1 + leftInset, y2 - bottomInset } }; 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 @@ -688,7 +692,7 @@ namespace ogfx { x1 + rTL, y1 + topInset } }; 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 @@ -700,7 +704,7 @@ namespace ogfx { x1 + rBL, y2 } }; 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 @@ -712,7 +716,7 @@ namespace ogfx { x1, y2 - rBL } }; 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 @@ -724,7 +728,7 @@ namespace ogfx { x2 - rightInset, y2 - rBR } }; 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 @@ -738,7 +742,7 @@ namespace ogfx { x1, y1 + topInset } }; 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 @@ -751,7 +755,7 @@ namespace ogfx { x2 - rightInset, y1 + topInset } }; 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 @@ -764,7 +768,7 @@ namespace ogfx { x1, y2 - rBL } }; 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 @@ -777,7 +781,7 @@ namespace ogfx { x2 - rightInset, y2 - rBR } }; 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) @@ -793,10 +797,10 @@ namespace ogfx // Corner fills 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 (rTR > 0) generate_filled_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, M_PI * 1.5f, color, segments(rTR)); - if (rBR > 0) generate_filled_ellipse_stroke({ x2 - rBR, y2 - rBR }, rBR, rBR, 0.0f, color, segments(rBR)); - if (rBL > 0) generate_filled_ellipse_stroke({ x1 + rBL, y2 - rBL }, rBL, rBL, M_PI * 0.5f, color, segments(rBL)); + 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), true); + 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), true); } void BasicRenderer2D::fillCircle(const Vec2& center, f32 radius, const Color& color) @@ -805,7 +809,7 @@ namespace ogfx return; 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) @@ -836,7 +840,7 @@ namespace ogfx f32 radiusY = rect.h * 0.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) @@ -968,7 +972,7 @@ namespace ogfx 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 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 endAngle = baseAngle + M_PI * 0.5f; + Vec2 uvs = { 0.0f, 0.0f }; + if (use_null_tex) + uvs = m_fontGlyphAtlas.getNullTextureSlotUVs(); + // Generate arc vertices for (i32 i = 0; i <= segments; i++) { @@ -1005,7 +1013,7 @@ namespace ogfx m_vertices[m_vertexCount++] = { { x, y }, 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) - { - 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) + 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; @@ -1165,6 +1048,10 @@ namespace ogfx f32 angleRange = endAngle - startAngle; + Vec2 uvs = { 0.0f, 0.0f }; + if (use_null_tex) + uvs = m_fontGlyphAtlas.getNullTextureSlotUVs(); + // Generate vertices: outer arc + inner arc for (i32 i = 0; i <= segments; i++) { @@ -1178,14 +1065,14 @@ namespace ogfx m_vertices[m_vertexCount++] = { { center.x + cs * outerRX, center.y + sn * outerRY }, col, - { 0, 0 } + { uvs.x, uvs.y } }; // Inner arc m_vertices[m_vertexCount++] = { { center.x + cs * innerRX, center.y + sn * innerRY }, 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 if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices) @@ -1228,6 +1115,10 @@ namespace ogfx 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++) { f32 t = f32(i) / f32(segments); @@ -1239,7 +1130,7 @@ namespace ogfx m_vertices[m_vertexCount++] = { { x, y }, 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 if (m_vertexCount + segments + 2 >= MaxVertices || m_indexCount + segments * 3 >= MaxIndices) @@ -1269,6 +1160,10 @@ namespace ogfx { 0, 0 } }; + Vec2 uvs = { 0.0f, 0.0f }; + if (use_null_tex) + uvs = m_fontGlyphAtlas.getNullTextureSlotUVs(); + // Outer ring vertices for (i32 i = 0; i <= segments; i++) { @@ -1281,7 +1176,7 @@ namespace ogfx m_vertices[m_vertexCount++] = { { x, y }, 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) return; @@ -1304,20 +1199,24 @@ namespace ogfx OX_ERROR("Maximum number of vertices exceeded in single draw call."); 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(); - m_texture = texture; + m_texture = tmpTex; if (m_vertexCount + vertCount >= MaxVertices || m_indexCount + indexCount >= MaxIndices) flushBatch(); SDL_FColor col = COLOR_CAST(color); i32 base = m_vertexCount; - bool hasTexCoords = texCoords != nullptr; + Vec2 whiteUV = m_fontGlyphAtlas.getNullTextureSlotUVs(); for (i32 i = 0; i < vertCount; i++) { Vec2 tc { 0.0f, 0.0f }; - if (hasTexCoords) + if (use_null_tex) + tc = whiteUV; + else tc = texCoords[i]; m_vertices[m_vertexCount++] = { { verts[i].x, verts[i].y }, diff --git a/src/ogfx/render/BasicRenderer.hpp b/src/ogfx/render/BasicRenderer.hpp index 3b7adb1..3cef174 100644 --- a/src/ogfx/render/BasicRenderer.hpp +++ b/src/ogfx/render/BasicRenderer.hpp @@ -70,7 +70,7 @@ namespace ogfx i32 setFontSize(i32 fontSize); 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 TTF_Font* getSDLFont(void) { return m_font; } inline bool isValid(void) const { return m_initialized && m_fontOpen && (m_font != nullptr || m_fontFromMemory); } @@ -126,13 +126,11 @@ namespace ogfx private: void init_arrays(void); - void generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color); - void generate_quarter_circle(const Vec2& center, f32 radius, f32 thickness, f32 startAngle, const Color& color, i32 segments); - void generate_circle_stroke(const Vec2& center, f32 radius, f32 thickness, 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); - void generate_filled_ellipse_stroke(const Vec2& center, f32 radiusX, f32 radiusY, f32 startAngle, const Color& color, i32 segments); - 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 generate_half_circle(const Vec2& center, const Vec2& dir, f32 radius, i32 segments, const Color& color, 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, bool use_null_tex = false); + 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_filled_ellipse(const Vec2& center, f32 radiusX, f32 radiusY, const Color& color, i32 segments, bool use_null_tex = false); + 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 print_ttf_error(const String& funcName); inline i32 set_error_state(i32 err) { m_errorState = err; return m_errorState; } @@ -152,6 +150,7 @@ namespace ogfx SDL_Texture* m_texture { nullptr }; u32 m_drawCallCount { 0 }; + u32 m_lastFrameDrawCallCount { 0 }; bool m_fontOpen { false }; TTF_Font* m_font { nullptr }; @@ -159,6 +158,7 @@ namespace ogfx i32 m_fontSize { DefaultFontSize }; bool m_fontFromMemory { false }; FontGlyphAtlas m_fontGlyphAtlas; + SDL_Texture* m_lastUsedGlyphAtlasTex { nullptr }; SignalHandler m_sigHandler { *this }; inline static constexpr i32 DefaultFontSize { 16 }; diff --git a/src/ogfx/render/FontGlyphAtlas.cpp b/src/ogfx/render/FontGlyphAtlas.cpp index 6f9b0ee..28a42bc 100644 --- a/src/ogfx/render/FontGlyphAtlas.cpp +++ b/src/ogfx/render/FontGlyphAtlas.cpp @@ -41,27 +41,27 @@ namespace ogfx const stdvec FontGlyphAtlas::processString(const String& str, TTF_Font* font, u32 fontSize) { if (m_currentAtlasCount <= 0) - return {}; + return {}; - // Pass 1: ensure all glyphs are rasterized (map may rehash here) - for (auto& c : str) - { - const GlyphInfo* dummy; - if (!rasterize_glyph(String("").addChar(c), font, fontSize, &dummy)) - return {}; - } + // Pass 1: ensure all glyphs are rasterized (map may rehash here) + for (auto& c : str) + { + const GlyphInfo* dummy; + if (!rasterize_glyph(String("").addChar(c), font, fontSize, &dummy)) + return {}; + } - // Pass 2: collect stable pointers (no more insertions, no rehash risk) - stdvec glyphs; - glyphs.reserve(str.len()); - for (auto& c : str) - { - auto cps = String("").addChar(c).getUTF8Codepoints(); - if (cps.size() != 1) return {}; - GlyphKey key { cps[0], u64(font), fontSize }; - glyphs.push_back(&m_uvs[key]); - } - return glyphs; + // Pass 2: collect stable pointers (no more insertions, no rehash risk) + stdvec glyphs; + glyphs.reserve(str.len()); + for (auto& c : str) + { + auto cps = String("").addChar(c).getUTF8Codepoints(); + if (cps.size() != 1) return {}; + GlyphKey key { cps[0], u64(font), fontSize }; + glyphs.push_back(&m_uvs[key]); + } + return glyphs; } 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()) { *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}); @@ -169,9 +169,17 @@ namespace ogfx { // Clear the texture to transparent void* pixels = nullptr; i32 pitch = 0; - if (SDL_LockTexture(tex, nullptr, &pixels, &pitch) == 0) + if (SDL_LockTexture(tex, nullptr, &pixels, &pitch)) { memset(pixels, 0x00, pitch * AtlasTextureDimension); + auto u8_pixels = cast(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); } } @@ -179,7 +187,7 @@ namespace ogfx m_atlases[m_currentAtlasCount] = tex; m_currentAtlasCount++; - m_penX = 0; + m_penX = 4; m_penY = 0; m_rowHeight = 0; diff --git a/src/ogfx/render/FontGlyphAtlas.hpp b/src/ogfx/render/FontGlyphAtlas.hpp index 04877a6..c0250a5 100644 --- a/src/ogfx/render/FontGlyphAtlas.hpp +++ b/src/ogfx/render/FontGlyphAtlas.hpp @@ -1,21 +1,21 @@ /* - OmniaFramework - A collection of useful functionality - Copyright (C) 2025 OmniaX-Dev + OmniaFramework - A collection of useful functionality + 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 - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - OmniaFramework is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with OmniaFramework. If not, see . + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . */ #pragma once @@ -39,25 +39,25 @@ namespace ogfx }; struct GlyphKey { - u32 codepoint; - u64 fontID; - u32 pixelSize; + u32 codepoint; + u64 fontID; + u32 pixelSize; - bool operator==(const GlyphKey& other) const noexcept + bool operator==(const GlyphKey& other) const noexcept { - return codepoint == other.codepoint && - fontID == other.fontID && - pixelSize == other.pixelSize; - } + return codepoint == other.codepoint && + fontID == other.fontID && + pixelSize == other.pixelSize; + } }; struct GlyphKeyHasher { size_t operator()(const GlyphKey& k) const noexcept { - size_t h = std::hash()(k.codepoint); - h ^= std::hash()(k.fontID) + 0x9e3779b9 + (h << 6) + (h >> 2); - h ^= std::hash()(k.pixelSize) + 0x9e3779b9 + (h << 6) + (h >> 2); - return h; + size_t h = std::hash()(k.codepoint); + h ^= std::hash()(k.fontID) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= std::hash()(k.pixelSize) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; } }; public: @@ -65,6 +65,8 @@ namespace ogfx inline FontGlyphAtlas(BasicRenderer2D& renderer) { init(renderer); } FontGlyphAtlas init(BasicRenderer2D& renderer); const stdvec 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: 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 MaxAtlasCount { 16 }; - public: + private: std::unordered_map m_uvs; SDL_Texture* m_atlases[FontGlyphAtlas::MaxAtlasCount]; i32 m_currentAtlasCount { 0 }; BasicRenderer2D* m_renderer { nullptr }; i32 m_penX { 0 }; - i32 m_penY { 0 }; - i32 m_rowHeight { 0 }; + i32 m_penY { 0 }; + i32 m_rowHeight { 0 }; + Vec2 m_nullTexUVs { 2.0f / cast(AtlasTextureDimension), 2.0f / cast(AtlasTextureDimension) }; }; } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 0d051b4..15afd54 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -74,7 +74,7 @@ class Window : public ogfx::gui::Window m_label4.setText("Label4"); m_label5.setText("Label5"); - m_panel1.setSize(300, 300); + m_panel1.setSize(700, 300); m_panel1.allowVScroll(false); m_panel1.allowHScroll(false); m_panel1.setTitle("Panel 1"); @@ -99,7 +99,7 @@ class Window : public ogfx::gui::Window m_panel2.addWidget(m_btn1, { 0, 300 }); addWidget(m_check1, { 30, 30 }); - addWidget(m_panel2, { 30, 100 }); + addWidget(m_panel2, { 500, 100 }); m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList()); setTheme(m_theme); @@ -118,6 +118,7 @@ class Window : public ogfx::gui::Window void onRedraw(ogfx::BasicRenderer2D& gfx) override { gfx.drawAnimation(m_anim, { 200, 200 }); + // std::cout << (i32)gfx.getDrawCallCount() << "\n"; // gfx.fillRect(m_panel2.getGlobalPureContentBounds(), { 0, 255, 0, 120 }); }