Tables: fixed various border overshooting issues, some mostly visible with transparent borders: (#4678, #9415)

As a side effect of cleaning up a few things, altering border size becomes closer to being correct.
This commit is contained in:
ocornut 2026-10-29 20:14:03 +01:00
parent 0d0cd87dd2
commit c1581882a4
3 changed files with 30 additions and 35 deletions

View File

@ -79,14 +79,17 @@ Other Changes:
- Modals: fixed an assert displaying a modal in an environment where
`io.DisplayFramebufferScale` is not an integer (regression in 1.93.0). (#9598)
- Tables:
- In non-scrolling tables, clamp lower border coordinates to stay within visible
range (no visible changes, but ImDrawCmd AABB becomes more sensible).
- Fixed various border overshooting issues, some mostly visible with transparent
borders: (#4678)
- On non-scrolling tables, inner horizontal borders slightly overlapped
outer vertical ones with certain settings.
- On non-scrolling tables, clamping lower border coordinates to stay within
visible range (no visible change, but it makes ImDrawCmd AABB more sensible).
- Fixed vertical inner borders slightly overlapping bottom outer border.
- Fixed various issues with using `ImGuiTableFlags_NoHostExtendY`:
- Fixed selectable highlight straying off the Y2 clipping limit.
- Fixed parent window work rectangle affecting the Y2 clipping limit.
- Fixed the Y2 clipping limit overlapping bottom outer border.
- Fixed vertical inner borders slightly overlapping bottom outer border
(may be visible with transparent borders).
- ImVec2: added a check for cases where a custom `IM_VEC2_CLASS_EXTRA` macro
adds an extra `operator=` to ImVec2. This isn't supported but should not
be needed as you should have a constructor to convert from your type to ImVec2.

View File

@ -653,6 +653,7 @@ struct IMGUI_API ImRect
void TranslateY(float dy) { Min.y += dy; Max.y += dy; }
void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display.
void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped.
void ShrinkClamp(const ImVec2& inset_min, const ImVec2& inset_max) { Min.x = ImMin(Min.x + inset_min.x, Max.x); Min.y = ImMin(Min.y + inset_min.y, Max.y); Max.x = ImMax(Max.x - inset_max.x, Min.x); Max.y = ImMax(Max.y - inset_max.y, Min.y); }
bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; }
ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); }
const ImVec4& AsVec4() const { return *(const ImVec4*)&Min.x; }

View File

@ -483,27 +483,22 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG
const float border_size = table->BorderSize = TABLE_BORDER_SIZE;
// Make borders not overlap our contents by offsetting HostClipRect (#6765, #7428, #3752)
// (we normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the
// limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap
// problem only affect scrolling tables in this case we can get away with doing it without extra cost).
if (inner_window != outer_window)
// - We normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the
// limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap
// problem only affect scrolling tables in this case we can get away with doing it without extra cost).
// - (FIXME: Because inner_window's Scrollbar doesn't know about border size, since it's not encoded in window->WindowBorderSize,
// it already overlaps it and doesn't need an extra offset. Ideally we should be able to pass custom border size with
// different x/y values to BeginChild().)
// - For non-scrolling window, this is applied to table->InnerClipRect below.
ImRect border_size_outer =
{
// FIXME: Because inner_window's Scrollbar doesn't know about border size, since it's not encoded in window->WindowBorderSize,
// it already overlaps it and doesn't need an extra offset. Ideally we should be able to pass custom border size with
// different x/y values to BeginChild().
if (flags & ImGuiTableFlags_BordersOuterV)
{
table->HostClipRect.Min.x = ImMin(table->HostClipRect.Min.x + border_size, table->HostClipRect.Max.x);
if (inner_window->DecoOuterSizeX2 == 0.0f)
table->HostClipRect.Max.x = ImMax(table->HostClipRect.Max.x - border_size, table->HostClipRect.Min.x);
}
if (flags & ImGuiTableFlags_BordersOuterH)
{
table->HostClipRect.Min.y = ImMin(table->HostClipRect.Min.y + border_size, table->HostClipRect.Max.y);
if (inner_window->DecoOuterSizeY2 == 0.0f)
table->HostClipRect.Max.y = ImMax(table->HostClipRect.Max.y - border_size, table->HostClipRect.Min.y);
}
}
(flags & ImGuiTableFlags_BordersOuterV) ? border_size : 0.0f,
(flags & ImGuiTableFlags_BordersOuterH) ? border_size : 0.0f,
((flags & ImGuiTableFlags_BordersOuterV) && ((inner_window == outer_window) || inner_window->DecoOuterSizeX2 == 0.0f)) ? border_size : 0.0f,
((flags & ImGuiTableFlags_BordersOuterH) && ((inner_window == outer_window) || inner_window->DecoOuterSizeY2 == 0.0f)) ? border_size : 0.0f,
};
if (inner_window != outer_window)
table->HostClipRect.ShrinkClamp(border_size_outer.Min, border_size_outer.Max);
// Padding and Spacing
// - None ........Content..... Pad .....Content........
@ -528,17 +523,13 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG
table->RowBgColorCounter = 0;
table->LastRowFlags = ImGuiTableRowFlags_None;
table->InnerClipRect = (inner_window == outer_window) ? table->WorkRect : inner_window->ClipRect;
if (inner_window == outer_window)
table->InnerClipRect.ShrinkClamp(border_size_outer.Min, border_size_outer.Max);
table->InnerClipRect.ClipWith(table->WorkRect); // We need this to honor inner_width
table->InnerClipRect.ClipWithFull(table->HostClipRect);
if (flags & ImGuiTableFlags_NoHostExtendY)
{
if (flags & ImGuiTableFlags_BordersOuterH)
table->InnerClipRect.Max.y = ImClamp(table->OuterRect.Max.y - border_size, table->InnerClipRect.Min.y, table->InnerClipRect.Max.y);
}
else
{
if ((flags & ImGuiTableFlags_NoHostExtendY) == 0)
table->InnerClipRect.Max.y = table->HostClipRect.Max.y;
}
table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow
table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow()
table->RowCellPaddingY = 0.0f;
@ -1513,10 +1504,10 @@ void ImGui::EndTable()
// but since the later is likely to be impossible to do we'd rather update both axes together.
if (table->Flags & ImGuiTableFlags_ScrollX)
{
const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? table->BorderSize : 0.0f;
const float border_size_outer_w = (table->Flags & ImGuiTableFlags_BordersOuterV) ? table->BorderSize : 0.0f;
float max_pos_x = inner_window->DC.CursorMaxPos.x;
if (table->RightMostEnabledColumn != -1)
max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border);
max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - border_size_outer_w);
if (table->ResizedColumn != -1)
max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2);
inner_window->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth;
@ -3049,7 +3040,7 @@ void ImGui::TableDrawBorders(ImGuiTable* table)
// Draw bottom-most row border between it and above outer border.
const float border_y = table->RowPosY2;
if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y)
inner_drawlist->AddLineH(table->BorderX1 + border_size_outer_w, table->BorderX2 - border_size_outer_w, border_y, table->BorderColorLight, border_size_inner);
inner_drawlist->AddLineH(table->BorderX1, table->BorderX2, border_y, table->BorderColorLight, border_size_inner);
}
inner_drawlist->PopClipRect();