Tables: moved + renamed TableSetupDrawChannels(), TableMergeDrawChannels() to their own section.

This commit is contained in:
ocornut 2020-12-01 17:31:59 +01:00
parent b7c83e4bac
commit 5180025de5
2 changed files with 257 additions and 247 deletions

View File

@ -2283,12 +2283,12 @@ namespace ImGui
IMGUI_API ImGuiTable* TableFindByID(ImGuiID id);
IMGUI_API bool BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f);
IMGUI_API void TableBeginApplyRequests(ImGuiTable* table);
IMGUI_API void TableUpdateDrawChannels(ImGuiTable* table);
IMGUI_API void TableSetupDrawChannels(ImGuiTable* table);
IMGUI_API void TableUpdateLayout(ImGuiTable* table);
IMGUI_API void TableUpdateBorders(ImGuiTable* table);
IMGUI_API void TableDrawBorders(ImGuiTable* table);
IMGUI_API void TableDrawContextMenu(ImGuiTable* table);
IMGUI_API void TableReorderDrawChannelsForMerge(ImGuiTable* table);
IMGUI_API void TableMergeDrawChannels(ImGuiTable* table);
IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table);
IMGUI_API void TableSortSpecsBuild(ImGuiTable* table);
IMGUI_API void TableFixColumnSortDirection(ImGuiTableColumn* column);

View File

@ -6,6 +6,7 @@
Index of this file:
// [SECTION] Tables: BeginTable, EndTable, etc.
// [SECTION] Tables: Drawing
// [SECTION] Tables: Sorting
// [SECTION] Tables: Headers
// [SECTION] Tables: Context Menu
@ -81,7 +82,7 @@ Index of this file:
// - TableSetupColumn() user submit columns details (optional)
// - TableSetupScrollFreeze() user submit scroll freeze information (optional)
// - TableUpdateLayout() [Internal] automatically called by the FIRST call to TableNextRow() or TableHeadersRow(): lock all widths, columns positions, clipping rectangles
// | TableUpdateDrawChannels() - setup ImDrawList channels
// | TableSetupDrawChannels() - setup ImDrawList channels
// | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission
// | TableDrawContextMenu() - draw right-click context menu
//-----------------------------------------------------------------------------
@ -99,7 +100,7 @@ Index of this file:
//-----------------------------------------------------------------------------
// - EndTable() user ends the table
// | TableDrawBorders() - draw outer borders, inner vertical borders
// | TableReorderDrawChannelsForMerge() - merge draw channels if clipping isn't required
// | TableMergeDrawChannels() - merge draw channels if clipping isn't required
// | EndChild() - (if ScrollX/ScrollY is set)
//-----------------------------------------------------------------------------
@ -938,7 +939,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table)
table->Flags &= ~ImGuiTableFlags_Resizable;
// Allocate draw channels
TableUpdateDrawChannels(table);
TableSetupDrawChannels(table);
// Borders
if (table->Flags & ImGuiTableFlags_Resizable)
@ -1111,7 +1112,7 @@ void ImGui::EndTable()
// Flatten channels and merge draw calls
if ((table->Flags & ImGuiTableFlags_NoClip) == 0)
TableReorderDrawChannelsForMerge(table);
TableMergeDrawChannels(table);
table->DrawSplitter.Merge(inner_window->DrawList);
if (!(table->Flags & ImGuiTableFlags_ScrollX) && inner_window != outer_window)
@ -1394,247 +1395,6 @@ void ImGui::TableSetColumnWidth(int column_n, float width)
}
}
// Allocate draw channels. Called by TableUpdateLayout()
// - We allocate them following storage order instead of display order so reordering columns won't needlessly
// increase overall dormant memory cost.
// - We isolate headers draw commands in their own channels instead of just altering clip rects.
// This is in order to facilitate merging of draw commands.
// - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels.
// - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other
// channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged.
// - We allocate 1 or 2 background draw channels. This is because we know PushTableBackground() is only used for
// horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4).
// Draw channel allocation (before merging):
// - NoClip --> 2+D+1 channels: bg0 + bg1 + foreground (same clip rect == 1 draw call) (FIXME-TABLE: could merge bg1 and foreground?)
// - Clip --> 2+D+N channels
// - FreezeRows --> 2+D+N*2 (unless scrolling value is zero)
// - FreezeRows || FreezeColunns --> 3+D+N*2 (unless scrolling value is zero)
// Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0.
void ImGui::TableUpdateDrawChannels(ImGuiTable* table)
{
const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1;
const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClip) ? 1 : table->ColumnsEnabledCount;
const int channels_for_bg = 1 + 1 * freeze_row_multiplier;
const int channels_for_dummy = (table->ColumnsEnabledCount < table->ColumnsCount || table->EnabledUnclippedMaskByIndex != table->EnabledMaskByIndex) ? +1 : 0;
const int channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy;
table->DrawSplitter.Split(table->InnerWindow->DrawList, channels_total);
table->DummyDrawChannel = (ImU8)((channels_for_dummy > 0) ? channels_total - 1 : -1);
table->Bg1DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG1_FROZEN;
table->Bg1DrawChannelUnfrozen = (ImU8)((table->FreezeRowsCount > 0) ? 2 + channels_for_row : TABLE_DRAW_CHANNEL_BG1_FROZEN);
int draw_channel_current = 2;
for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
{
ImGuiTableColumn* column = &table->Columns[column_n];
if (!column->IsClipped)
{
column->DrawChannelFrozen = (ImU8)(draw_channel_current);
column->DrawChannelUnfrozen = (ImU8)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row + 1 : 0));
if (!(table->Flags & ImGuiTableFlags_NoClip))
draw_channel_current++;
}
else
{
column->DrawChannelFrozen = column->DrawChannelUnfrozen = table->DummyDrawChannel;
}
column->DrawChannelCurrent = column->DrawChannelFrozen;
}
}
// This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable().
//
// Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve
// this we merge their clip rect and make them contiguous in the channel list, so they can be merged
// by the call to DrawSplitter.Merge() following to the call to this function.
//
// We reorder draw commands by arranging them into a maximum of 4 distinct groups:
//
// 1 group: 2 groups: 2 groups: 4 groups:
// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze
// [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll
//
// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled).
// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group
// based on its position (within frozen rows/columns groups or not).
// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect.
//
// This function assume that each column are pointing to a distinct draw channel,
// otherwise merge_group->ChannelsCount will not match set bit count of merge_group->ChannelsMask.
//
// Column channels will not be merged into one of the 1-4 groups in the following cases:
// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value).
// Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui:: bounds
// matches, by e.g. calling SetCursorScreenPos().
// - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here..
// we could do better but it's going to be rare and probably not worth the hassle.
// Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd.
//
// This function is particularly tricky to understand.. take a breath.
void ImGui::TableReorderDrawChannelsForMerge(ImGuiTable* table)
{
ImGuiContext& g = *GImGui;
ImDrawListSplitter* splitter = &table->DrawSplitter;
const bool has_freeze_v = (table->FreezeRowsCount > 0);
const bool has_freeze_h = (table->FreezeColumnsCount > 0);
// Track which groups we are going to attempt to merge, and which channels goes into each group.
struct MergeGroup
{
ImRect ClipRect;
int ChannelsCount;
ImBitArray<IMGUI_TABLE_MAX_DRAW_CHANNELS> ChannelsMask;
};
int merge_group_mask = 0x00;
MergeGroup merge_groups[4];
memset(merge_groups, 0, sizeof(merge_groups));
// 1. Scan channels and take note of those which can be merged
for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
{
if (!(table->EnabledUnclippedMaskByIndex & ((ImU64)1 << column_n)))
continue;
ImGuiTableColumn* column = &table->Columns[column_n];
const int merge_group_sub_count = has_freeze_v ? 2 : 1;
for (int merge_group_sub_n = 0; merge_group_sub_n < merge_group_sub_count; merge_group_sub_n++)
{
const int channel_no = (merge_group_sub_n == 0) ? column->DrawChannelFrozen : column->DrawChannelUnfrozen;
// Don't attempt to merge if there are multiple draw calls within the column
ImDrawChannel* src_channel = &splitter->_Channels[channel_no];
if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0)
src_channel->_CmdBuffer.pop_back();
if (src_channel->_CmdBuffer.Size != 1)
continue;
// Find out the width of this merge group and check if it will fit in our column
// (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it)
if (!(column->Flags & ImGuiTableColumnFlags_NoClip))
{
float content_max_x;
if (!has_freeze_v)
content_max_x = ImMax(column->ContentMaxXUnfrozen, column->ContentMaxXHeadersUsed); // No row freeze
else if (merge_group_sub_n == 0)
content_max_x = ImMax(column->ContentMaxXFrozen, column->ContentMaxXHeadersUsed); // Row freeze: use width before freeze
else
content_max_x = column->ContentMaxXUnfrozen; // Row freeze: use width after freeze
if (content_max_x > column->ClipRect.Max.x)
continue;
}
const int merge_group_n = (has_freeze_h && column_n < table->FreezeColumnsCount ? 0 : 1) + (has_freeze_v && merge_group_sub_n == 0 ? 0 : 2);
IM_ASSERT(channel_no < IMGUI_TABLE_MAX_DRAW_CHANNELS);
MergeGroup* merge_group = &merge_groups[merge_group_n];
if (merge_group->ChannelsCount == 0)
merge_group->ClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
merge_group->ChannelsMask.SetBit(channel_no);
merge_group->ChannelsCount++;
merge_group->ClipRect.Add(src_channel->_CmdBuffer[0].ClipRect);
merge_group_mask |= (1 << merge_group_n);
}
// Invalidate current draw channel
// (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data)
column->DrawChannelCurrent = (ImU8)-1;
}
// [DEBUG] Display merge groups
#if 0
if (g.IO.KeyShift)
for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++)
{
MergeGroup* merge_group = &merge_groups[merge_group_n];
if (merge_group->ChannelsCount == 0)
continue;
char buf[32];
ImFormatString(buf, 32, "MG%d:%d", merge_group_n, merge_group->ChannelsCount);
ImVec2 text_pos = merge_group->ClipRect.Min + ImVec2(4, 4);
ImVec2 text_size = CalcTextSize(buf, NULL);
GetForegroundDrawList()->AddRectFilled(text_pos, text_pos + text_size, IM_COL32(0, 0, 0, 255));
GetForegroundDrawList()->AddText(text_pos, IM_COL32(255, 255, 0, 255), buf, NULL);
GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 255, 0, 255));
}
#endif
// 2. Rewrite channel list in our preferred order
if (merge_group_mask != 0)
{
// We skip channel 0 (Bg0) and 1 (Bg1 frozen) from the shuffling since they won't move - see channels allocation in TableUpdateDrawChannels().
const int LEADING_DRAW_CHANNELS = 2;
g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - LEADING_DRAW_CHANNELS); // Use shared temporary storage so the allocation gets amortized
ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data;
ImBitArray<IMGUI_TABLE_MAX_DRAW_CHANNELS> remaining_mask; // We need 132-bit of storage
remaining_mask.ClearBits();
remaining_mask.SetBitRange(LEADING_DRAW_CHANNELS, splitter->_Count - 1);
remaining_mask.ClearBit(table->Bg1DrawChannelUnfrozen);
IM_ASSERT(has_freeze_v == false || table->Bg1DrawChannelUnfrozen != TABLE_DRAW_CHANNEL_BG1_FROZEN);
int remaining_count = splitter->_Count - (has_freeze_v ? LEADING_DRAW_CHANNELS + 1 : LEADING_DRAW_CHANNELS);
//ImRect host_rect = (table->InnerWindow == table->OuterWindow) ? table->InnerClipRect : table->HostClipRect;
ImRect host_rect = table->HostClipRect;
for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++)
{
if (int merge_channels_count = merge_groups[merge_group_n].ChannelsCount)
{
MergeGroup* merge_group = &merge_groups[merge_group_n];
ImRect merge_clip_rect = merge_group->ClipRect;
// Extend outer-most clip limits to match those of host, so draw calls can be merged even if
// outer-most columns have some outer padding offsetting them from their parent ClipRect.
// The principal cases this is dealing with are:
// - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge
// - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge
// FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit
// within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect.
if ((merge_group_n & 1) == 0 || !has_freeze_h)
merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x);
if ((merge_group_n & 2) == 0 || !has_freeze_v)
merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y);
if ((merge_group_n & 1) != 0)
merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x);
if ((merge_group_n & 2) != 0 && (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0)
merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y);
#if 0
GetOverlayDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, ~0, 1.0f);
GetOverlayDrawList()->AddLine(merge_group->ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200));
GetOverlayDrawList()->AddLine(merge_group->ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200));
#endif
remaining_count -= merge_group->ChannelsCount;
for (int n = 0; n < IM_ARRAYSIZE(remaining_mask.Storage); n++)
remaining_mask.Storage[n] &= ~merge_group->ChannelsMask.Storage[n];
for (int n = 0; n < splitter->_Count && merge_channels_count != 0; n++)
{
// Copy + overwrite new clip rect
if (!merge_group->ChannelsMask.TestBit(n))
continue;
merge_group->ChannelsMask.ClearBit(n);
merge_channels_count--;
ImDrawChannel* channel = &splitter->_Channels[n];
IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect)));
channel->_CmdBuffer[0].ClipRect = merge_clip_rect.ToVec4();
memcpy(dst_tmp++, channel, sizeof(ImDrawChannel));
}
}
// Make sure Bg1DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0 and Bg1 frozen are fixed to 0 and 1)
if (merge_group_n == 1 && has_freeze_v)
memcpy(dst_tmp++, &splitter->_Channels[table->Bg1DrawChannelUnfrozen], sizeof(ImDrawChannel));
}
// Append unmergeable channels that we didn't reorder at the end of the list
for (int n = 0; n < splitter->_Count && remaining_count != 0; n++)
{
if (!remaining_mask.TestBit(n))
continue;
ImDrawChannel* channel = &splitter->_Channels[n];
memcpy(dst_tmp++, channel, sizeof(ImDrawChannel));
remaining_count--;
}
IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size);
memcpy(splitter->_Channels.Data + LEADING_DRAW_CHANNELS, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - LEADING_DRAW_CHANNELS) * sizeof(ImDrawChannel));
}
}
// We use a default parameter of 'init_width_or_weight == -1',
// - with ImGuiTableColumnFlags_WidthFixed, width <= 0 --> init width == auto
// - with ImGuiTableColumnFlags_WidthFixed, width > 0 --> init width == manual
@ -2212,6 +1972,256 @@ void ImGui::TableSetBgColor(ImGuiTableBgTarget bg_target, ImU32 color, int colum
}
}
//-------------------------------------------------------------------------
// [SECTION] Tables: Drawing
//-------------------------------------------------------------------------
// - TablePushBackgroundChannel() [Internal]
// - TablePopBackgroundChannel() [Internal]
// - TableSetupDrawChannels() [Internal]
// - TableReorderDrawChannelsForMerge() [Internal]
//-------------------------------------------------------------------------
// Allocate draw channels. Called by TableUpdateLayout()
// - We allocate them following storage order instead of display order so reordering columns won't needlessly
// increase overall dormant memory cost.
// - We isolate headers draw commands in their own channels instead of just altering clip rects.
// This is in order to facilitate merging of draw commands.
// - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels.
// - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other
// channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged.
// - We allocate 1 or 2 background draw channels. This is because we know PushTableBackground() is only used for
// horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4).
// Draw channel allocation (before merging):
// - NoClip --> 2+D+1 channels: bg0 + bg1 + foreground (same clip rect == 1 draw call) (FIXME-TABLE: could merge bg1 and foreground?)
// - Clip --> 2+D+N channels
// - FreezeRows --> 2+D+N*2 (unless scrolling value is zero)
// - FreezeRows || FreezeColunns --> 3+D+N*2 (unless scrolling value is zero)
// Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0.
void ImGui::TableSetupDrawChannels(ImGuiTable* table)
{
const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1;
const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClip) ? 1 : table->ColumnsEnabledCount;
const int channels_for_bg = 1 + 1 * freeze_row_multiplier;
const int channels_for_dummy = (table->ColumnsEnabledCount < table->ColumnsCount || table->EnabledUnclippedMaskByIndex != table->EnabledMaskByIndex) ? +1 : 0;
const int channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy;
table->DrawSplitter.Split(table->InnerWindow->DrawList, channels_total);
table->DummyDrawChannel = (ImU8)((channels_for_dummy > 0) ? channels_total - 1 : -1);
table->Bg1DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG1_FROZEN;
table->Bg1DrawChannelUnfrozen = (ImU8)((table->FreezeRowsCount > 0) ? 2 + channels_for_row : TABLE_DRAW_CHANNEL_BG1_FROZEN);
int draw_channel_current = 2;
for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
{
ImGuiTableColumn* column = &table->Columns[column_n];
if (!column->IsClipped)
{
column->DrawChannelFrozen = (ImU8)(draw_channel_current);
column->DrawChannelUnfrozen = (ImU8)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row + 1 : 0));
if (!(table->Flags & ImGuiTableFlags_NoClip))
draw_channel_current++;
}
else
{
column->DrawChannelFrozen = column->DrawChannelUnfrozen = table->DummyDrawChannel;
}
column->DrawChannelCurrent = column->DrawChannelFrozen;
}
}
// This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable().
// For simplicity we call it TableMergeDrawChannels() but in fact it only reorder channels + overwrite ClipRect,
// actual merging is done by table->DrawSplitter.Merge() which is called right after TableMergeDrawChannels().
//
// Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve
// this we merge their clip rect and make them contiguous in the channel list, so they can be merged
// by the call to DrawSplitter.Merge() following to the call to this function.
// We reorder draw commands by arranging them into a maximum of 4 distinct groups:
//
// 1 group: 2 groups: 2 groups: 4 groups:
// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze
// [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll
//
// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled).
// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group
// based on its position (within frozen rows/columns groups or not).
// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect.
// This function assume that each column are pointing to a distinct draw channel,
// otherwise merge_group->ChannelsCount will not match set bit count of merge_group->ChannelsMask.
//
// Column channels will not be merged into one of the 1-4 groups in the following cases:
// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value).
// Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui:: bounds
// matches, by e.g. calling SetCursorScreenPos().
// - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here..
// we could do better but it's going to be rare and probably not worth the hassle.
// Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd.
//
// This function is particularly tricky to understand.. take a breath.
void ImGui::TableMergeDrawChannels(ImGuiTable* table)
{
ImGuiContext& g = *GImGui;
ImDrawListSplitter* splitter = &table->DrawSplitter;
const bool has_freeze_v = (table->FreezeRowsCount > 0);
const bool has_freeze_h = (table->FreezeColumnsCount > 0);
// Track which groups we are going to attempt to merge, and which channels goes into each group.
struct MergeGroup
{
ImRect ClipRect;
int ChannelsCount;
ImBitArray<IMGUI_TABLE_MAX_DRAW_CHANNELS> ChannelsMask;
};
int merge_group_mask = 0x00;
MergeGroup merge_groups[4];
memset(merge_groups, 0, sizeof(merge_groups));
// 1. Scan channels and take note of those which can be merged
for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
{
if (!(table->EnabledUnclippedMaskByIndex & ((ImU64)1 << column_n)))
continue;
ImGuiTableColumn* column = &table->Columns[column_n];
const int merge_group_sub_count = has_freeze_v ? 2 : 1;
for (int merge_group_sub_n = 0; merge_group_sub_n < merge_group_sub_count; merge_group_sub_n++)
{
const int channel_no = (merge_group_sub_n == 0) ? column->DrawChannelFrozen : column->DrawChannelUnfrozen;
// Don't attempt to merge if there are multiple draw calls within the column
ImDrawChannel* src_channel = &splitter->_Channels[channel_no];
if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0)
src_channel->_CmdBuffer.pop_back();
if (src_channel->_CmdBuffer.Size != 1)
continue;
// Find out the width of this merge group and check if it will fit in our column
// (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it)
if (!(column->Flags & ImGuiTableColumnFlags_NoClip))
{
float content_max_x;
if (!has_freeze_v)
content_max_x = ImMax(column->ContentMaxXUnfrozen, column->ContentMaxXHeadersUsed); // No row freeze
else if (merge_group_sub_n == 0)
content_max_x = ImMax(column->ContentMaxXFrozen, column->ContentMaxXHeadersUsed); // Row freeze: use width before freeze
else
content_max_x = column->ContentMaxXUnfrozen; // Row freeze: use width after freeze
if (content_max_x > column->ClipRect.Max.x)
continue;
}
const int merge_group_n = (has_freeze_h && column_n < table->FreezeColumnsCount ? 0 : 1) + (has_freeze_v && merge_group_sub_n == 0 ? 0 : 2);
IM_ASSERT(channel_no < IMGUI_TABLE_MAX_DRAW_CHANNELS);
MergeGroup* merge_group = &merge_groups[merge_group_n];
if (merge_group->ChannelsCount == 0)
merge_group->ClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
merge_group->ChannelsMask.SetBit(channel_no);
merge_group->ChannelsCount++;
merge_group->ClipRect.Add(src_channel->_CmdBuffer[0].ClipRect);
merge_group_mask |= (1 << merge_group_n);
}
// Invalidate current draw channel
// (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data)
column->DrawChannelCurrent = (ImU8)-1;
}
// [DEBUG] Display merge groups
#if 0
if (g.IO.KeyShift)
for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++)
{
MergeGroup* merge_group = &merge_groups[merge_group_n];
if (merge_group->ChannelsCount == 0)
continue;
char buf[32];
ImFormatString(buf, 32, "MG%d:%d", merge_group_n, merge_group->ChannelsCount);
ImVec2 text_pos = merge_group->ClipRect.Min + ImVec2(4, 4);
ImVec2 text_size = CalcTextSize(buf, NULL);
GetForegroundDrawList()->AddRectFilled(text_pos, text_pos + text_size, IM_COL32(0, 0, 0, 255));
GetForegroundDrawList()->AddText(text_pos, IM_COL32(255, 255, 0, 255), buf, NULL);
GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 255, 0, 255));
}
#endif
// 2. Rewrite channel list in our preferred order
if (merge_group_mask != 0)
{
// We skip channel 0 (Bg0) and 1 (Bg1 frozen) from the shuffling since they won't move - see channels allocation in TableUpdateDrawChannels().
const int LEADING_DRAW_CHANNELS = 2;
g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - LEADING_DRAW_CHANNELS); // Use shared temporary storage so the allocation gets amortized
ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data;
ImBitArray<IMGUI_TABLE_MAX_DRAW_CHANNELS> remaining_mask; // We need 132-bit of storage
remaining_mask.ClearBits();
remaining_mask.SetBitRange(LEADING_DRAW_CHANNELS, splitter->_Count - 1);
remaining_mask.ClearBit(table->Bg1DrawChannelUnfrozen);
IM_ASSERT(has_freeze_v == false || table->Bg1DrawChannelUnfrozen != TABLE_DRAW_CHANNEL_BG1_FROZEN);
int remaining_count = splitter->_Count - (has_freeze_v ? LEADING_DRAW_CHANNELS + 1 : LEADING_DRAW_CHANNELS);
//ImRect host_rect = (table->InnerWindow == table->OuterWindow) ? table->InnerClipRect : table->HostClipRect;
ImRect host_rect = table->HostClipRect;
for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++)
{
if (int merge_channels_count = merge_groups[merge_group_n].ChannelsCount)
{
MergeGroup* merge_group = &merge_groups[merge_group_n];
ImRect merge_clip_rect = merge_group->ClipRect;
// Extend outer-most clip limits to match those of host, so draw calls can be merged even if
// outer-most columns have some outer padding offsetting them from their parent ClipRect.
// The principal cases this is dealing with are:
// - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge
// - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge
// FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit
// within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect.
if ((merge_group_n & 1) == 0 || !has_freeze_h)
merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x);
if ((merge_group_n & 2) == 0 || !has_freeze_v)
merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y);
if ((merge_group_n & 1) != 0)
merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x);
if ((merge_group_n & 2) != 0 && (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0)
merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y);
#if 0
GetOverlayDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, ~0, 1.0f);
GetOverlayDrawList()->AddLine(merge_group->ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200));
GetOverlayDrawList()->AddLine(merge_group->ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200));
#endif
remaining_count -= merge_group->ChannelsCount;
for (int n = 0; n < IM_ARRAYSIZE(remaining_mask.Storage); n++)
remaining_mask.Storage[n] &= ~merge_group->ChannelsMask.Storage[n];
for (int n = 0; n < splitter->_Count && merge_channels_count != 0; n++)
{
// Copy + overwrite new clip rect
if (!merge_group->ChannelsMask.TestBit(n))
continue;
merge_group->ChannelsMask.ClearBit(n);
merge_channels_count--;
ImDrawChannel* channel = &splitter->_Channels[n];
IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect)));
channel->_CmdBuffer[0].ClipRect = merge_clip_rect.ToVec4();
memcpy(dst_tmp++, channel, sizeof(ImDrawChannel));
}
}
// Make sure Bg1DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0 and Bg1 frozen are fixed to 0 and 1)
if (merge_group_n == 1 && has_freeze_v)
memcpy(dst_tmp++, &splitter->_Channels[table->Bg1DrawChannelUnfrozen], sizeof(ImDrawChannel));
}
// Append unmergeable channels that we didn't reorder at the end of the list
for (int n = 0; n < splitter->_Count && remaining_count != 0; n++)
{
if (!remaining_mask.TestBit(n))
continue;
ImDrawChannel* channel = &splitter->_Channels[n];
memcpy(dst_tmp++, channel, sizeof(ImDrawChannel));
remaining_count--;
}
IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size);
memcpy(splitter->_Channels.Data + LEADING_DRAW_CHANNELS, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - LEADING_DRAW_CHANNELS) * sizeof(ImDrawChannel));
}
}
//-------------------------------------------------------------------------
// [SECTION] Tables: Sorting
//-------------------------------------------------------------------------