From 04b37d6d23de8583bf9efb9c13d3d0cd9ba5bcf0 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 7 Sep 2026 18:40:01 +0200 Subject: [PATCH] Added ImGuiItemFlags_MixedValue support for Drags, Sliders, InputScalar, InputText, RadioButton, Combo. (#5518, #5677, #6865) --- docs/CHANGELOG.txt | 5 ++++ imgui.cpp | 1 + imgui.h | 3 ++- imgui_demo.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++++- imgui_internal.h | 2 +- imgui_widgets.cpp | 38 +++++++++++++++++++--------- 6 files changed, 97 insertions(+), 15 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 39d871e4c..5008da1d3 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -74,6 +74,9 @@ Other Changes: hasn't been owned by another items. This is more correct and also necessary to avoid Drags/Sliders cancelling also triggering a cancel menu. (#8564, #9534) - Misc: + - Added `ImGuiItemFlags_MixedValue` flag for representing mixed/indeterminate values + in Checkbox(), RadioButton(), DragXXX(), SliderXXX(), InputXXX(), Combo() functions. + The scoped flag is designed for usage by advanced property editors. (#5518, #5677, #6865) - Fixed `GetBackgroundDrawList()`/`GetForegroundDrawList()` not being properly reset every frame when cumulated session time is very large (e.g. a few days). [@lailoken] - Added `IM_NODEBUGSTEP` helper to tag functions with `__declspec(non_user_code)` (MSVC) @@ -90,6 +93,8 @@ Other Changes: - Reworked assert in PushTexture() to allow convenience grace period of using a texture that was queue for destroy during the frame. Make it safe to stick to an existing texture during the frame. (#9528, #8465) +- Demo: + - Added `Widgets->Mixed Values` section. (#5518, #5677, #6865) - Backends: - QNX: added QNX Screen backend. (#9492) [@mgorchak-blackberry] - SDL2: fixed querying framebuffer scale/density when using Metal without diff --git a/imgui.cpp b/imgui.cpp index 2d8e2418f..2ef75ef1f 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4380,6 +4380,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) MouseCursor = ImGuiMouseCursor_Arrow; MouseStationaryTimer = 0.0f; + MixedValueLabel = "-"; InputTextPasswordFontBackupFlags = ImFontFlags_None; InputTextReactivateId = 0; TempInputId = 0; diff --git a/imgui.h b/imgui.h index 5fa9ca947..1329bad3e 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.93.0 WIP" -#define IMGUI_VERSION_NUM 19295 +#define IMGUI_VERSION_NUM 19296 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 @@ -1258,6 +1258,7 @@ 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_MixedValue = 1 << 9, // false // [BETA] Represent a mixed/indeterminate value. Replace value label with "-" and apply edits on validation. Only supported by some widgets: Checkbox, RadioButton, Sliders and Drags. //--------------------------------------------------------------------------------- // LiveEdit refers to applying edits to backing variables _while_ typing a value using the keyboard. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 314488d94..3c76b8d84 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -87,6 +87,7 @@ Index of this file: // [SECTION] DemoWindowWidgetsImages() // [SECTION] DemoWindowWidgetsListBoxes() // [SECTION] DemoWindowWidgetsLiveEdit() +// [SECTION] DemoWindowWidgetsMixedValues() // [SECTION] DemoWindowWidgetsMultiComponents() // [SECTION] DemoWindowWidgetsPlotting() // [SECTION] DemoWindowWidgetsProgressBars() @@ -2020,7 +2021,7 @@ static void DemoWindowWidgetsLiveEdit(ImGuiDemoWindowData* demo_data) { if (ImGui::TreeNode("Live Edit Flags")) { - IMGUI_DEMO_MARKER("Widgets/Live Edit Flgs"); + IMGUI_DEMO_MARKER("Widgets/Live Edit Flags"); ImGui::TextWrapped("Select whether to apply keyboard edits to backing variables _while_ typing."); @@ -2050,6 +2051,59 @@ static void DemoWindowWidgetsLiveEdit(ImGuiDemoWindowData* demo_data) } } +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsMixedValues() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsMixedValues() +{ + if (ImGui::TreeNode("Mixed Values")) + { + // This is designed for advanced property editors which are generally reusable and data-driven. + HelpMarker("Using ImGuiItemFlags_MixedValue."); + + static float items[3] = { 12.0f, 0.0f, 0.0f }; + float* item_ref = &items[0]; + + ImGui::SeparatorText("Scalar/Text Widgets"); + const bool is_mixed = memcmp(&items[0], &items[1], sizeof(float)) != 0 || memcmp(&items[0], &items[2], sizeof(float)) != 0; + + // Demonstrate Drags, Sliders, Inputs + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, is_mixed); + bool edited = false; + edited |= ImGui::DragFloat("DragFloat", item_ref); + edited |= ImGui::SliderFloat("SliderFloat", item_ref, 0.0f, 100.0f); + edited |= ImGui::InputFloat("InputFloat", item_ref, 1.0f); + if (edited) + for (float& item : items) + if (&item != item_ref) + item = *item_ref; + ImGui::PopItemFlag(); + + ImGui::Text("Underlying data:"); + ImGui::InputFloat("item 0 (ref)", &items[0]); + ImGui::InputFloat("item 1", &items[1]); + ImGui::InputFloat("item 2", &items[2]); + + // Demonstrate Checkbox() + // (this is automatically used by e.g. CheckboxFlags()) + ImGui::SeparatorText("Others Widgets"); + bool b_on = true, b_off = false; + ImGui::Checkbox("Checkbox On", &b_on); + ImGui::Checkbox("Checkbox Off", &b_off); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, true); + ImGui::Checkbox("Checkbox Mixed", &b_off); + ImGui::RadioButton("RadioButton Mixed", true); + ImGui::SameLine(); + ImGui::RadioButton("RadioButton Mixed##2", true); // Showing 2 radio buttons makes the example more clear + int combo_idx = 0; + ImGui::Combo("Combo", &combo_idx, "One\0Two\0Three\0"); + ImGui::PopItemFlag(); + + ImGui::TreePop(); + } +} + //----------------------------------------------------------------------------- // [SECTION] DemoWindowWidgetsMultiComponents() //----------------------------------------------------------------------------- @@ -2230,12 +2284,14 @@ static void DemoWindowWidgetsQueryingStatuses() }; static int item_type = 4; static bool item_disabled = false; + static bool item_mixedvalue = false; static bool liveedit_flags_override = false; static ImGuiItemFlags liveedit_flags = 0; ImGui::Combo("Item Type", &item_type, item_names, IM_COUNTOF(item_names), IM_COUNTOF(item_names)); ImGui::SameLine(); HelpMarker("Testing how various types of items are interacting with the IsItemXXX functions. Note that the bool return value of most ImGui function is generally equivalent to calling ImGui::IsItemHovered()."); ImGui::Checkbox("Item Disabled", &item_disabled); + ImGui::Checkbox("Item MixedValue", &item_mixedvalue); ImGui::Checkbox("Override LiveEdit:", &liveedit_flags_override); ImGui::SameLine(); if (!liveedit_flags_override) @@ -2260,6 +2316,8 @@ static void DemoWindowWidgetsQueryingStatuses() static char str[16] = {}; if (item_disabled) ImGui::BeginDisabled(true); + if (item_mixedvalue) + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, true); if (item_type == 0) { ImGui::Text("ITEM: Text"); } // Testing text items with no identifier/interaction if (item_type == 1) { ret = ImGui::Button("ITEM: Button"); } // Testing button if (item_type == 2) { ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); ret = ImGui::Button("ITEM: Button"); ImGui::PopItemFlag(); } // Testing button (with repeater) @@ -2343,6 +2401,8 @@ static void DemoWindowWidgetsQueryingStatuses() ImGui::PopItemFlag(); ImGui::PopItemFlag(); } + if (item_mixedvalue) + ImGui::PopItemFlag(); if (item_disabled) ImGui::EndDisabled(); @@ -4505,6 +4565,7 @@ static void DemoWindowWidgets(ImGuiDemoWindowData* demo_data) DemoWindowWidgetsImages(); DemoWindowWidgetsListBoxes(); DemoWindowWidgetsLiveEdit(demo_data); + DemoWindowWidgetsMixedValues(); DemoWindowWidgetsMultiComponents(); DemoWindowWidgetsPlotting(); DemoWindowWidgetsProgressBars(); diff --git a/imgui_internal.h b/imgui_internal.h index 19d7043b8..ca9ef8266 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -993,7 +993,6 @@ enum ImGuiItemFlagsPrivate_ { // Controlled by user ImGuiItemFlags_ReadOnly = 1 << 11, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. - ImGuiItemFlags_MixedValue = 1 << 12, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) ImGuiItemFlags_NoWindowHoverableCheck = 1 << 13, // false // Disable hoverable check in ItemHoverable() ImGuiItemFlags_AllowOverlap = 1 << 14, // false // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame. ImGuiItemFlags_NoNavDisableMouseHover = 1 << 15, // false // Nav keyboard/gamepad mode doesn't disable hover highlight (behave as if NavHighlightItemUnderNav==false). @@ -2305,6 +2304,7 @@ struct ImGuiContext ImVec2 WheelingAxisAvg; // Item/widgets state and tracking information + const char* MixedValueLabel; // Value replacement when displaying a mixed value. Default to "-" (Unreal uses "Multiple values", Unity uses "---"). May be interpreted as a format: must not contain single %. ImGuiID DebugDrawIdConflictsId; // Set when we detect multiple items with the same identifier ImGuiID DebugHookIdInfoId; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] ImGuiID HoveredId; // Hovered widget, filled during the frame diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 84718fb1f..fd433914c 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -1398,7 +1398,7 @@ bool ImGui::RadioButton(const char* label, bool active) RenderNavCursor(total_bb, id); const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); - if (active) + if (active && (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) == 0) { const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark)); @@ -2176,7 +2176,9 @@ bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(vo // Call the getter to obtain the preview string which is a parameter to BeginCombo() const char* preview_value = NULL; - if (*current_item >= 0 && *current_item < items_count) + if ((g.NextItemData.ItemFlagsSet | g.CurrentItemFlags) & ImGuiItemFlags_MixedValue) + preview_value = ""; + else if (*current_item >= 0 && *current_item < items_count) preview_value = getter(user_data, *current_item); // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. @@ -2815,8 +2817,9 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, MarkItemEdited(id); // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + const char* format_for_display = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) ? g.MixedValueLabel : format; char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format_for_display); if (g.LogEnabled) LogSetNextTextDecoration("{", "}"); RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); @@ -3422,8 +3425,9 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + const char* format_for_display = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) ? g.MixedValueLabel : format; char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format_for_display); if (g.LogEnabled) LogSetNextTextDecoration("{", "}"); RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); @@ -3576,8 +3580,9 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. // For the vertical slider we allow centered text to overlap the frame padding + const char* format_for_display = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) ? g.MixedValueLabel : format; char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format_for_display); RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false); @@ -3815,7 +3820,7 @@ bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImG // Only mark as edited if new value is different g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited; - bool value_changed = memcmp(&data_backup, p_data, data_type_size) != 0; + bool value_changed = memcmp(&data_backup, p_data, data_type_size) != 0 || (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue); if (value_changed) MarkItemEdited(id); return value_changed; @@ -3892,6 +3897,8 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); } } + if (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) + value_changed |= ret; // Step buttons if (has_step_buttons) @@ -4857,6 +4864,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; + const bool is_mixed = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; if (is_resizable) IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! @@ -5021,7 +5029,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Select the buffer to render. const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state; - bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0) && !is_mixed; // Password pushes a temporary font with only a fallback glyph if (is_password && !is_displaying_hint) @@ -5448,7 +5456,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEditOnInputText) { // Apply when modified - if (strcmp(state->TextSrc, buf) != 0) + if (strcmp(state->TextSrc, buf) != 0 || (is_mixed && validated)) { apply_new_text = state->TextSrc; apply_new_text_length = state->TextLen; @@ -5458,7 +5466,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ 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); + value_changed = (validated || clear_active_id || revert_edit) && (strcmp(state->TextSrc, buf) != 0 || (is_mixed && validated)); apply_new_text = value_changed ? state->TextSrc : NULL; apply_new_text_length = value_changed ? state->TextLen : 0; } @@ -5543,7 +5551,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Display hint when contents is empty // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368) - const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0) && !is_mixed; if (new_is_displaying_hint != is_displaying_hint) { if (is_password && !is_displaying_hint) @@ -5552,10 +5560,16 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_password && !is_displaying_hint) PushPasswordFont(); } - if (is_displaying_hint) + if (is_mixed && g.ActiveId != id && apply_new_text == NULL) + { + buf_display = g.MixedValueLabel; + buf_display_end = buf_display + strlen(g.MixedValueLabel); + render_cursor = render_selection = false; + } + else if (is_displaying_hint) { buf_display = hint; - buf_display_end = hint + ImStrlen(hint); + buf_display_end = buf_display + ImStrlen(buf_display); } else {