From 18d63b12bef5f902b4d2349b10e874add7b62802 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Apr 2026 15:57:36 +0200 Subject: [PATCH] Added ImGuiItemFlags_LiveEdit flag, and much-awaited support for disabling it. (#9476, #701) cc #3936, #3946, #5904, #6284, #8149, #8065, #8665, #9117, #9299, #700, #1351, #1875, #2060, #2215, #2380, #2550, #3083, #3338, #3556, #4373, #4714, #4885, #5184,#5777, #6707, #6766, #8004, #8303, #8915, #9308 --- imgui.cpp | 7 +++-- imgui.h | 5 +++- imgui_internal.h | 4 +-- imgui_widgets.cpp | 76 +++++++++++++++++++++++++++++++++-------------- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 2fbc33f6c..5c7f4df5c 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4833,6 +4833,7 @@ void ImGui::MarkItemEdited(ImGuiID id) // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. ImGuiContext& g = *GImGui; + //IM_ASSERT(g.LastItemData.ID == id); // Failing cases include: "widgets_inputtext_scrolling", "widgets_inputtext_multiline_status", "widgets_selectable_input" = case of e.g TempInputText() overlayed manually with different ID (#2718) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_EditedInternal; if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited) return; @@ -4843,14 +4844,14 @@ void ImGui::MarkItemEdited(ImGuiID id) // FIXME: Can't we fully rely on LastItemData yet? g.ActiveIdHasBeenEditedThisFrame = true; g.ActiveIdHasBeenEditedBefore = true; - if (g.DeactivatedItemData.ID == id) - g.DeactivatedItemData.HasBeenEditedBefore = true; } + if (g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.HasBeenEditedBefore = true; // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) // FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction. - IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.DeactivatedItemData.ID == id || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); } bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) diff --git a/imgui.h b/imgui.h index 0216b893b..672f24e66 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 19285 +#define IMGUI_VERSION_NUM 19286 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 @@ -1246,6 +1246,8 @@ enum ImGuiItemFlags_ ImGuiItemFlags_AutoClosePopups = 1 << 4, // true // MenuItem()/Selectable() automatically close their parent popup window. ImGuiItemFlags_AllowDuplicateId = 1 << 5, // false // Allow submitting an item with the same identifier as an item already submitted this frame without triggering a warning tooltip if io.ConfigDebugHighlightIdConflicts is set. ImGuiItemFlags_Disabled = 1 << 6, // false // [Internal] Disable interactions. DOES NOT affect visuals. This is used by BeginDisabled()/EndDisabled() and only provided here so you can read back via GetItemFlags(). + + ImGuiItemFlags_LiveEdit = 1 << 7, // true // WIP }; // Flags for ImGui::InputText() @@ -1275,6 +1277,7 @@ enum ImGuiInputTextFlags_ ImGuiInputTextFlags_DisplayEmptyRefVal = 1 << 14, // InputFloat(), InputInt(), InputScalar() etc. only: when value is zero, do not display it. Generally used with ImGuiInputTextFlags_ParseEmptyRefVal. ImGuiInputTextFlags_NoHorizontalScroll = 1 << 15, // Disable following the cursor horizontally ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + //ImGuiInputTextFlags_NoLiveEdit = 1 << 25, // Elide display / Alignment ImGuiInputTextFlags_ElideLeft = 1 << 17, // When text doesn't fit, elide left side to ensure right side stays visible. Useful for path/filenames. Single-line only! diff --git a/imgui_internal.h b/imgui_internal.h index b92274846..2d456b98d 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1006,7 +1006,7 @@ enum ImGuiItemFlagsPrivate_ ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData() ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData() - ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead. + ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups | ImGuiItemFlags_LiveEdit, // Please don't change, use PushItemFlag() instead. // Obsolete //ImGuiItemFlags_SelectableDontClosePopup = !ImGuiItemFlags_AutoClosePopups, // Can't have a redirect as we inverted the behavior @@ -3799,7 +3799,7 @@ namespace ImGui IMGUI_API void InputTextDeactivateHook(ImGuiID id); IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL); - inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return g.ActiveId == id && g.TempInputId == id; } + inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.TempInputId == id && g.ActiveId == id) || (g.InputTextDeactivatedState.ID == id); } inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active IMGUI_API void SetNextItemRefVal(ImGuiDataType data_type, void* p_data); inline bool IsItemActiveAsInputText() { ImGuiContext& g = *GImGui; return g.ActiveId != 0 && g.ActiveId == g.LastItemData.ID && g.InputTextState.ID == g.LastItemData.ID; } // This may be useful to apply workaround that a based on distinguish whenever an item is active as a text input field. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f1517273a..e790eeeb8 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3722,8 +3722,9 @@ bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - const bool init = (g.TempInputId != id); - if (init) + const bool is_deactivated = (g.InputTextDeactivatedState.ID == id); + const bool is_active = (g.TempInputId == id); + if (!is_active && !is_deactivated) ClearActiveID(); ImVec2 backup_pos = window->DC.CursorPos; @@ -3731,13 +3732,13 @@ bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; // Using ImGuiInputTextFlags_MergedItem above will skip ItemAdd() so we poke here. bool value_changed = InputTextEx(label, NULL, buf, (int)buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_TempInput | ImGuiInputTextFlags_AutoSelectAll, callback, user_data); KeepAliveID(id); // Not done because of ImGuiInputTextFlags_TempInput - if (init) + if (!is_active && !is_deactivated) { // First frame we started displaying the InputText widget, we expect it to take the active id. IM_ASSERT(g.ActiveId == id); g.TempInputId = g.ActiveId; } - if (g.ActiveId != id) + if (is_active && g.ActiveId != id) g.TempInputId = 0; window->DC.CursorPos = backup_pos; return value_changed; @@ -3841,8 +3842,23 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data } // Apply - bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved. - bool value_changed = input_edited ? DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL) : false; + bool value_changed = false; + if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEdit) + { + bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved. + if (input_edited) + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + } + else + { + //g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_Edited; + if (g.DeactivatedItemData.ID == g.LastItemData.ID) + { + //g.DeactivatedItemData.HasBeenEditedBefore = false; // Will be set below by MarkItemEdited() + //if (IsItemDeactivated()) // Should be unnecessary + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + } + } // Step buttons if (has_step_buttons) @@ -4584,7 +4600,7 @@ void ImGui::InputTextDeactivateHook(ImGuiID id) { ImGuiContext& g = *GImGui; ImGuiInputTextState* state = &g.InputTextState; - if (id == 0 || state->ID != id) + if (id == 0 || state->ID != id || g.ActiveId != id) return; if (!state->EditedBefore) return; @@ -4820,7 +4836,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const bool user_clicked = hovered && io.MouseClicked[0]; const bool input_requested_by_nav = (g.ActiveId != id) && (g.NavActivateId == id); const bool input_requested_by_reactivate = (g.InputTextReactivateId == id); // for io.ConfigInputTextEnterKeepActive - const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput)); + const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput) && g.InputTextDeactivatedState.ID != id); const ImGuiID scrollbar_id = (is_multiline && state != NULL) ? GetWindowScrollbarID(draw_window, ImGuiAxis_Y) : 0; const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == scrollbar_id; const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == scrollbar_id; @@ -4852,7 +4868,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ state->CursorAnimReset(); // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714) - InputTextDeactivateHook(state->ID); + if (state->ID != id && state->ID == g.ActiveId && (init_make_active && g.ActiveId != id)) + InputTextDeactivateHook(state->ID); // <-- this is essentially an earlier call to what SetActiveID() would do below. // Take a copy of the initial buffer value. // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) @@ -5389,13 +5406,27 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } } - // Will copy result string if modified. + // Write back result string if modified. // FIXME-OPT: Could mark dirty state from the stb_textedit callbacks - if (!is_readonly && strcmp(state->TextSrc, buf) != 0) + if (!is_readonly) { - apply_new_text = state->TextSrc; - apply_new_text_length = state->TextLen; - value_changed = true; + if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEdit) + { + // Apply when modified + if (strcmp(state->TextSrc, buf) != 0) + { + apply_new_text = state->TextSrc; + apply_new_text_length = state->TextLen; + value_changed = true; + } + } + else + { + // Apply on validation/deactivation, otherwise cancel out previous apply attempts (e.g. revert) + value_changed = ((validated || clear_active_id || revert_edit) && strcmp(state->TextSrc, buf) != 0); + apply_new_text = value_changed ? state->TextSrc : NULL; + apply_new_text_length = value_changed ? state->TextLen : 0; + } } } @@ -5403,14 +5434,15 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // This is used when e.g. losing focus or tabbing out into another InputText() which may already be using the temp buffer. if (g.InputTextDeactivatedState.ID == id) { - // The state only exists after an Edit. More-over we cannot use IsItemDeactivatedAfterEdit(). - if (g.ActiveId != id && IsItemDeactivated() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) - { - apply_new_text = g.InputTextDeactivatedState.TextA.Data; - apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; - value_changed = true; - //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); - } + // The state only exists after an Edit. IsItemDeactivatedAfterEdit() is not valid in every code path (see "widgets_inputtext_status_noliveedit" test). + if ((g.ActiveId != id && IsItemDeactivated()) || (g.ActiveId == id && (flags & ImGuiInputTextFlags_TempInput))) + if (!is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) + { + apply_new_text = g.InputTextDeactivatedState.TextA.Data; + apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; + value_changed = true; + //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); + } g.InputTextDeactivatedState.ID = 0; }