Skip to content

Feature Request: Add Right-Click Cancellation for DragScalar Widgets (DragInt, DragFloat, etc.) #8564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Toikron opened this issue Apr 10, 2025 · 1 comment

Comments

@Toikron
Copy link

Toikron commented Apr 10, 2025

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 other DragScalar() 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 ESC often 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.
  • 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 DragScalar call, 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 DragScalar operation using a right-click while the drag is active (mouse button still held down).

    The desired behavior would be:

    1. User starts dragging a DragScalar widget (e.g., DragFloat).
    2. While the left mouse button is still held down and the drag is active, the user right-clicks.
    3. The value associated with the DragScalar widget immediately reverts to the value it had just before the drag operation started.
    4. The drag operation is cancelled (effectively, ClearActiveID() should be called internally for the widget).
    5. The change is not marked as an edit (no MarkItemEdited() call for the cancelled drag).
  • Alternatives Considered:
    The current alternative is to manually implement this logic around every DragScalar call using IsItemActivated(), 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 DragScalar function internally:

    • Store the initial value when the drag interaction starts (SetActiveID is called for the drag).
    • Inside DragScalar, after DragBehavior is called, check if the widget is still active (g.ActiveId == id) and if IsMouseClicked(ImGuiMouseButton_Right) is true.
    • If so, restore the saved initial value, set the internal value_changed flag to false to prevent MarkItemEdited, and call ClearActiveID().
    • Temporary state should ideally be stored within ImGuiContext to avoid issues with multi-context setups, rather than using static variables.

    This feature would significantly improve the usability of DragScalar widgets, particularly in editor-like applications.

Screenshots/Video:

Image

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
}
@ocornut
Copy link
Owner

ocornut commented Apr 10, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants