Feature Request: Add Right-Click Cancellation for DragScalar Widgets (DragInt, DragFloat, etc.)
Version/Branch of Dear ImGui:
Version 1.92, Branch: master/docking
Back-ends:
imgui_widgets.cpp
Compiler, OS:
All
Full config/build information:
No response
Details:
-
Problem Description: Currently, when using
DragInt(),DragFloat(), or otherDragScalar()based widgets, there is no built-in mechanism to easily cancel the drag operation and revert the value to its state before the drag started.- Pressing
ESCoften confirms the current value or closes a parent popup/window, rather than cancelling the drag. - Right-clicking during a drag operation currently has no default effect for cancelling the drag.
- Pressing
-
Motivation / Goal: This forces developers who need this cancellation behavior (which is common in many editing applications, like Blender's property dragging) to implement manual state tracking around each
DragScalarcall, which adds boilerplate code. It is also inconvenient for users who accidentally start a drag or drag too far, as they have no simple way to abort other than potentially confirming an unwanted value and then manually undoing or correcting it. We need a simple, built-in way to cancel the drag and revert the value. -
Proposed Solution: I propose adding a built-in mechanism to cancel a
DragScalaroperation using a right-click while the drag is active (mouse button still held down).The desired behavior would be:
- User starts dragging a
DragScalarwidget (e.g.,DragFloat). - While the left mouse button is still held down and the drag is active, the user right-clicks.
- The value associated with the
DragScalarwidget immediately reverts to the value it had just before the drag operation started. - The drag operation is cancelled (effectively,
ClearActiveID()should be called internally for the widget). - The change is not marked as an edit (no
MarkItemEdited()call for the cancelled drag).
- User starts dragging a
-
Alternatives Considered: The current alternative is to manually implement this logic around every
DragScalarcall usingIsItemActivated(),IsItemActive(),IsMouseClicked(ImGuiMouseButton_Right), storing the initial value, and restoring it. This works but adds significant boilerplate code, especially in interfaces with many draggable numeric fields. A built-in solution would be much cleaner and provide a consistent user experience. -
Additional Context / Implementation Idea: A possible implementation approach could involve modifying the
DragScalarfunction internally:- Store the initial value when the drag interaction starts (
SetActiveIDis called for the drag). - Inside
DragScalar, afterDragBehavioris called, check if the widget is still active (g.ActiveId == id) and ifIsMouseClicked(ImGuiMouseButton_Right)is true. - If so, restore the saved initial value, set the internal
value_changedflag tofalseto preventMarkItemEdited, and callClearActiveID(). - Temporary state should ideally be stored within
ImGuiContextto avoid issues with multi-context setups, rather than using static variables.
This feature would significantly improve the usability of
DragScalarwidgets, particularly in editor-like applications. - Store the initial value when the drag interaction starts (
Screenshots/Video:
Minimal, Complete and Verifiable Example code:
Here is my working example in imgui_widgets.cpp you can copy paste for testing it:
// Find the existing DragScalar function in the imgui_widgets.cpp file and replace it with the following
// [MY MODIFICATION START] - Static variables for cancellation feature (Warning: Potential reentrancy/multi-context issues)
#include <imgui.h> // Added for ImGuiDataTypeStorage
#include "imgui_internal.h" // Added for ImGuiContext (to use GImGui)
static ImGuiDataTypeStorage GDragScalarStartValue; // To store the drag start value
static ImGuiID GDragScalarActiveID = 0; // To track which DragScalar is active
// [MY MODIFICATION END]
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional.
// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
const float w = CalcItemWidth();
const ImVec2 label_size = CalcTextSize(label, NULL, true);
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
ItemSize(total_bb, style.FramePadding.y);
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
return false;
// Default format string when passing NULL
if (format == NULL)
format = DataTypeGetInfo(data_type)->PrintFmt;
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
if (!temp_input_is_active)
{
// Tabbing or CTRL-clicking 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);
if (make_active && (clicked || double_clicked))
SetKeyOwner(ImGuiKey_MouseLeft, 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;
// (Optional) simple click (without moving) turns Drag into an InputText
if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
{
g.NavActivateId = id;
g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
temp_input_is_active = true;
}
// Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
// [MY MODIFICATION START] - Store initial value on activation for drag cancellation
if (make_active)
{
memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size); // Keep original backup too if needed elsewhere
// Store value if activating drag (not text input)
if (!temp_input_is_active)
{
memcpy(&GDragScalarStartValue, p_data, DataTypeGetInfo(data_type)->Size);
GDragScalarActiveID = id;
}
}
// [MY MODIFICATION END]
if (make_active && !temp_input_is_active)
{
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
}
}
if (temp_input_is_active)
{
// [MY MODIFICATION START] - Reset drag tracking if we switch to text input
if (GDragScalarActiveID == id)
GDragScalarActiveID = 0;
// [MY MODIFICATION END]
// Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
bool clamp_enabled = false;
if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
{
const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
clamp_enabled = true;
else if (clamp_range_dir == 0)
clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
}
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
}
// Draw frame
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
RenderNavCursor(frame_bb, id);
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding);
// Drag behavior
bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
// [MY MODIFICATION START] Right-click cancellation check
// Check for cancellation *after* DragBehavior potentially modified the value this frame
bool cancelled = false;
if (GDragScalarActiveID == id && g.ActiveId == id) // If our specific drag scalar is active
{
// Use 'false' for the second parameter of IsMouseClicked to potentially catch click even if mouse moved slightly off widget.
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right, false))
{
memcpy(p_data, &GDragScalarStartValue, DataTypeGetInfo(data_type)->Size); // Restore value
ClearActiveID(); // Cancel drag input processing
value_changed = false; // Ensure MarkItemEdited is not called
cancelled = true; // Flag that cancellation happened
// GDragScalarActiveID will be reset below
}
}
// Reset tracking if drag naturally ends or was just cancelled
if (GDragScalarActiveID == id && g.ActiveId != id)
{
GDragScalarActiveID = 0;
}
// [MY MODIFICATION END]
if (value_changed) // This check now happens *after* potential cancellation override
MarkItemEdited(id);
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
char value_buf[64];
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
if (g.LogEnabled)
LogSetNextTextDecoration("{", "}");
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
return value_changed; // Return the potentially overridden value_changed
}
Thanks for your suggestion. The problem with adding right-click it that it could interfere with existing code using right-click for other things. Even though I agree it's not much likely to be used while item is active.
I will first add support for this mapped to the Escape key. I realize it's rather awkward, but this way we at least have the 99% code ready and we can more easily add right-click later if we figure out a design that can work or decide that adding right-click is fine.