From 30c29d291fbb4428055de676775ad591296a2bf2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Tue, 24 Sep 2024 20:51:51 +0200 Subject: [PATCH] Error Handling: enabled experimental recovery systems. (#1651, #5654) Setup a couple of features to configure them, including ways to display error tooltips instead of assserting. --- docs/CHANGELOG.txt | 18 ++++- imgui.cpp | 196 ++++++++++++++++++++++++++++----------------- imgui.h | 21 ++++- imgui_demo.cpp | 16 ++++ imgui_internal.h | 44 ++++++---- imgui_tables.cpp | 2 + imgui_widgets.cpp | 2 +- 7 files changed, 203 insertions(+), 96 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 51ef272c5..511901085 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -43,8 +43,22 @@ Breaking changes: Other changes: -- Error Handling: rewired asserts in PopID(), PopFont(), PopItemFlag(), EndDisabled(), - PopTextWrapPos(), PopFocusScope(), PopItemWidth() to use IM_ASSERT_USER_ERROR(). (#1651) +- 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) diff --git a/imgui.cpp b/imgui.cpp index cb84751f5..306a730ce 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1401,6 +1401,11 @@ ImGuiIO::ImGuiIO() ConfigDebugBeginReturnValueOnce = false; ConfigDebugBeginReturnValueLoop = false; + ConfigErrorRecovery = true; + ConfigErrorRecoveryEnableAssert = true; + ConfigErrorRecoveryEnableDebugLog = true; + ConfigErrorRecoveryEnableTooltip = true; + // Inputs Behaviors MouseDoubleClickTime = 0.30f; MouseDoubleClickMaxDist = 6.0f; @@ -3978,6 +3983,12 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) LogDepthRef = 0; LogDepthToExpand = LogDepthToExpandDefault = 2; + ErrorCallback = NULL; + ErrorCallbackUserData = NULL; + ErrorFirst = true; + ErrorCountCurrentFrame = 0; + StackSizesInBeginForCurrentWindow = NULL; + DebugDrawIdConflictsCount = 0; DebugLogFlags = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_OutputToTTY; DebugLocateId = 0; @@ -4211,6 +4222,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; g.CurrentDpiScale = 1.0f; // FIXME-DPI: WIP this is modified in docking if (window) @@ -5249,6 +5261,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 @@ -5467,6 +5483,9 @@ void ImGui::EndFrame() CallContextHooks(&g, ImGuiContextHookType_EndFramePre); + // [EXPERIMENTAL] Recover from errors + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverState(&g.StackSizesInNewFrame); ErrorCheckEndFrameSanityChecks(); ErrorCheckEndFrameFinalizeErrorTooltip(); @@ -6960,12 +6979,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++; @@ -7693,7 +7713,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); } @@ -8454,7 +8478,7 @@ 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(0, "Calling PopFocusScope() too many times!"); return; @@ -10303,9 +10327,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. @@ -10404,6 +10429,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)) @@ -10426,43 +10455,35 @@ static void ImGui::ErrorCheckEndFrameSanityChecks() 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?"); - } - } - if (g.CurrentWindowStack.Size >= 1) - IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow); - - IM_ASSERT_USER_ERROR(g.GroupStack.Size == 0, "Missing EndGroup call!"); + IM_ASSERT(g.CurrentWindowStack.Size == 1); + IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow); } -// 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() +// 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) +{ + ImGuiContext& g = *GImGui; + state_out->SizeOfWindowStack = (short)g.CurrentWindowStack.Size; + state_out->SizeOfIDStack = (short)g.CurrentWindow->IDStack.Size; + 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; +} + +// 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(); // 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! @@ -10480,12 +10501,17 @@ void ImGui::ErrorCheckEndFrameRecover() End(); } } + if (g.CurrentWindowStack.Size == state_in->SizeOfWindowStack) + ErrorRecoveryTryToRecoverWindowState(state_in); } -// Must be called before End()/EndChild() -void ImGui::ErrorCheckEndWindowRecover() +// 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) { IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); @@ -10493,8 +10519,6 @@ void ImGui::ErrorCheckEndWindowRecover() } ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(window != NULL); - ImGuiStackSizes* state_in = &g.CurrentWindowStack.back().StackSizesOnBegin; // FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. while (g.CurrentTabBar != NULL && g.CurrentTabBar->Window == window) //-V1044 @@ -10560,45 +10584,52 @@ void ImGui::ErrorCheckEndWindowRecover() 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; -} -// Compare to detect usage errors -void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx) -{ - ImGuiContext& g = *ctx; - ImGuiWindow* window = g.CurrentWindow; - IM_UNUSED(window); + 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; - // 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!"); + // 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 - // 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!"); + // Output to callback + if (g.ErrorCallback != NULL) + g.ErrorCallback(&g, g.ErrorCallbackUserData, msg); + + // Return whether we should assert + return g.IO.ConfigErrorRecoveryEnableAssert; } void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip() @@ -10625,6 +10656,21 @@ void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip() g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage"); EndErrorTooltip(); } + + 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 } diff --git a/imgui.h b/imgui.h index f86efc428..4ab39be3f 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 19122 +#define IMGUI_VERSION_NUM 19123 #define IMGUI_HAS_TABLE /* @@ -2265,6 +2265,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. @@ -2293,7 +2310,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 8fb410d9b..60d944657 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -545,6 +545,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."); diff --git a/imgui_internal.h b/imgui_internal.h index a00a675bd..52664a1c5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -132,6 +132,7 @@ struct ImGuiContext; // Main Dear ImGui context struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine struct ImGuiDataVarInfo; // Variable information (e.g. to access style variables from an enum) struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +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 @@ -148,7 +149,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) @@ -1253,9 +1253,10 @@ 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 SizeOfColorStack; short SizeOfStyleVarStack; @@ -1266,18 +1267,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 @@ -1885,12 +1884,17 @@ struct ImGuiLocEntry //----------------------------------------------------------------------------- // Macros used by Recoverable 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. +// - 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) IM_ASSERT((_EXPR) && _MSG) +#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 (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); + +typedef void (*ImGuiErrorCallback)(ImGuiContext* ctx, void* user_data, const char* msg); // Function signature for g.ErrorCallback //----------------------------------------------------------------------------- // [SECTION] Metrics, Debug Tools @@ -2313,8 +2317,14 @@ struct ImGuiContext int LogDepthToExpand; int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. - // Error handling + // 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.) @@ -3419,9 +3429,11 @@ namespace ImGui IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); // 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 ErrorCheckEndFrameRecover(); - IMGUI_API void ErrorCheckEndWindowRecover(); IMGUI_API void ErrorCheckEndFrameFinalizeErrorTooltip(); IMGUI_API bool BeginErrorTooltip(); IMGUI_API void EndErrorTooltip(); 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 5641134ff..22ff7a9e5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7551,7 +7551,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);