From aab17456a73b1014c2e41a639444369844d6e928 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 23 Mar 2026 15:08:27 +0100 Subject: [PATCH 01/13] Tables: mark setting as dirty when forcing reordering. Otherwise the manufactured case of reloading a session that previously had e.g. a slider to adjust freeze count, resetting to 1 on new session, would keep showing you reordering items until another thing triggers marking dirty. --- imgui_tables.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 3eef6b565..cebc21ed1 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -1692,6 +1692,7 @@ void ImGui::TableSetupScrollFreeze(int columns, int rows) { ImSwap(table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder, table->Columns[table->DisplayOrderToIndex[column_n]].DisplayOrder); ImSwap(table->DisplayOrderToIndex[order_n], table->DisplayOrderToIndex[column_n]); + table->IsSettingsDirty = true; } } } From 587c4cb87b1bc4ce55fdb5da2c158ee4a0a044b0 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 15:24:37 +0200 Subject: [PATCH 02/13] Tables: rename locals in TableDrawDefaultContextMenu() to reduce confusion. --- imgui_tables.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/imgui_tables.cpp b/imgui_tables.cpp index cebc21ed1..49e8fa9d3 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3513,17 +3513,17 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags return; bool want_separator = false; - const int column_n = (table->ContextPopupColumn >= 0 && table->ContextPopupColumn < table->ColumnsCount) ? table->ContextPopupColumn : -1; - ImGuiTableColumn* column = (column_n != -1) ? &table->Columns[column_n] : NULL; + const int context_column_n = (table->ContextPopupColumn >= 0 && table->ContextPopupColumn < table->ColumnsCount) ? table->ContextPopupColumn : -1; + ImGuiTableColumn* context_column = (context_column_n != -1) ? &table->Columns[context_column_n] : NULL; // Sizing if (flags_for_section_to_display & ImGuiTableFlags_Resizable) { - if (column != NULL) + if (context_column != NULL) { - const bool can_resize = !(column->Flags & ImGuiTableColumnFlags_NoResize) && column->IsEnabled; + const bool can_resize = !(context_column->Flags & ImGuiTableColumnFlags_NoResize) && context_column->IsEnabled; if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableSizeOne), NULL, false, can_resize)) // "###SizeOne" - TableSetColumnWidthAutoSingle(table, column_n); + TableSetColumnWidthAutoSingle(table, context_column_n); } const char* size_all_desc; @@ -3573,22 +3573,22 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags want_separator = true; PushItemFlag(ImGuiItemFlags_AutoClosePopups, false); - for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) { - ImGuiTableColumn* other_column = &table->Columns[other_column_n]; - if (other_column->Flags & ImGuiTableColumnFlags_Disabled) + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->Flags & ImGuiTableColumnFlags_Disabled) continue; - const char* name = TableGetColumnName(table, other_column_n); + const char* name = TableGetColumnName(table, column_n); if (name == NULL || name[0] == 0) name = ""; // Make sure we can't hide the last active column - bool menu_item_active = (other_column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; - if (other_column->IsUserEnabled && table->ColumnsEnabledCount <= 1) + bool menu_item_active = (column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + if (column->IsUserEnabled && table->ColumnsEnabledCount <= 1) menu_item_active = false; - if (MenuItem(name, NULL, other_column->IsUserEnabled, menu_item_active)) - other_column->IsUserEnabledNextFrame = !other_column->IsUserEnabled; + if (MenuItem(name, NULL, column->IsUserEnabled, menu_item_active)) + column->IsUserEnabledNextFrame = !column->IsUserEnabled; } PopItemFlag(); } From 7be8076e9b56d55f11a7ed86268ac3eb8b69d13e Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 14:24:58 +0200 Subject: [PATCH 03/13] Tables: rework TableSetColumnDisplayOrder() to take src_order. (#9312) --- imgui_internal.h | 6 +++--- imgui_tables.cpp | 33 ++++++++++++++++----------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 1fb905759..6b4382a55 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3044,7 +3044,7 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. - ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumnSrcOrder; // Display order of column being reordered. (not cleared) ImGuiTableColumnIdx ReorderColumnDstOrder; // Requested display order of column being reordered. ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. @@ -3559,8 +3559,8 @@ namespace ImGui IMGUI_API float TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n); IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); - IMGUI_API void TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order); - IMGUI_API void TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order); + IMGUI_API void TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order); + IMGUI_API void TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order); IMGUI_API void TableRemove(ImGuiTable* table); IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 49e8fa9d3..0f1821373 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -585,7 +585,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->IsSettingsDirty = true; // Records itself into .ini file even when in default state (#7934) table->InstanceInteracted = -1; table->ContextPopupColumn = -1; - table->ReorderColumn = table->ResizedColumn = table->LastResizedColumn = -1; + table->ReorderColumnSrcOrder = table->ResizedColumn = table->LastResizedColumn = -1; table->AutoFitSingleColumn = -1; table->HoveredColumnBody = table->HoveredColumnBorder = -1; for (int n = 0; n < columns_count; n++) @@ -703,9 +703,9 @@ void ImGui::TableBeginApplyRequests(ImGuiTable* table) if (table->InstanceCurrent == 0) { table->HeldHeaderColumn = -1; - if (table->ReorderColumn != -1 && table->ReorderColumnDstOrder != -1) + if (table->ReorderColumnSrcOrder != -1 && table->ReorderColumnDstOrder != -1) { - TableSetColumnDisplayOrder(table, table->ReorderColumn, table->ReorderColumnDstOrder); + TableSetColumnDisplayOrder(table, table->ReorderColumnSrcOrder, table->ReorderColumnDstOrder); table->ReorderColumnDstOrder = -1; } } @@ -721,17 +721,16 @@ void ImGui::TableBeginApplyRequests(ImGuiTable* table) } // Apply immediately. See TableQueueSetColumnDisplayOrder() for additional checks/constraints. -void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) +void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order) { - IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + IM_ASSERT(src_order >= 0 && src_order < table->ColumnsCount); IM_ASSERT(dst_order >= 0 && dst_order < table->ColumnsCount); - ImGuiTableColumn* src_column = &table->Columns[column_n]; - const int src_order = src_column->DisplayOrder; if (src_order == dst_order) return; const int reorder_dir = (dst_order < src_order) ? -1 : +1; + ImGuiTableColumn* src_column = &table->Columns[table->DisplayOrderToIndex[src_order]]; src_column->DisplayOrder = (ImGuiTableColumnIdx)dst_order; for (int order_n = src_order + reorder_dir; order_n != dst_order + reorder_dir; order_n += reorder_dir) table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder -= (ImGuiTableColumnIdx)reorder_dir; @@ -749,9 +748,9 @@ void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_ // - That we don't cross the frozen column limit. // (that TableSetupScrollFreeze() enforce a display order range for frozen columns. // so reordering a column across the frozen column barrier is illegal and will be undone.) -void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) +void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order) { - ImGuiTableColumn* src_column = &table->Columns[column_n]; + ImGuiTableColumn* src_column = &table->Columns[table->DisplayOrderToIndex[src_order]]; ImGuiTableColumn* dst_column = &table->Columns[table->DisplayOrderToIndex[dst_order]]; if ((src_column->Flags | dst_column->Flags) & ImGuiTableColumnFlags_NoReorder) // FIXME: Perform a sweep test? return; @@ -759,7 +758,7 @@ void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int int dst_i = (dst_column->IndexWithinEnabledSet != -1) ? dst_column->IndexWithinEnabledSet : table->Columns.index_from_ptr(dst_column); if ((src_i < table->FreezeColumnsRequest) != (dst_i < table->FreezeColumnsRequest)) return; - table->ReorderColumn = (ImGuiTableColumnIdx)column_n; + table->ReorderColumnSrcOrder = (ImGuiTableColumnIdx)src_order; table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; } @@ -3242,10 +3241,10 @@ void ImGui::TableHeader(const char* label) table->InstanceInteracted = table->InstanceCurrent; if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) if (ImGuiTableColumn* prev_column = (column->PrevEnabledColumn != -1) ? &table->Columns[column->PrevEnabledColumn] : NULL) - TableQueueSetColumnDisplayOrder(table, column_n, prev_column->DisplayOrder); + TableQueueSetColumnDisplayOrder(table, column->DisplayOrder, prev_column->DisplayOrder); if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) if (ImGuiTableColumn* next_column = (column->NextEnabledColumn != -1) ? &table->Columns[column->NextEnabledColumn] : NULL) - TableQueueSetColumnDisplayOrder(table, column_n, next_column->DisplayOrder); + TableQueueSetColumnDisplayOrder(table, column->DisplayOrder, next_column->DisplayOrder); } // Sort order arrow @@ -3267,7 +3266,7 @@ void ImGui::TableHeader(const char* label) } // Handle clicking on column header to adjust Sort Order - if (pressed && table->ReorderColumn != column_n) + if (pressed && table->ReorderColumnSrcOrder != column->DisplayOrder) { ImGuiSortDirection sort_direction = TableGetColumnNextSortDirection(column); TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift); @@ -3584,10 +3583,10 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags name = ""; // Make sure we can't hide the last active column - bool menu_item_active = (column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + bool menu_item_enabled = (column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; if (column->IsUserEnabled && table->ColumnsEnabledCount <= 1) - menu_item_active = false; - if (MenuItem(name, NULL, column->IsUserEnabled, menu_item_active)) + menu_item_enabled = false; + if (MenuItem(name, NULL, column->IsUserEnabled, menu_item_enabled)) column->IsUserEnabledNextFrame = !column->IsUserEnabled; } PopItemFlag(); @@ -4058,7 +4057,7 @@ void ImGui::DebugNodeTable(ImGuiTable* table) BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); - BulletText("ResizedColumn: %d, ReorderColumn: %d, HeldHeaderColumn: %d", table->ResizedColumn, table->ReorderColumn, table->HeldHeaderColumn); + BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn Orders: %d->%d", table->ResizedColumn, table->HeldHeaderColumn, table->ReorderColumnSrcOrder, table->ReorderColumnDstOrder); for (int n = 0; n < table->InstanceCurrent + 1; n++) { ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); From ed4cffece7fa876bac45d57b9fbc984a74d974e1 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 24 Mar 2026 16:02:20 +0100 Subject: [PATCH 04/13] Tables: store LeftMostUnfrozenOrder and amend TableQueueSetColumnDisplayOrder(). (#9312) --- imgui_internal.h | 1 + imgui_tables.cpp | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 6b4382a55..435e77a9b 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3055,6 +3055,7 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx LeftMostUnfrozenOrder; // Display order of the left-most unfrozen column, which is used to determine where the freezing line should be. ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 0f1821373..528ea76c0 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -754,10 +754,9 @@ void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, in ImGuiTableColumn* dst_column = &table->Columns[table->DisplayOrderToIndex[dst_order]]; if ((src_column->Flags | dst_column->Flags) & ImGuiTableColumnFlags_NoReorder) // FIXME: Perform a sweep test? return; - int src_i = (src_column->IndexWithinEnabledSet != -1) ? src_column->IndexWithinEnabledSet : table->Columns.index_from_ptr(src_column); // FIXME: Hidden columns don't count into the FreezeColumns count, so what to do here is ill-defined. For now we use regular index. - int dst_i = (dst_column->IndexWithinEnabledSet != -1) ? dst_column->IndexWithinEnabledSet : table->Columns.index_from_ptr(dst_column); - if ((src_i < table->FreezeColumnsRequest) != (dst_i < table->FreezeColumnsRequest)) - return; + if (src_column->IsUserEnabled) + if ((src_order < table->LeftMostUnfrozenOrder) != (dst_order < table->LeftMostUnfrozenOrder)) + return; table->ReorderColumnSrcOrder = (ImGuiTableColumnIdx)src_order; table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; } @@ -833,6 +832,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) ImBitArrayClearAllBits(table->EnabledMaskByIndex, table->ColumnsCount); ImBitArrayClearAllBits(table->EnabledMaskByDisplayOrder, table->ColumnsCount); table->LeftMostEnabledColumn = -1; + table->LeftMostUnfrozenOrder = -1; table->MinColumnWidth = ImMax(1.0f, g.Style.FramePadding.x * 1.0f); // g.Style.ColumnsMinSpacing; // FIXME-TABLE // [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns. @@ -896,6 +896,8 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) else table->LeftMostEnabledColumn = (ImGuiTableColumnIdx)column_n; column->IndexWithinEnabledSet = table->ColumnsEnabledCount++; + if (table->ColumnsEnabledCount == table->FreezeColumnsRequest) + table->LeftMostUnfrozenOrder = (ImGuiTableColumnIdx)(order_n + 1); ImBitArraySetBit(table->EnabledMaskByIndex, column_n); ImBitArraySetBit(table->EnabledMaskByDisplayOrder, column->DisplayOrder); prev_visible_column_idx = column_n; @@ -929,6 +931,8 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) if ((table->Flags & ImGuiTableFlags_Sortable) && table->SortSpecsCount == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) table->IsSortSpecsDirty = true; table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + if (table->LeftMostUnfrozenOrder == -1) + table->LeftMostUnfrozenOrder = table->ColumnsEnabledCount; IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid From 9fedea83f02fc2ef111a06f16c47562def475b3f Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 16:00:49 +0200 Subject: [PATCH 05/13] Tables: TableQueueSetColumnDisplayOrder() enforce handling ImGuiTableColumnFlags_NoReorder as advertised: can't reorder through them. (#9312) This would never using interactive reordering as reordering from headers was done on a 1 by 1 basis which was already covered in the test. Hard to tell what's a sensible design for this tbh. Expecting _NoOrder columns to be sequential and leading/trailing anyhow. --- imgui_tables.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 528ea76c0..3aa69fdd2 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -743,19 +743,20 @@ void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst table->IsSettingsDirty = true; } -// Reorder requested by user indirection needs to verify -// - That we don't reorder columns with the ImGuiTableColumnFlags_NoReorder flag. -// - That we don't cross the frozen column limit. -// (that TableSetupScrollFreeze() enforce a display order range for frozen columns. -// so reordering a column across the frozen column barrier is illegal and will be undone.) +// Reorder requested by user interaction. void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order) { ImGuiTableColumn* src_column = &table->Columns[table->DisplayOrderToIndex[src_order]]; - ImGuiTableColumn* dst_column = &table->Columns[table->DisplayOrderToIndex[dst_order]]; - if ((src_column->Flags | dst_column->Flags) & ImGuiTableColumnFlags_NoReorder) // FIXME: Perform a sweep test? + + // Verify that we don't cross the frozen column limit. + // TableSetupScrollFreeze() enforce a display order range for frozen columns. Reordering across the frozen column barrier is illegal and will be undone. + if (src_column->IsUserEnabled && (src_order < table->LeftMostUnfrozenOrder) != (dst_order < table->LeftMostUnfrozenOrder)) return; - if (src_column->IsUserEnabled) - if ((src_order < table->LeftMostUnfrozenOrder) != (dst_order < table->LeftMostUnfrozenOrder)) + + // Verify that we don't reorder columns with the ImGuiTableColumnFlags_NoReorder flag, nor cross through them. + int reorder_dir = (src_order < dst_order) ? +1 : -1; + for (int order_n = src_order; (src_order < dst_order && order_n <= dst_order) || (dst_order < src_order && order_n >= dst_order); order_n += reorder_dir) + if (table->Columns[table->DisplayOrderToIndex[order_n]].Flags & ImGuiTableColumnFlags_NoReorder) return; table->ReorderColumnSrcOrder = (ImGuiTableColumnIdx)src_order; table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; From 2ac782fbeda308cac83d2391cc04bbae6a68beac Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 17:53:55 +0200 Subject: [PATCH 06/13] Revert "Tables: rework TableSetColumnDisplayOrder() to take src_order. (#9312)" This reverts commit 7be8076e9b56d55f11a7ed86268ac3eb8b69d13e. Not a good idea as it's easier to reason and persist column index. + clear ReorderColumnDstOrder properly in reinit. --- imgui_internal.h | 6 +++--- imgui_tables.cpp | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 435e77a9b..4d5a119ab 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3044,7 +3044,7 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. - ImGuiTableColumnIdx ReorderColumnSrcOrder; // Display order of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) ImGuiTableColumnIdx ReorderColumnDstOrder; // Requested display order of column being reordered. ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. @@ -3560,8 +3560,8 @@ namespace ImGui IMGUI_API float TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n); IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); - IMGUI_API void TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order); - IMGUI_API void TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order); + IMGUI_API void TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order); + IMGUI_API void TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order); IMGUI_API void TableRemove(ImGuiTable* table); IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 3aa69fdd2..06b6c6859 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -585,7 +585,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->IsSettingsDirty = true; // Records itself into .ini file even when in default state (#7934) table->InstanceInteracted = -1; table->ContextPopupColumn = -1; - table->ReorderColumnSrcOrder = table->ResizedColumn = table->LastResizedColumn = -1; + table->ReorderColumn = table->ReorderColumnDstOrder = table->ResizedColumn = table->LastResizedColumn = -1; table->AutoFitSingleColumn = -1; table->HoveredColumnBody = table->HoveredColumnBorder = -1; for (int n = 0; n < columns_count; n++) @@ -703,9 +703,9 @@ void ImGui::TableBeginApplyRequests(ImGuiTable* table) if (table->InstanceCurrent == 0) { table->HeldHeaderColumn = -1; - if (table->ReorderColumnSrcOrder != -1 && table->ReorderColumnDstOrder != -1) + if (table->ReorderColumn != -1 && table->ReorderColumnDstOrder != -1) { - TableSetColumnDisplayOrder(table, table->ReorderColumnSrcOrder, table->ReorderColumnDstOrder); + TableSetColumnDisplayOrder(table, table->ReorderColumn, table->ReorderColumnDstOrder); table->ReorderColumnDstOrder = -1; } } @@ -721,16 +721,17 @@ void ImGui::TableBeginApplyRequests(ImGuiTable* table) } // Apply immediately. See TableQueueSetColumnDisplayOrder() for additional checks/constraints. -void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order) +void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) { - IM_ASSERT(src_order >= 0 && src_order < table->ColumnsCount); + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); IM_ASSERT(dst_order >= 0 && dst_order < table->ColumnsCount); + ImGuiTableColumn* src_column = &table->Columns[column_n]; + const int src_order = src_column->DisplayOrder; if (src_order == dst_order) return; const int reorder_dir = (dst_order < src_order) ? -1 : +1; - ImGuiTableColumn* src_column = &table->Columns[table->DisplayOrderToIndex[src_order]]; src_column->DisplayOrder = (ImGuiTableColumnIdx)dst_order; for (int order_n = src_order + reorder_dir; order_n != dst_order + reorder_dir; order_n += reorder_dir) table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder -= (ImGuiTableColumnIdx)reorder_dir; @@ -744,9 +745,10 @@ void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst } // Reorder requested by user interaction. -void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, int dst_order) +void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) { - ImGuiTableColumn* src_column = &table->Columns[table->DisplayOrderToIndex[src_order]]; + ImGuiTableColumn* src_column = &table->Columns[column_n]; + int src_order = src_column->DisplayOrder; // Verify that we don't cross the frozen column limit. // TableSetupScrollFreeze() enforce a display order range for frozen columns. Reordering across the frozen column barrier is illegal and will be undone. @@ -758,7 +760,7 @@ void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int src_order, in for (int order_n = src_order; (src_order < dst_order && order_n <= dst_order) || (dst_order < src_order && order_n >= dst_order); order_n += reorder_dir) if (table->Columns[table->DisplayOrderToIndex[order_n]].Flags & ImGuiTableColumnFlags_NoReorder) return; - table->ReorderColumnSrcOrder = (ImGuiTableColumnIdx)src_order; + table->ReorderColumn = (ImGuiTableColumnIdx)src_order; table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; } @@ -3246,10 +3248,10 @@ void ImGui::TableHeader(const char* label) table->InstanceInteracted = table->InstanceCurrent; if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) if (ImGuiTableColumn* prev_column = (column->PrevEnabledColumn != -1) ? &table->Columns[column->PrevEnabledColumn] : NULL) - TableQueueSetColumnDisplayOrder(table, column->DisplayOrder, prev_column->DisplayOrder); + TableQueueSetColumnDisplayOrder(table, column_n, prev_column->DisplayOrder); if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) if (ImGuiTableColumn* next_column = (column->NextEnabledColumn != -1) ? &table->Columns[column->NextEnabledColumn] : NULL) - TableQueueSetColumnDisplayOrder(table, column->DisplayOrder, next_column->DisplayOrder); + TableQueueSetColumnDisplayOrder(table, column_n, next_column->DisplayOrder); } // Sort order arrow @@ -3271,7 +3273,7 @@ void ImGui::TableHeader(const char* label) } // Handle clicking on column header to adjust Sort Order - if (pressed && table->ReorderColumnSrcOrder != column->DisplayOrder) + if (pressed && table->ReorderColumn != column_n) { ImGuiSortDirection sort_direction = TableGetColumnNextSortDirection(column); TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift); @@ -4062,7 +4064,7 @@ void ImGui::DebugNodeTable(ImGuiTable* table) BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); - BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn Orders: %d->%d", table->ResizedColumn, table->HeldHeaderColumn, table->ReorderColumnSrcOrder, table->ReorderColumnDstOrder); + BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn: %d", table->ResizedColumn, table->HeldHeaderColumn, table->ReorderColumn); for (int n = 0; n < table->InstanceCurrent + 1; n++) { ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); From 9df0d38d45643ab9a873ea6618c1af658251395a Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 18:30:29 +0200 Subject: [PATCH 07/13] Tables: amend 2ac782f bad WIP merge broken reorder. (#9312) --- imgui_tables.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 06b6c6859..ad0540cc9 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -760,7 +760,7 @@ void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int for (int order_n = src_order; (src_order < dst_order && order_n <= dst_order) || (dst_order < src_order && order_n >= dst_order); order_n += reorder_dir) if (table->Columns[table->DisplayOrderToIndex[order_n]].Flags & ImGuiTableColumnFlags_NoReorder) return; - table->ReorderColumn = (ImGuiTableColumnIdx)src_order; + table->ReorderColumn = (ImGuiTableColumnIdx)column_n; table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; } From 6603cdef98a38f023f4eaaaa50dab55d3d690cb2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 20:03:30 +0200 Subject: [PATCH 08/13] Tables: fixed dragging a header to reorder outside of visible bounds (due to horizontal scrolling) from losing active id. --- docs/CHANGELOG.txt | 3 +++ imgui_internal.h | 1 + imgui_tables.cpp | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 4c7c28a1e..d71ed5b5c 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -92,6 +92,9 @@ Other Changes: buffer on the IsItemDeactivatedAfterEdit() frame. This could create issues when using the idiom of not applying edits before IsItemDeactivatedAfterEdit(). (#9308, #8915, #8273) +- Tables: + - Fixed dragging a header to reorder outside of visible bounds (due to horizontal scrolling) + from losing active id. - Style: - Border sizes are now scaled (and rounded) by ScaleAllSizes(). - When using large values with ScallAllSizes(), the following items thickness diff --git a/imgui_internal.h b/imgui_internal.h index 4d5a119ab..dc1c4baf5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3044,6 +3044,7 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. + ImGuiTableColumnIdx LastHeldHeaderColumn; // Index of column header being held from previous frame. ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) ImGuiTableColumnIdx ReorderColumnDstOrder; // Requested display order of column being reordered. ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. diff --git a/imgui_tables.cpp b/imgui_tables.cpp index ad0540cc9..a64f4c053 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -702,6 +702,7 @@ void ImGui::TableBeginApplyRequests(ImGuiTable* table) // Note: we don't clear ReorderColumn after handling the request (FIXME: clarify why or add a test). if (table->InstanceCurrent == 0) { + table->LastHeldHeaderColumn = table->HeldHeaderColumn; table->HeldHeaderColumn = -1; if (table->ReorderColumn != -1 && table->ReorderColumnDstOrder != -1) { @@ -3136,7 +3137,7 @@ void ImGui::TableHeadersRow() const int columns_count = TableGetColumnCount(); for (int column_n = 0; column_n < columns_count; column_n++) { - if (!TableSetColumnIndex(column_n)) + if (!TableSetColumnIndex(column_n) && table->LastHeldHeaderColumn != column_n) continue; // Push an id to allow empty/unnamed headers. This is also idiomatic as it ensure there is a consistent ID path to access columns (for e.g. automation) @@ -4064,7 +4065,7 @@ void ImGui::DebugNodeTable(ImGuiTable* table) BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); - BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn: %d", table->ResizedColumn, table->HeldHeaderColumn, table->ReorderColumn); + BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn: %d", table->LastResizedColumn, table->LastHeldHeaderColumn, table->ReorderColumn); for (int n = 0; n < table->InstanceCurrent + 1; n++) { ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); From 07acc8bc87aeb95708a52fb6ccf3f73b0efe59c9 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 20:05:17 +0200 Subject: [PATCH 09/13] Tables: Angled Headers: angled section for column being reordered via the regular headers stay highlighted during reordering. Easily possible thanks to 6603cde. --- docs/CHANGELOG.txt | 2 ++ imgui_tables.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index d71ed5b5c..9dcb1b286 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -95,6 +95,8 @@ Other Changes: - Tables: - Fixed dragging a header to reorder outside of visible bounds (due to horizontal scrolling) from losing active id. + - Angled Headers: angled section for column being reordered via the regular headers + stays highlighted during reordering. - Style: - Border sizes are now scaled (and rounded) by ScaleAllSizes(). - When using large values with ScallAllSizes(), the following items thickness diff --git a/imgui_tables.cpp b/imgui_tables.cpp index a64f4c053..3d6ac81fd 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3308,7 +3308,7 @@ void ImGui::TableAngledHeadersRow() // Which column needs highlight? const ImGuiID row_id = GetID("##AngledHeaders"); ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); - int highlight_column_n = table->HighlightColumnHeader; + int highlight_column_n = (table->LastHeldHeaderColumn != -1) ? table->LastHeldHeaderColumn : table->HighlightColumnHeader; if (highlight_column_n == -1 && table->HoveredColumnBody != -1) if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) highlight_column_n = table->HoveredColumnBody; From 505bc9a31291de2b4537f7a7c56a0f3e65cbcf0d Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 19:07:59 +0200 Subject: [PATCH 10/13] Tables: clarify FrozenColumns order to be in order space not in visible space. + Removed LeftMostUnfrozenOrder + Angled Headers comments. While not strictly part of #9312, presenting a suitable reordering menu was made difficult by the old behavior. --- docs/CHANGELOG.txt | 9 +++++++++ imgui.h | 2 +- imgui_internal.h | 1 - imgui_tables.cpp | 18 ++++++------------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 9dcb1b286..0e0c17ec6 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -93,6 +93,15 @@ Other Changes: using the idiom of not applying edits before IsItemDeactivatedAfterEdit(). (#9308, #8915, #8273) - Tables: + - Fixed and clarified the behavior of using TableSetupScrollFreeze() with columns>1, + and where some of the columns within that range were Hidable. + - Before: TableSetupScrollFreeze(N, 0) made the first N _visible_ columns + part of the scroll freeze. So if you intentionally hide columns = 12345') #define IMGUI_VERSION "1.92.7 WIP" -#define IMGUI_VERSION_NUM 19268 +#define IMGUI_VERSION_NUM 19269 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 diff --git a/imgui_internal.h b/imgui_internal.h index dc1c4baf5..cb2aa1d97 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3056,7 +3056,6 @@ struct IMGUI_API ImGuiTable ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) - ImGuiTableColumnIdx LeftMostUnfrozenOrder; // Display order of the left-most unfrozen column, which is used to determine where the freezing line should be. ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 3d6ac81fd..348cd2955 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -753,7 +753,7 @@ void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int // Verify that we don't cross the frozen column limit. // TableSetupScrollFreeze() enforce a display order range for frozen columns. Reordering across the frozen column barrier is illegal and will be undone. - if (src_column->IsUserEnabled && (src_order < table->LeftMostUnfrozenOrder) != (dst_order < table->LeftMostUnfrozenOrder)) + if (src_column->IsUserEnabled && (src_order < table->FreezeColumnsRequest) != (dst_order < table->FreezeColumnsRequest)) return; // Verify that we don't reorder columns with the ImGuiTableColumnFlags_NoReorder flag, nor cross through them. @@ -836,7 +836,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) ImBitArrayClearAllBits(table->EnabledMaskByIndex, table->ColumnsCount); ImBitArrayClearAllBits(table->EnabledMaskByDisplayOrder, table->ColumnsCount); table->LeftMostEnabledColumn = -1; - table->LeftMostUnfrozenOrder = -1; table->MinColumnWidth = ImMax(1.0f, g.Style.FramePadding.x * 1.0f); // g.Style.ColumnsMinSpacing; // FIXME-TABLE // [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns. @@ -900,8 +899,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) else table->LeftMostEnabledColumn = (ImGuiTableColumnIdx)column_n; column->IndexWithinEnabledSet = table->ColumnsEnabledCount++; - if (table->ColumnsEnabledCount == table->FreezeColumnsRequest) - table->LeftMostUnfrozenOrder = (ImGuiTableColumnIdx)(order_n + 1); ImBitArraySetBit(table->EnabledMaskByIndex, column_n); ImBitArraySetBit(table->EnabledMaskByDisplayOrder, column->DisplayOrder); prev_visible_column_idx = column_n; @@ -935,8 +932,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) if ((table->Flags & ImGuiTableFlags_Sortable) && table->SortSpecsCount == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) table->IsSortSpecsDirty = true; table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; - if (table->LeftMostUnfrozenOrder == -1) - table->LeftMostUnfrozenOrder = table->ColumnsEnabledCount; IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid @@ -1077,7 +1072,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column // Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping. - int visible_n = 0; bool has_at_least_one_column_requesting_output = false; bool offset_x_frozen = (table->FreezeColumnsCount > 0); float offset_x = ((table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x) + table->OuterPaddingX - table->CellSpacingX1; @@ -1092,7 +1086,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // Initial nav layer: using FreezeRowsCount, NOT FreezeRowsRequest, so Header line changes layer when frozen column->NavLayerCurrent = (ImS8)(table->FreezeRowsCount > 0 ? ImGuiNavLayer_Menu : (ImGuiNavLayer)table->NavLayer); - if (offset_x_frozen && table->FreezeColumnsCount == visible_n) + if (offset_x_frozen && table->FreezeColumnsCount == column_n) { offset_x += work_rect.Min.x - table->OuterRect.Min.x; offset_x_frozen = false; @@ -1212,11 +1206,10 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) column->CannotSkipItemsQueue >>= 1; } - if (visible_n < table->FreezeColumnsCount) + if (column_n < table->FreezeColumnsCount) host_clip_rect.Min.x = ImClamp(column->MaxX + TABLE_BORDER_SIZE, host_clip_rect.Min.x, host_clip_rect.Max.x); offset_x += column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; - visible_n++; } // In case the table is visible (e.g. decorations) but all columns clipped, we keep a column visible. @@ -1297,7 +1290,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // [Part 13] Setup inner window decoration size (for scrolling / nav tracking to properly take account of frozen rows/columns) if (table->FreezeColumnsRequest > 0) - table->InnerWindow->DecoInnerSizeX1 = table->Columns[table->DisplayOrderToIndex[table->FreezeColumnsRequest - 1]].MaxX - table->OuterRect.Min.x; + table->InnerWindow->DecoInnerSizeX1 = table->Columns[table->DisplayOrderToIndex[table->FreezeColumnsRequest - 1]].MaxX - table->OuterRect.Min.x; // FIXME-FROZEN if (table->FreezeRowsRequest > 0) table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight; table_instance->LastFrozenHeight = 0.0f; @@ -3362,13 +3355,14 @@ void ImGui::TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label const ImVec2 header_angled_vector = unit_right * (row_height / -sin_a); // vector from bottom-left to top-left, and from bottom-right to top-right // Declare row, override and draw our own background + // FIXME-TABLE: Generally broken when overlapping frozen columns limit. TableNextRow(ImGuiTableRowFlags_Headers, row_height); TableNextColumn(); const ImRect row_r(table->WorkRect.Min.x, table->BgClipRect.Min.y, table->WorkRect.Max.x, table->RowPosY2); table->DrawSplitter->SetCurrentChannel(draw_list, TABLE_DRAW_CHANNEL_BG0); float clip_rect_min_x = table->BgClipRect.Min.x; if (table->FreezeColumnsCount > 0) - clip_rect_min_x = ImMax(clip_rect_min_x, table->Columns[table->FreezeColumnsCount - 1].MaxX); + clip_rect_min_x = ImMax(clip_rect_min_x, table->Columns[table->DisplayOrderToIndex[table->FreezeColumnsCount - 1]].MaxX); TableSetBgColor(ImGuiTableBgTarget_RowBg0, 0); // Cancel PushClipRect(table->BgClipRect.Min, table->BgClipRect.Max, false); // Span all columns draw_list->AddRectFilled(ImVec2(table->BgClipRect.Min.x, row_r.Min.y), ImVec2(table->BgClipRect.Max.x, row_r.Max.y), GetColorU32(ImGuiCol_TableHeaderBg, 0.25f)); // FIXME-STYLE: Change row background with an arbitrary color. From 836278db6c22af3a84f61796d5b319f11ed6d545 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 20 Mar 2026 21:22:53 +0100 Subject: [PATCH 11/13] Tables: context menu now present columns in display order. (#9312) --- docs/CHANGELOG.txt | 1 + imgui_tables.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 0e0c17ec6..9b9a3bb8a 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -93,6 +93,7 @@ Other Changes: using the idiom of not applying edits before IsItemDeactivatedAfterEdit(). (#9308, #8915, #8273) - Tables: + - Context menu now presents columns in display order. (#9312) - Fixed and clarified the behavior of using TableSetupScrollFreeze() with columns>1, and where some of the columns within that range were Hidable. - Before: TableSetupScrollFreeze(N, 0) made the first N _visible_ columns diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 348cd2955..bba8fbb87 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3574,8 +3574,9 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags want_separator = true; PushItemFlag(ImGuiItemFlags_AutoClosePopups, false); - for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) { + const int column_n = table->DisplayOrderToIndex[order_n]; ImGuiTableColumn* column = &table->Columns[column_n]; if (column->Flags & ImGuiTableColumnFlags_Disabled) continue; From 0867f6113ab39bb38c3b1b9c0b85e2bdca07fcd5 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 14:13:34 +0200 Subject: [PATCH 12/13] Tables: reorder allowed check sweep through columns, checks _NoOrder barrier, and reworked to be easy to use from context menu. (#9312) The sweep doesn't make a difference for reordering with headers as reordering is currently done 1 by 1. But menu ordering will change that. --- imgui_tables.cpp | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/imgui_tables.cpp b/imgui_tables.cpp index bba8fbb87..12a3e93f7 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -745,24 +745,37 @@ void ImGui::TableSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_ table->IsSettingsDirty = true; } -// Reorder requested by user interaction. -void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) +static int TableGetMaxDisplayOrderAllowed(ImGuiTable* table, int src_order, int dst_order) { - ImGuiTableColumn* src_column = &table->Columns[column_n]; - int src_order = src_column->DisplayOrder; + dst_order = ImClamp(dst_order, 0, table->ColumnsCount - 1); + if (src_order == dst_order) + return dst_order; - // Verify that we don't cross the frozen column limit. + // Cannot cross over the frozen column limit when interactively reordering. // TableSetupScrollFreeze() enforce a display order range for frozen columns. Reordering across the frozen column barrier is illegal and will be undone. - if (src_column->IsUserEnabled && (src_order < table->FreezeColumnsRequest) != (dst_order < table->FreezeColumnsRequest)) - return; + if (table->FreezeColumnsRequest > 0) + dst_order = (src_order < table->FreezeColumnsRequest) ? ImMin(dst_order, (int)table->FreezeColumnsRequest - 1) : ImMax(dst_order, (int)table->FreezeColumnsRequest); - // Verify that we don't reorder columns with the ImGuiTableColumnFlags_NoReorder flag, nor cross through them. + // Cannot cross over a column with the ImGuiTableColumnFlags_NoReorder flag. int reorder_dir = (src_order < dst_order) ? +1 : -1; for (int order_n = src_order; (src_order < dst_order && order_n <= dst_order) || (dst_order < src_order && order_n >= dst_order); order_n += reorder_dir) if (table->Columns[table->DisplayOrderToIndex[order_n]].Flags & ImGuiTableColumnFlags_NoReorder) - return; + { + dst_order = (order_n == src_order) ? src_order : order_n - reorder_dir; + break; + } + return dst_order; +} + +// Reorder requested by user interaction. +void ImGui::TableQueueSetColumnDisplayOrder(ImGuiTable* table, int column_n, int dst_order) +{ + const int src_order = table->Columns[column_n].DisplayOrder; table->ReorderColumn = (ImGuiTableColumnIdx)column_n; - table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; + table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)-1; + dst_order = TableGetMaxDisplayOrderAllowed(table, src_order, dst_order); + if (dst_order != src_order) + table->ReorderColumnDstOrder = (ImGuiTableColumnIdx)dst_order; } // Adjust flags: default width mode + stretch columns are not allowed when auto extending From bf10275aa7e0b63b1b925983656c616e95d90115 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Apr 2026 19:43:21 +0200 Subject: [PATCH 13/13] Tables: allow reordering columns by dragging them in the context menu. (#9312) --- docs/CHANGELOG.txt | 1 + imgui_tables.cpp | 55 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 9b9a3bb8a..a2b8cbb63 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -93,6 +93,7 @@ Other Changes: using the idiom of not applying edits before IsItemDeactivatedAfterEdit(). (#9308, #8915, #8273) - Tables: + - Allow reordering columns by dragging them in the context menu. (#9312) - Context menu now presents columns in display order. (#9312) - Fixed and clarified the behavior of using TableSetupScrollFreeze() with columns>1, and where some of the columns within that range were Hidable. diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 12a3e93f7..e95a580db 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3511,6 +3511,36 @@ bool ImGui::TableBeginContextMenuPopup(ImGuiTable* table) return false; } +// FIXME: Copied from MenuItem() for the purpose of being able to pass _SelectOnRelease (#9312) +static bool MenuItemForColumnReorder(const char* label, bool selected, bool enabled) +{ + using namespace ImGui; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); + float min_w = offsets->DeclColumns(0.0f, label_size.x, 0.0f, checkmark_w); // Feedback for next frame + float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + + ImGuiID id = GetID(label); + ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_SpanAvailWidth; + if (g.ActiveId == id) + selectable_flags |= ImGuiSelectableFlags_Highlight; // Stays highlighted while dragging. + const bool has_been_moved = (g.ActiveId == id) && g.ActiveIdHasBeenEditedBefore; // But disable toggling once moved. + + BeginDisabled(!enabled); // Don't use ImGuiSelectableFlags_Disabled so that Check mark is also affected. + bool ret = Selectable(label, false, selectable_flags, ImVec2(min_w, label_size.y)) && !has_been_moved; // Can't use IsMouseDragging(0) as button is released already. + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && selected) + RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); + EndDisabled(); + + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); + return ret; +} + // Output context menu into current window (generally a popup) // FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? // Sections to display are pulled from 'flags_for_section_to_display', which is typically == table->Flags. @@ -3586,6 +3616,12 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags Separator(); want_separator = true; + // While reordering: we calculate min/max allowed range once here so we can avoid a O(N log N) in the loop (because the query itself does a sweep scan). + // This assume that reordering constraints output a single range, otherwise would need to either call TableGetMaxDisplayOrderAllowed() for each item below, or cache this once per frame into columns. + const bool is_reordering = (g.ActiveId != 0 && g.ActiveIdWindow == g.CurrentWindow && table->ReorderColumn != -1 && g.ActiveIdHasBeenEditedBefore); // FIXME: This is a bit of a hack. + const int reorder_src_order = is_reordering ? table->Columns[table->ReorderColumn].DisplayOrder : -1; + const int reorder_min_order = is_reordering ? TableGetMaxDisplayOrderAllowed(table, reorder_src_order, 0) : 0; + const int reorder_max_order = is_reordering ? TableGetMaxDisplayOrderAllowed(table, reorder_src_order, table->ColumnsCount - 1) : table->ColumnsCount - 1; PushItemFlag(ImGuiItemFlags_AutoClosePopups, false); for (int order_n = 0; order_n < table->ColumnsCount; order_n++) { @@ -3602,8 +3638,25 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags bool menu_item_enabled = (column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; if (column->IsUserEnabled && table->ColumnsEnabledCount <= 1) menu_item_enabled = false; - if (MenuItem(name, NULL, column->IsUserEnabled, menu_item_enabled)) + if (is_reordering && (column->DisplayOrder < reorder_min_order || column->DisplayOrder > reorder_max_order)) + menu_item_enabled = false; + if (MenuItemForColumnReorder(name, column->IsUserEnabled, menu_item_enabled)) column->IsUserEnabledNextFrame = !column->IsUserEnabled; + + // Drag to reorder + // FIXME: It is currently not possible to reorder columns marked with ImGuiTableColumnFlags_NoHide. + if (IsItemActive() && IsMouseDragging(0) && g.ActiveIdSource == ImGuiInputSource_Mouse && (table->Flags & ImGuiTableFlags_Reorderable)) + { + g.ActiveIdHasBeenEditedBefore = true; // Disable toggle in MenuItemForColumnReorder() + start dimming to display allowed reorder targets. + table->ReorderColumn = (ImGuiTableColumnIdx)column_n; + if (!IsItemHovered()) + { + int reorder_dir = (g.IO.MousePos.y < (g.LastItemData.Rect.Min.y + g.LastItemData.Rect.Max.y) * 0.5f) ? -1 : +1; + float reorder_amount = (reorder_dir < 0 ? g.LastItemData.Rect.Min.y - g.IO.MousePos.y : g.IO.MousePos.y - g.LastItemData.Rect.Max.y) / g.LastItemData.Rect.GetHeight(); + int dst_order = column->DisplayOrder + (int)ImCeil(reorder_amount) * reorder_dir; // Estimated target order, will be validated and clamped. + TableQueueSetColumnDisplayOrder(table, column_n, dst_order); + } + } } PopItemFlag(); }