diff --git a/backends/imgui_impl_opengl3.cpp b/backends/imgui_impl_opengl3.cpp index 9b5d431b5..6e6ae9f99 100644 --- a/backends/imgui_impl_opengl3.cpp +++ b/backends/imgui_impl_opengl3.cpp @@ -25,8 +25,10 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) // 2026-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. -// 2026-06-23: OpenGL: GLSL version detection assume GLSL 410 when GL context is 4.1. Fixes an issue running on macOS with Wine. (#9427, #6577) +// 2026-06-17: OpenGL: Expose selected render state in ImGui_ImplOpenGL3_RenderState, Allowing to dynamically select between use of glBindSampler() and glTexParameter(). You can access in 'void* platform_io.Renderer_RenderState' during rendering. +// 2026-06-03: OpenGL: GLSL version detection assume GLSL 410 when GL context is 4.1. Fixes an issue running on macOS with Wine. (#9427, #6577) // 2026-04-23: OpenGL: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState, DrawCallback_SetSamplerLinear, DrawCallback_SetSamplerNearest. (#9378) +// (Breaking): this change prioritize using glBindSampler() when available, which would override glTexParameter() settings you may have set on custom textures. // 2026-03-12: OpenGL: Fixed invalid assert in ImGui_ImplOpenGL3_UpdateTexture() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295) // 2025-12-11: OpenGL: Fixed embedded loader multiple init/shutdown cycles broken on some platforms. (#8792, #9112) // 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown. @@ -256,8 +258,6 @@ struct ImGui_ImplOpenGL3_Data bool HasBindSampler; bool HasClipOrigin; bool UseBufferSubData; - bool UseTexParameterToSetSampler; - GLuint NextSampler; // Used if !HasBindSampler && UseTexParameterToSetSampler. #ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER GLuint TexSamplers[2]; // Used if HasBindSimpler. (0=linear, 1=nearest) #endif @@ -337,7 +337,7 @@ void ImGui_ImplOpenGL3_NewFrame() IM_ASSERT(0 && "ImGui_ImplOpenGL3_CreateDeviceObjects() failed!"); } -static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_width, int fb_height, GLuint vertex_array_object) +static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, ImGui_ImplOpenGL3_RenderState* render_state, int fb_width, int fb_height, GLuint vertex_array_object) { ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); @@ -391,8 +391,11 @@ static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_wid glUniformMatrix4fv(bd->AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]); #ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER - if (bd->HasBindSampler) - glBindSampler(0, bd->TexSamplers[0]); // We use combined texture/sampler state. Applications using GL 3.3 and GL ES 3.0 may set that otherwise. + if (render_state->UseBindSampler) + { + render_state->CurrentSampler = bd->TexSamplers[0]; + glBindSampler(0, render_state->CurrentSampler); // We use combined texture/sampler state. Applications using GL 3.3 and GL ES 3.0 may set that otherwise. + } #endif (void)vertex_array_object; @@ -413,13 +416,42 @@ static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_wid // Draw callbacks static void ImGui_ImplOpenGL3_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. +static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) +{ + ImGui_ImplOpenGL3_RenderState* render_state = ImGui_ImplOpenGL3_GetRenderState(); #ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER -static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); if (bd->HasBindSampler) { glBindSampler(0, bd->TexSamplers[0]); } else { bd->UseTexParameterToSetSampler = true; bd->NextSampler = GL_LINEAR; } } -static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); if (bd->HasBindSampler) { glBindSampler(0, bd->TexSamplers[1]); } else { bd->UseTexParameterToSetSampler = true; bd->NextSampler = GL_NEAREST; } } -#else -static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); bd->UseTexParameterToSetSampler = true; bd->NextSampler = GL_LINEAR; } -static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); bd->UseTexParameterToSetSampler = true; bd->NextSampler = GL_NEAREST; } + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->HasBindSampler) + { + render_state->CurrentSampler = bd->TexSamplers[0]; + render_state->UseTexParameterFilter = false; + glBindSampler(0, render_state->CurrentSampler); + } + else #endif + { + render_state->UseTexParameterFilter = true; + render_state->CurrentTexParameterFilter = GL_LINEAR; + } +} +static void ImGui_ImplOpenGL3_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) +{ + ImGui_ImplOpenGL3_RenderState* render_state = ImGui_ImplOpenGL3_GetRenderState(); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->HasBindSampler) + { + render_state->CurrentSampler = bd->TexSamplers[1]; + render_state->UseTexParameterFilter = false; + glBindSampler(0, render_state->CurrentSampler); + } + else +#endif + { + render_state->UseTexParameterFilter = true; + render_state->CurrentTexParameterFilter = GL_NEAREST; + } +} // OpenGL3 Render function. // Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly. @@ -489,7 +521,17 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) #ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY GL_CALL(glGenVertexArrays(1, &vertex_array_object)); #endif - ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); + + // Setup render state structure (for callbacks and custom texture bindings) + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + ImGui_ImplOpenGL3_RenderState render_state; + render_state.UseBindSampler = bd->HasBindSampler; + render_state.UseTexParameterFilter = false; + render_state.CurrentSampler = 0; + render_state.CurrentTexParameterFilter = 0; + platform_io.Renderer_RenderState = &render_state; + + ImGui_ImplOpenGL3_SetupRenderState(draw_data, &render_state, fb_width, fb_height, vertex_array_object); // Will project scissor/clipping rectangles into framebuffer space ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports @@ -536,7 +578,7 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) { // User callback, registered via ImDrawList::AddCallback() if (pcmd->UserCallback == ImGui_ImplOpenGL3_DrawCallback_ResetRenderState) - ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); + ImGui_ImplOpenGL3_SetupRenderState(draw_data, &render_state, fb_width, fb_height, vertex_array_object); else pcmd->UserCallback(draw_list, pcmd); } @@ -555,11 +597,11 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) GL_CALL(glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID())); // Emulate sampler change (even though it is technically part of texture data) - // As a sort of hack/workaround, we only start writing using glTextParameter() if sampler is ever changed explicitly. - if (!bd->HasBindSampler && bd->UseTexParameterToSetSampler) + // As a sort of hack/workaround, we only start writing using glTexParameter() if sampler is ever changed explicitly. + if (render_state.UseTexParameterFilter) { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, bd->NextSampler); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, bd->NextSampler); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, render_state.CurrentTexParameterFilter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, render_state.CurrentTexParameterFilter); } #ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET @@ -571,6 +613,7 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) } } } + platform_io.Renderer_RenderState = nullptr; // Destroy the temporary VAO #ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY diff --git a/backends/imgui_impl_opengl3.h b/backends/imgui_impl_opengl3.h index 7855c43cf..62cee8b38 100644 --- a/backends/imgui_impl_opengl3.h +++ b/backends/imgui_impl_opengl3.h @@ -66,4 +66,17 @@ IMGUI_IMPL_API void ImGui_ImplOpenGL3_UpdateTexture(ImTextureData* tex); #endif +// [BETA] Selected render state data shared with callbacks. +// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplOpenGL3_RenderDrawData() call. +// (Please open an issue if you feel you need access to more data) +struct ImGui_ImplOpenGL3_RenderState +{ + bool UseBindSampler; + bool UseTexParameterFilter; + unsigned int CurrentSampler; // (GLuint) Used if UseBindSampler == true, otherwise always 0 + unsigned int CurrentTexParameterFilter; // (GLuint) Used if UseTexParameterToSetSampler == true +}; + +static inline ImGui_ImplOpenGL3_RenderState* ImGui_ImplOpenGL3_GetRenderState() { return (ImGui_ImplOpenGL3_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState; } + #endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_vulkan.cpp b/backends/imgui_impl_vulkan.cpp index 8df12926f..570b914c0 100644 --- a/backends/imgui_impl_vulkan.cpp +++ b/backends/imgui_impl_vulkan.cpp @@ -566,8 +566,17 @@ static void ImGui_ImplVulkan_SetupRenderState(ImDrawData* draw_data, VkPipeline // Draw callbacks static void ImGui_ImplVulkan_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. -static void ImGui_ImplVulkan_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData(); vkCmdBindDescriptorSets(bd->RenderState->CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, bd->PipelineLayout, 1, 1, &bd->SamplerLinearDS, 0, nullptr); } -static void ImGui_ImplVulkan_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData(); vkCmdBindDescriptorSets(bd->RenderState->CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, bd->PipelineLayout, 1, 1, &bd->SamplerNearestDS, 0, nullptr); } +static void ImGui_ImplVulkan_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData(); vkCmdBindDescriptorSets(bd->RenderState->CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, bd->RenderState->PipelineLayout, 1, 1, &bd->SamplerLinearDS, 0, nullptr); } +static void ImGui_ImplVulkan_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplVulkan_Data* bd = ImGui_ImplVulkan_GetBackendData(); vkCmdBindDescriptorSets(bd->RenderState->CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, bd->RenderState->PipelineLayout, 1, 1, &bd->SamplerNearestDS, 0, nullptr); } + +// If you want to use your own sampler, you can create your own callback, access ImGui_ImplVulkan_RenderState for command-buffer and pipeline layout, e.g. +/* +void ImGui_ImplVulkan_DrawCallback_SetSamplerCustom(const ImDrawList*, const ImDrawCmd* cmd) +{ + ImGui_ImplVulkan_RenderState* render_state = (ImGui_ImplVulkan_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState; + vkCmdBindDescriptorSets(render_state->CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, render_state->PipelineLayout, 1, 1, (VkDescriptorSet*)cmd->UserCallbackData, 0, nullptr); +} +*/ // Render function void ImGui_ImplVulkan_RenderDrawData(ImDrawData* draw_data, VkCommandBuffer command_buffer, VkPipeline pipeline) diff --git a/backends/imgui_impl_vulkan.h b/backends/imgui_impl_vulkan.h index a3eda9717..3ca126ca9 100644 --- a/backends/imgui_impl_vulkan.h +++ b/backends/imgui_impl_vulkan.h @@ -175,6 +175,7 @@ IMGUI_IMPL_API bool ImGui_ImplVulkan_LoadFunctions(uint32_t api_vers // [BETA] Selected render state data shared with callbacks. // This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplVulkan_RenderDrawData() call. +// ImGui_ImplVulkan_RenderState* render_state = (ImGui_ImplVulkan_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState; // (Please open an issue if you feel you need access to more data) struct ImGui_ImplVulkan_RenderState { diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 37d197abe..e97deaee9 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -52,10 +52,26 @@ Other Changes: for user code to bypass it without using a clickable item. (#9382) - Clicking on a window's empty-space to move/focus a window checks for lack of queued focus request. (#9382) + - Fixed double-click collapse toggle not owning the mouse button. If a + `SetNextWindowPos()` with pivot was queued in the same frame, the second + click could trigger another item in the same window. (#9439) [@Cleroth] - InputText: - 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 @@ -71,6 +87,9 @@ Other Changes: calling ClearFonts() during rendering. - Fixed an issue where passing a manually created ImFontAtlas to CreateContext() would incorrectly destroy it in DestroyContext() when ref-count gets back to zero. (#9426) +- Nav: + - Fixed context menu activation with gamepad erroneously testing for _NavEnableKeyboard + instead of _NavEnableGamepad. (#9454, #8803, #9270) [@Clownacy] - DrawList: - Minor optimization to `AddLine()`, `AddLineH()`, `AddLineV()` functions. (#4091) - Added `ImDrawListFlags_TextNoPixelSnap` to disable snapping of AddText() @@ -86,6 +105,8 @@ Other Changes: - OpenGL3: - GLSL version detection assume GLSL 410 when GL context is 4.1. Fixes an issue running on macOS with Wine. [#9427, #6577) [@perminovVS] + - Expose selected render state in ImGui_ImplOpenGL3_RenderState, allowing to + dynamically select between use of glBindSampler() and glTexParameter(). (#9378) - Win32: - Uses `SetProcessDpiAwarenessContext()` instead of `SetThreadDpiAwarenessContext()` when available, fixing OpenGL DPI scaling issues as e.g. NVIDIA drivers tends @@ -152,6 +173,8 @@ Breaking Changes: - DirectX10, DirectX11, SDLGPU3, Vulkan: removed samplers from `ImGui_ImplXXXX_RenderState`. Prefer to use backend-agnostic DrawCallback_SetSamplerLinear which works everywhere! (#9378) If there is a legit need/request for them or any render state we can always add them back. + - OpenGL3: the new sampler support system prioritize using glBindSampler() when available, + which would override glTexParameter() settings you may have set on custom textures. (#9378) Other Changes: diff --git a/imgui.cpp b/imgui.cpp index 0bade0291..1164e8651 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -5949,8 +5949,11 @@ void ImGui::NewFrame() if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); for (ImGuiTableTempData& table_temp_data : g.TablesTempData) + { + table_temp_data.ReconcileColumnsRequests.clear(); // Unusual: clear every frame because this is rarely used. if (table_temp_data.LastTimeActive >= 0.0f && table_temp_data.LastTimeActive < memory_compact_start_time) TableGcCompactTransientBuffers(&table_temp_data); + } if (g.GcCompactAll) GcCompactTransientMiscBuffers(); g.GcCompactAll = false; @@ -8160,7 +8163,10 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) ImRect title_bar_rect = window->TitleBarRect(); if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && g.ActiveId == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max)) if (g.IO.MouseClickedCount[0] == 2 && GetKeyOwner(ImGuiKey_MouseLeft) == ImGuiKeyOwner_NoOwner) + { window->WantCollapseToggle = true; + SetKeyOwner(ImGuiKey_MouseLeft, window->MoveId); // Claim input the same way ButtonBehavior() does. Prevent a same-frame move from triggering other items. (#9439) + } if (window->WantCollapseToggle) { window->Collapsed = !window->Collapsed; @@ -15065,7 +15071,7 @@ static void ImGui::NavUpdateContextMenuRequest() ImGuiContext& g = *GImGui; g.NavOpenContextMenuItemId = g.NavOpenContextMenuWindowId = 0; const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; - const bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0; if ((!nav_keyboard_active && !nav_gamepad_active) || g.NavWindow == NULL) return; diff --git a/imgui.h b/imgui.h index 7c3c01fea..cc7af93ff 100644 --- a/imgui.h +++ b/imgui.h @@ -30,7 +30,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.92.9 WIP" -#define IMGUI_VERSION_NUM 19283 +#define IMGUI_VERSION_NUM 19284 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 #define IMGUI_HAS_VIEWPORT // In 'docking' WIP branch. @@ -657,7 +657,7 @@ namespace ImGui // Widgets: Images // - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. - // - Image() pads adds style.ImageBorderSize on each side, ImageButton() adds style.FramePadding on each side. + // - Image() adds style.ImageBorderSize on each side, ImageButton() adds style.FramePadding on each side. // - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. // - An obsolete version of Image(), before 1.91.9 (March 2025), had a 'tint_col' parameter which is now supported by the ImageWithBg() function. IMGUI_API void Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1)); @@ -817,7 +817,7 @@ namespace ImGui // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. - // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. + // - Note that MenuItem() keyboard shortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. diff --git a/imgui_internal.h b/imgui_internal.h index 14bb841f2..5a121b4ae 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3127,7 +3127,7 @@ struct IMGUI_API ImGuiTabBar #define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code which cannot be used as a regular color. #define IMGUI_TABLE_MAX_COLUMNS 512 // Arbitrary "safety" maximum, may be lifted in the future if needed. Must fit in ImGuiTableColumnIdx/ImGuiTableDrawChannelIdx. -// [Internal] sizeof() ~ 112 +// [Internal] sizeof() ~ 120 // We use the terminology "Enabled" to refer to a column that is not Hidden by user/api. // We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping. // This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped". @@ -3168,7 +3168,11 @@ 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 IsPreserveWidthAuto : 1; + bool IsJustCreated : 1; + bool IsLoadedSettings : 1; + bool IsNeedReconcileSrc : 1; + bool IsNeedReconcileDst : 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 @@ -3186,10 +3190,28 @@ struct ImGuiTableColumn PrevEnabledColumn = NextEnabledColumn = -1; SortOrder = -1; SortDirection = ImGuiSortDirection_None; + IsJustCreated = true; DrawChannelCurrent = DrawChannelFrozen = DrawChannelUnfrozen = (ImU8)-1; } }; +// Passed to TableSetupColumn() +// sizeof() ~ 24+120 bytes +struct ImGuiTableReconcileColumnData +{ + // Setup data + ImGuiID ID; + ImS16 NameOffset; + ImGuiTableColumnFlags Flags; + float InitWidthOrWeight; + ImGuiID UserData; + + // Reconcile data + 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. +}; + // Transient cell data stored per row. // sizeof() ~ 6 bytes struct ImGuiTableCellData @@ -3323,6 +3345,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). @@ -3351,7 +3374,7 @@ struct IMGUI_API ImGuiTable // - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. // - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. // FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs. -// sizeof() ~ 136 bytes. +// sizeof() ~ 176 bytes. struct IMGUI_API ImGuiTableTempData { ImGuiID WindowID; // Shortcut to g.Tables[TableIndex]->OuterWindow->ID. @@ -3360,6 +3383,11 @@ struct IMGUI_API ImGuiTableTempData float AngledHeadersExtraWidth; // Used in EndTable() ImVector AngledHeadersRequests; // Used in TableAngledHeadersRow() // FIXME: Single instance is enough? + // Topology change + ImVector ReconcileColumnsRequests; // Used in TableSetupColumn(), TableUpdateLayout(). Cleared every frame. + void* OldColumnsRawData; // Used in BeginTable() -> TableUpdateLayout() when resizing. + ImSpan OldColumnsData; + ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() ImDrawListSplitter DrawSplitter; @@ -3386,6 +3414,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() { @@ -3396,6 +3425,7 @@ struct ImGuiTableColumnSettings SortDirection = ImGuiSortDirection_None; IsEnabled = -1; IsStretch = 0; + IsLoaded = false; } }; @@ -3435,6 +3465,7 @@ namespace ImGui IMGUI_API void TableBeginInitMemory(ImGuiTable* table, int columns_count); IMGUI_API void TableApplyQueuedRequests(ImGuiTable* table); IMGUI_API void TableSetupDrawChannels(ImGuiTable* table); + IMGUI_API void TableReconcileColumns(ImGuiTable* table); IMGUI_API void TableUpdateLayout(ImGuiTable* table); IMGUI_API void TableUpdateBorders(ImGuiTable* table); IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); @@ -3448,6 +3479,7 @@ namespace ImGui IMGUI_API void TableFixDisplayOrder(ImGuiTable* table); IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); IMGUI_API void TableSortSpecsBuild(ImGuiTable* table); + IMGUI_API void TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags init_mask); IMGUI_API ImGuiSortDirection TableGetColumnNextSortDirection(ImGuiTableColumn* column); IMGUI_API void TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column); IMGUI_API float TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column); @@ -3471,6 +3503,7 @@ namespace ImGui // Tables: Settings IMGUI_API void TableLoadSettings(ImGuiTable* table); IMGUI_API void TableLoadSettingsForColumns(ImGuiTable* table); + 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 50bb3dba9..128cd0136 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -45,7 +45,6 @@ Index of this file: // - TableSetupScrollFreeze() user submit scroll freeze information (optional) //----------------------------------------------------------------------------- // - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow(). -// | TableLoadSettingsForColumns() - on settings load // | TableApplyQueuedRequests() - apply queued resizing/reordering/hiding requests // | - TableSetColumnWidth() - apply resizing width (for mouse resize, often requested by previous frame) // | - TableUpdateColumnsWeightFromWidth()- recompute columns weights (of stretch columns) from their respective width @@ -364,6 +363,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG g.TablesTempData.resize(g.TablesTempDataStacked, ImGuiTableTempData()); ImGuiTableTempData* temp_data = table->TempData = &g.TablesTempData[g.TablesTempDataStacked - 1]; temp_data->TableIndex = table_idx; + temp_data->ReconcileColumnsRequests.resize(0); // FIXME: Use shrink(0) everywhere? table->DrawSplitter = &table->TempData->DrawSplitter; table->DrawSplitter->Clear(); @@ -375,6 +375,7 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG const int previous_frame_active = table->LastFrameActive; const int instance_no = (previous_frame_active != g.FrameCount) ? 0 : table->InstanceCurrent + 1; const ImGuiTableFlags previous_flags = table->Flags; + const bool is_new_table = (previous_frame_active == -1); table->ID = id; table->Flags = flags; table->LastFrameActive = g.FrameCount; @@ -564,30 +565,36 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->MemoryCompacted = false; // Setup memory buffer (clear data if columns count changed) - ImGuiTableColumn* old_columns_to_preserve = NULL; - void* old_columns_raw_data = NULL; const int old_columns_count = table->Columns.size(); if (old_columns_count != 0 && old_columns_count != columns_count) { - // Attempt to preserve width and other settings on column count/specs change (#4046) + // Attempt to preserve width and other settings on column count/specs change (#4046, #9108) IMGUI_DEBUG_LOG_TABLE("[table] Table 0x%08X column count %d -> %d, recreating storage.\n", table->ID, old_columns_count, columns_count); - old_columns_to_preserve = table->Columns.Data; - old_columns_raw_data = table->RawData; // Free at end of function + IM_ASSERT(temp_data->OldColumnsRawData == NULL); + temp_data->OldColumnsRawData = table->RawData; // Freed during layout + temp_data->OldColumnsData = table->Columns; + for (ImGuiTableColumn& src_column : table->TempData->OldColumnsData) + src_column.IsNeedReconcileSrc = true; table->RawData = NULL; } if (table->RawData == NULL) { TableBeginInitMemory(table, columns_count); - table->IsInitializing = table->IsSettingsRequestLoad = true; + table->IsInitializing = true; } if (table->IsResetAllRequest) TableResetSettings(table); if (table->IsInitializing) { // Initialize - table->SettingsOffset = -1; + if (is_new_table) + { + table->SettingsOffset = -1; + table->IsSettingsRequestLoad = true; + } 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; @@ -596,9 +603,9 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG for (int n = 0; n < columns_count; n++) { ImGuiTableColumn* column = &table->Columns[n]; - if (old_columns_to_preserve && n < old_columns_count) + if (temp_data->OldColumnsData.Data && n < temp_data->OldColumnsData.size()) { - *column = old_columns_to_preserve[n]; + *column = temp_data->OldColumnsData[n]; } else { @@ -612,8 +619,6 @@ bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImG table->DisplayOrderToIndex[n] = column->DisplayOrder; } } - if (old_columns_raw_data) - IM_FREE(old_columns_raw_data); // Load settings if (table->IsSettingsRequestLoad) @@ -843,7 +848,37 @@ static void TableSetupColumnFlags(ImGuiTable* table, ImGuiTableColumn* column, I void ImGui::TableUpdateLayout(ImGuiTable* table) { ImGuiContext& g = *GImGui; + ImGuiTableTempData* temp_data = table->TempData; IM_ASSERT(table->IsLayoutLocked == false); + const int columns_count = table->ColumnsCount; + + // Reconcile moved columns + if (temp_data->ReconcileColumnsRequests.Size > 0) + TableReconcileColumns(table); + if (temp_data->OldColumnsRawData) + { + IM_FREE(temp_data->OldColumnsRawData); + temp_data->OldColumnsRawData = NULL; + temp_data->OldColumnsData.clear(); + } + + // Apply columns settings + if (table->IsSettingsRequestLoad) + TableLoadSettingsForColumns(table); + if (table->IsInitializing || table->IsSettingsRequestLoad) + { + for (ImGuiTableColumn& column : table->Columns) + { + ImGuiTableFlags init_flags; + if (table->IsSettingsRequestLoad) + init_flags = column.IsLoadedSettings ? ~table->SettingsLoadedFlags : ~0; + else + init_flags = column.IsJustCreated ? ~0 : 0; + TableInitColumnDefaults(table, &column, init_flags); + } + TableFixDisplayOrder(table); // Call even for non _Reorderable table as we loaded .ini data. + table->IsSettingsRequestLoad = false; + } // Apply queued resizing/reordering/hiding requests TableApplyQueuedRequests(table); @@ -854,7 +889,6 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) // FIXME-DPI: Provide consistent standards for reference size. Perhaps using g.CurrentDpiScale would be more self explanatory. // This is will lead us to non-rounded WidthRequest in columns, which should work but is a poorly tested path. const float new_ref_scale_unit = g.FontSize; // g.Font->GetCharAdvance('A') ? - const int columns_count = table->ColumnsCount; if (table->RefScale != 0.0f && table->RefScale != new_ref_scale_unit) { const float scale_factor = new_ref_scale_unit / table->RefScale; @@ -893,7 +927,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) { TableSetupColumnFlags(table, column, ImGuiTableColumnFlags_None); column->NameOffset = -1; - column->UserData = 0; + column->ID = column->UserData = 0; column->InitStretchWeightOrWidth = -1.0f; } @@ -906,6 +940,7 @@ void ImGui::TableUpdateLayout(ImGuiTable* table) table->IsSettingsDirty = true; } column->IsEnabled = column->IsUserEnabled && (column->Flags & ImGuiTableColumnFlags_Disabled) == 0; + column->IsJustCreated = false; if (column->IsEnabled != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) ? 0 : 1)) table->IsDefaultVisibility = false; @@ -1645,9 +1680,9 @@ void ImGui::EndTable() NavUpdateCurrentWindowIsScrollPushableX(); } -// Called in TableSetupColumn() when initializing and in TableLoadSettings() for defaults before applying stored settings. +// Called in TableUpdateLayout() when initializing/after loading settings. // 'init_mask' specify which fields to initialize. -static void TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags init_mask) +void ImGui::TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags init_mask) { ImGuiTableColumnFlags flags = column->Flags; if (init_mask & ImGuiTableFlags_Resizable) @@ -1659,7 +1694,7 @@ static void TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, column->AutoFitQueue = 0x00; } if (init_mask & ImGuiTableFlags_Reorderable) - column->DisplayOrder = (ImGuiTableColumnIdx)table->Columns.index_from_ptr(column); + column->DisplayOrder = (ImGuiTableColumnIdx)((table->Flags & ImGuiTableFlags_Reorderable) ? -1 : table->Columns.index_from_ptr(column)); if (init_mask & ImGuiTableFlags_Hideable) column->IsUserEnabled = column->IsUserEnabledNextFrame = (flags & ImGuiTableColumnFlags_DefaultHide) ? 0 : 1; if (init_mask & ImGuiTableFlags_Sortable) @@ -1696,20 +1731,10 @@ static void TableSetupColumnApply(ImGuiTable* table, int idx, ImGuiID id, ImS16 column->ID = id; column->UserData = user_data; column->NameOffset = name_offset; - flags = column->Flags; - - // Initialize defaults column->InitStretchWeightOrWidth = init_width_or_weight; - if (table->IsInitializing) - { - ImGuiTableFlags init_flags = ~table->SettingsLoadedFlags; - if (column->WidthRequest < 0.0f && column->StretchWeight < 0.0f) - init_flags |= ImGuiTableFlags_Resizable; - TableInitColumnDefaults(table, column, init_flags); - } } -void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_data_for_sort_specs) +void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_data) { ImGuiContext& g = *GImGui; ImGuiTable* table = g.CurrentTable; @@ -1726,10 +1751,104 @@ void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, flo name_offset = (ImS16)table->ColumnsNames.size(); table->ColumnsNames.append(label, label + ImStrlen(label) + 1); } - const ImGuiID column_id = (label != NULL && label[0] != 0) ? ImHashStr(label) : 0; - TableSetupColumnApply(table, table->DeclColumnsCount, column_id, name_offset, flags, init_width_or_weight, user_data_for_sort_specs); - table->DeclColumnsCount++; + + // When ID changed or a column moved: defer the request until layout where we will process full reconcile. + const int column_idx = table->DeclColumnsCount++; + ImGuiTableColumn* column = &table->Columns[column_idx]; + + // If topology change goes into reconcile mode + if (table->IsReconcileMode == false && column->ID != column_id) + { + table->IsReconcileMode = true; + table->TempData->ReconcileColumnsRequests.reserve(table->ColumnsCount - column_idx); + } + + // Fast/common path + if (table->IsReconcileMode == false) + { + TableSetupColumnApply(table, column_idx, column_id, name_offset, flags, init_width_or_weight, user_data); + column->IsNeedReconcileSrc = column->IsNeedReconcileDst = false; + if (column_idx < table->TempData->OldColumnsData.size()) + table->TempData->OldColumnsData[column_idx].IsNeedReconcileSrc = false; + 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; + column->NameOffset = name_offset; // Allow TableGetColumnName() to work before layout + column->IsNeedReconcileSrc = column->IsNeedReconcileDst = true; +} + +// 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 columns for table 0x%08X\n", table->ID); + + ImSpan& dst_columns = table->Columns; + ImSpan& src_columns = table->TempData->OldColumnsData.empty() ? table->Columns : table->TempData->OldColumnsData; + + // 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.IsNeedReconcileSrc) + { + ImGuiTableColumn& dst_column = dst_columns[reconcile_data.ColumnNewIdx]; + IM_ASSERT(src_column.IsNeedReconcileSrc && dst_column.IsNeedReconcileDst); + src_column.IsNeedReconcileSrc = dst_column.IsNeedReconcileDst = false; + reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)src_columns.index_from_ptr(&src_column); + reconcile_data.ColumnOldData = src_column; + 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.IsNeedReconcileSrc) + continue; + while (dst_idx < reconcile_requests.Size && reconcile_requests[dst_idx].ColumnOldIdx != (ImGuiTableColumnIdx)-1) + dst_idx++; + if (dst_idx == reconcile_requests.Size) + break; + ImGuiTableReconcileColumnData& reconcile_data = reconcile_requests[dst_idx]; + ImGuiTableColumn& dst_column = dst_columns[reconcile_data.ColumnNewIdx]; + IM_ASSERT(src_column.IsNeedReconcileSrc && dst_column.IsNeedReconcileDst); + dst_column.IsNeedReconcileSrc = dst_column.IsNeedReconcileDst = false; + reconcile_data.ColumnOldIdx = (ImGuiTableColumnIdx)src_columns.index_from_ptr(&src_column); + reconcile_data.ColumnOldData = src_column; + } + + // 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) + { + table->Columns[reconcile_data.ColumnNewIdx] = reconcile_data.ColumnOldData; // When old column was not found clear anyway with default-constructed data (which will set IsJustCreated=true) + 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? + table->IsReconcileMode = false; + reconcile_requests.resize(0); // GC-ed once in NewFrame() } // [Public] @@ -3721,6 +3840,7 @@ void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags // - TableResetSettings() // - TableSaveSettings() [Internal] // - TableLoadSettings() [Internal] +// - TableLoadSettingsForColumns() [Internal] // - TableSettingsHandler_ClearAll() [Internal] // - TableSettingsHandler_ApplyAll() [Internal] // - TableSettingsHandler_ReadOpen() [Internal] @@ -3755,6 +3875,8 @@ static size_t TableSettingsCalcChunkSize(int columns_count) ImGuiTableSettings* ImGui::TableSettingsCreate(ImGuiID id, int columns_count) { ImGuiContext& g = *GImGui; + //ImGuiTableSettings* old_settings = TableSettingsFindByID(id); // Comment out sanity check to avoid unnecessary lookups. + //IM_ASSERT(old_settings == NULL || old_settings->ColumnsCountMax < columns_count); ImGuiTableSettings* settings = g.SettingsTables.alloc_chunk(TableSettingsCalcChunkSize(columns_count)); TableSettingsInit(settings, id, columns_count, columns_count); return settings; @@ -3774,16 +3896,12 @@ ImGuiTableSettings* ImGui::TableSettingsFindByID(ImGuiID id) // Get settings for a given table, NULL if none ImGuiTableSettings* ImGui::TableGetBoundSettings(ImGuiTable* table) { - if (table->SettingsOffset != -1) - { - ImGuiContext& g = *GImGui; - ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); - IM_ASSERT(settings->ID == table->ID); - if (settings->ColumnsCountMax >= table->ColumnsCount) - return settings; // OK - settings->ID = 0; // Invalidate storage, we won't fit because of a count change - } - return NULL; + if (table->SettingsOffset == -1) + return NULL; + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + IM_ASSERT(settings->ID == table->ID); + return settings; } // Restore initial state of table (with or without saved settings) @@ -3804,6 +3922,11 @@ void ImGui::TableSaveSettings(ImGuiTable* table) // Bind or create settings data ImGuiContext& g = *GImGui; ImGuiTableSettings* settings = TableGetBoundSettings(table); + if (settings != NULL && table->ColumnsCount > settings->ColumnsCountMax) + { + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + settings = NULL; + } if (settings == NULL) { settings = TableSettingsCreate(table->ID, table->ColumnsCount); @@ -3854,9 +3977,11 @@ void ImGui::TableSaveSettings(ImGuiTable* table) void ImGui::TableLoadSettings(ImGuiTable* table) { ImGuiContext& g = *GImGui; - table->IsSettingsRequestLoad = false; if (table->Flags & ImGuiTableFlags_NoSavedSettings) + { + table->IsSettingsRequestLoad = false; // Done return; + } // Bind settings ImGuiTableSettings* settings; @@ -3876,44 +4001,83 @@ void ImGui::TableLoadSettings(ImGuiTable* table) table->SettingsLoadedFlags = settings->SaveFlags; table->RefScale = settings->RefScale; + // TableUpdateLayout() will then call TableLoadSettingsForColumns() to apply the data. +} - // Initialize default columns settings - for (int column_n = 0; column_n < table->ColumnsCount; column_n++) - { - ImGuiTableColumn* column = &table->Columns[column_n]; - TableInitColumnDefaults(table, column, ~0); - column->AutoFitQueue = 0x00; - } +// NB: This was written to be similar to the logic in TableReconcileColumns(). +void ImGui::TableLoadSettingsForColumns(ImGuiTable* table) +{ + for (ImGuiTableColumn& column : table->Columns) + column.IsLoadedSettings = false; + ImGuiTableSettings* settings = TableGetBoundSettings(table); + if (settings == NULL) + return; + + table->SettingsLoadedFlags |= ImGuiTableFlags_Reorderable; // We handle above in code above. // Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); - for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + + // Fast path + int matches = 0; + for (int n = 0; n < table->ColumnsCount; n++) { - int column_n = column_settings->Index; - if (column_n < 0 || column_n >= table->ColumnsCount) - continue; - - ImGuiTableColumn* column = &table->Columns[column_n]; - if (settings->SaveFlags & ImGuiTableFlags_Resizable) - { - if (column_settings->IsStretch) - column->StretchWeight = column_settings->WidthOrWeight; - else - column->WidthRequest = column_settings->WidthOrWeight; - } - if (settings->SaveFlags & ImGuiTableFlags_Reorderable) - column->DisplayOrder = column_settings->DisplayOrder; - if ((settings->SaveFlags & ImGuiTableFlags_Hideable) && column_settings->IsEnabled != -1) - column->IsUserEnabled = column->IsUserEnabledNextFrame = column_settings->IsEnabled == 1; - column->SortOrder = column_settings->SortOrder; - column->SortDirection = column_settings->SortDirection; + if (n >= settings->ColumnsCount || column_settings[n].ID != table->Columns[n].ID) + break; + TableLoadSettingsForColumn(&table->Columns[n], &column_settings[n], settings->SaveFlags); + matches++; } + if (matches == settings->ColumnsCount) + return; + const int settings_start_n = matches; // Small optimization + for (int n = settings_start_n; n < settings->ColumnsCount; n++) + column_settings[n].IsLoaded = false; - // Fix display order and build index - if (settings->SaveFlags & ImGuiTableFlags_Reorderable) - TableFixDisplayOrder(table); - for (int column_n = 0; column_n < table->ColumnsCount; column_n++) - table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; + // Find matches for named columns + for (ImGuiTableColumn& column : table->Columns) + if (column.ID != 0 && !column.IsLoadedSettings) + for (int n = settings_start_n; n < settings->ColumnsCount; n++) + if (column_settings[n].ID == column.ID && !column_settings[n].IsLoaded) + { + TableLoadSettingsForColumn(&column, &column_settings[n], settings->SaveFlags); + matches++; + break; + } + + // Remaining entries are matched sequentially + int dst_idx = 0; + for (int n = settings_start_n; n < settings->ColumnsCount; n++) + if (!column_settings[n].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[n], settings->SaveFlags); + dst_idx++; + } +} + +void ImGui::TableLoadSettingsForColumn(ImGuiTableColumn* column, ImGuiTableColumnSettings* column_settings, ImGuiTableFlags load_flags) +{ + column->IsLoadedSettings = true; + column_settings->IsLoaded = true; + if (load_flags & ImGuiTableFlags_Resizable) + { + if (column_settings->IsStretch) + column->StretchWeight = column_settings->WidthOrWeight; + else + column->WidthRequest = column_settings->WidthOrWeight; + column->AutoFitQueue = 0x00; + } + if (load_flags & ImGuiTableFlags_Reorderable) + column->DisplayOrder = column_settings->DisplayOrder; + else + column->DisplayOrder = column_settings->Index; // Because default depends on previous Index, we need to set that up and cannot rely on TableInitColumnDefaults() + if ((load_flags & ImGuiTableFlags_Hideable) && column_settings->IsEnabled != -1) + column->IsUserEnabled = column->IsUserEnabledNextFrame = (column_settings->IsEnabled == 1); + column->SortOrder = column_settings->SortOrder; + column->SortDirection = column_settings->SortDirection; } struct ImGuiTableFixDisplayOrderColumnData @@ -3928,7 +4092,8 @@ static int IMGUI_CDECL TableFixDisplayOrderComparer(const void* lhs, const void* const ImGuiTable* table = ((const ImGuiTableFixDisplayOrderColumnData*)lhs)->Table; const ImGuiTableColumnIdx lhs_idx = ((const ImGuiTableFixDisplayOrderColumnData*)lhs)->Idx; const ImGuiTableColumnIdx rhs_idx = ((const ImGuiTableFixDisplayOrderColumnData*)rhs)->Idx; - const int order_delta = (table->Columns[lhs_idx].DisplayOrder - table->Columns[rhs_idx].DisplayOrder); + // Assume ImGuiTableColumnIdx == signed short, to turn -1 into a large value so that it always sort after. + const int order_delta = ((unsigned short)table->Columns[lhs_idx].DisplayOrder - (unsigned short)table->Columns[rhs_idx].DisplayOrder); return (order_delta > 0) ? +1 : (order_delta < 0) ? -1 : (lhs_idx > rhs_idx) ? +1 : -1; } @@ -3946,6 +4111,8 @@ void ImGui::TableFixDisplayOrder(ImGuiTable* table) ImQsort(fdo_columns, (size_t)table->ColumnsCount, sizeof(ImGuiTableFixDisplayOrderColumnData), TableFixDisplayOrderComparer); for (int n = 0; n < table->ColumnsCount; n++) table->Columns[fdo_columns[n].Idx].DisplayOrder = (ImGuiTableColumnIdx)n; + for (int n = 0; n < table->ColumnsCount; n++) + table->DisplayOrderToIndex[table->Columns[n].DisplayOrder] = (ImGuiTableColumnIdx)n; } static void TableSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) @@ -4111,7 +4278,7 @@ void ImGui::TableGcCompactTransientBuffers(ImGuiTableTempData* temp_data) temp_data->LastTimeActive = -1.0f; } -// Compact and remove unused settings data (currently only used by TestEngine) +// Compact and remove unused or resize settings data (only TestEngine mark unused, but resizing table down can lead to compaction) void ImGui::TableGcCompactSettings() { ImGuiContext& g = *GImGui; @@ -4127,6 +4294,9 @@ void ImGui::TableGcCompactSettings() if (settings->ID != 0) memcpy(new_chunk_stream.alloc_chunk(TableSettingsCalcChunkSize(settings->ColumnsCount)), settings, TableSettingsCalcChunkSize(settings->ColumnsCount)); g.SettingsTables.swap(new_chunk_stream); + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + if (ImGuiTable* table = g.Tables.TryGetMapData(table_n)) + table->SettingsOffset = -1; } @@ -4255,7 +4425,7 @@ void ImGui::DebugNodeTableSettings(ImGuiTableSettings* settings, ImGuiTable* tab #else // #ifndef IMGUI_DISABLE_DEBUG_TOOLS void ImGui::DebugNodeTable(ImGuiTable*) {} -void ImGui::DebugNodeTableSettings(ImGuiTableSettings*, ImGuiTable* table) {} +void ImGui::DebugNodeTableSettings(ImGuiTableSettings*, ImGuiTable*) {} #endif diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index fb4a637c4..d88830349 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1584,7 +1584,7 @@ bool ImGui::TextLinkOpenURL(const char* label, const char* url) bool pressed = TextLink(label); if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL) g.PlatformIO.Platform_OpenInShellFn(&g, url); - SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label + SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when it is the same as the label. if (BeginPopupContextItem()) { if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))