diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 94385d264..83b9c3983 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -207,6 +207,17 @@ Other Changes: default value of each `ImGuiItemFlags_LiveEditXXX`, because this is not expected to be a user preference but a programmer/widget preferences. And we strive to make the toolkit consistent. +- Drags: added value ladder (inspired by Houdini): hold middle mouse button over + a DragXXX widget to open a vertical ladder of step magnitudes: drag vertically + to select the step magnitude, horizontally to add/subtract steps of that size. + Release middle mouse button to confirm, press Escape to cancel and revert. + Also works with keyboard/gamepad navigation: activating the widget (Space) + opens the ladder, Up/Down select the step magnitude, Left/Right add/subtract + steps, Space confirms, Escape cancels and reverts. + Enable globally with `io.ConfigDragValueLadder` or per-widget with + `ImGuiSliderFlags_ValueLadder`. Not supported with ImGuiSliderFlags_Logarithmic. + Disabled when the widget would yield fewer than 3 rungs (e.g. small integer + ranges). (#9463) - Windows: - Clicking on a window's empty-space to move/focus a window checks for lack of mouse button ownership. This gives an additional opportunity diff --git a/imgui.cpp b/imgui.cpp index 906285d38..44002ef7d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1743,6 +1743,7 @@ ImGuiIO::ImGuiIO() ConfigInputTextCursorBlink = true; ConfigInputTextEnterKeepActive = false; ConfigDragClickToInputText = false; + ConfigDragValueLadder = false; ConfigColorEditFlags = ImGuiColorEditFlags_DefaultOptions_; // Current settings for ColorEdit/ColorPicker widgets. May be further edited by users, unless you also set ImGuiColorEditFlags_NoOptions. ConfigWindowsResizeFromEdges = true; ConfigWindowsMoveFromTitleBarOnly = false; diff --git a/imgui.h b/imgui.h index 150f1afa1..dd8934bda 100644 --- a/imgui.h +++ b/imgui.h @@ -2075,6 +2075,7 @@ enum ImGuiSliderFlags_ ImGuiSliderFlags_ClampZeroRange = 1 << 10, // Clamp even if min==max==0.0f. Otherwise due to legacy reason DragXXX functions don't clamp with those values. When your clamping limits are dynamic you almost always want to use it. ImGuiSliderFlags_NoSpeedTweaks = 1 << 11, // Disable keyboard modifiers altering tweak speed. Useful if you want to alter tweak speed yourself based on your own logic. ImGuiSliderFlags_ColorMarkers = 1 << 12, // DragScalarN(), SliderScalarN(): Draw R/G/B/A color markers on each component. + ImGuiSliderFlags_ValueLadder = 1 << 13, // DragXXX() only: hold middle mouse button over the widget (or activate it with keyboard/gamepad nav) to open a value ladder: drag vertically/Up/Down to select step magnitude, horizontally/Left/Right to add/subtract steps. (Shared behavior flag: io.ConfigDragValueLadder) ImGuiSliderFlags_AlwaysClamp = ImGuiSliderFlags_ClampOnInput | ImGuiSliderFlags_ClampZeroRange, //ImGuiSliderFlags_LiveEditOnInput = 1 << 13, // Shortcut to enable LiveEdit for this field. //ImGuiSliderFlags_NoLiveEditOnInput= 1 << 14, // Shortcut to disable LiveEdit for this field. @@ -2595,6 +2596,7 @@ struct ImGuiIO bool ConfigInputTextEnterKeepActive; // = false // [BETA] Pressing Enter will reactivate item and select all text (single-line only). Ctrl+Enter or Shift+Enter will deactivate normally. ImGuiColorEditFlags ConfigColorEditFlags; // = // Current settings for ColorEdit/ColorPicker widgets. Must have one bit of ImGuiColorEditFlags_DisplayMask_, one bit of ImGuiColorEditFlags_DataTypeMask_, one bit of ImGuiColorEditFlags_PickerMask_, one bit of ImGuiColorEditFlags_InputMask_. Defaults to ImGuiColorEditFlags_DefaultOptions_. May be further edited by users, unless you also set ImGuiColorEditFlags_NoOptions. bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigDragValueLadder; // = false // [BETA] Enable value ladder on DragXXX widgets: hold middle mouse button (or activate with keyboard/gamepad nav) to open it, drag vertically/Up/Down to select step magnitude, horizontally/Left/Right to add/subtract steps. (Also available per-widget via ImGuiSliderFlags_ValueLadder.) bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires ImGuiBackendFlags_HasMouseCursors for better mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. bool ConfigWindowsCopyContentsWithCtrlC; // = false // [EXPERIMENTAL] Ctrl+C copy the contents of focused window into the clipboard. Experimental because: (1) has known issues with nested Begin/End pairs (2) text output quality varies (3) text output is in submission order rather than spatial order. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index aac56fae8..ee4f60750 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -602,6 +602,8 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::SameLine(); HelpMarker("Pressing Enter will reactivate item and select all text (single-line only)."); ImGui::Checkbox("io.ConfigDragClickToInputText", &io.ConfigDragClickToInputText); ImGui::SameLine(); HelpMarker("Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving)."); + ImGui::Checkbox("io.ConfigDragValueLadder", &io.ConfigDragValueLadder); + ImGui::SameLine(); HelpMarker("Enable holding middle mouse button over DragXXX widgets to open a value ladder: drag vertically to select step magnitude, horizontally to add/subtract steps. Release to confirm, press Escape to cancel."); ImGui::Checkbox("io.ConfigMacOSXBehaviors", &io.ConfigMacOSXBehaviors); ImGui::SameLine(); HelpMarker("Swap Cmd<>Ctrl keys, enable various MacOS style behaviors."); ImGui::Text("Also see Style->Rendering for rendering options."); @@ -1892,6 +1894,8 @@ static void DemoWindowWidgetsDragsAndSliders() ImGui::CheckboxFlags("ImGuiSliderFlags_WrapAround", &flags, ImGuiSliderFlags_WrapAround); ImGui::SameLine(); HelpMarker("Enable wrapping around from max to min and from min to max (only supported by DragXXX() functions)"); ImGui::CheckboxFlags("ImGuiSliderFlags_ColorMarkers", &flags, ImGuiSliderFlags_ColorMarkers); + ImGui::CheckboxFlags("ImGuiSliderFlags_ValueLadder", &flags, ImGuiSliderFlags_ValueLadder); + ImGui::SameLine(); HelpMarker("Hold middle mouse button over the widget to open a value ladder: drag vertically to select step magnitude, horizontally to add/subtract steps. Release to confirm, press Escape to cancel. (only supported by DragXXX() functions)"); // Drags static float drag_f = 0.5f; @@ -1919,6 +1923,44 @@ static void DemoWindowWidgetsDragsAndSliders() ImGui::TreePop(); } + + if (ImGui::TreeNode("Value Ladder")) + { + IMGUI_DEMO_MARKER("Widgets/Value Ladder"); + // Demonstrate the value ladder: stepped value editing with explicit magnitude selection, inspired by Houdini. + // Enabled here per-widget with ImGuiSliderFlags_ValueLadder. Use io.ConfigDragValueLadder to enable it on all DragXXX widgets. + ImGui::TextWrapped("Hold middle mouse button over a widget to open a value ladder: drag vertically to select step magnitude, horizontally to add/subtract steps. Release to confirm, press Escape to cancel. With keyboard/gamepad navigation: activate the widget (Space) to open the ladder, Up/Down to select step magnitude, Left/Right to add/subtract steps."); + ImGui::Checkbox("io.ConfigDragValueLadder (enable on all DragXXX widgets)", &ImGui::GetIO().ConfigDragValueLadder); + ImGui::Spacing(); + + const ImGuiSliderFlags flags = ImGuiSliderFlags_ValueLadder; + static float pos = 0.0f; + static float angle = 0.0f; + static float precise = 0.5f; + static float clamped = 0.5f; + static int count = 50; + static double d_value = 1000.0; + static float f3[3] = { 1.0f, 2.0f, 3.0f }; + ImGui::DragFloat("Unbounded float", &pos, 1.0f, 0.0f, 0.0f, "%.2f", flags); + ImGui::SameLine(); HelpMarker("No min/max: the ladder shines here as any size of change is reachable, from 0.01 to 10000."); + ImGui::DragFloat("Angle (wrap 0 -> 360)", &angle, 1.0f, 0.0f, 360.0f, "%.1f deg", flags | ImGuiSliderFlags_WrapAround); + ImGui::SameLine(); HelpMarker("v_speed=1 preselects the '1' rung. Move up to '10' or '100' for coarse rotation."); + static float angle2 = 0.0f; + ImGui::DragFloat("Angle (%.2f precision)", &angle2, 1.0f, 0.0f, 360.0f, "%.2f deg", flags | ImGuiSliderFlags_WrapAround); + ImGui::SameLine(); HelpMarker("Same as above but displaying two digits after the decimal point, adding a 0.01 rung."); + ImGui::DragFloat("High precision", &precise, 0.001f, 0.0f, 0.0f, "%.4f", flags); + ImGui::SameLine(); HelpMarker("The smallest rung comes from the display format: %.4f offers steps down to 0.0001."); + ImGui::DragFloat("Clamped (0 -> 1)", &clamped, 0.005f, 0.0f, 1.0f, "%.3f", flags); + ImGui::DragInt("Integer (0 -> 1000)", &count, 1.0f, 0, 1000, "%d", flags); + ImGui::SameLine(); HelpMarker("Integer types offer no rung smaller than 1."); + static int small_count = 5; + ImGui::DragInt("Integer (0 -> 100)", &small_count, 1.0f, 0, 100, "%d", flags); + ImGui::SameLine(); HelpMarker("No ladder here: this range yields fewer than 3 rungs, so the ladder is disabled (it would provide little to no value)."); + ImGui::DragScalar("Double (unbounded)", ImGuiDataType_Double, &d_value, 1.0f, NULL, NULL, "%.3f", flags); + ImGui::DragFloat3("DragFloat3", f3, 0.01f, 0.0f, 0.0f, "%.2f", flags); + ImGui::SameLine(); HelpMarker("Multi-component widgets get a ladder on each component."); + ImGui::TreePop(); + } } //----------------------------------------------------------------------------- diff --git a/imgui_internal.h b/imgui_internal.h index c0b850eaf..dd63a2d7b 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -190,6 +190,7 @@ struct ImGuiTableColumnsSettings; // Storage for a column .ini settings struct ImGuiTreeNodeStackData; // Temporary storage for TreeNode(). struct ImGuiTypingSelectState; // Storage for GetTypingSelectRequest() struct ImGuiTypingSelectRequest; // Storage for GetTypingSelectRequest() (aimed to be public) +struct ImGuiValueLadderState; // Storage for value ladder (middle mouse button drag on DragXXX widgets) struct ImGuiWindow; // Storage for one window struct ImGuiWindowDockStyle; // Storage for window-style data which needs to be stored for docking purpose struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) @@ -1876,6 +1877,27 @@ struct IMGUI_API ImGuiTypingSelectState void Clear() { SearchBuffer[0] = 0; SingleCharModeLock = false; } // We preserve remaining data for easier debugging }; +//----------------------------------------------------------------------------- +// [SECTION] Value ladder support +//----------------------------------------------------------------------------- + +// Storage for value ladder: stepped value editing with explicit magnitude selection, activated by holding +// middle mouse button over a DragXXX widget. See io.ConfigDragValueLadder and ImGuiSliderFlags_ValueLadder. +struct IMGUI_API ImGuiValueLadderState +{ + ImGuiID SourceId; // Widget owning the ladder (0 when inactive) + int RungCount; + int RungIndex; // Currently selected magnitude (index into RungSteps[]) + double RungSteps[16]; // Step value of each rung, ascending (displayed with largest at top) + ImVec2 Pos; // Top-left corner of the ladder overlay, in screen space + ImVec2 Size; + float RowHeight; + float PendingAccumX; // Horizontal mouse movement not yet converted into steps + float StepDeltaToApply; // Computed by ValueLadderUpdate(), consumed by DragBehaviorT() + + ImGuiValueLadderState() { memset((void*)this, 0, sizeof(*this)); } +}; + //----------------------------------------------------------------------------- // [SECTION] Columns support //----------------------------------------------------------------------------- @@ -2772,6 +2794,7 @@ struct ImGuiContext ImVector ClipboardHandlerData; // If no custom clipboard handler is defined ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once ImGuiTypingSelectState TypingSelectState; // State for GetTypingSelectRequest() + ImGuiValueLadderState ValueLadderState; // State for value ladder (middle mouse button drag on DragXXX widgets) // Platform support ImGuiPlatformImeData PlatformImeData; // Data updated by current frame. Will be applied at end of the frame. For some backends, this is required to have WantVisible=true in order to receive text message. @@ -4096,6 +4119,7 @@ namespace ImGui IMGUI_API const ImGuiDataTypeInfo* DataTypeGetInfo(ImGuiDataType data_type); IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format); IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg_1, const void* arg_2); + IMGUI_API double DataTypeAsDouble(ImGuiDataType data_type, const void* p_data); IMGUI_API bool DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty = NULL); IMGUI_API int DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2); IMGUI_API bool DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 50da35265..d1ec93f28 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -2263,6 +2263,7 @@ bool ImGui::Combo(const char* label, int* current_item, const char* items_separa // - DataTypeGetInfo() // - DataTypeFormatString() // - DataTypeApplyOp() +// - DataTypeAsDouble() // - DataTypeApplyFromText() // - DataTypeCompare() // - DataTypeClamp() @@ -2376,6 +2377,26 @@ void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const IM_ASSERT(0); } +double ImGui::DataTypeAsDouble(ImGuiDataType data_type, const void* p_data) +{ + switch (data_type) + { + case ImGuiDataType_S8: return (double)*(const ImS8*)p_data; + case ImGuiDataType_U8: return (double)*(const ImU8*)p_data; + case ImGuiDataType_S16: return (double)*(const ImS16*)p_data; + case ImGuiDataType_U16: return (double)*(const ImU16*)p_data; + case ImGuiDataType_S32: return (double)*(const ImS32*)p_data; + case ImGuiDataType_U32: return (double)*(const ImU32*)p_data; + case ImGuiDataType_S64: return (double)*(const ImS64*)p_data; + case ImGuiDataType_U64: return (double)*(const ImU64*)p_data; + case ImGuiDataType_Float: return (double)*(const float*)p_data; + case ImGuiDataType_Double: return *(const double*)p_data; + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return 0.0; +} + // User can input math operators (e.g. +100) to edit a numerical values. // NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess.. bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty) @@ -2546,6 +2567,164 @@ TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, // - DragInt4() // - DragIntRange2() //------------------------------------------------------------------------- +// Value ladder (hold middle mouse button over a Drag widget): [Internal] +// - ValueLadderCalcRange() +// - ValueLadderCalcRungs() +// - ValueLadderStart() +// - ValueLadderUpdate() +// - ValueLadderRender() +//------------------------------------------------------------------------- + +// Compute (max - min) as double. Integer overflow is clamped by DataTypeApplyOp(). +static double ValueLadderCalcRange(ImGuiDataType data_type, const void* p_min, const void* p_max) +{ + ImGuiDataTypeStorage range_storage; + ImGui::DataTypeApplyOp(data_type, '-', &range_storage, p_max, p_min); + return ImGui::DataTypeAsDouble(data_type, &range_storage); +} + +// Compute the rung magnitudes as a range of powers of ten and return the rung count. +// Rungs go from the smallest step representable with the display format (1 for integers) up to 10000. +// For a bounded widget, rungs covering the whole range or more are dropped: capped at the largest power of ten smaller than the range span. +static int ValueLadderCalcRungs(ImGuiDataType data_type, const void* p_min, const void* p_max, const char* format, int* out_min_exp) +{ + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + int min_exp = -ImClamp(decimal_precision, 0, 9); + int max_exp = 4; + if (p_min != NULL && p_max != NULL) + { + const double range = ValueLadderCalcRange(data_type, p_min, p_max); + if (range > 0.0 && range < (double)FLT_MAX) + { + int range_exp = (int)ImFloor(ImLog(range) / ImLog(10.0)); + if (ImPow(10.0, (double)range_exp) >= range) + range_exp--; + max_exp = ImClamp(range_exp, is_floating_point ? -9 : 0, max_exp); + if (max_exp < min_exp) + min_exp = max_exp; // Add steps smaller than the display format rather than ending up with no rungs at all. + } + } + *out_min_exp = min_exp; + return ImMin(max_exp - min_exp + 1, (int)IM_COUNTOF(GImGui->ValueLadderState.RungSteps)); +} + +// Initiate a value ladder for the given widget: build magnitude rungs and lay out the overlay so the selected rung is centered on anchor_pos. +static void ValueLadderStart(ImGuiWindow* window, ImGuiID id, ImGuiDataType data_type, const void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImVec2 anchor_pos) +{ + using namespace ImGui; + ImGuiContext& g = *GImGui; + ImGuiValueLadderState* ladder = &g.ValueLadderState; + ladder->SourceId = id; + ladder->PendingAccumX = 0.0f; + ladder->StepDeltaToApply = 0.0f; + + int min_exp; + ladder->RungCount = ValueLadderCalcRungs(data_type, p_min, p_max, format, &min_exp); + for (int n = 0; n < ladder->RungCount; n++) + ladder->RungSteps[n] = ImPow(10.0, (double)(min_exp + n)); + + // Start on the rung nearest to the widget's tweak speed (1 when unspecified). + int rung_index = -min_exp; + if (v_speed > 0.0f) + rung_index = (int)ImFloor(ImLog(v_speed) / ImLog(10.0f) + 0.5f) - min_exp; + ladder->RungIndex = ImClamp(rung_index, 0, ladder->RungCount - 1); + + // Lay out a vertical stack with the largest step at top and the selected rung centered on the anchor, clamped into the viewport. + // Each rung is two text lines tall so the selected rung can display the step value above the current widget value. + float max_label_width = 0.0f; + for (int n = 0; n < ladder->RungCount; n++) + { + char label[32]; + ImFormatString(label, IM_COUNTOF(label), "%g", ladder->RungSteps[n]); + max_label_width = ImMax(max_label_width, CalcTextSize(label).x); + } + char value_buf[64]; + const int value_buf_len = DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); + max_label_width = ImMax(max_label_width, CalcTextSize(value_buf, value_buf + value_buf_len).x); + ladder->RowHeight = g.FontSize * 3.0f + g.Style.FramePadding.y * 2.0f; + ladder->Size = ImVec2(max_label_width + g.Style.FramePadding.x * 4.0f, ladder->RowHeight * ladder->RungCount); + ladder->Pos.x = anchor_pos.x - ladder->Size.x * 0.5f; + ladder->Pos.y = anchor_pos.y - ladder->RowHeight * 0.5f - (float)(ladder->RungCount - 1 - ladder->RungIndex) * ladder->RowHeight; + const ImRect viewport_rect = window->Viewport->GetMainRect(); + ladder->Pos = ImClamp(ladder->Pos, viewport_rect.Min, ImMax(viewport_rect.Min, viewport_rect.Max - ladder->Size)); +} + +// Update rung selection and convert inputs into steps. +// Mouse: vertical position selects the magnitude, horizontal movement adds/subtracts steps. +// Keyboard/Gamepad: Up/Down select the magnitude, Left/Right add/subtract steps. +static void ValueLadderUpdate() +{ + using namespace ImGui; + ImGuiContext& g = *GImGui; + ImGuiValueLadderState* ladder = &g.ValueLadderState; + ladder->StepDeltaToApply = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + { + const int rung_delta = -(int)GetNavTweakPressedAmount(ImGuiAxis_Y); // Up = larger steps + ladder->RungIndex = ImClamp(ladder->RungIndex + rung_delta, 0, ladder->RungCount - 1); + ladder->StepDeltaToApply = (float)((double)GetNavTweakPressedAmount(ImGuiAxis_X) * ladder->RungSteps[ladder->RungIndex]); + return; + } + if (!IsMousePosValid()) + return; + + // Vertical position selects the magnitude (largest step at top). + const int display_row = ImClamp((int)((g.IO.MousePos.y - ladder->Pos.y) / ladder->RowHeight), 0, ladder->RungCount - 1); + ladder->RungIndex = ladder->RungCount - 1 - display_row; + + // Horizontal movement adds/subtracts one step of the selected magnitude per threshold-worth of pixels. + ladder->PendingAccumX += g.IO.MouseDelta.x; + const float pixels_per_step = ImMax(4.0f, ImFloor(g.FontSize * 0.75f)); + const int steps = (int)(ladder->PendingAccumX / pixels_per_step); + ladder->PendingAccumX -= (float)steps * pixels_per_step; + ladder->StepDeltaToApply = (float)((double)steps * ladder->RungSteps[ladder->RungIndex]); +} + +// Draw the ladder overlay and current value on the viewport foreground, so it cannot interfere with inputs. +static void ValueLadderRender(ImGuiWindow* window, ImGuiDataType data_type, const void* p_data, const char* format) +{ + using namespace ImGui; + ImGuiContext& g = *GImGui; + ImGuiValueLadderState* ladder = &g.ValueLadderState; + ImDrawList* draw_list = GetForegroundDrawList(window->Viewport); + + // Widen the ladder when the current value text outgrows it (never shrink to avoid jittering), keeping it centered. + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format); + const ImVec2 value_size = CalcTextSize(value_buf, value_buf_end); + const float min_width = value_size.x + g.Style.FramePadding.x * 4.0f; + if (ladder->Size.x < min_width) + { + ladder->Pos.x -= (min_width - ladder->Size.x) * 0.5f; + ladder->Size.x = min_width; + const ImRect viewport_rect = window->Viewport->GetMainRect(); + ladder->Pos = ImClamp(ladder->Pos, viewport_rect.Min, ImMax(viewport_rect.Min, viewport_rect.Max - ladder->Size)); + } + + const ImRect bb(ladder->Pos, ladder->Pos + ladder->Size); + draw_list->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_PopupBg), g.Style.PopupRounding); + for (int n = 0; n < ladder->RungCount; n++) + { + const int rung_n = ladder->RungCount - 1 - n; // Largest step at top + const bool is_selected = (rung_n == ladder->RungIndex); + const ImVec2 row_min(bb.Min.x, bb.Min.y + (float)n * ladder->RowHeight); + const ImVec2 row_max(bb.Max.x, row_min.y + ladder->RowHeight); + if (is_selected) + draw_list->AddRectFilled(row_min, row_max, GetColorU32(ImGuiCol_FrameBgActive), g.Style.PopupRounding); + if (n > 0) + draw_list->AddLine(row_min, ImVec2(row_max.x, row_min.y), GetColorU32(ImGuiCol_Separator)); + char label[32]; + ImFormatString(label, IM_COUNTOF(label), "%g", ladder->RungSteps[rung_n]); + const ImVec2 label_size = CalcTextSize(label); + // Selected rung shows step value above and current widget value below; others show the step value vertically centered. + const float label_y = is_selected ? (row_min.y + ((row_max.y - row_min.y) - g.FontSize * 2.0f) * 0.5f) : (row_min.y + ((row_max.y - row_min.y) - g.FontSize) * 0.5f); + draw_list->AddText(ImVec2(row_min.x + ((row_max.x - row_min.x) - label_size.x) * 0.5f, label_y), GetColorU32(ImGuiCol_Text), label); + if (is_selected) + draw_list->AddText(ImVec2(row_min.x + ((row_max.x - row_min.x) - value_size.x) * 0.5f, label_y + g.FontSize), GetColorU32(ImGuiCol_Text), value_buf, value_buf_end); + } + draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), g.Style.PopupRounding); +} // This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls) template @@ -2564,7 +2743,14 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings float adjust_delta = 0.0f; - if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + const bool is_ladder = (g.ValueLadderState.SourceId != 0 && g.ValueLadderState.SourceId == g.ActiveId); + if (is_ladder) + { + // Value ladder: delta computed by ValueLadderUpdate() with an explicit magnitude, bypass v_speed and speed tweaks. + adjust_delta = g.ValueLadderState.StepDeltaToApply; + g.ValueLadderState.StepDeltaToApply = 0.0f; + } + else if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) { adjust_delta = g.IO.MouseDelta[axis]; if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) @@ -2581,10 +2767,11 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor; v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); } - adjust_delta *= v_speed; + if (!is_ladder) + adjust_delta *= v_speed; // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter. - if (axis == ImGuiAxis_Y) + if (axis == ImGuiAxis_Y && !is_ladder) adjust_delta = -adjust_delta; // For logarithmic use our range is effectively 0..1 so scale the delta into that range @@ -2697,9 +2884,20 @@ bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v // Things we can do easily outside the DragBehaviorT<> template, saves code generation. ImGuiContext& g = *GImGui; + ImGuiValueLadderState* ladder = (id != 0 && g.ValueLadderState.SourceId == id) ? &g.ValueLadderState : NULL; if (g.ActiveId == id) { - if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) + if (ladder != NULL && Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, id)) + { + // Escape cancels the value ladder and reverts to the initial value. + const bool value_changed = (DataTypeCompare(data_type, p_v, g.ActiveIdValueOnActivation.Data) != 0); + memcpy(p_v, g.ActiveIdValueOnActivation.Data, DataTypeGetInfo(data_type)->Size); + ClearActiveID(); + ladder->SourceId = 0; + return value_changed; + } + // Value ladder driven by mouse is held with the middle mouse button. + if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[(ladder != NULL) ? ImGuiMouseButton_Middle : ImGuiMouseButton_Left]) ClearActiveID(); else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) ClearActiveID(); @@ -2714,9 +2912,15 @@ bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v } } if (g.ActiveId != id) + { + if (ladder != NULL) + ladder->SourceId = 0; return false; + } if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) return false; + if (ladder != NULL) + ValueLadderUpdate(); switch (data_type) { @@ -2785,9 +2989,21 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, // Tabbing or Ctrl+Click on Drag turns it into an InputText const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id)); - const bool make_active = (clicked || double_clicked || g.NavActivateId == id); + bool ladder_enabled = ((flags & ImGuiSliderFlags_ValueLadder) || g.IO.ConfigDragValueLadder) && !(flags & ImGuiSliderFlags_Logarithmic); + const bool ladder_requested = ladder_enabled && ((hovered && IsMouseClicked(ImGuiMouseButton_Middle, ImGuiInputFlags_None, id)) || g.NavActivateId == id); + if (ladder_requested) + { + // The ladder provides little to no value when only very few magnitudes are reachable (e.g. small integer ranges). + int rung_min_exp; + if (ValueLadderCalcRungs(data_type, p_min, p_max, format, &rung_min_exp) < 3) + ladder_enabled = false; + } + const bool ladder_clicked = ladder_enabled && hovered && IsMouseClicked(ImGuiMouseButton_Middle, ImGuiInputFlags_None, id); + const bool make_active = (clicked || double_clicked || ladder_clicked || g.NavActivateId == id); if (make_active && (clicked || double_clicked)) SetKeyOwner(ImGuiKey_MouseLeft, id); + if (make_active && ladder_clicked) + SetKeyOwner(ImGuiKey_MouseMiddle, id); if (make_active && temp_input_allowed) if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) temp_input_is_active = true; @@ -2811,6 +3027,14 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, SetFocusID(id, window); FocusWindow(window); g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (ladder_clicked) + ValueLadderStart(window, id, data_type, p_data, v_speed, p_min, p_max, format, g.IO.MousePos); + else if (ladder_enabled && g.NavActivateId == id) + { + // Keyboard/Gamepad activation (Space): open the ladder over the widget, claim Up/Down for magnitude selection. + ValueLadderStart(window, id, data_type, p_data, v_speed, p_min, p_max, format, frame_bb.GetCenter()); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + } } } @@ -2848,6 +3072,10 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, 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); + // Value ladder overlay (drawn on the viewport foreground so it may spill outside the window) + if (g.ActiveId == id && g.ValueLadderState.SourceId == id) + ValueLadderRender(window, data_type, p_data, format); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); return value_changed; }