diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 876eeaf71..bfccaabca 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -43,6 +43,25 @@ Breaking changes: Other changes: +- Error Handling: Enabled/improved error recovery systems. (#1651, #5654) + - Error recovery is not perfect nor guaranteed! It is a feature to ease development. + - Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT(). + - You not are not supposed to rely on it in the course of a normal application run. + - Possible usage: facilitate recovery from errors triggered from a scripting language or + after specific exceptions handlers. Surface errors to programmers in less agressive ways. + - Always ensure that on programmers seats you have at minimum Asserts or Tooltips enabled + when making direct imgui API calls! Otherwise it would severely hinder your ability to + catch and correct mistakes! + - Added io.ConfigErrorRecovery to enable error recovery support. + - Added io.ConfigErrorRecoveryEnableAssert to assert on recoverable errors. + - Added io.ConfigErrorRecoveryEnableDebugLog to output to debug log on recoverable errors. + - Added io.ConfigErrorRecoveryEnableTooltip to enable displaying an error tooltip on recoverable errors. + The tooltip include a way to enable asserts if they were disabled. + - All options are enabled by default. + - Read https://github.com/ocornut/imgui/wiki/Error-Handling for a bit more details. +- Windows: BeginChild(): made it possible to call SetNextWindowSize() on a child window + using ImGuiChildFlags_ResizeX,ImGuiChildFlags_ResizeY in order to override its current + size. (#1710, #8020) - Scrollbar: Shift+Click scroll to clicked location (pre-1.90.8 default). (#8002, #7328) - Scrollbar: added io.ConfigScrollbarScrollByPage setting (default to true). (#8002, #7328) Set io.ConfigScrollbarScrollByPage=false to enforce always scrolling to clicked location. diff --git a/imgui.cpp b/imgui.cpp index fe5eb82e1..4969829f3 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1440,6 +1440,11 @@ ImGuiIO::ImGuiIO() ConfigDebugBeginReturnValueOnce = false; ConfigDebugBeginReturnValueLoop = false; + ConfigErrorRecovery = true; + ConfigErrorRecoveryEnableAssert = true; + ConfigErrorRecoveryEnableDebugLog = true; + ConfigErrorRecoveryEnableTooltip = true; + // Inputs Behaviors MouseDoubleClickTime = 0.30f; MouseDoubleClickMaxDist = 6.0f; @@ -3333,7 +3338,7 @@ void ImGui::PopStyleColor(int count) ImGuiContext& g = *GImGui; if (g.ColorStack.Size < count) { - IM_ASSERT_USER_ERROR(g.ColorStack.Size > count, "Calling PopStyleColor() too many times!"); + IM_ASSERT_USER_ERROR(0, "Calling PopStyleColor() too many times!"); count = g.ColorStack.Size; } while (count > 0) @@ -3456,7 +3461,7 @@ void ImGui::PopStyleVar(int count) ImGuiContext& g = *GImGui; if (g.StyleVarStack.Size < count) { - IM_ASSERT_USER_ERROR(g.StyleVarStack.Size > count, "Calling PopStyleVar() too many times!"); + IM_ASSERT_USER_ERROR(0, "Calling PopStyleVar() too many times!"); count = g.StyleVarStack.Size; } while (count > 0) @@ -4059,8 +4064,16 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) LogDepthRef = 0; LogDepthToExpand = LogDepthToExpandDefault = 2; - DebugLogFlags = ImGuiDebugLogFlags_OutputToTTY; + ErrorCallback = NULL; + ErrorCallbackUserData = NULL; + ErrorFirst = true; + ErrorCountCurrentFrame = 0; + StackSizesInBeginForCurrentWindow = NULL; + + DebugDrawIdConflictsCount = 0; + DebugLogFlags = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_OutputToTTY; DebugLocateId = 0; + DebugLogSkippedErrors = 0; DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; DebugLogAutoDisableFrames = 0; DebugLocateFrames = 0; @@ -4311,6 +4324,7 @@ static void SetCurrentWindow(ImGuiWindow* window) { ImGuiContext& g = *GImGui; g.CurrentWindow = window; + g.StackSizesInBeginForCurrentWindow = g.CurrentWindow ? &g.CurrentWindowStack.back().StackSizesInBegin : NULL; g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL; if (window) { @@ -5236,7 +5250,8 @@ void ImGui::NewFrame() KeepAliveID(g.DragDropPayload.SourceId); // [DEBUG] - g.DebugDrawIdConflicts = 0; + if (!g.IO.ConfigDebugHighlightIdConflicts || !g.IO.KeyCtrl) // Count is locked while holding CTRL + g.DebugDrawIdConflicts = 0; if (g.IO.ConfigDebugHighlightIdConflicts && g.HoveredIdPreviousFrameItemCount > 1) g.DebugDrawIdConflicts = g.HoveredIdPreviousFrame; @@ -5342,8 +5357,25 @@ void ImGui::NewFrame() // (needs to be before UpdateMouseMovingWindowNewFrame so the window is already offset and following the mouse on the detaching frame) DockContextNewFrameUpdateUndocking(&g); + // Mark all windows as not visible and compact unused memory. + IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); + const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; + for (ImGuiWindow* window : g.Windows) + { + window->WasActive = window->Active; + window->Active = false; + window->WriteAccessed = false; + window->BeginCountPreviousFrame = window->BeginCount; + window->BeginCount = 0; + + // Garbage collect transient buffers of recently unused windows + if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) + GcCompactTransientWindowBuffers(window); + } + // Find hovered window // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) + // (currently needs to be done after the WasActive=Active loop and FindHoveredWindowEx uses ->Active) UpdateHoveredWindowAndCaptureFlags(); // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) @@ -5365,22 +5397,6 @@ void ImGui::NewFrame() // Mouse wheel scrolling, scale UpdateMouseWheel(); - // Mark all windows as not visible and compact unused memory. - IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); - const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; - for (ImGuiWindow* window : g.Windows) - { - window->WasActive = window->Active; - window->Active = false; - window->WriteAccessed = false; - window->BeginCountPreviousFrame = window->BeginCount; - window->BeginCount = 0; - - // Garbage collect transient buffers of recently unused windows - if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) - GcCompactTransientWindowBuffers(window); - } - // Garbage collect transient buffers of recently unused tables for (int i = 0; i < g.TablesLastTimeActive.Size; i++) if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) @@ -5434,6 +5450,10 @@ void ImGui::NewFrame() Begin("Debug##Default"); IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true); + // Store stack sizes + g.ErrorCountCurrentFrame = 0; + ErrorRecoveryStoreState(&g.StackSizesInNewFrame); + // [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack, // allowing to validate correct Begin/End behavior in user code. #ifndef IMGUI_DISABLE_DEBUG_TOOLS @@ -5700,32 +5720,13 @@ void ImGui::EndFrame() return; IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?"); -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - if (g.DebugDrawIdConflicts != 0) - { - PushStyleColor(ImGuiCol_PopupBg, ImLerp(g.Style.Colors[ImGuiCol_PopupBg], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.10f)); - if (g.DebugItemPickerActive == false && BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) - { - SeparatorText("MESSAGE FROM DEAR IMGUI"); - Text("Programmer error: %d visible items with conflicting ID!", g.HoveredIdPreviousFrameItemCount); - BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!"); - BulletText("Empty label e.g. Button(\"\") == same ID as parent widget/node. Use Button(\"##xx\") instead!"); - BulletText("Press F1 to open \"FAQ -> About the ID Stack System\" and read details."); - BulletText("Press CTRL+P to activate Item Picker and debug-break in item call-stack."); - BulletText("Set io.ConfigDebugDetectIdConflicts=false to disable this warning in non-programmers builds."); - EndTooltip(); - } - PopStyleColor(); - if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_P, ImGuiInputFlags_RouteGlobal)) - DebugStartItemPicker(); - if (Shortcut(ImGuiKey_F1, ImGuiInputFlags_RouteGlobal) && g.PlatformIO.Platform_OpenInShellFn != NULL) - g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage"); - } -#endif - CallContextHooks(&g, ImGuiContextHookType_EndFramePre); + // [EXPERIMENTAL] Recover from errors + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverState(&g.StackSizesInNewFrame); ErrorCheckEndFrameSanityChecks(); + ErrorCheckEndFrameFinalizeErrorTooltip(); // Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) ImGuiPlatformImeData* ime_data = &g.PlatformImeData; @@ -5936,7 +5937,7 @@ void ImGui::FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_vi { ImGuiWindow* window = g.Windows[i]; IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. - if (!window->Active || window->Hidden) + if (!window->WasActive || window->Hidden) continue; if (window->Flags & ImGuiWindowFlags_NoMouseInputs) continue; @@ -6205,18 +6206,35 @@ bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, I window_flags |= ImGuiWindowFlags_NoMove; } - // Forward child flags - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasChildFlags; - g.NextWindowData.ChildFlags = child_flags; - // Forward size // Important: Begin() has special processing to switch condition to ImGuiCond_FirstUseEver for a given axis when ImGuiChildFlags_ResizeXXX is set. // (the alternative would to store conditional flags per axis, which is possible but more code) const ImVec2 size_avail = GetContentRegionAvail(); const ImVec2 size_default((child_flags & ImGuiChildFlags_AutoResizeX) ? 0.0f : size_avail.x, (child_flags & ImGuiChildFlags_AutoResizeY) ? 0.0f : size_avail.y); - const ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); + ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); + + // A SetNextWindowSize() call always has priority (#8020) + // (since the code in Begin() never supported SizeVal==0.0f aka auto-resize via SetNextWindowSize() call, we don't support it here for now) + // FIXME: We only support ImGuiCond_Always in this path. Supporting other paths would requires to obtain window pointer. + if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) != 0 && (g.NextWindowData.SizeCond & ImGuiCond_Always) != 0) + { + if (g.NextWindowData.SizeVal.x > 0.0f) + { + size.x = g.NextWindowData.SizeVal.x; + child_flags &= ~ImGuiChildFlags_ResizeX; + } + if (g.NextWindowData.SizeVal.y > 0.0f) + { + size.y = g.NextWindowData.SizeVal.y; + child_flags &= ~ImGuiChildFlags_ResizeY; + } + } SetNextWindowSize(size); + // Forward child flags + g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasChildFlags; + g.NextWindowData.ChildFlags = child_flags; + // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. // FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround. // e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it. @@ -7372,12 +7390,13 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Add to stack g.CurrentWindow = window; - ImGuiWindowStackData window_stack_data; + g.CurrentWindowStack.resize(g.CurrentWindowStack.Size + 1); + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); window_stack_data.Window = window; window_stack_data.ParentLastItemDataBackup = g.LastItemData; - window_stack_data.StackSizesOnBegin.SetToContextState(&g); window_stack_data.DisabledOverrideReenable = (flags & ImGuiWindowFlags_Tooltip) && (g.CurrentItemFlags & ImGuiItemFlags_Disabled); - g.CurrentWindowStack.push_back(window_stack_data); + ErrorRecoveryStoreState(&window_stack_data.StackSizesInBegin); + g.StackSizesInBeginForCurrentWindow = &window_stack_data.StackSizesInBegin; if (flags & ImGuiWindowFlags_ChildMenu) g.BeginMenuDepth++; @@ -8251,7 +8270,11 @@ void ImGui::End() g.BeginMenuDepth--; if (window->Flags & ImGuiWindowFlags_Popup) g.BeginPopupStack.pop_back(); - window_stack_data.StackSizesOnBegin.CompareWithContextState(&g); + + // Error handling, state recovery + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverWindowState(&window_stack_data.StackSizesInBegin); + g.CurrentWindowStack.pop_back(); SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); if (g.CurrentWindow) @@ -8482,7 +8505,11 @@ void ImGui::PushFont(ImFont* font) void ImGui::PopFont() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.FontStack.Size > 0); + if (g.FontStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!"); + return; + } g.FontStack.pop_back(); ImFont* font = g.FontStack.Size == 0 ? GetDefaultFont() : g.FontStack.back(); SetCurrentFont(font); @@ -8505,7 +8532,11 @@ void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) void ImGui::PopItemFlag() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.ItemFlagsStack.Size > 1); // Too many calls to PopItemFlag() - we always leave a 0 at the bottom of the stack. + if (g.ItemFlagsStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemFlag() too many times!"); + return; + } g.ItemFlagsStack.pop_back(); g.CurrentItemFlags = g.ItemFlagsStack.back(); } @@ -8535,7 +8566,11 @@ void ImGui::BeginDisabled(bool disabled) void ImGui::EndDisabled() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.DisabledStackSize > 0); + if (g.DisabledStackSize <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling EndDisabled() too many times!"); + return; + } g.DisabledStackSize--; bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; //PopItemFlag(); @@ -8570,14 +8605,21 @@ void ImGui::EndDisabledOverrideReenable() void ImGui::PushTextWrapPos(float wrap_pos_x) { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; window->DC.TextWrapPosStack.push_back(window->DC.TextWrapPos); window->DC.TextWrapPos = wrap_pos_x; } void ImGui::PopTextWrapPos() { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.TextWrapPosStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopTextWrapPos() too many times!"); + return; + } window->DC.TextWrapPos = window->DC.TextWrapPosStack.back(); window->DC.TextWrapPosStack.pop_back(); } @@ -9064,9 +9106,9 @@ void ImGui::PushFocusScope(ImGuiID id) void ImGui::PopFocusScope() { ImGuiContext& g = *GImGui; - if (g.FocusScopeStack.Size == 0) + if (g.FocusScopeStack.Size <= g.StackSizesInBeginForCurrentWindow->SizeOfFocusScopeStack) { - IM_ASSERT_USER_ERROR(g.FocusScopeStack.Size > 0, "Calling PopFocusScope() too many times!"); + IM_ASSERT_USER_ERROR(0, "Calling PopFocusScope() too many times!"); return; } g.FocusScopeStack.pop_back(); @@ -9337,7 +9379,11 @@ ImGuiID ImGui::GetIDWithSeed(int n, ImGuiID seed) void ImGui::PopID() { ImGuiWindow* window = GImGui->CurrentWindow; - IM_ASSERT(window->IDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + if (window->IDStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopID() too many times!"); + return; + } window->IDStack.pop_back(); } @@ -10919,9 +10965,10 @@ bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID own // - ErrorCheckUsingSetCursorPosToExtendParentBoundaries() // - ErrorCheckNewFrameSanityChecks() // - ErrorCheckEndFrameSanityChecks() -// - ErrorCheckEndFrameRecover() -// - ErrorCheckEndWindowRecover() -// - ImGuiStackSizes +// - ErrorRecoveryStoreState() +// - ErrorRecoveryTryToRecoverState() +// - ErrorRecoveryTryToRecoverWindowState() +// - ErrorLog() //----------------------------------------------------------------------------- // Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues. @@ -11020,6 +11067,10 @@ static void ImGui::ErrorCheckNewFrameSanityChecks() IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); #endif + // Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way. + if (g.IO.ConfigErrorRecovery) + IM_ASSERT(g.IO.ConfigErrorRecoveryEnableAssert || g.IO.ConfigErrorRecoveryEnableDebugLog || g.IO.ConfigErrorRecoveryEnableTooltip || g.ErrorCallback != NULL); + // Remap legacy clipboard handlers (OBSOLETED in 1.91.1, August 2024) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS if (g.IO.GetClipboardTextFn != NULL && (g.PlatformIO.Platform_GetClipboardTextFn == NULL || g.PlatformIO.Platform_GetClipboardTextFn == Platform_GetClipboardTextFn_DefaultImpl)) @@ -11070,126 +11121,113 @@ static void ImGui::ErrorCheckNewFrameSanityChecks() static void ImGui::ErrorCheckEndFrameSanityChecks() { - ImGuiContext& g = *GImGui; - // Verify that io.KeyXXX fields haven't been tampered with. Key mods should not be modified between NewFrame() and EndFrame() // One possible reason leading to this assert is that your backends update inputs _AFTER_ NewFrame(). // It is known that when some modal native windows called mid-frame takes focus away, some backends such as GLFW will // send key release events mid-frame. This would normally trigger this assertion and lead to sheared inputs. // We silently accommodate for this case by ignoring the case where all io.KeyXXX modifiers were released (aka key_mod_flags == 0), // while still correctly asserting on mid-frame key press events. + ImGuiContext& g = *GImGui; const ImGuiKeyChord key_mods = GetMergedModsFromKeys(); IM_ASSERT((key_mods == 0 || g.IO.KeyMods == key_mods) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); IM_UNUSED(key_mods); - // [EXPERIMENTAL] Recover from errors: You may call this yourself before EndFrame(). - //ErrorCheckEndFrameRecover(); - - // Report when there is a mismatch of Begin/BeginChild vs End/EndChild calls. Important: Remember that the Begin/BeginChild API requires you - // to always call End/EndChild even if Begin/BeginChild returns false! (this is unfortunately inconsistent with most other Begin* API). - if (g.CurrentWindowStack.Size != 1) - { - if (g.CurrentWindowStack.Size > 1) - { - ImGuiWindow* window = g.CurrentWindowStack.back().Window; // <-- This window was not Ended! - IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?"); - IM_UNUSED(window); - while (g.CurrentWindowStack.Size > 1) - End(); - } - else - { - IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?"); - } - } - - IM_ASSERT_USER_ERROR(g.GroupStack.Size == 0, "Missing EndGroup call!"); + IM_ASSERT(g.CurrentWindowStack.Size == 1); + IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow); } -// Default implementation of ImGuiErrorLogCallback that pipe errors to DebugLog: appears in tty + Tools->DebugLog -void ImGui::ErrorLogCallbackToDebugLog(void*, const char* fmt, ...) +// Save current stack sizes. Called e.g. by NewFrame() and by Begin() but may be called for manual recovery. +void ImGui::ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out) { -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - va_list args; - va_start(args, fmt); - DebugLogV(fmt, args); - va_end(args); -#else - IM_UNUSED(fmt); -#endif + ImGuiContext& g = *GImGui; + state_out->SizeOfWindowStack = (short)g.CurrentWindowStack.Size; + state_out->SizeOfIDStack = (short)g.CurrentWindow->IDStack.Size; + state_out->SizeOfTreeStack = (short)g.CurrentWindow->DC.TreeDepth; // NOT g.TreeNodeStack.Size which is a partial stack! + state_out->SizeOfColorStack = (short)g.ColorStack.Size; + state_out->SizeOfStyleVarStack = (short)g.StyleVarStack.Size; + state_out->SizeOfFontStack = (short)g.FontStack.Size; + state_out->SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; + state_out->SizeOfGroupStack = (short)g.GroupStack.Size; + state_out->SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; + state_out->SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; + state_out->SizeOfDisabledStack = (short)g.DisabledStackSize; } -// Experimental recovery from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. -// Must be called during or before EndFrame(). -// This is generally flawed as we are not necessarily End/Popping things in the right order. -// FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. -void ImGui::ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data) +// Chosen name "Try to recover" over e.g. "Restore" to suggest this is not a 100% guaranteed recovery. +// Called by e.g. EndFrame() but may be called for manual recovery. +// Attempt to recover full window stack. +void ImGui::ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in) { // PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations" ImGuiContext& g = *GImGui; - while (g.CurrentWindowStack.Size > 0) //-V1044 + while (g.CurrentWindowStack.Size > state_in->SizeOfWindowStack) //-V1044 { - ErrorCheckEndWindowRecover(log_callback, user_data); + // Recap: + // - Begin()/BeginChild() return false to indicate the window is collapsed or fully clipped. + // - Always call a matching End() for each Begin() call, regardless of its return value! + // - Begin/End and BeginChild/EndChild logic is KNOWN TO BE INCONSISTENT WITH ALL OTHER BEGIN/END FUNCTIONS. + // - We will fix that in a future major update. ImGuiWindow* window = g.CurrentWindow; - if (g.CurrentWindowStack.Size == 1) - { - IM_ASSERT(window->IsFallbackWindow); - break; - } if (window->Flags & ImGuiWindowFlags_ChildWindow) { - if (log_callback) log_callback(user_data, "Recovered from missing EndChild() for '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndChild()"); EndChild(); } else { - if (log_callback) log_callback(user_data, "Recovered from missing End() for '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing End()"); End(); } } + if (g.CurrentWindowStack.Size == state_in->SizeOfWindowStack) + ErrorRecoveryTryToRecoverWindowState(state_in); } -// Must be called before End()/EndChild() -void ImGui::ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data) +// Called by e.g. End() but may be called for manual recovery. +// Read '// Error Handling [BETA]' block in imgui_internal.h for details. +// Attempt to recover from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. +void ImGui::ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in) { ImGuiContext& g = *GImGui; + while (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow) { - if (log_callback) log_callback(user_data, "Recovered from missing EndTable() in '%s'\n", g.CurrentTable->OuterWindow->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); EndTable(); } ImGuiWindow* window = g.CurrentWindow; - ImGuiStackSizes* stack_sizes = &g.CurrentWindowStack.back().StackSizesOnBegin; - IM_ASSERT(window != NULL); + + // FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. while (g.CurrentTabBar != NULL && g.CurrentTabBar->Window == window) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing EndTabBar() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndTabBar()"); EndTabBar(); } while (g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Storage->Window == window) { - if (log_callback) log_callback(user_data, "Recovered from missing EndMultiSelect() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndMultiSelect()"); EndMultiSelect(); } - while (window->DC.TreeDepth > 0) + while (window->DC.TreeDepth > state_in->SizeOfTreeStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing TreePop() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing TreePop()"); TreePop(); } - while (g.GroupStack.Size > stack_sizes->SizeOfGroupStack) //-V1044 + while (g.GroupStack.Size > state_in->SizeOfGroupStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing EndGroup() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndGroup()"); EndGroup(); } - while (window->IDStack.Size > 1) + IM_ASSERT(g.GroupStack.Size == state_in->SizeOfGroupStack); + while (window->IDStack.Size > state_in->SizeOfIDStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing PopID() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing PopID()"); PopID(); } - while (g.DisabledStackSize > stack_sizes->SizeOfDisabledStack) //-V1044 + while (g.DisabledStackSize > state_in->SizeOfDisabledStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing EndDisabled() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing EndDisabled()"); if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) EndDisabled(); else @@ -11198,70 +11236,150 @@ void ImGui::ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, vo g.CurrentWindowStack.back().DisabledOverrideReenable = false; } } - while (g.ColorStack.Size > stack_sizes->SizeOfColorStack) + IM_ASSERT(g.DisabledStackSize == state_in->SizeOfDisabledStack); + while (g.ColorStack.Size > state_in->SizeOfColorStack) { - if (log_callback) log_callback(user_data, "Recovered from missing PopStyleColor() in '%s' for ImGuiCol_%s\n", window->Name, GetStyleColorName(g.ColorStack.back().Col)); + IM_ASSERT_USER_ERROR(0, "Missing PopStyleColor()"); PopStyleColor(); } - while (g.ItemFlagsStack.Size > stack_sizes->SizeOfItemFlagsStack) //-V1044 + while (g.ItemFlagsStack.Size > state_in->SizeOfItemFlagsStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing PopItemFlag() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing PopItemFlag()"); PopItemFlag(); } - while (g.StyleVarStack.Size > stack_sizes->SizeOfStyleVarStack) //-V1044 + while (g.StyleVarStack.Size > state_in->SizeOfStyleVarStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing PopStyleVar() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing PopStyleVar()"); PopStyleVar(); } - while (g.FontStack.Size > stack_sizes->SizeOfFontStack) //-V1044 + while (g.FontStack.Size > state_in->SizeOfFontStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing PopFont() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing PopFont()"); PopFont(); } - while (g.FocusScopeStack.Size > stack_sizes->SizeOfFocusScopeStack + 1) //-V1044 + while (g.FocusScopeStack.Size > state_in->SizeOfFocusScopeStack) //-V1044 { - if (log_callback) log_callback(user_data, "Recovered from missing PopFocusScope() in '%s'\n", window->Name); + IM_ASSERT_USER_ERROR(0, "Missing PopFocusScope()"); PopFocusScope(); } + //IM_ASSERT(g.FocusScopeStack.Size == state_in->SizeOfFocusScopeStack); } -// Save current stack sizes for later compare -void ImGuiStackSizes::SetToContextState(ImGuiContext* ctx) +bool ImGui::ErrorLog(const char* msg) { - ImGuiContext& g = *ctx; + ImGuiContext& g = *GImGui; + + // Output to debug log +#ifndef IMGUI_DISABLE_DEBUG_TOOLS ImGuiWindow* window = g.CurrentWindow; - SizeOfIDStack = (short)window->IDStack.Size; - SizeOfColorStack = (short)g.ColorStack.Size; - SizeOfStyleVarStack = (short)g.StyleVarStack.Size; - SizeOfFontStack = (short)g.FontStack.Size; - SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; - SizeOfGroupStack = (short)g.GroupStack.Size; - SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; - SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; - SizeOfDisabledStack = (short)g.DisabledStackSize; + + if (g.IO.ConfigErrorRecoveryEnableDebugLog) + { + if (g.ErrorFirst) + IMGUI_DEBUG_LOG_ERROR("[imgui-error] (current settings: Assert=%d, Log=%d, Tooltip=%d)\n", + g.IO.ConfigErrorRecoveryEnableAssert, g.IO.ConfigErrorRecoveryEnableDebugLog, g.IO.ConfigErrorRecoveryEnableTooltip); + IMGUI_DEBUG_LOG_ERROR("[imgui-error] In window '%s': %s\n", window ? window->Name : "NULL", msg); + } + g.ErrorFirst = false; + + // Output to tooltip + if (g.IO.ConfigErrorRecoveryEnableTooltip) + { + if (BeginErrorTooltip()) + { + if (g.ErrorCountCurrentFrame < 20) + { + Text("In window '%s': %s", window ? window->Name : "NULL", msg); + if (window && (!window->IsFallbackWindow || window->WasActive)) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 0, 0, 255)); + } + if (g.ErrorCountCurrentFrame == 20) + Text("(and more errors)"); + // EndFrame() will amend debug buttons to this window, after all errors have been submitted. + EndErrorTooltip(); + } + g.ErrorCountCurrentFrame++; + } +#endif + + // Output to callback + if (g.ErrorCallback != NULL) + g.ErrorCallback(&g, g.ErrorCallbackUserData, msg); + + // Return whether we should assert + return g.IO.ConfigErrorRecoveryEnableAssert; } -// Compare to detect usage errors -void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx) +void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip() { - ImGuiContext& g = *ctx; - ImGuiWindow* window = g.CurrentWindow; - IM_UNUSED(window); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugDrawIdConflicts != 0 && g.IO.KeyCtrl == false) + g.DebugDrawIdConflictsCount = g.HoveredIdPreviousFrameItemCount; + if (g.DebugDrawIdConflicts != 0 && g.DebugItemPickerActive == false && BeginErrorTooltip()) + { + Text("Programmer error: %d visible items with conflicting ID!", g.DebugDrawIdConflictsCount); + BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!"); + BulletText("Empty label e.g. Button(\"\") == same ID as parent widget/node. Use Button(\"##xx\") instead!"); + BulletText("Set io.ConfigDebugDetectIdConflicts=false to disable this warning in non-programmers builds."); + Separator(); + Text("(Hold CTRL and: use"); + SameLine(); + if (SmallButton("Item Picker")) + DebugStartItemPicker(); + SameLine(); + Text("to break in item call-stack, or"); + SameLine(); + if (SmallButton("Open FAQ->About ID Stack System") && g.PlatformIO.Platform_OpenInShellFn != NULL) + g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage"); + EndErrorTooltip(); + } - // Window stacks - // NOT checking: DC.ItemWidth, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) - IM_ASSERT(SizeOfIDStack == window->IDStack.Size && "PushID/PopID or TreeNode/TreePop Mismatch!"); + if (g.ErrorCountCurrentFrame > 0 && BeginErrorTooltip()) // Amend at end of frame + { + Separator(); + Text("(Hold CTRL and:"); + SameLine(); + if (SmallButton("Enable Asserts")) + g.IO.ConfigErrorRecoveryEnableAssert = true; + //SameLine(); + //if (SmallButton("Hide Error Tooltips")) + // g.IO.ConfigErrorRecoveryEnableTooltip = false; // Too dangerous + SameLine(0, 0); + Text(")"); + EndErrorTooltip(); + } +#endif +} - // Global stacks - // For color, style and font stacks there is an incentive to use Push/Begin/Pop/.../End patterns, so we relax our checks a little to allow them. - IM_ASSERT(SizeOfGroupStack == g.GroupStack.Size && "BeginGroup/EndGroup Mismatch!"); - IM_ASSERT(SizeOfBeginPopupStack == g.BeginPopupStack.Size && "BeginPopup/EndPopup or BeginMenu/EndMenu Mismatch!"); - IM_ASSERT(SizeOfDisabledStack == g.DisabledStackSize && "BeginDisabled/EndDisabled Mismatch!"); - IM_ASSERT(SizeOfItemFlagsStack >= g.ItemFlagsStack.Size && "PushItemFlag/PopItemFlag Mismatch!"); - IM_ASSERT(SizeOfColorStack >= g.ColorStack.Size && "PushStyleColor/PopStyleColor Mismatch!"); - IM_ASSERT(SizeOfStyleVarStack >= g.StyleVarStack.Size && "PushStyleVar/PopStyleVar Mismatch!"); - IM_ASSERT(SizeOfFontStack >= g.FontStack.Size && "PushFont/PopFont Mismatch!"); - IM_ASSERT(SizeOfFocusScopeStack == g.FocusScopeStack.Size && "PushFocusScope/PopFocusScope Mismatch!"); +// Pseudo-tooltip. Follow mouse until CTRL is held. When CTRL is held we lock position, allowing to click it. +bool ImGui::BeginErrorTooltip() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = FindWindowByName("##Tooltip_Error"); + const bool use_locked_pos = (g.IO.KeyCtrl && window && window->WasActive); + PushStyleColor(ImGuiCol_PopupBg, ImLerp(g.Style.Colors[ImGuiCol_PopupBg], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.15f)); + if (use_locked_pos) + SetNextWindowPos(g.ErrorTooltipLockedPos); + bool is_visible = Begin("##Tooltip_Error", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + PopStyleColor(); + if (is_visible && g.CurrentWindow->BeginCount == 1) + { + SeparatorText("MESSAGE FROM DEAR IMGUI"); + BringWindowToDisplayFront(g.CurrentWindow); + BringWindowToFocusFront(g.CurrentWindow); + g.ErrorTooltipLockedPos = GetWindowPos(); + } + else if (!is_visible) + { + End(); + } + return is_visible; +} + +void ImGui::EndErrorTooltip() +{ + End(); } //----------------------------------------------------------------------------- @@ -11593,7 +11711,13 @@ void ImGui::PushMultiItemsWidths(int components, float w_full) void ImGui::PopItemWidth() { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.ItemWidthStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemWidth() too many times!"); + return; + } window->DC.ItemWidth = window->DC.ItemWidthStack.back(); window->DC.ItemWidthStack.pop_back(); } @@ -22092,12 +22216,24 @@ static void ShowDebugLogFlag(const char* name, ImGuiDebugLogFlags flags) ImGuiContext& g = *GImGui; ImVec2 size(ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui::CalcTextSize(name).x, ImGui::GetFrameHeight()); SameLineOrWrap(size); // FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout. + + bool highlight_errors = (flags == ImGuiDebugLogFlags_EventError && g.DebugLogSkippedErrors > 0); + if (highlight_errors) + ImGui::PushStyleColor(ImGuiCol_Text, ImLerp(g.Style.Colors[ImGuiCol_Text], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.30f)); if (ImGui::CheckboxFlags(name, &g.DebugLogFlags, flags) && g.IO.KeyShift && (g.DebugLogFlags & flags) != 0) { g.DebugLogAutoDisableFrames = 2; g.DebugLogAutoDisableFlags |= flags; } - ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); + if (highlight_errors) + { + ImGui::PopStyleColor(); + ImGui::SetItemTooltip("%d past errors skipped.", g.DebugLogSkippedErrors); + } + else + { + ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); + } } void ImGui::ShowDebugLogWindow(bool* p_open) @@ -22115,6 +22251,7 @@ void ImGui::ShowDebugLogWindow(bool* p_open) CheckboxFlags("All", &g.DebugLogFlags, all_enable_flags); SetItemTooltip("(except InputRouting which is spammy)"); + ShowDebugLogFlag("Errors", ImGuiDebugLogFlags_EventError); ShowDebugLogFlag("ActiveId", ImGuiDebugLogFlags_EventActiveId); ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper); ShowDebugLogFlag("Docking", ImGuiDebugLogFlags_EventDocking); @@ -22130,6 +22267,7 @@ void ImGui::ShowDebugLogWindow(bool* p_open) { g.DebugLogBuf.clear(); g.DebugLogIndex.clear(); + g.DebugLogSkippedErrors = 0; } SameLine(); if (SmallButton("Copy")) diff --git a/imgui.h b/imgui.h index 381265ea8..6c6219463 100644 --- a/imgui.h +++ b/imgui.h @@ -29,7 +29,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.91.3 WIP" -#define IMGUI_VERSION_NUM 19121 +#define IMGUI_VERSION_NUM 19123 #define IMGUI_HAS_TABLE #define IMGUI_HAS_VIEWPORT // Viewport WIP branch #define IMGUI_HAS_DOCK // Docking WIP branch @@ -2355,6 +2355,23 @@ struct ImGuiIO // Debug options //------------------------------------------------------------------ + // Options to configure how we handle recoverable errors [EXPERIMENTAL] + // - Error recovery is not perfect nor guaranteed! It is a feature to ease development. + // - Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT(). + // - You not are not supposed to rely on it in the course of a normal application run. + // - Possible usage: facilitate recovery from errors triggered from a scripting language or after specific exceptions handlers. + // - Always ensure that on programmers seat you have at minimum Asserts or Tooltips enabled when making direct imgui API calls! + // Otherwise it would severely hinder your ability to catch and correct mistakes! + // Read https://github.com/ocornut/imgui/wiki/Error-Handling for details about typical usage scenarios: + // - Programmer seats: keep asserts (default), or disable asserts and keep error tooltips (new and nice!) + // - Non-programmer seats: maybe disable asserts, but make sure errors are resurfaced (visible log entries, use callback etc.) + // - Recovery after error from scripting language: record stack sizes before running script, disable assert, trigger breakpoint from ErrorCallback, recover with ErrorRecoveryTryToRecoverState(), restore settings. + // - Recovery after an exception handler: record stack sizes before try {} block, disable assert, set log callback, recover with ErrorRecoveryTryToRecoverState(), restore settings. + bool ConfigErrorRecovery; // = true // Enable error recovery support. Some errors won't be detected and lead to direct crashes if recovery is disabled. + bool ConfigErrorRecoveryEnableAssert; // = true // Enable asserts on recoverable error. By default call IM_ASSERT() when returning from a failing IM_ASSERT_USER_ERROR() + bool ConfigErrorRecoveryEnableDebugLog; // = true // Enable debug log output on recoverable errors. + bool ConfigErrorRecoveryEnableTooltip; // = true // Enable tooltip on recoverable errors. The tooltip include a way to enable asserts if they were disabled. + // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. @@ -2383,7 +2400,7 @@ struct ImGuiIO bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) //------------------------------------------------------------------ - // Platform Functions + // Platform Identifiers // (the imgui_impl_xxxx backend files are setting those up for you) //------------------------------------------------------------------ diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 2b8eebcd6..d3a85741b 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -599,6 +599,22 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::SameLine(); HelpMarker("Swap Cmd<>Ctrl keys, enable various MacOS style behaviors."); ImGui::Text("Also see Style->Rendering for rendering options."); + // Read https://github.com/ocornut/imgui/wiki/Error-Handling + ImGui::SeparatorText("Error Handling"); + ImGui::Checkbox("io.ConfigErrorRecovery", &io.ConfigErrorRecovery); + ImGui::SameLine(); HelpMarker( + "Options to configure how we handle recoverable errors.\n" + "- Error recovery is not perfect nor guaranteed! It is a feature to ease development.\n" + "- You not are not supposed to rely on it in the course of a normal application run.\n" + "- Possible usage: facilitate recovery from errors triggered from a scripting language or after specific exceptions handlers.\n" + "- Always ensure that on programmers seat you have at minimum Asserts or Tooltips enabled when making direct imgui API call!" + "Otherwise it would severely hinder your ability to catch and correct mistakes!"); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableAssert", &io.ConfigErrorRecoveryEnableAssert); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableDebugLog", &io.ConfigErrorRecoveryEnableDebugLog); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableTooltip", &io.ConfigErrorRecoveryEnableTooltip); + if (!io.ConfigErrorRecoveryEnableAssert && !io.ConfigErrorRecoveryEnableDebugLog && !io.ConfigErrorRecoveryEnableTooltip) + io.ConfigErrorRecoveryEnableAssert = io.ConfigErrorRecoveryEnableDebugLog = io.ConfigErrorRecoveryEnableTooltip = true; + ImGui::SeparatorText("Debug"); ImGui::Checkbox("io.ConfigDebugIsDebuggerPresent", &io.ConfigDebugIsDebuggerPresent); ImGui::SameLine(); HelpMarker("Enable various tools calling IM_DEBUG_BREAK().\n\nRequires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application."); @@ -4006,6 +4022,9 @@ static void ShowDemoWindowLayout() ImGui::SeparatorText("Manual-resize"); { HelpMarker("Drag bottom border to resize. Double-click bottom border to auto-fit to vertical contents."); + //if (ImGui::Button("Set Height to 200")) + // ImGui::SetNextWindowSize(ImVec2(-FLT_MIN, 200.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); if (ImGui::BeginChild("ResizableChild", ImVec2(-FLT_MIN, ImGui::GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) for (int n = 0; n < 10; n++) diff --git a/imgui_internal.h b/imgui_internal.h index 3cf841f1e..ae1c594cc 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -28,6 +28,7 @@ Index of this file: // [SECTION] Viewport support // [SECTION] Settings support // [SECTION] Localization support +// [SECTION] Error handling, State recovery support // [SECTION] Metrics, Debug tools // [SECTION] Generic context hooks // [SECTION] ImGuiContext (main imgui context) @@ -135,6 +136,7 @@ struct ImGuiDockContext; // Docking system context struct ImGuiDockRequest; // Docking system dock/undock queued request struct ImGuiDockNode; // Docking system node (hold a list of Windows OR two child dock nodes) struct ImGuiDockNodeSettings; // Storage for a dock node in .ini file (we preserve those even if the associated dock node isn't active during the session) +struct ImGuiErrorRecoveryState; // Storage of stack sizes for error handling and recovery struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id @@ -151,7 +153,6 @@ struct ImGuiOldColumnData; // Storage data for a single column for lega struct ImGuiOldColumns; // Storage data for a columns set for legacy Columns() api struct ImGuiPopupData; // Storage for current popup stack struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file -struct ImGuiStackSizes; // Storage of stack sizes for debugging/asserting struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it struct ImGuiTabBar; // Storage for a tab bar struct ImGuiTabItem; // Storage for a tab item (within a tab bar) @@ -179,7 +180,7 @@ typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // E // Flags typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) typedef int ImGuiDebugLogFlags; // -> enum ImGuiDebugLogFlags_ // Flags: for ShowDebugLogWindow(), g.DebugLogFlags -typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow(); +typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow() typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for g.LastItemData.StatusFlags typedef int ImGuiOldColumnFlags; // -> enum ImGuiOldColumnFlags_ // Flags: for BeginColumns() typedef int ImGuiNavHighlightFlags; // -> enum ImGuiNavHighlightFlags_ // Flags: for RenderNavHighlight() @@ -193,8 +194,6 @@ typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // F typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() typedef int ImGuiWindowRefreshFlags; // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy() -typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); - //----------------------------------------------------------------------------- // [SECTION] Context pointer // See implementation of this variable in imgui.cpp for comments and details. @@ -227,6 +226,7 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer #else #define IMGUI_DEBUG_LOG(...) ((void)0) #endif +#define IMGUI_DEBUG_LOG_ERROR(...) do { ImGuiContext& g2 = *GImGui; if (g2.DebugLogFlags & ImGuiDebugLogFlags_EventError) IMGUI_DEBUG_LOG(__VA_ARGS__); else g2.DebugLogSkippedErrors++; } while (0) #define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) #define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) #define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) @@ -250,12 +250,6 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer #define IM_ASSERT_PARANOID(_EXPR) #endif -// Error handling -// Down the line in some frameworks/languages we would like to have a way to redirect those to the programmer and recover from more faults. -#ifndef IM_ASSERT_USER_ERROR -#define IM_ASSERT_USER_ERROR(_EXP,_MSG) IM_ASSERT((_EXP) && _MSG) // Recoverable User Error -#endif - // Misc Macros #define IM_PI 3.14159265358979323846f #ifdef _WIN32 @@ -1278,10 +1272,12 @@ struct ImGuiTreeNodeStackData ImRect NavRect; // Used for nav landing }; -// sizeof() = 18 -struct IMGUI_API ImGuiStackSizes +// sizeof() = 20 +struct IMGUI_API ImGuiErrorRecoveryState { + short SizeOfWindowStack; short SizeOfIDStack; + short SizeOfTreeStack; short SizeOfColorStack; short SizeOfStyleVarStack; short SizeOfFontStack; @@ -1291,18 +1287,16 @@ struct IMGUI_API ImGuiStackSizes short SizeOfBeginPopupStack; short SizeOfDisabledStack; - ImGuiStackSizes() { memset(this, 0, sizeof(*this)); } - void SetToContextState(ImGuiContext* ctx); - void CompareWithContextState(ImGuiContext* ctx); + ImGuiErrorRecoveryState() { memset(this, 0, sizeof(*this)); } }; // Data saved for each window pushed into the stack struct ImGuiWindowStackData { - ImGuiWindow* Window; - ImGuiLastItemData ParentLastItemDataBackup; - ImGuiStackSizes StackSizesOnBegin; // Store size of various stacks for asserting - bool DisabledOverrideReenable; // Non-child window override disabled flag + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiErrorRecoveryState StackSizesInBegin; // Store size of various stacks for asserting + bool DisabledOverrideReenable; // Non-child window override disabled flag }; struct ImGuiShrinkWidthItem @@ -2076,6 +2070,22 @@ struct ImGuiLocEntry const char* Text; }; +//----------------------------------------------------------------------------- +// [SECTION] Error handling, State recovery support +//----------------------------------------------------------------------------- + +// Macros used by Recoverable Error handling +// - Only dispatch error if _EXPR: evaluate as assert (similar to an assert macro). +// - The message will always be a string literal, in order to increase likelihood of being display by an assert handler. +// - The intent is that you may rewire this macro to dispatch dynamically: +// - On programmers machines, when debugger is attached, on direct imgui API usage error: always assert! +// - On exception recovery and script language recovery: you may decide to error log. +#ifndef IM_ASSERT_USER_ERROR +#define IM_ASSERT_USER_ERROR(_EXPR,_MSG) do { if (!(_EXPR) && ImGui::ErrorLog(_MSG)) { IM_ASSERT((_EXPR) && _MSG); } } while (0) // Recoverable User Error +#endif + + +typedef void (*ImGuiErrorCallback)(ImGuiContext* ctx, void* user_data, const char* msg); // Function signature for g.ErrorCallback //----------------------------------------------------------------------------- // [SECTION] Metrics, Debug Tools @@ -2085,18 +2095,19 @@ enum ImGuiDebugLogFlags_ { // Event types ImGuiDebugLogFlags_None = 0, - ImGuiDebugLogFlags_EventActiveId = 1 << 0, - ImGuiDebugLogFlags_EventFocus = 1 << 1, - ImGuiDebugLogFlags_EventPopup = 1 << 2, - ImGuiDebugLogFlags_EventNav = 1 << 3, - ImGuiDebugLogFlags_EventClipper = 1 << 4, - ImGuiDebugLogFlags_EventSelection = 1 << 5, - ImGuiDebugLogFlags_EventIO = 1 << 6, - ImGuiDebugLogFlags_EventInputRouting = 1 << 7, - ImGuiDebugLogFlags_EventDocking = 1 << 8, - ImGuiDebugLogFlags_EventViewport = 1 << 9, + ImGuiDebugLogFlags_EventError = 1 << 0, // Error submitted by IM_ASSERT_USER_ERROR() + ImGuiDebugLogFlags_EventActiveId = 1 << 1, + ImGuiDebugLogFlags_EventFocus = 1 << 2, + ImGuiDebugLogFlags_EventPopup = 1 << 3, + ImGuiDebugLogFlags_EventNav = 1 << 4, + ImGuiDebugLogFlags_EventClipper = 1 << 5, + ImGuiDebugLogFlags_EventSelection = 1 << 6, + ImGuiDebugLogFlags_EventIO = 1 << 7, + ImGuiDebugLogFlags_EventInputRouting = 1 << 8, + ImGuiDebugLogFlags_EventDocking = 1 << 9, + ImGuiDebugLogFlags_EventViewport = 1 << 10, - ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting | ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, + ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting | ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, ImGuiDebugLogFlags_OutputToTTY = 1 << 20, // Also send output to TTY ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, // Also send output to Test Engine }; @@ -2516,11 +2527,22 @@ struct ImGuiContext int LogDepthToExpand; int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. + // Error Handling + ImGuiErrorCallback ErrorCallback; // = NULL. May be exposed in public API eventually. + void* ErrorCallbackUserData; // = NULL + ImVec2 ErrorTooltipLockedPos; + bool ErrorFirst; + int ErrorCountCurrentFrame; // [Internal] Number of errors submitted this frame. + ImGuiErrorRecoveryState StackSizesInNewFrame; // [Internal] + ImGuiErrorRecoveryState*StackSizesInBeginForCurrentWindow; // [Internal] + // Debug Tools // (some of the highly frequently used data are interleaved in other structures above: DebugBreakXXX fields, DebugHookIdInfo, DebugLocateId etc.) + int DebugDrawIdConflictsCount; // Locked count (preserved when holding CTRL) ImGuiDebugLogFlags DebugLogFlags; ImGuiTextBuffer DebugLogBuf; ImGuiTextIndex DebugLogIndex; + int DebugLogSkippedErrors; ImGuiDebugLogFlags DebugLogAutoDisableFlags; ImU8 DebugLogAutoDisableFrames; ImU8 DebugLocateFrames; // For DebugLocateItemOnHover(). This is used together with DebugLocateId which is in a hot/cached spot above. @@ -3706,11 +3728,15 @@ namespace ImGui IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); - // Error Checking, State Recovery - IMGUI_API void ErrorLogCallbackToDebugLog(void* user_data, const char* fmt, ...); - IMGUI_API void ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); - IMGUI_API void ErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, void* user_data = NULL); + // Error handling, State Recovery + IMGUI_API bool ErrorLog(const char* msg); + IMGUI_API void ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out); + IMGUI_API void ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in); + IMGUI_API void ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in); IMGUI_API void ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + IMGUI_API void ErrorCheckEndFrameFinalizeErrorTooltip(); + IMGUI_API bool BeginErrorTooltip(); + IMGUI_API void EndErrorTooltip(); // Debug Tools IMGUI_API void DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size); // size >= 0 : alloc, size = -1 : free diff --git a/imgui_tables.cpp b/imgui_tables.cpp index dc7f21098..4a6fbed7c 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -1484,7 +1484,9 @@ void ImGui::EndTable() { short backup_nav_layers_active_mask = inner_window->DC.NavLayersActiveMask; inner_window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; // So empty table don't appear to navigate differently. + g.CurrentTable = NULL; // To avoid error recovery recursing EndChild(); + g.CurrentTable = table; inner_window->DC.NavLayersActiveMask = backup_nav_layers_active_mask; } else diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7295e6463..d162f442e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7398,6 +7398,7 @@ void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flag static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) { ImGuiContext& g = *GImGui; + IM_UNUSED(function); for (const ImGuiSelectionRequest& req : io->Requests) { if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); @@ -7557,7 +7558,7 @@ ImGuiMultiSelectIO* ImGui::EndMultiSelect() ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; ImGuiMultiSelectState* storage = ms->Storage; ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(ms->FocusScopeId == g.CurrentFocusScopeId); + IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!"); IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect); @@ -8164,6 +8165,13 @@ void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) //------------------------------------------------------------------------- // This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. +// This handle some subtleties with capturing info from the label, but for 99% uses it could essentially be rewritten as: +// if (ImGui::BeginChild("...", ImVec2(ImGui::CalcItemWidth(), ImGui::GetTextLineHeight() * 7.5f), ImGuiChildFlags_FrameStyle)) +// { .... } +// ImGui::EndChild(); +// ImGui::SameLine(); +// ImGui::AlignTextToFramePadding(); +// ImGui::Text("Label"); // Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" // Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height). bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)