From b522576054ea396f3456def5c1ac575af0a4a0c7 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 19 Jun 2026 19:28:25 +0200 Subject: [PATCH] Tables: tracking topology changes by default. Match by ID then in sequential order for remaining. (#9108, #4046) Removed ImGuiTableFlags_TrackTopologyChanges. --- docs/CHANGELOG.txt | 13 ++++ imgui.h | 1 - imgui_internal.h | 16 ++-- imgui_tables.cpp | 187 +++++++++++++++++++++++++++++---------------- 4 files changed, 144 insertions(+), 73 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 9b34d83f6..06dd0b1ab 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -59,6 +59,19 @@ Other Changes: - Added `style.InputTextCursorSize` to configure cursor/caret thickness. (#7031, #9409) This is automatically scaled by `style.ScaleAllSizes()`. - Tables: + - Redesigned/rewrote code to reconcile columns and settings on topology changes. (#9108) + - When a column label is passed to TableSetupColumn(), the underlying identifier + is used to match live columns data and .ini settings data when changing. + This makes it possible to add/remove columns from a table without losing + neither live data neither .ini settings data. + - PS: Note that this is distinct from toggling column visibility or reordering + columns, which was always possible. The new matching makes it easier to create + tables that are entirely customized by user or code, without losing state. + - Columns without identifiers or with duplicate identifiers are matched + sequentially, matching old behavior. + - Column ID are stored in .ini file. + - Code is being tested both for live topology changes and for loading .ini + data with mismatched topology. - Context Menu: added a "Reset" sub-menu with a "Reset Visibility" option. (which is greyed out when using default settings) - Headers: fixed label being clipped early to reserve space for a sort marker diff --git a/imgui.h b/imgui.h index e4a8d0e07..720e3c8cd 100644 --- a/imgui.h +++ b/imgui.h @@ -2077,7 +2077,6 @@ enum ImGuiTableFlags_ ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). // Miscellaneous ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, // Highlight column headers when hovered (may evolve into a fuller highlight) - ImGuiTableFlags_TrackTopologyChanges = 1 << 29, // [BETA] Saved columns data keyed by their identifier. Allow data to persist after column addition/deletion/reordering. Requires valid identifier for all columns. // [Internal] Combinations and masks ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, diff --git a/imgui_internal.h b/imgui_internal.h index c3cea78cd..ed5d9c9e7 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2934,9 +2934,10 @@ struct ImGuiTableColumn bool IsVisibleY; bool IsRequestOutput; // Return value for TableSetColumnIndex() / TableNextColumn(): whether we request user to output contents or not. bool IsSkipItems; // Do we want item submissions to this column to be completely ignored (no layout will happen). - bool IsPreserveWidthAuto; - bool IsJustCreated; - bool IsLoadedSettings; + bool IsPreserveWidthAuto : 1; + bool IsJustCreated : 1; + bool IsLoadedSettings : 1; + bool SrcFoundReconcileTarget : 1; ImS8 NavLayerCurrent; // ImGuiNavLayer in 1 byte ImU8 AutoFitQueue : 4; // Queue of 4 values for the next 4 frames to request auto-fit ImU8 CannotSkipItemsQueue : 4; // Queue of 4 values for the next 4 frames to disable Clipped/SkipItem @@ -2971,8 +2972,8 @@ struct ImGuiTableReconcileColumnData ImGuiID UserData; // Reconcile data - ImGuiTableColumnIdx ColumnNewIdx; - ImGuiTableColumnIdx ColumnOldIdx; + ImGuiTableColumnIdx ColumnNewIdx; // Index in the current table. + ImGuiTableColumnIdx ColumnOldIdx; // Index in the previous frame table. ImGuiTableColumn ColumnOldData; // Full backup of the column. Could be avoided by storing 1 of them and applying reconcile in the right order. Not worth bothering. }; @@ -3109,6 +3110,7 @@ struct IMGUI_API ImGuiTable bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. bool IsInsideRow; // Set when inside TableBeginRow()/TableEndRow(). bool IsInitializing; + bool IsReconcileMode; bool IsSortSpecsDirty; bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). @@ -3177,6 +3179,7 @@ struct ImGuiTableColumnSettings ImU8 SortDirection : 2; ImS8 IsEnabled : 2; // "Visible" in .ini file ImU8 IsStretch : 1; + bool IsLoaded : 1; // Using during loading to mark finding a matching column. ImGuiTableColumnSettings() { @@ -3187,6 +3190,7 @@ struct ImGuiTableColumnSettings SortDirection = ImGuiSortDirection_None; IsEnabled = -1; IsStretch = 0; + IsLoaded = false; } }; @@ -3264,7 +3268,7 @@ namespace ImGui // Tables: Settings IMGUI_API void TableLoadSettings(ImGuiTable* table); IMGUI_API void TableLoadSettingsForColumns(ImGuiTable* table); - IMGUI_API void TableLoadSettingsForColumn(ImGuiTableColumn* column, const ImGuiTableColumnSettings* column_settings, ImGuiTableFlags load_flags); + IMGUI_API void TableLoadSettingsForColumn(ImGuiTableColumn* column, ImGuiTableColumnSettings* column_settings, ImGuiTableFlags load_flags); IMGUI_API void TableSaveSettings(ImGuiTable* table); IMGUI_API void TableResetSettings(ImGuiTable* table); IMGUI_API ImGuiTableSettings* TableGetBoundSettings(ImGuiTable* table); diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 75af671e1..3eb79f6f7 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -592,6 +592,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG } table->IsSortSpecsDirty = true; table->IsSettingsDirty = true; // Records itself into .ini file even when in default state (#7934) + table->IsReconcileMode = false; table->InstanceInteracted = -1; table->ContextPopupColumn = -1; table->ReorderColumn = table->ReorderColumnDstOrder = table->ResizedColumn = table->LastResizedColumn = -1; @@ -850,12 +851,8 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) const int columns_count = table->ColumnsCount; // Reconcile moved columns - if (table->Flags & ImGuiTableFlags_TrackTopologyChanges) - { - IM_ASSERT_USER_ERROR(table->DeclColumnsCount == columns_count, "When using TrackTopologyChanges: must call TableSetupColumn() with a valid identifier for all columns!"); - if (temp_data->ReconcileColumnsRequests.Size > 0) - TableReconcileColumns(table); - } + if (temp_data->ReconcileColumnsRequests.Size > 0) + TableReconcileColumns(table); if (temp_data->OldColumnsRawData) { IM_FREE(temp_data->OldColumnsRawData); @@ -1756,59 +1753,93 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo // When ID changed or a column moved: defer the request until layout where we will process full reconcile. const int column_idx = table->DeclColumnsCount++; - if (table->Flags & ImGuiTableFlags_TrackTopologyChanges) + ImGuiTableColumn* column = &table->Columns[column_idx]; + + // If topology change goes into reconcile mode + if (!table->IsReconcileMode && column->ID != column_id) { - // On column count change: search the previous columns: - // - the live Columns[] array is truncated on shrink, so a moved column may only exist in old data. - // - the live Columns[] array may already be partially reset by the TableInitColumnDefaults() call above. - // When count changed, even a never-updated slot (prev_column_id == 0 while initializing) may have old data to recover. - // Defer applying data, TableUpdateLayout() will call TableSetupColumnApply(). - IM_ASSERT_USER_ERROR(column_id != 0, "When using TrackTopologyChanges: Must call TableSetupColumn() with a valid identifier for all columns!"); - ImGuiTableColumn* column = &table->Columns[column_idx]; - const bool may_have_old_data = (column->ID != 0 || !table->IsInitializing || !table->TempData->OldColumnsData.empty()); - if (column->ID != column_id && column_id != 0 && may_have_old_data) - { - table->TempData->ReconcileColumnsRequests.push_back(ImGuiTableReconcileColumnData()); - ImGuiTableReconcileColumnData& reconcile_data = table->TempData->ReconcileColumnsRequests.back(); - reconcile_data.ID = column_id; - reconcile_data.NameOffset = name_offset; - reconcile_data.Flags = flags; - reconcile_data.InitWidthOrWeight = init_width_or_weight; - reconcile_data.UserData = user_data; - reconcile_data.ColumnNewIdx = (ImGuiTableColumnIdx)column_idx; - reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)-1; - ImSpan& search_columns = table->TempData->OldColumnsData.empty() ? table->Columns : table->TempData->OldColumnsData; - for (ImGuiTableColumn& old_column : search_columns) - if (old_column.ID == column_id) - { - reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)search_columns.index_from_ptr(&old_column); - reconcile_data.ColumnOldData = old_column; - break; - } - column->NameOffset = name_offset; // Allow TableGetColumnName() to work before layout. - return; - } + table->IsReconcileMode = true; + table->TempData->ReconcileColumnsRequests.reserve(table->ColumnsCount - column_idx); } - - TableSetupColumnApply(table, column_idx, column_id, name_offset, flags, init_width_or_weight, user_data); + + // Fast/common path + if (table->IsReconcileMode == false) + { + TableSetupColumnApply(table, column_idx, column_id, name_offset, flags, init_width_or_weight, user_data); + return; + } + + // Reconcile path: defer applying data to TableUpdateLayout() -> TableReconcileMovedColumns() -> TableSetupColumnApply(). + // On column count change: search the previous columns: + // - the live Columns[] array is truncated on shrink, so a moved column may only exist in old data. + // - the live Columns[] array may already be partially reset. + table->TempData->ReconcileColumnsRequests.push_back(ImGuiTableReconcileColumnData()); + ImGuiTableReconcileColumnData& reconcile_data = table->TempData->ReconcileColumnsRequests.back(); + reconcile_data.ID = column_id; + reconcile_data.NameOffset = name_offset; + reconcile_data.Flags = flags; + reconcile_data.InitWidthOrWeight = init_width_or_weight; + reconcile_data.UserData = user_data; + reconcile_data.ColumnNewIdx = (ImGuiTableColumnIdx)column_idx; + reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)-1; + + // Allow TableGetColumnName() to work before layout + column->NameOffset = name_offset; } +// NB: This was written to be similar to the logic in TableLoadSettingsForColumns(). void ImGui::TableReconcileColumns(ImGuiTable* table) { ImGuiContext& g = *GImGui; ImGuiTableTempData* temp_data = table->TempData; IM_UNUSED(g); - IMGUI_DEBUG_LOG_TABLE("[table] Reconcile moved columns for table 0x%08X\n", table->ID); - for (ImGuiTableReconcileColumnData& reconcile_data : temp_data->ReconcileColumnsRequests) + IMGUI_DEBUG_LOG_TABLE("[table] Reconcile columns for table 0x%08X\n", table->ID); + + ImSpan& src_columns = table->TempData->OldColumnsData.empty() ? table->Columns : table->TempData->OldColumnsData; + for (ImGuiTableColumn& src_column : src_columns) + src_column.SrcFoundReconcileTarget = false; + + // Find matches for named columns. + int matches = 0; + ImVector& reconcile_requests = temp_data->ReconcileColumnsRequests; + for (ImGuiTableReconcileColumnData& reconcile_data : reconcile_requests) + if (reconcile_data.ID != 0) + for (ImGuiTableColumn& src_column : src_columns) + if (src_column.ID == reconcile_data.ID && !src_column.SrcFoundReconcileTarget) + { + reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)src_columns.index_from_ptr(&src_column); + reconcile_data.ColumnOldData = src_column; + src_column.SrcFoundReconcileTarget = true; + matches++; + break; + } + + // Remaining entries are matched sequentially. + int dst_idx = 0; // index in reconcile array + if (matches != reconcile_requests.Size) + for (ImGuiTableColumn& src_column : src_columns) + if (!src_column.SrcFoundReconcileTarget) + { + while (dst_idx < reconcile_requests.Size && reconcile_requests[dst_idx].ColumnOldIdx != (ImGuiTableColumnIdx)-1) + dst_idx++; + if (dst_idx == reconcile_requests.Size) + break; + reconcile_requests[dst_idx].ColumnOldIdx = (ImGuiTableColumnIdx)src_columns.index_from_ptr(&src_column); + reconcile_requests[dst_idx].ColumnOldData = src_column; + IM_ASSERT(src_column.SrcFoundReconcileTarget == false); + } + + // Apply in the final pass. Because it is possible that src_columns == table->Columns we went through a temporary copy. + for (ImGuiTableReconcileColumnData& reconcile_data : reconcile_requests) { - ImGuiTableColumn* column = &table->Columns[reconcile_data.ColumnNewIdx]; - *column = reconcile_data.ColumnOldData; // When old column was not found clear anyway with default-constructed data. + table->Columns[reconcile_data.ColumnNewIdx] = reconcile_data.ColumnOldData; // When old column was not found clear anyway with default-constructed data. TableSetupColumnApply(table, reconcile_data.ColumnNewIdx, reconcile_data.ID, reconcile_data.NameOffset, reconcile_data.Flags, reconcile_data.InitWidthOrWeight, reconcile_data.UserData); IMGUI_DEBUG_LOG_TABLE("[table] - old %d -> new %d \"%s\"\n", reconcile_data.ColumnOldIdx, reconcile_data.ColumnNewIdx, TableGetColumnName(table, reconcile_data.ColumnNewIdx)); // Log at the end so NameOffset was copied. } TableFixDisplayOrder(table); table->IsSettingsDirty = true; // FIXME-RECONCILE: Necessary? - temp_data->ReconcileColumnsRequests.resize(0); // GC-ed once in NewFrame() + table->IsReconcileMode = false; + reconcile_requests.resize(0); // GC-ed once in NewFrame() } // [Public] @@ -3926,8 +3957,6 @@ void ImGui::TableSaveSettings(ImGuiTable* table) settings->SaveFlags |= ImGuiTableFlags_Sortable; if (column->IsUserEnabled != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) == 0)) settings->SaveFlags |= ImGuiTableFlags_Hideable; - if (column->ID != 0) - settings->SaveFlags |= ImGuiTableFlags_TrackTopologyChanges; } settings->SaveFlags &= table->Flags; settings->RefScale = save_ref_scale ? table->RefScale : 0.0f; @@ -3965,6 +3994,7 @@ void ImGui::TableLoadSettings(ImGuiTable* table) // TableUpdateLayout() will then call TableLoadSettingsForColumns() to apply the data. } +// NB: This was written to be similar to the logic in TableReconcileColumns(). void ImGui::TableLoadSettingsForColumns(ImGuiTable* table) { for (ImGuiTableColumn& column : table->Columns) @@ -3974,31 +4004,57 @@ void ImGui::TableLoadSettingsForColumns(ImGuiTable* table) return; // Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn + int matches = 0; ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); - for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + for (int column_settings_n = 0; column_settings_n < settings->ColumnsCount; column_settings_n++, column_settings++) { - int column_n = column_settings->Index; - if ((table->Flags & ImGuiTableFlags_TrackTopologyChanges) && (settings->SaveFlags & ImGuiTableFlags_TrackTopologyChanges))// && has_columns_id) - if (column_n >= table->ColumnsCount || table->Columns[column_n].ID != column_settings->ID) - { - column_n = -1; // FIXME-RECONCILE - for (int other_n = 0; other_n < table->ColumnsCount && column_n == -1; other_n++) - if (table->Columns[other_n].ID == column_settings->ID) - column_n = other_n; - } - - if (column_n < 0 || column_n >= table->ColumnsCount) - continue; - TableLoadSettingsForColumn(&table->Columns[column_n], column_settings, settings->SaveFlags); + const int src_idx = column_settings->Index; + int dst_idx = src_idx; + column_settings->IsLoaded = false; + if (dst_idx >= 0 && dst_idx < table->ColumnsCount && table->Columns[dst_idx].ID == column_settings->ID) + { + // Fast/Common path + TableLoadSettingsForColumn(&table->Columns[dst_idx], column_settings, settings->SaveFlags); + matches++; + } + else if (column_settings->ID != 0) + { + // Find match for named columns. + for (int other_n = 0; other_n < table->ColumnsCount; other_n++) + if (table->Columns[other_n].ID == column_settings->ID) + { + dst_idx = other_n; + TableLoadSettingsForColumn(&table->Columns[dst_idx], column_settings, settings->SaveFlags); + matches++; + break; + } + } } + + // Remaining entries are matched sequentially + if (matches != settings->ColumnsCount) + { + int dst_idx = 0; + column_settings = settings->GetColumnSettings(); + for (int column_settings_n = 0; column_settings_n < settings->ColumnsCount; column_settings_n++, column_settings++) + if (!column_settings->IsLoaded) + { + while (dst_idx < table->ColumnsCount && table->Columns[dst_idx].IsLoadedSettings) + dst_idx++; + if (dst_idx >= table->ColumnsCount) + break; + TableLoadSettingsForColumn(&table->Columns[dst_idx], column_settings, settings->SaveFlags); + dst_idx++; + } + } + table->SettingsLoadedFlags |= ImGuiTableFlags_Reorderable; // We handle above in code above. } -void ImGui::TableLoadSettingsForColumn(ImGuiTableColumn* column, const ImGuiTableColumnSettings* column_settings, ImGuiTableFlags load_flags) +void ImGui::TableLoadSettingsForColumn(ImGuiTableColumn* column, ImGuiTableColumnSettings* column_settings, ImGuiTableFlags load_flags) { column->IsLoadedSettings = true; - if (load_flags & ImGuiTableFlags_TrackTopologyChanges) - column->ID = column_settings->ID; + column_settings->IsLoaded = true; if (load_flags & ImGuiTableFlags_Resizable) { if (column_settings->IsStretch) @@ -4115,7 +4171,7 @@ static void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->IsEnabled = (ImU8)n; settings->SaveFlags |= ImGuiTableFlags_Hideable; } if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (ImGuiTableColumnIdx)n; settings->SaveFlags |= ImGuiTableFlags_Reorderable; } if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (ImGuiTableColumnIdx)n; column->SortDirection = (c == '^') ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending; settings->SaveFlags |= ImGuiTableFlags_Sortable; } - if (sscanf(line, "ID=0x%08X%n", (ImU32*)&n, &r) == 1) { line = ImStrSkipBlank(line + r); column->ID = (ImGuiID)n; settings->SaveFlags |= ImGuiTableFlags_TrackTopologyChanges; } + if (sscanf(line, "ID=0x%08X%n", (ImU32*)&n, &r) == 1) { line = ImStrSkipBlank(line + r); column->ID = (ImGuiID)n; } } } @@ -4133,7 +4189,6 @@ static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandle const bool save_visible = (settings->SaveFlags & ImGuiTableFlags_Hideable) != 0; const bool save_order = (settings->SaveFlags & ImGuiTableFlags_Reorderable) != 0; const bool save_sort = (settings->SaveFlags & ImGuiTableFlags_Sortable) != 0; - const bool save_id = (settings->SaveFlags & ImGuiTableFlags_TrackTopologyChanges) != 0; // We need to save the [Table] entry even if all the bools are false, since this records a table with "default settings". buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve @@ -4153,7 +4208,7 @@ static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandle if (save_visible) { buf->appendf(" Visible=%d", column->IsEnabled); } if (save_order) { buf->appendf(" Order=%d", column->DisplayOrder); } if (save_sort && column->SortOrder != -1) { buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? 'v' : '^'); } - if (save_id && column->ID != 0) { buf->appendf(" ID=0x%08X", column->ID); } + if (column->ID != 0) { buf->appendf(" ID=0x%08X", column->ID); } buf->append("\n"); } buf->append("\n");