From ba66bdab2242f1072ff4ad7d88b55b103a437b78 Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Mon, 27 Apr 2026 23:39:17 +0200 Subject: [PATCH] DrawList: Textured Round Corners: first version. (1962) --- imgui.h | 9 ++- imgui_draw.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++++++- imgui_internal.h | 4 +- 3 files changed, 163 insertions(+), 5 deletions(-) diff --git a/imgui.h b/imgui.h index 51e0f9ff6..cb0baa89f 100644 --- a/imgui.h +++ b/imgui.h @@ -3323,6 +3323,10 @@ struct ImGuiSelectionExternalStorage #define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (32) #endif +#ifndef IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX +#define IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX (16) +#endif + // ImDrawIdx: vertex index. [Compile-time configurable type] // - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). // - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. @@ -3489,6 +3493,7 @@ struct ImDrawList ImVector _CallbacksDataBuf; // [Internal] float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content. float _InvFringeScale; // [internal] 1.0 / _FringeScale // FIXME: Consider renaming to _PixelDensity. + bool _FringeScaleIsInteger; // [Internal] true if _FringeScale is a whole number, used to select fast path for rendering const char* _OwnerName; // Pointer to owner window's name for debugging // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData(). @@ -3633,6 +3638,7 @@ struct ImDrawList IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); + IMGUI_API void _AddMirrored9Slice(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags); }; // All draw data to render a Dear ImGui frame @@ -3993,7 +3999,8 @@ struct ImFontAtlas ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel. May change as new texture gets created. ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. ImVector Sources; // Source/configuration data - ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + ImVec4 TexUvCornerFills[IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX]; // UVs for baked anti-aliased corners int TexNextUniqueID; // Next value to be stored in TexData->UniqueID int FontNextUniqueID; // Next value to be stored in ImFont->FontID ImVector DrawListSharedDatas; // List of users for this atlas. Typically one per Dear ImGui context. diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 872e17068..dda9c6f45 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -731,6 +731,7 @@ void ImDrawList::_SetPixelDensity(float pixel_density) IM_ASSERT(pixel_density > 0.0f); _FringeScale = 1.0f / pixel_density; _InvFringeScale = pixel_density; + _FringeScaleIsInteger = ImIsTruncated(_FringeScale); } // Reserve space for a number of vertices and indices. @@ -1609,14 +1610,84 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl PathStroke(col, thickness, ImDrawFlags_Closed); } +void ImDrawList::_AddMirrored9Slice(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags) +{ + // 9-slice with corner mirroring + const ImVec2 uv_tl(tex_uvs.x, tex_uvs.y); + const ImVec2 uv_tr(tex_uvs.z, tex_uvs.y); + const ImVec2 uv_br(tex_uvs.z, tex_uvs.w); + const ImVec2 uv_bl(tex_uvs.x, tex_uvs.w); + + PrimReserve(9 * 2 * 3, 4 * 4); + + const float r_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? r : 0.0f; + const float r_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? r : 0.0f; + const float r_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? r : 0.0f; + const float r_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? r : 0.0f; + + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + + PrimWriteVtx(ImVec2(p_min.x, p_min.y), uv_tl, col); + PrimWriteVtx(ImVec2(p_min.x + r_tl, p_min.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_max.x - r_tr, p_min.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv_tl, col); + + PrimWriteVtx(ImVec2(p_min.x, p_min.y + r_tl), uv_bl, col); + PrimWriteVtx(ImVec2(p_min.x + r_tl, p_min.y + r_tl), uv_br, col); + PrimWriteVtx(ImVec2(p_max.x - r_tr, p_min.y + r_tr), uv_br, col); + PrimWriteVtx(ImVec2(p_max.x, p_min.y + r_tr), uv_bl, col); + + PrimWriteVtx(ImVec2(p_min.x, p_max.y - r_bl), uv_bl, col); + PrimWriteVtx(ImVec2(p_min.x + r_bl, p_max.y - r_bl), uv_br, col); + PrimWriteVtx(ImVec2(p_max.x - r_br, p_max.y - r_br), uv_br, col); + PrimWriteVtx(ImVec2(p_max.x, p_max.y - r_br), uv_bl, col); + + PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv_tl, col); + PrimWriteVtx(ImVec2(p_min.x + r_bl, p_max.y), uv_tr, col); + PrimWriteVtx(ImVec2(p_max.x - r_br, p_max.y ), uv_tr, col); + PrimWriteVtx(ImVec2(p_max.x, p_max.y), uv_tl, col); + + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + PrimWriteIdx(idx+4); PrimWriteIdx(idx+0); PrimWriteIdx(idx+1); + PrimWriteIdx(idx+4); PrimWriteIdx(idx+1); PrimWriteIdx(idx+5); + idx++; + } + idx++; + } +} + void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) { if ((col & IM_COL32_A_MASK) == 0) return; - if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + + if (ImIsTruncated4(p_min.x, p_min.y, p_max.x, p_min.y) && ImIsTruncated(rounding)) { - PrimReserve(6, 4); - PrimRect(p_min, p_max, col); + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + rounding = ImMin(rounding, ImFabs(p_max.x - p_min.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(p_max.y - p_min.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + + const int s_rounding = (int)rounding; + if (s_rounding <= 0 || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PrimReserve(6, 4); + PrimRect(p_min, p_max, col); + } + else if (s_rounding <= IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX) + { + ImVec4 tex_uvs = _Data->TexUvCornerFills[s_rounding - 1]; + _AddMirrored9Slice(p_min, p_max, col, ImMax(2.0f, (float)s_rounding), tex_uvs, flags); + } + else + { + PathRect(p_min, p_max, (float)s_rounding, flags); + PathFillConvex(col); + } } else { @@ -3760,6 +3831,82 @@ static void ImFontAtlasBuildUpdateTexDataBasic(ImFontAtlas* atlas) atlas->TexUvWhitePixel = ImVec2((r.x + 0.5f) * atlas->TexUvScale.x, (r.y + 0.5f) * atlas->TexUvScale.y); } +static ImU8 SampleCorner(float x, float y, float cx, float cy, float r) +{ + const float dx = ImMin(x, cx) - cx; + const float dy = ImMin(y, cy) - cy; + const float d = ImSqrt(dx*dx + dy*dy); + const float aa_width = 1.0f; + return (ImU8)(ImClamp(1.0f - (d - r + aa_width * 0.5f) / aa_width, 0.0f, 1.0f) * 255.0f); +} + +static void ImFontAtlasBuildUpdateTexDataCorners(ImFontAtlas* atlas) +{ + // Pack and store identifier so we can refresh UV coordinates on texture resize. + ImTextureData* tex = atlas->TexData; + ImFontAtlasBuilder* builder = atlas->Builder; + + ImFontAtlasRect r; + bool add_and_draw = atlas->GetCustomRect(builder->PackIdCornersTexData, &r) == false; + if (add_and_draw) + { + ImVec2i pack_size(0, 0); + for (int i = 0; i < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; i++) + { + pack_size.x += ImMax(2, i+1) + 2; + pack_size.y = ImMax(pack_size.y, (i+1) + 2); + } + builder->PackIdCornersTexData = atlas->AddCustomRect(pack_size.x, pack_size.y, &r); + IM_ASSERT(builder->PackIdCornersTexData != ImFontAtlasRectId_Invalid); + } + + int x = r.x; + int y = r.y; + for (int n = 0; n < IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX; n++) // +1 because of the zero-width row + { + const int s = ImMax(2, n+1); // We need at least 2px so that stretching has solid color. + const int w = s + 2; + const int h = s + 2; + + // Corner circle + const float cx = 1.0f + (float)(n+1); + const float cy = 1.0f + (float)(n+1); + const float rad = (float)(n+1); + IM_ASSERT((x + w) <= (r.x + r.w) && (y + h) <= (r.y + r.h)); // Make sure we're inside the texture bounds before we start writing pixels + + // Write each slice + if (add_and_draw && tex->Format == ImTextureFormat_Alpha8) + { + ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(x, y); + const int pitch = tex->Width; + for (int ly = 0; ly < h; ly++) + { + for (int lx = 0; lx < h; lx++) + write_ptr[lx] = SampleCorner((float)lx + 0.5f, (float)ly + 0.5f, cx, cy, rad); + write_ptr += pitch; + } + } + else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32) + { + ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(x, y); + const int pitch = tex->Width; + for (int ly = 0; ly < h; ly++) + { + for (int lx = 0; lx < h; lx++) + write_ptr[lx] = IM_COL32(255, 255, 255, SampleCorner((float)lx + 0.5f, (float)ly + 0.5f, cx, cy, rad)); + write_ptr += pitch; + } + } + + // Refresh UV coordinates + ImVec2 uv0 = ImVec2((float)(x + 1), (float)(y + 1)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(x + 1 + s), (float)(y + 1 + s)) * atlas->TexUvScale; + atlas->TexUvCornerFills[n] = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + + x += w; + } +} + static void ImFontAtlasBuildUpdateTexDataLines(ImFontAtlas* atlas) { if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) @@ -4195,6 +4342,7 @@ void ImFontAtlasUpdateDrawListsSharedData(ImFontAtlas* atlas) { shared_data->TexUvWhitePixel = atlas->TexUvWhitePixel; shared_data->TexUvLines = atlas->TexUvLines; + shared_data->TexUvCornerFills = atlas->TexUvCornerFills; } } @@ -4213,6 +4361,7 @@ static void ImFontAtlasBuildUpdateTexData(ImFontAtlas* atlas) { ImFontAtlasBuildUpdateTexDataBasic(atlas); ImFontAtlasBuildUpdateTexDataLines(atlas); + ImFontAtlasBuildUpdateTexDataCorners(atlas); } // Create a new texture, discard previous one diff --git a/imgui_internal.h b/imgui_internal.h index 7dcf4b722..92aa25c02 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -919,6 +919,7 @@ struct IMGUI_API ImDrawListSharedData { ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas (== FontAtlas->TexUvWhitePixel) const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas (== FontAtlas->TexUvLines) + const ImVec4* TexUvCornerFills; // UV of rounded corner (== FontAtlas->TexUvCornerFills) ImFontAtlas* FontAtlas; // Current font atlas ImFont* Font; // Current font (used for simplified AddText overload) float FontSize; // Current font size (used for for simplified AddText overload) @@ -4324,8 +4325,9 @@ struct ImFontAtlasBuilder // Custom rectangle identifiers ImFontAtlasRectId PackIdMouseCursors; // White pixel + mouse cursors. Also happen to be fallback in case of packing failure. ImFontAtlasRectId PackIdLinesTexData; + ImFontAtlasRectId PackIdCornersTexData; - ImFontAtlasBuilder() { memset((void*)this, 0, sizeof(*this)); FrameCount = -1; RectsIndexFreeListStart = -1; PackIdMouseCursors = PackIdLinesTexData = -1; } + ImFontAtlasBuilder() { memset((void*)this, 0, sizeof(*this)); FrameCount = -1; RectsIndexFreeListStart = -1; PackIdMouseCursors = PackIdLinesTexData = PackIdCornersTexData = -1; } }; IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas);