From 30e53cc6d8ca2e6804ab2999db6c84cdc7f98baa Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Wed, 22 Apr 2026 22:24:55 +0300 Subject: [PATCH] DrawList: improved handling of rounded stroked rectangles, where rounding is less than stroke thickness # Conflicts: # imgui.h # imgui_draw.cpp --- imgui.h | 4 +-- imgui_draw.cpp | 83 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/imgui.h b/imgui.h index cc7c65159..97febeb8a 100644 --- a/imgui.h +++ b/imgui.h @@ -3648,8 +3648,8 @@ struct ImDrawList IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); IMGUI_API void _AddRectFilledBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, ImVec4 tex_uvs, ImDrawFlags flags); IMGUI_API void _AddRectBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, float t, ImVec4 tex_uvs, ImDrawFlags flags); - IMGUI_API void _AddPolylineThin(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, ImVec4 tex_uvs); - IMGUI_API void _AddPolylineThick(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags); + IMGUI_API void _AddPolylineThin(const ImVec2* points, ImVec2* normals, float* sqr_lengths, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, ImVec4 tex_uvs); + IMGUI_API void _AddPolylineThick(const ImVec2* points, ImVec2* normals, float* sqr_lengths, const int points_count, ImU32 col, float thickness, ImDrawFlags flags); }; // All draw data to render a Dear ImGui frame diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 711fc47ca..3c191ca13 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -877,16 +877,10 @@ static void CalcSegmentNormals(const ImVec2* points, const int points_count, ImV } } -void ImDrawList::_AddPolylineThin(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, ImVec4 tex_uvs) +void ImDrawList::_AddPolylineThin(const ImVec2* points, ImVec2* normals, float* sqr_lengths, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, ImVec4 tex_uvs) { const bool closed = (flags & ImDrawFlags_Closed) != 0; const bool miters_only = (flags & ImDrawFlags_MiterOnly) != 0; - - _Data->TempBuffer.reserve_discard(points_count * 2); - ImVec2* normals = _Data->TempBuffer.Data; - float* sqr_lengths = (float*)(normals + points_count); - CalcSegmentNormals(points, points_count, normals, sqr_lengths, closed); - const ImU32 col_trans = col & ~IM_COL32_A_MASK; thickness += _FringeScale; @@ -1216,16 +1210,11 @@ void ImDrawList::_AddPolylineThin(const ImVec2* points, const int points_count, PrimUnreserve(idx_count - idx_used, vtx_count - vtx_used); } -void ImDrawList::_AddPolylineThick(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags) +void ImDrawList::_AddPolylineThick(const ImVec2* points, ImVec2* normals, float* sqr_lengths, const int points_count, ImU32 col, float thickness, ImDrawFlags flags) { const bool closed = (flags & ImDrawFlags_Closed) != 0; const bool miters_only = (flags & ImDrawFlags_MiterOnly) != 0; - _Data->TempBuffer.reserve_discard(points_count * 2); - ImVec2* normals = _Data->TempBuffer.Data; - float* sqr_lengths = (float*)(normals + points_count); - CalcSegmentNormals(points, points_count, normals, sqr_lengths, closed); - const ImU32 col_trans = col & ~IM_COL32_A_MASK; // TODO: we can expand this by having one very long texture strip for the lines, @@ -1562,14 +1551,19 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 const int int_thickness = (int)screen_thickness; const bool can_use_thin = ImAbs(screen_thickness - (float)int_thickness) < 0.01f && (int_thickness >= 1 && int_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX); + _Data->TempBuffer.reserve_discard(points_count * 2); + ImVec2* normals = _Data->TempBuffer.Data; + float* sqr_lengths = (float*)(normals + points_count); + CalcSegmentNormals(points, points_count, normals, sqr_lengths, (flags & ImDrawFlags_Closed) != 0); + if (can_use_thin) { const ImVec4 tex_uvs = _Data->TexUvLines[int_thickness]; - _AddPolylineThin(points, points_count, col, (float)int_thickness * _FringeScale, flags, tex_uvs); + _AddPolylineThin(points, normals, sqr_lengths, points_count, col, (float)int_thickness * _FringeScale, flags, tex_uvs); } else { - _AddPolylineThick(points, points_count, col, thickness, flags); + _AddPolylineThick(points, normals, sqr_lengths, points_count, col, thickness, flags); } } @@ -2475,6 +2469,25 @@ void ImDrawList::_AddRectBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 c PrimUnreserve(idx_allocated - idx_used, vtx_allocated - vtx_used); } +static void RectCornerArc(ImDrawList* draw_list, float cx, float cy, float radius, const int arc_step, const int arc_point_count, const int corner) +{ + if (radius == 0.f) + { + draw_list->_Path.push_back(ImVec2(cx, cy)); + return; + } + + int arc_idx = (IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4) * corner; + for (int i = 0; i < arc_point_count; i++) + { + ImVec2 pt; + pt.x = cx - draw_list->_Data->ArcFastVtx[arc_idx].x * radius; + pt.y = cy - draw_list->_Data->ArcFastVtx[arc_idx].y * radius; + draw_list->_Path.push_back(pt); + arc_idx = (arc_idx + arc_step) % IM_DRAWLIST_ARCFAST_TABLE_SIZE; + } +} + // p_min = upper-left, p_max = lower-right void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, float thickness, ImDrawFlags flags) { @@ -2505,8 +2518,8 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl ImDrawFlags stroke_pos = _GetStrokePos(flags, ImDrawFlags_StrokeInside); #if 0 // shiny_polyline // FIXME-POLYLINE + const bool has_rounding = (rounding >= _FringeScale); - const ImDrawFlags stroke_pos = (flags & ImDrawFlags_StrokeMask_); ImVec2 adjusted_min = p_min; ImVec2 adjusted_max = p_max; float adjusted_rounding = rounding; @@ -2542,9 +2555,41 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl return; } - // TODO: does not handle well the case that radius < thickness/2. - PathRect(adjusted_min, adjusted_max, adjusted_rounding, flags); - PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | flags); + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + if (!has_rounding || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PathLineTo(adjusted_min); + PathLineTo(ImVec2(adjusted_max.x, adjusted_min.y)); + PathLineTo(adjusted_max); + PathLineTo(ImVec2(adjusted_min.x, adjusted_max.y)); + } + else + { + // Constrain rounding to rect dimensions + adjusted_rounding = ImMin(adjusted_rounding, ImFabs(width) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + adjusted_rounding = ImMin(adjusted_rounding, ImFabs(height) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + // We have to have some kind of curve to render the corner with PathStroke(). + // The small value limits the rounding to about half the stroke radius. + adjusted_rounding = ImMax(_FringeScale * 0.25f, adjusted_rounding); + + const float rounding_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? adjusted_rounding : 0.0f; + const float rounding_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? adjusted_rounding : 0.0f; + const float rounding_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? adjusted_rounding : 0.0f; + const float rounding_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? adjusted_rounding : 0.0f; + + // Use custom arcs so that we can specify the point count. + const int arc_step = ImClamp(IM_DRAWLIST_ARCFAST_SAMPLE_MAX / this->_CalcCircleAutoSegmentCount(ImMax(adjusted_rounding, thickness * 0.5f)), 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); + const int arc_point_count = (IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4) / arc_step + 1; + + RectCornerArc(this, adjusted_min.x + rounding_tl, adjusted_min.y + rounding_tl, rounding_tl, arc_step, arc_point_count, 0); + RectCornerArc(this, adjusted_max.x - rounding_tr, adjusted_min.y + rounding_tr, rounding_tr, arc_step, arc_point_count, 1); + RectCornerArc(this, adjusted_max.x - rounding_br, adjusted_max.y - rounding_br, rounding_br, arc_step, arc_point_count, 2); + RectCornerArc(this, adjusted_min.x + rounding_bl, adjusted_max.y - rounding_bl, rounding_bl, arc_step, arc_point_count, 3); + } + + PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly); #else const bool is_truncated = _FringeScaleIsInteger && ImIsTruncated4(p_min.x, p_min.y, p_max.x, p_max.y) && ImIsTruncated4(rounding, thickness, 0.0f, 0.0f); if ((stroke_pos == ImDrawFlags_StrokeInside || stroke_pos == ImDrawFlags_StrokeOutside || stroke_pos == ImDrawFlags_StrokeCenterPixelAligned) && is_truncated)