Skip to content

Commit 5a2b1e8

Browse files
committed
InputText: Fixed a tricky edge case, ensuring value is always written back on the frame where IsItemDeactivated() returns true (#4714)
Altered ItemAdd() clipping rule to keep previous-frame ActiveId unclipped to support that late commit. Also, MarkItemEdited() may in theory need to do: if (g.ActiveIdPreviousFrame == id) g.ActiveIdPreviousFrameHasBeenEditedBefore = true; But this should already be set so not adding now.
1 parent 314e644 commit 5a2b1e8

File tree

5 files changed

+79
-13
lines changed

5 files changed

+79
-13
lines changed

docs/CHANGELOG.txt

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ Breaking Changes:
3939

4040
Other changes:
4141

42+
- InputText: Fixed a tricky edge case, ensuring value is always written back on the
43+
frame where IsItemDeactivated() returns true, in order to allow usage without user
44+
retaining underlying data. While we don't really want to encourage user not retaining
45+
underlying data, in the absence of a "late commit" behavior/flag we understand it may
46+
be desirable to take advantage of this trick. (#4714)
4247
- Backends: OpenGL3: Fixed GL loader crash when GL_VERSION returns NULL. (#6154, #4445, #3530)
4348
- Backends: GLFW: Fixed key modifiers handling on secondary viewports. (#6248, #6034) [@aiekick]
4449
- Examples: Windows: Added 'misc/debuggers/imgui.natstepfilter' file to all Visual Studio projects,

imgui.cpp

+28-11
Original file line numberDiff line numberDiff line change
@@ -3587,6 +3587,7 @@ void ImGui::Shutdown()
35873587
g.ClipboardHandlerData.clear();
35883588
g.MenusIdSubmittedThisFrame.clear();
35893589
g.InputTextState.ClearFreeMemory();
3590+
g.InputTextDeactivatedState.ClearFreeMemory();
35903591

35913592
g.SettingsWindows.clear();
35923593
g.SettingsHandlers.clear();
@@ -3759,13 +3760,23 @@ void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window)
37593760
{
37603761
ImGuiContext& g = *GImGui;
37613762

3762-
// While most behaved code would make an effort to not steal active id during window move/drag operations,
3763-
// we at least need to be resilient to it. Cancelling the move is rather aggressive and users of 'master' branch
3764-
// may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that.
3765-
if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId)
3763+
// Clear previous active id
3764+
if (g.ActiveId != 0)
37663765
{
3767-
IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n");
3768-
g.MovingWindow = NULL;
3766+
// While most behaved code would make an effort to not steal active id during window move/drag operations,
3767+
// we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch
3768+
// may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that.
3769+
if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId)
3770+
{
3771+
IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n");
3772+
g.MovingWindow = NULL;
3773+
}
3774+
3775+
// This could be written in a more general way (e.g associate a hook to ActiveId),
3776+
// but since this is currently quite an exception we'll leave it as is.
3777+
// One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveId()
3778+
if (g.InputTextState.ID == g.ActiveId)
3779+
InputTextDeactivateHook(g.ActiveId);
37693780
}
37703781

37713782
// Set active id
@@ -3840,11 +3851,17 @@ void ImGui::MarkItemEdited(ImGuiID id)
38403851
// This marking is solely to be able to provide info for IsItemDeactivatedAfterEdit().
38413852
// 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.
38423853
ImGuiContext& g = *GImGui;
3843-
IM_ASSERT(g.ActiveId == id || g.ActiveId == 0 || g.DragDropActive);
3844-
IM_UNUSED(id); // Avoid unused variable warnings when asserts are compiled out.
3854+
if (g.ActiveId == id || g.ActiveId == 0)
3855+
{
3856+
g.ActiveIdHasBeenEditedThisFrame = true;
3857+
g.ActiveIdHasBeenEditedBefore = true;
3858+
}
3859+
3860+
// We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343)
3861+
// We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714)
3862+
IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id);
3863+
38453864
//IM_ASSERT(g.CurrentWindow->DC.LastItemId == id);
3846-
g.ActiveIdHasBeenEditedThisFrame = true;
3847-
g.ActiveIdHasBeenEditedBefore = true;
38483865
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
38493866
}
38503867

@@ -9293,7 +9310,7 @@ bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGu
92939310
// return false;
92949311
const bool is_rect_visible = bb.Overlaps(window->ClipRect);
92959312
if (!is_rect_visible)
9296-
if (id == 0 || (id != g.ActiveId && id != g.NavId))
9313+
if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId))
92979314
if (!g.LogEnabled)
92989315
return false;
92999316

imgui.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
// Library Version
2424
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM > 12345')
2525
#define IMGUI_VERSION "1.89.5 WIP"
26-
#define IMGUI_VERSION_NUM 18941
26+
#define IMGUI_VERSION_NUM 18942
2727
#define IMGUI_HAS_TABLE
2828

2929
/*

imgui_internal.h

+12
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ struct ImGuiDataVarInfo; // Variable information (e.g. to avoid style
130130
struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum
131131
struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup()
132132
struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box
133+
struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id
133134
struct ImGuiLastItemData; // Status storage for last submitted items
134135
struct ImGuiLocEntry; // A localization entry.
135136
struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only
@@ -1048,6 +1049,15 @@ struct IMGUI_API ImGuiMenuColumns
10481049
void CalcNextTotalWidth(bool update_offsets);
10491050
};
10501051

1052+
// Internal temporary state for deactivating InputText() instances.
1053+
struct IMGUI_API ImGuiInputTextDeactivatedState
1054+
{
1055+
ImGuiID ID; // widget id owning the text state (which just got deactivated)
1056+
ImVector<char> TextA; // text buffer
1057+
1058+
ImGuiInputTextDeactivatedState() { memset(this, 0, sizeof(*this)); }
1059+
void ClearFreeMemory() { ID = 0; TextA.clear(); }
1060+
};
10511061
// Internal state of the currently focused/edited text input box
10521062
// For a given item ID, access with ImGui::GetInputTextState()
10531063
struct IMGUI_API ImGuiInputTextState
@@ -1935,6 +1945,7 @@ struct ImGuiContext
19351945
// Widget state
19361946
ImVec2 MouseLastValidPos;
19371947
ImGuiInputTextState InputTextState;
1948+
ImGuiInputTextDeactivatedState InputTextDeactivatedState;
19381949
ImFont InputTextPasswordFont;
19391950
ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc.
19401951
ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets
@@ -3131,6 +3142,7 @@ namespace ImGui
31313142

31323143
// InputText
31333144
IMGUI_API bool InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback = NULL, void* user_data = NULL);
3145+
IMGUI_API void InputTextDeactivateHook(ImGuiID id);
31343146
IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags);
31353147
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);
31363148
inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputId == id); }

imgui_widgets.cpp

+33-1
Original file line numberDiff line numberDiff line change
@@ -3999,6 +3999,21 @@ static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* st
39993999
p[i] = ImStb::STB_TEXTEDIT_GETCHAR(state, first_diff + i);
40004000
}
40014001

4002+
// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4003+
// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4004+
// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4005+
// but that more likely be attractive when we do have _NoLiveEdit flag available.
4006+
void ImGui::InputTextDeactivateHook(ImGuiID id)
4007+
{
4008+
ImGuiContext& g = *GImGui;
4009+
ImGuiInputTextState* state = &g.InputTextState;
4010+
if (id == 0 || state->ID != id)
4011+
return;
4012+
g.InputTextDeactivatedState.ID = state->ID;
4013+
g.InputTextDeactivatedState.TextA.resize(state->CurLenA + 1);
4014+
memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->CurLenA + 1);
4015+
}
4016+
40024017
// Edit a string of text
40034018
// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
40044019
// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
@@ -4113,6 +4128,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
41134128
state = &g.InputTextState;
41144129
state->CursorAnimReset();
41154130

4131+
// 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)
4132+
InputTextDeactivateHook(state->ID);
4133+
41164134
// Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar)
41174135
// From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode)
41184136
const int buf_len = (int)strlen(buf);
@@ -4525,6 +4543,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
45254543
// Push records into the undo stack so we can CTRL+Z the revert operation itself
45264544
apply_new_text = state->InitialTextA.Data;
45274545
apply_new_text_length = state->InitialTextA.Size - 1;
4546+
value_changed = true;
45284547
ImVector<ImWchar> w_text;
45294548
if (apply_new_text_length > 0)
45304549
{
@@ -4638,10 +4657,24 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
46384657
{
46394658
apply_new_text = state->TextA.Data;
46404659
apply_new_text_length = state->CurLenA;
4660+
value_changed = true;
46414661
}
46424662
}
46434663
}
46444664

4665+
// Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
4666+
if (g.InputTextDeactivatedState.ID == id)
4667+
{
4668+
if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly)
4669+
{
4670+
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
4671+
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
4672+
value_changed |= (strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0);
4673+
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
4674+
}
4675+
g.InputTextDeactivatedState.ID = 0;
4676+
}
4677+
46454678
// Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
46464679
if (apply_new_text != NULL)
46474680
{
@@ -4669,7 +4702,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
46694702

46704703
// If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
46714704
ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
4672-
value_changed = true;
46734705
}
46744706

46754707
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)

0 commit comments

Comments
 (0)