From ff3e7feba76e692ea10540a5f796da28e9672339 Mon Sep 17 00:00:00 2001 From: Ben Carter Date: Thu, 5 Dec 2019 15:48:41 +0900 Subject: [PATCH] Texture-based round corners: Added support for multiple stroke widths --- imgui.cpp | 3 +- imgui.h | 7 +- imgui_demo.cpp | 13 +- imgui_draw.cpp | 344 ++++++++++++++++++++++++++++++++--------------- imgui_internal.h | 9 +- 5 files changed, 254 insertions(+), 122 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index aa5dcb35..509c9903 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -5577,8 +5577,7 @@ static bool AddResizeGrip(ImDrawList* dl, const ImVec2& corner, unsigned int rad const ImVec2 uv[] = { ImVec2(ImLerp(uvs.x, uvs.z, 0.5f), ImLerp(uvs.y, uvs.w, 0.5f)), - ImVec2(uvs.x, uvs.y),//ImLerp(uvs.w, uvs.y, 0.1f)), - //ImVec2(uvs.x, uvs.w), + ImVec2(uvs.x, uvs.y), ImVec2(uvs.z, uvs.w), }; diff --git a/imgui.h b/imgui.h index c7d119df..39f3cf49 100644 --- a/imgui.h +++ b/imgui.h @@ -2704,10 +2704,11 @@ struct ImFontGlyphRangesBuilder // Data for texture-based rounded corners for a given radius struct ImFontRoundedCornerData { - ImVec4 TexUvFilled; // UV of filled round corner quad in the atlas + ImVec4 TexUvFilled; // UV of filled round corner quad in the atlas (only valid when stroke width is 1) ImVec4 TexUvStroked; // UV of stroked round corner quad in the atlas float ParametricStrokeWidth; // Pre-calculated value for stroke width divided by the radius - int RectId; // Rect ID in the atlas + int RectId; // Rect ID in the atlas, or -1 if there is no data + bool StrokedUsesAlternateUVs; // True if stroked drawing should use the alternate (i.e. other corner) UVs }; // See ImFontAtlas::AddCustomRectXXX functions. @@ -2844,7 +2845,7 @@ struct ImFontAtlas int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines - ImVector TexRoundCornerData; // Data for texture-based round corners indexed by size [0] is 1px, [n] is (n+1)px (index up to ImFontAtlasRoundCornersMaxSize - 1). + ImVector TexRoundCornerData; // Data for texture-based round corners indexed by radius/size (from 1 to ImFontAtlasRoundCornersMaxSize) and stroke width (from 1 to ImFontAtlasRoundCornersMaxStrokeWidth), with index = stroke_width_index + (radius_index * ImFontAtlasRoundCornersMaxStrokeWidth). // [Obsolete] //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ diff --git a/imgui_demo.cpp b/imgui_demo.cpp index b8d145da..fee9ddb0 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -258,7 +258,6 @@ static void GetVtxIdxDelta(ImDrawList* dl, int* vtx, int *idx) } // https://github.com/ocornut/imgui/issues/1962 -// FIXME-ROUNDCORNERS: Figure out how to support multiple thickness, might hard-code common steps (1.0, 1.5, 2.0, 3.0), not super satisfactory but may be best static void TestTextureBasedRender() { ImGuiIO& io = ImGui::GetIO(); @@ -272,7 +271,11 @@ static void TestTextureBasedRender() static int segments = 20; static int ngon_segments = 6; - ImGui::SliderFloat("radius", &radius, 0.0f, (float)64 /*ImFontAtlasRoundCornersMaxSize*/, "%.0f"); + ImGui::SliderFloat("radius", &radius, 0.0f, 64.0f /*(float)ImFontAtlasRoundCornersMaxSize*/, "%.0f"); + + static float stroke_width = 1.0f; + + ImGui::SliderFloat("stroke_width", &stroke_width, 1.0f, 10.0f, "%.0f"); int vtx_n = 0; int idx_n = 0; @@ -299,7 +302,7 @@ static void TestTextureBasedRender() GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); ImVec2 min = ImGui::GetItemRectMin(); ImVec2 size = ImGui::GetItemRectSize(); - draw_list->AddCircle(ImVec2(min.x + size.x * 0.5f, min.y + size.y * 0.5f), radius, IM_COL32(255,0,255,255), segments); + draw_list->AddCircle(ImVec2(min.x + size.x * 0.5f, min.y + size.y * 0.5f), radius, IM_COL32(255,0,255,255), segments, stroke_width); GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); ImGui::Text("AddCircle\n %d vtx, %d idx", vtx_n, idx_n); } @@ -336,7 +339,7 @@ static void TestTextureBasedRender() ImVec2 r_max = ImGui::GetItemRectMax(); GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); - draw_list->AddRect(r_min, r_max, IM_COL32(255,0,255,255), radius, corner_flags); + draw_list->AddRect(r_min, r_max, IM_COL32(255,0,255,255), radius, corner_flags, stroke_width); GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); ImGui::Text("AddRect\n %d vtx, %d idx", vtx_n, idx_n); } @@ -367,7 +370,7 @@ static void TestTextureBasedRender() GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); ImVec2 min = ImGui::GetItemRectMin(); ImVec2 size = ImGui::GetItemRectSize(); - draw_list->AddNgon(ImVec2(min.x + size.x * 0.5f, min.y + size.y * 0.5f), radius, IM_COL32(255, 0, 255, 255), ngon_segments); + draw_list->AddNgon(ImVec2(min.x + size.x * 0.5f, min.y + size.y * 0.5f), radius, IM_COL32(255, 0, 255, 255), ngon_segments, stroke_width); GetVtxIdxDelta(draw_list, &vtx_n, &idx_n); ImGui::Text("AddNgon\n %d vtx, %d idx", vtx_n, idx_n); } diff --git a/imgui_draw.cpp b/imgui_draw.cpp index b50eafdd..c1ff3acb 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1394,7 +1394,7 @@ void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float th // Returns true if the rectangle was drawn, false for some reason it couldn't // be (in which case the caller should try again with the regular path drawing API) // We are using the textures generated by ImFontAtlasBuildRenderRoundCornersTexData() -inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, ImDrawFlags flags, bool fill) +inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, float thickness, ImDrawFlags flags, bool fill) { if (!(draw_list->Flags & ImDrawListFlags_TexturedRoundCorners)) // Disabled by the draw list flags return false; @@ -1406,32 +1406,36 @@ inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImV #endif const ImDrawListSharedData* data = draw_list->_Data; - const int rad = (int)rounding; - if (data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedRoundCorners) // No data in font return false; - if ((rad <= 0) || // Zero radius causes issues with the [rad - 1] UV lookup below + // Filled rectangles have no stroke width + const int stroke_width = fill ? 1 : (int)thickness; + + if ((stroke_width <= 0) || + (stroke_width > ImFontAtlasRoundCornersMaxStrokeWidth)) + return false; // We can't handle this + + // If we have a >1 stroke width, we actually need to increase the radius appropriately as well to match how the geometry renderer does things + const int rad = (int)rounding + (stroke_width - 1); + + if ((rad <= 0) || // We don't support zero radius (rad > ImFontAtlasRoundCornersMaxSize)) - { - // We can't handle this - return false; - } + return false; // We can't handle this // Debug command to force this render path to only execute when shift is held if (!ImGui::GetIO().KeyShift) return false; + const unsigned int index = (stroke_width - 1) + ((rad - 1) * ImFontAtlasRoundCornersMaxStrokeWidth); + ImFontRoundedCornerData& round_corner_data = (*data->TexRoundCornerData)[index]; + + if (round_corner_data.RectId < 0) + return false; // No data for this configuration + ImTextureID tex_id = data->Font->ContainerAtlas->TexID; IM_ASSERT(tex_id == draw_list->_TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. - // The width of our stroke for unfilled mode - // Something of a placeholder at the moment - used for calculations but without appropriately-generated - // textures won't actually achieve anything - const float stroke_width = 1.0f; - - ImFontRoundedCornerData& round_corner_data = (*data->TexRoundCornerData)[rad - 1]; - // Calculate UVs for the three points we are interested in from the texture // corner_uv[0] is the innermost point of the circle (solid for filled circles) // corner_uv[1] is either straight down or across from it (depending on if we are using the filled or stroked version) @@ -1439,11 +1443,14 @@ inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImV // corner_uv[1] is always solid (either inside the circle or on the line), whilst corner_uv[2] is always blank // This represents a 45 degree "wedge" of circle, which then gets mirrored here to produce a 90 degree curve // See ImFontAtlasBuildRenderRoundCornersTexData() for more details of the texture contents + // If use_alternative_uvs is true then this means we are drawing a stroked texture that has been packed into the "filled" + // corner of the rectangle, so we need to calculate UVs appropriately const ImVec4& uvs = fill ? round_corner_data.TexUvFilled : round_corner_data.TexUvStroked; + const bool use_alternative_uvs = fill | round_corner_data.StrokedUsesAlternateUVs; const ImVec2 corner_uv[3] = { ImVec2(uvs.x, uvs.y), - fill ? ImVec2(uvs.x, uvs.w) : ImVec2(uvs.z, uvs.y), + use_alternative_uvs ? ImVec2(uvs.x, uvs.w) : ImVec2(uvs.z, uvs.y), ImVec2(uvs.z, uvs.w) }; @@ -1476,14 +1483,17 @@ inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImV // MAX2/MAY2/etc are those vertices offset inwards by the line width // (only used for unfilled rectangles) - const ImVec2 ca(a.x, a.y), cb(b.x, a.y); + // Adjust size to account for the fact that wider strokes draw "outside the box" + const float stroke_width_size_expansion = stroke_width - 1.0f; + + const ImVec2 ca(a.x - stroke_width_size_expansion, a.y - stroke_width_size_expansion), cb(b.x + stroke_width_size_expansion, a.y - stroke_width_size_expansion); const ImVec2 may(ca.x + rad, ca.y), mby(cb.x - rad, cb.y); const ImVec2 may2(may.x, may.y + stroke_width), mby2(mby.x, mby.y + stroke_width); const ImVec2 max(ca.x, ca.y + rad), mbx(cb.x, cb.y + rad); const ImVec2 max2(max.x + stroke_width, max.y), mbx2(mbx.x - stroke_width, mbx.y); const ImVec2 ia(ca.x + rad, ca.y + rad), ib(cb.x - rad, cb.y + rad); - const ImVec2 cc(b.x, b.y), cd(a.x, b.y); + const ImVec2 cc(b.x + stroke_width_size_expansion, b.y + stroke_width_size_expansion), cd(a.x - stroke_width_size_expansion, b.y + stroke_width_size_expansion); const ImVec2 mdx(cd.x, cd.y - rad), mcx(cc.x, cc.y - rad); const ImVec2 mdx2(mdx.x + stroke_width, mdx.y), mcx2(mcx.x - stroke_width, mcx.y); const ImVec2 mdy(cd.x + rad, cd.y), mcy(cc.x - rad, cc.y); @@ -1516,7 +1526,7 @@ inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImV // each occupy one side of the texture #define VTX_WRITE_LERPED(d, corner, px, py) \ draw_list->_VtxWritePtr[d].pos = ImVec2(ImLerp(i##corner.x, c##corner.x, px), ImLerp(i##corner.y, c##corner.y, py)); \ - draw_list->_VtxWritePtr[d].uv = ((px < py) ^ fill) ? \ + draw_list->_VtxWritePtr[d].uv = ((px < py) ^ use_alternative_uvs) ? \ ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, py), ImLerp(corner_uv[0].y, corner_uv[b##corner ? 2 : 1].y, px)) : \ ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, px), ImLerp(corner_uv[0].y, corner_uv[b##corner ? 2 : 1].y, py)); \ draw_list->_VtxWritePtr[d].col = col @@ -1525,12 +1535,16 @@ inline bool AddRoundCornerRect(ImDrawList* draw_list, const ImVec2& a, const ImV #define VTX_WRITE_LERPED_X(d, corner, px) \ draw_list->_VtxWritePtr[d].pos = ImVec2(ImLerp(i##corner.x, c##corner.x, px), i##corner.y); \ - draw_list->_VtxWritePtr[d].uv = ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, px), corner_uv[0].y); \ + draw_list->_VtxWritePtr[d].uv = use_alternative_uvs ? \ + ImVec2(corner_uv[0].x, ImLerp(corner_uv[0].y, corner_uv[b##corner ? 2 : 1].y, px)) : \ + ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, px), corner_uv[0].y); \ draw_list->_VtxWritePtr[d].col = col #define VTX_WRITE_LERPED_Y(d, corner, py) \ draw_list->_VtxWritePtr[d].pos = ImVec2(i##corner.x, ImLerp(i##corner.y, c##corner.y, py)); \ - draw_list->_VtxWritePtr[d].uv = ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, py), corner_uv[0].y); \ + draw_list->_VtxWritePtr[d].uv = use_alternative_uvs ? \ + ImVec2(corner_uv[0].x, ImLerp(corner_uv[0].y, corner_uv[b##corner ? 2 : 1].y, py)) : \ + ImVec2(ImLerp(corner_uv[0].x, corner_uv[b##corner ? 2 : 1].x, py), corner_uv[0].y); \ draw_list->_VtxWritePtr[d].col = col // Set up the outer corners (vca-vcd being the four outermost corners) @@ -1752,7 +1766,7 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl // Try to use fast path if we can if (rounding > 0) - if (AddRoundCornerRect(this, p_min, p_max, col, rounding, flags, /* fill */ false)) + if (AddRoundCornerRect(this, p_min, p_max, col, rounding, thickness, flags, /* fill */ false)) return; if (Flags & ImDrawListFlags_AntiAliasedLines) @@ -1781,7 +1795,7 @@ void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 c else { // Try fast path first - if (AddRoundCornerRect(this, p_min, p_max, col, rounding, flags, /* fill */ true)) + if (AddRoundCornerRect(this, p_min, p_max, col, rounding, 1.0f, flags, /* fill */ true)) return; PathRect(p_min, p_max, rounding, flags); @@ -1854,7 +1868,7 @@ void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImV // Draw a circle using the rounded corner textures // Returns true if the circle was drawn, or false if for some reason it could not be // (in which case the caller should try the regular circle drawing code) -inline bool AddRoundCornerCircle(ImDrawList* draw_list, const ImVec2& center, float radius, ImU32 col, bool fill) +inline bool AddRoundCornerCircle(ImDrawList* draw_list, const ImVec2& center, float radius, float thickness, ImU32 col, bool fill) { if (!(draw_list->Flags & ImDrawListFlags_TexturedRoundCorners)) // Disabled by the draw list flags return false; @@ -1866,15 +1880,29 @@ inline bool AddRoundCornerCircle(ImDrawList* draw_list, const ImVec2& center, fl if (data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedRoundCorners) // No data in font return false; - const int rad = (int)radius; - if (rad < 1 || rad > ImFontAtlasRoundCornersMaxSize) // Radius 0 will cause issues with the UV lookup below + // Filled rectangles have no stroke width + const int stroke_width = fill ? 1 : (int)thickness; + + if ((stroke_width <= 0) || + (stroke_width > ImFontAtlasRoundCornersMaxStrokeWidth)) + return false; // We can't handle this + + // If we have a >1 stroke width, we actually need to increase the radius appropriately as well to match how the geometry renderer does things + const int rad = (int)radius + (stroke_width - 1); + + if ((rad <= 0) || // We don't support zero radius + (rad > ImFontAtlasRoundCornersMaxSize)) return false; // We can't handle this // Debug command to force this render path to only execute when shift is held if (!ImGui::GetIO().KeyShift) return false; - ImFontRoundedCornerData& round_corner_data = (*data->TexRoundCornerData)[rad - 1]; + const unsigned int index = (stroke_width - 1) + ((rad - 1) * ImFontAtlasRoundCornersMaxStrokeWidth); + ImFontRoundedCornerData& round_corner_data = (*data->TexRoundCornerData)[index]; + + if (round_corner_data.RectId < 0) + return false; // No data for this configuration // Calculate UVs for the three points we are interested in from the texture // corner_uv[0] is the innermost point of the circle (solid for filled circles) @@ -1883,17 +1911,17 @@ inline bool AddRoundCornerCircle(ImDrawList* draw_list, const ImVec2& center, fl // corner_uv[1] is always solid (either inside the circle or on the line), whilst corner_uv[2] is always blank // This represents a 45 degree "wedge" of circle, which then gets mirrored here to produce a 90 degree curve // See ImFontAtlasBuildRenderRoundCornersTexData() for more details of the texture contents + // If use_alternative_uvs is true then this means we are drawing a stroked texture that has been packed into the "filled" + // corner of the rectangle, so we need to calculate UVs appropriately const ImVec4& uvs = fill ? round_corner_data.TexUvFilled : round_corner_data.TexUvStroked; + const bool use_alternative_uvs = fill | round_corner_data.StrokedUsesAlternateUVs; const ImVec2 corner_uv[3] = { ImVec2(uvs.x, uvs.y), - fill ? ImVec2(uvs.x, uvs.w) : ImVec2(uvs.z, uvs.y), - ImVec2(uvs.z, uvs.w), + use_alternative_uvs ? ImVec2(uvs.x, uvs.w) : ImVec2(uvs.z, uvs.y), + ImVec2(uvs.z, uvs.w) }; - // Our stroke width (requires a texture with the appropriate stroke width to actually do anything) - const float stroke_width = 1.0f; - // Calculate the circle bounds const ImVec2& c = center; ImVec2 tl = ImVec2(c.x - rad, c.y - rad); @@ -1943,17 +1971,25 @@ inline bool AddRoundCornerCircle(ImDrawList* draw_list, const ImVec2& center, fl // UV for the inside diagonal points ImVec2 uvbi = ImVec2(ImLerp(corner_uv[0].x, corner_uv[2].x, half_sqrt_two - width_offset_parametric), ImLerp(corner_uv[0].y, corner_uv[2].y, half_sqrt_two - width_offset_parametric)); + // Left/right/top/bottom interior positions + const ImVec2 lbi = ImVec2(ImLerp(tl.x, c.x, width_offset_parametric), c.y); + const ImVec2 rbi = ImVec2(ImLerp(br.x, c.x, width_offset_parametric), c.y); + const ImVec2 tbi = ImVec2(c.x, ImLerp(tl.y, c.y, width_offset_parametric)); + const ImVec2 bbi = ImVec2(c.x, ImLerp(br.y, c.y, width_offset_parametric)); + // UV for the interior cardinal points - ImVec2 uvi_cardinal = ImVec2(ImLerp(corner_uv[0].x, corner_uv[2].x, 1.0f - width_offset_parametric), corner_uv[0].y); + ImVec2 uvi_cardinal = use_alternative_uvs ? + ImVec2(corner_uv[0].x, ImLerp(corner_uv[2].y, corner_uv[0].y, width_offset_parametric)) : + ImVec2(ImLerp(corner_uv[2].x, corner_uv[0].x, width_offset_parametric), corner_uv[0].y); // Inner vertices, starting from the left - VTX_WRITE(8, ImVec2(tl.x + stroke_width, c.y), uvi_cardinal); + VTX_WRITE(8, lbi, uvi_cardinal); VTX_WRITE(9, tlbi, uvbi); - VTX_WRITE(10, ImVec2(c.x, tl.y + stroke_width), uvi_cardinal); + VTX_WRITE(10, tbi, uvi_cardinal); VTX_WRITE(11, trbi, uvbi); - VTX_WRITE(12, ImVec2(br.x - stroke_width, c.y), uvi_cardinal); + VTX_WRITE(12, rbi, uvi_cardinal); VTX_WRITE(13, brbi, uvbi); - VTX_WRITE(14, ImVec2(c.x, br.y - stroke_width), uvi_cardinal); + VTX_WRITE(14, bbi, uvi_cardinal); VTX_WRITE(15, blbi, uvbi); } @@ -2022,7 +2058,7 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu return; // First try the fast texture-based renderer, and only if that can't handle this fall back to paths - if (AddRoundCornerCircle(this, center, radius, col, false)) + if (AddRoundCornerCircle(this, center, radius, thickness, col, false)) return; // Obtain segment count @@ -2051,7 +2087,7 @@ void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, return; // First try the fast texture-based renderer, and only if that can't handle this fall back to paths - if (AddRoundCornerCircle(this, center, radius, col, true)) + if (AddRoundCornerCircle(this, center, radius, 1.0f, col, true)) return; if (num_segments <= 0) @@ -3341,17 +3377,47 @@ static void ImFontAtlasBuildRegisterRoundCornersCustomRects(ImFontAtlas* atlas) return; const int pad = FONT_ATLAS_ROUNDED_CORNER_TEX_PADDING; - const unsigned int max = ImFontAtlasRoundCornersMaxSize; + const unsigned int max_radius = ImFontAtlasRoundCornersMaxSize; + const unsigned int max_thickness = ImFontAtlasRoundCornersMaxStrokeWidth; - atlas->TexRoundCornerData.reserve(max); + atlas->TexRoundCornerData.reserve(max_radius * max_thickness); - for (unsigned int n = 0; n < max; n++) + for (unsigned int radius_index = 0; radius_index < max_radius; radius_index++) { - const int width = n + 1 + pad * 2; - const int height = n + 1 + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING + pad * 2; - ImFontRoundedCornerData corner_data; - corner_data.RectId = atlas->AddCustomRectRegular(width, height); - atlas->TexRoundCornerData.push_back(corner_data); + int spare_rect_id = -1; // The last rectangle ID we generated with a spare half + + for (unsigned int stroke_width_index = 0; stroke_width_index < max_thickness; stroke_width_index++) + { + //const unsigned int index = stroke_width_index + (radius_index * ImFontAtlasRoundCornersMaxStrokeWidth); + + const int width = radius_index + 1 + pad * 2; + const int height = radius_index + 1 + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING + pad * 2; + + ImFontRoundedCornerData corner_data; + + if (ImFontAtlasRoundCornersStrokeWidthMask & (1 << stroke_width_index)) + { + if ((stroke_width_index == 0) || (spare_rect_id < 0)) + { + corner_data.RectId = atlas->AddCustomRectRegular(width, height); + corner_data.StrokedUsesAlternateUVs = false; + if (stroke_width_index != 0) + spare_rect_id = corner_data.RectId; + } + else + { + // Pack this into the spare half of the previous rect + corner_data.RectId = spare_rect_id; + corner_data.StrokedUsesAlternateUVs = true; + spare_rect_id = -1; + } + } + else + corner_data.RectId = -1; // Set RectId to -1 if we don't want any data + + IM_ASSERT_PARANOID(atlas->TexRoundCornerData.size() == (int)index); + atlas->TexRoundCornerData.push_back(corner_data); + } } } @@ -3364,85 +3430,141 @@ static void ImFontAtlasBuildRenderRoundCornersTexData(ImFontAtlas* atlas) // Render the texture const int w = atlas->TexWidth; - const unsigned int max = ImFontAtlasRoundCornersMaxSize; + const unsigned int max = ImFontAtlasRoundCornersMaxSize * ImFontAtlasRoundCornersMaxStrokeWidth; const int pad = FONT_ATLAS_ROUNDED_CORNER_TEX_PADDING; IM_ASSERT(atlas->TexRoundCornerData.Size == (int)max); // ImFontAtlasBuildRegisterRoundCornersCustomRects() will have created this for us - for (unsigned int n = 0; n < max; n++) - { - const unsigned int id = n; - ImFontRoundedCornerData& data = atlas->TexRoundCornerData[id]; - ImFontAtlasCustomRect& r = atlas->CustomRects[data.RectId]; - IM_ASSERT(r.IsPacked()); - IM_ASSERT(r.Width == n + 1 + pad * 2 && r.Height == n + 1 + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING + pad * 2); - // What we're doing here is generating a rectangular image that contains the data for both the filled and - // stroked variants of the corner with the radius specified. We do it like this because we only need 45 degrees - // worth of curve (as each corner mirrors the texture to get the full 90 degrees), and hence with a little care - // we can put both variants into one texture by using two triangular regions. In practice this is a little more - // tricky than it first looks because if the two regions are packed tightly you get filtering errors where they meet, - // so we offset one vertically from the other by FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING pixels. - // The stroked version is at the top-right of the texture, and the filled version at the bottom-left. - const int radius = (int)(r.Width - pad * 2); - const float stroke_width = 1.0f; + const unsigned int max_radius = ImFontAtlasRoundCornersMaxSize; + const unsigned int max_thickness = ImFontAtlasRoundCornersMaxStrokeWidth; - // Pre-calcuate the parameteric stroke width - data.ParametricStrokeWidth = stroke_width / (float)radius; + atlas->TexRoundCornerData.reserve(max_radius * max_thickness); - for (int y = -pad; y < (int)(radius + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING); y++) - for (int x = -pad; x < (int)(radius); x++) + for (unsigned int radius_index = 0; radius_index < max_radius; radius_index++) + for (unsigned int stroke_width_index = 0; stroke_width_index < max_thickness; stroke_width_index++) + { + const unsigned int index = stroke_width_index + (radius_index * ImFontAtlasRoundCornersMaxStrokeWidth); + + const unsigned int radius = radius_index + 1; + const float stroke_width = (float)stroke_width_index + 1; + + ImFontRoundedCornerData& data = atlas->TexRoundCornerData[index]; + if (data.RectId < 0) + continue; // We don't want to generate data for this + + ImFontAtlasCustomRect& r = atlas->CustomRects[data.RectId]; + IM_ASSERT(r.IsPacked()); + IM_ASSERT(r.Width == radius + pad * 2 && r.Height == radius + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING + pad * 2); + + // If we are generating data for a stroke width > 0, then look for another stroke width sharing this rectangle + float other_stroke_width = -1.0f; + ImFontRoundedCornerData* other_data = NULL; + + if (stroke_width_index > 0) { - // We want the pad area to essentially contain a clamped version of the 0th row/column, so - // clamp here. Not doing this results in nasty filtering artifacts at low radii. - int cx = ImMax(x, 0); - int cy = ImMax(y, 0); - - // The XY region - // the data for stroked ones. We add half of FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING so that - // each side gets a buffer zone to avoid filtering artifacts. - const bool filled = x < (y - (FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING >> 1)); - if (filled) + // We use the fact that we know shared pairs will always appear together to both make this check fast and skip trying + // to generate the second half of the pair again when the main loop comes around + stroke_width_index++; + while (stroke_width_index < max_thickness) { - // The filled version starts a little further down the texture to give us the padding in the middle. - cy = ImMax(y - FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING, 0); - } + const unsigned int candidate_index = stroke_width_index + (radius_index * ImFontAtlasRoundCornersMaxStrokeWidth); + ImFontRoundedCornerData* candidate_data = &atlas->TexRoundCornerData[candidate_index]; - const float dist = ImSqrt((float)(cx*cx+cy*cy)) - (float)(radius - (filled ? 0 : stroke_width)); - float alpha = 0.0f; - if (filled) - { - alpha = ImClamp(-dist, 0.0f, 1.0f); - } - else - { - const float alpha1 = ImClamp(dist + stroke_width, 0.0f, 1.0f); - const float alpha2 = ImClamp(dist, 0.0f, 1.0f); - alpha = alpha1 - alpha2; - } + if (candidate_data->RectId == data.RectId) + { + other_data = candidate_data; + other_stroke_width = (float)stroke_width_index + 1; + other_data->ParametricStrokeWidth = ((other_stroke_width > 1.0f) ? (other_stroke_width + 2.0f) : other_stroke_width) / (float)radius; + break; + } - const unsigned int offset = (int)(r.X + pad + x) + (int)(r.Y + pad + y) * w; - atlas->TexPixelsAlpha8[offset] = (unsigned char)(0xFF * ImSaturate(alpha)); + stroke_width_index++; + } } - // We generate two sets of UVs for each rectangle, one for the filled portion and one for the unfilled bit. - for (unsigned int stage = 0; stage < 2; stage++) - { - ImFontAtlasCustomRect stage_rect = r; + // What we're doing here is generating a rectangular image that contains the data for both the filled and + // stroked variants of the corner with the radius specified. We do it like this because we only need 45 degrees + // worth of curve (as each corner mirrors the texture to get the full 90 degrees), and hence with a little care + // we can put both variants into one texture by using two triangular regions. In practice this is a little more + // tricky than it first looks because if the two regions are packed tightly you get filtering errors where they meet, + // so we offset one vertically from the other by FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING pixels. + // The stroked version is at the top-right of the texture, and the filled version at the bottom-left. - const bool filled = (stage == 0); - stage_rect.X += pad; - stage_rect.Y += pad + (filled ? FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING : 0); - stage_rect.Width -= (pad * 2); - stage_rect.Height -= (pad * 2) + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING; + // Pre-calculate the parametric stroke width (+2 to give space for texture filtering on non-single-pixel widths) + data.ParametricStrokeWidth = ((stroke_width > 1.0f) ? (stroke_width + 2.0f) : stroke_width) / (float)radius; - ImVec2 uv0, uv1; - atlas->CalcCustomRectUV(&stage_rect, &uv0, &uv1); + for (int y = -pad; y < (int)(radius + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING + pad); y++) + for (int x = -pad; x < (int)(radius); x++) + { + // We want the pad area to essentially contain a clamped version of the 0th row/column, so + // clamp here. Not doing this results in nasty filtering artifacts at low radii. + int cx = ImMax(x, 0); + int cy = ImMax(y, 0); - if (stage == 0) - data.TexUvFilled = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); - else - data.TexUvStroked = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + // The XY region + // the data for stroked ones. We add half of FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING so that + // each side gets a buffer zone to avoid filtering artifacts. + // For stroke widths > 1, we use the "filled" area to hold a second stroke width variant + const bool filled = x < (y - (FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING >> 1)); + if (filled) + { + // The filled version starts a little further down the texture to give us the padding in the middle. + cy = ImMax(y - FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING, 0); + } + + const float dist = ImSqrt((float)(cx*cx+cy*cy)) - (float)(radius - (filled ? 0 : (stroke_width * 0.5f) + 0.5f)); + float alpha = 0.0f; + if (filled) + { + if (stroke_width_index > 0) + { + if (other_data) + { + // Using the filled section to hold a second stroke width variant instead of filled if we are at a stroke width > 1 + const float other_dist = ImSqrt((float)(cx*cx + cy * cy)) - (float)(radius - ((other_stroke_width * 0.5f) + 0.5f)); + const float alpha1 = ImClamp(other_dist + other_stroke_width, 0.0f, 1.0f); + const float alpha2 = ImClamp(other_dist, 0.0f, 1.0f); + alpha = alpha1 - alpha2; + } + } + else + alpha = ImClamp(-dist, 0.0f, 1.0f); // Filled version + } + else + { + const float alpha1 = ImClamp(dist + stroke_width, 0.0f, 1.0f); + const float alpha2 = ImClamp(dist, 0.0f, 1.0f); + alpha = alpha1 - alpha2; + } + + const unsigned int offset = (int)(r.X + pad + x) + (int)(r.Y + pad + y) * w; + atlas->TexPixelsAlpha8[offset] = (unsigned char)(0xFF * ImSaturate(alpha)); + } + + // We generate two sets of UVs for each rectangle, one for the filled portion and one for the unfilled bit. + for (unsigned int stage = 0; stage < 2; stage++) + { + ImFontAtlasCustomRect stage_rect = r; + + const bool filled = (stage == 0); + stage_rect.X += pad; + stage_rect.Y += pad + (filled ? FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING : 0); + stage_rect.Width -= (pad * 2); + stage_rect.Height -= (pad * 2) + FONT_ATLAS_ROUNDED_CORNER_TEX_CENTER_PADDING; + + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(&stage_rect, &uv0, &uv1); + + if (stage == 0) + { + if (other_data) + other_data->TexUvStroked = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + else + data.TexUvFilled = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + } + else + data.TexUvStroked = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + } } - } } // This is called/shared by both the stb_truetype and the FreeType builder. diff --git a/imgui_internal.h b/imgui_internal.h index 2c489fa3..9c7da5db 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2889,7 +2889,6 @@ struct ImFontBuilderIO #ifdef IMGUI_ENABLE_STB_TRUETYPE IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); #endif -const unsigned int ImFontAtlasRoundCornersMaxSize = 32; // Maximum size of rounded corner texture to generate in fonts IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); @@ -2899,6 +2898,14 @@ IMGUI_API void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); +// Note that stroke width increases effective radius, so (e.g.) a max radius circle will have to use the fallback path if stroke width is > 1 +const unsigned int ImFontAtlasRoundCornersMaxSize = 32; // Maximum size of rounded corner texture to generate in fonts +const unsigned int ImFontAtlasRoundCornersMaxStrokeWidth = 5; // Maximum stroke width of rounded corner texture to generate in fonts +// Bit mask for which stroke widths should have textures generated for them (the default of 0xD means widths 1, 2 and 4) +// Only bits up to ImFontAtlasRoundCornersMaxStrokeWidth are considered, and bit 0 (stroke width 1) must always be set +// Optimally there should be an odd number of bits set, as the texture packing packs the data in pairs, with one half of one pair being occupied by the filled texture +const unsigned int ImFontAtlasRoundCornersStrokeWidthMask = 0xD; + //----------------------------------------------------------------------------- // [SECTION] Test Engine specific hooks (imgui_test_engine) //-----------------------------------------------------------------------------