Skip to content
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

Separating click and double-click events #8337

Open
wusikijeronii opened this issue Jan 21, 2025 · 8 comments
Open

Separating click and double-click events #8337

wusikijeronii opened this issue Jan 21, 2025 · 8 comments
Labels

Comments

@wusikijeronii
Copy link

wusikijeronii commented Jan 21, 2025

Version/Branch of Dear ImGui:

v1.91.8 WIP master

Back-ends:

custom

Compiler, OS:

Windows 11 + clang 18.1.8

Full config/build information:

No response

Details:

Hi,
I hope I'm not being too much of a bother ;)

I have a situation where I need to distinguish single click and double click events separately for an element. However, no matter what I try, I always run into one of these issues:

  • After a double click, the single click event also fires.
  • Instead of a double click event, I receive two separate single click events.

As a workaround, I implemented a structure similar to ImGui’s mouse state tracking. If MouseDoubleClickTime passes without a second click, I return a clickPended state. This works for me, but effectively, it's a simplified duplicate of ImGui’s internal context doing the same thing.

It would be great to have a native way to distinguish single clicks from double clicks, so they don't interfere with each other.

Would it be possible to introduce a built-in mechanism in ImGui to separate click and double-click logic more cleanly?

Screenshots/Video:

No response

Minimal, Complete and Verifiable Example code:

    extern APPLIB_API struct Context
    {
        ImGuiMouseCursor last_cursor = ImGuiMouseCursor_None;
        struct MouseData
        {
            int click_count = 0;
            f64 last_click_time = 0.0;
            bool is_dragging = false;
            MouseAction action = MouseAction::none;
        } mouse_data;
    } g_Ctx;

    inline bool isClicked() { return g_Ctx.mouse_data.action == MouseAction::click; }

    inline bool isClickedPended() { return g_Ctx.mouse_data.action == MouseAction::clickPended; }

    inline bool isDragStart() { return g_Ctx.mouse_data.action == MouseAction::dragStart; }

    inline bool isDragEnd() { return g_Ctx.mouse_data.action == MouseAction::dragEnd; }

    inline bool isDragging() { return g_Ctx.mouse_data.is_dragging; }

    inline bool isDoubleClicked() { return g_Ctx.mouse_data.action == MouseAction::doubleClick; }

    void updateMouseData()
    {
        static f64 thresold = ImGui::GetIO().MouseDoubleClickTime;
        auto &m = g_Ctx.mouse_data;
        m.action = MouseAction::none;
        if (ImGui::IsMouseClicked(0))
        {
            m.click_count++;
            m.last_click_time = window::getTime();
            m.is_dragging = false;
            m.action = MouseAction::click;
            window::pushEmptyEvent();
        }
        else if (m.click_count > 0)
        {
            ImVec2 dragDelta = ImGui::GetMouseDragDelta(0);
            if (!m.is_dragging && dragDelta != ImVec2())
            {
                m.is_dragging = true;
                m.action = MouseAction::dragStart;
            }
            window::pushEmptyEvent();
        }

        if (!m.is_dragging && m.click_count > 0)
        {
            if ((window::getTime() - m.last_click_time) > thresold)
            {
                if (m.click_count == 1)
                    m.action = MouseAction::clickPended;
                else if (m.click_count >= 2)
                    m.action = MouseAction::doubleClick;
                m.click_count = 0;
            }
        }
        if (m.is_dragging)
        {
            if (ImGui::IsMouseReleased(0))
            {
                m.is_dragging = false;
                m.click_count = 0;
                m.action = MouseAction::dragEnd;
            }
        }
    }

Here I also handle dragging since the click event is also triggered if the mouse is released (But ImGui has DragAPI to get around this, but that's just for the record.)
Example Callee code:

                        if (uikit::isDragStart())
                            logTrace("dragStart");
                        else if (uikit::isDragging())
                            logTrace("dragging");
                        else if (uikit::isDragEnd())
                            logTrace("dragEnd");
                        else if (uikit::isDoubleClicked())
                            logTrace("Double clicked");
                        else if (uikit::isClickedPended())
                            logTrace("Click");
@wusikijeronii
Copy link
Author

I took another look at how the other programs work. This is how I get a delay in a normal click. But in other programs, the click is triggered first. Then a double click. I guess I don't need such a complicated one. I will do as in other programs.

@ocornut
Copy link
Owner

ocornut commented Jan 22, 2025

It's very rare for application to make use of single click with "destructive" side-effects when a double-click is expected.
That said, regardless of rarity, I believe we should ensure that it is trivial to access this data in ImGui.
Basically we want a way to easily access a delayed "mouse release" event. It might just be a matter of storing a MouseReleasedTime[] array in ImGuiIO. In fact, you can probably simplify your code to:

const double time = ImGui::GetTime();
if (ImGui::IsMouseReleased(0))
    my_mouse_released_time[0] = time;

I believe that's the only storage you actually need that's missing.

Then something like:

bool IsMouseReleasedPending(ImGuiMouseButton mouse_button, float delay)
{
   const float time_since_release = current_time - my_mouse_released_time[0];
   const float dt = ImGui::GetIO().DeltaTime;
   return !IsMouseDown(0) && time_since_release - dt < delay && time_since_release >= delay)
};

use with IsMouseReleasedPending(ImGuiMouseButton_Left, io.MouseDoubleClickTime);

I would like to confirm that this can be used and I might add the data/function.

@wusikijeronii
Copy link
Author

wusikijeronii commented Jan 22, 2025

@ocornut Well, personally, I've lost the need. Maybe I should explain why that's a bad thing. The code I provided actually adds a delay after release, and as a result, the trigger actually occurs after about 0.4-0.5 sec. I changed it so that it updates at release but using the time received during the click. And even in this case there is a delay after the click. It's very noticeable. The UI becomes terribly unsmooth. I looked at other software: Maxon Cinema 4D, Blender, Affinity Photo. In Affinity Photo, the action always happens after release. In Cinema 4D and Blender, it depends on the context. If the object is selected, the event after release is processed. If the object is not selected, the event after the click is triggered. I think that my task is non-trivial and depends on the context of use. That's why I don't think it's worth making it a part of ImGUI API.

@ocornut
Copy link
Owner

ocornut commented Jan 22, 2025

The code I provided actually adds a delay after release, and as a result, the trigger actually occurs after about 0.4-0.5 sec.

Yes that's obviously the only way to distinguish single click from double-click.

If the object is selected, the event after release is processed. If the object is not selected, the event after the click is triggered

It's even most complicated than that usually (multi-select api needs to change quite a few edge cases) but yeah that's the basics of it.

That's why I don't think it's worth making it a part of ImGUI API.

Even if rare, some apps use the delayed reactions on mouse release. MS Explorer uses that for renaming.
If we can provide this signal in the lib we should. I believe the code I posted should be enough. I'll test it someday and will try to add it.

@wusikijeronii
Copy link
Author

You are probably right. And by the way, I now also think that this is quite enough. We can look at pending and, depending on the result, check the number of clicks or wait further. That is, it will be much easier to implement the necessary application logic with such the feature.

@ocornut
Copy link
Owner

ocornut commented Jan 22, 2025

I have pushed fdca6c0 with now adds a IsMouseReleasedWithDelay() helper function.

Example:

ImGui::Button("XXXX", ImVec2(100, 100));
if (ImGui::IsItemHovered())
{
    if (ImGui::IsMouseClicked(0))
        IMGUI_DEBUG_LOG("IsMouseClicked\n");
    if (ImGui::IsMouseReleased(0))
        IMGUI_DEBUG_LOG("IsMouseReleased\n");
    if (ImGui::IsMouseDoubleClicked(0))
        IMGUI_DEBUG_LOG("IsMouseDoubleClicked\n");
    if (ImGui::IsMouseReleasedWithDelay(0, io.MouseDoubleClickTime))
        IMGUI_DEBUG_LOG("IsMouseReleasedWithDelay %f, count %d\n", io.MouseDoubleClickTime, io.MouseClickedLastCount[0]);
    if (ImGui::IsMouseReleasedWithDelay(0, 1.0f))
        IMGUI_DEBUG_LOG("IsMouseReleasedWithDelay %f, count %d\n", 1.0f, io.MouseClickedLastCount[0]);
}

Note that it is likely important that you combine IsMouseReleasedWithDelay() with `io.MouseClickedLastCount == 1.

@wusikijeronii
Copy link
Author

I really wanted to use your commit in my project, but since it checks the release time instead of the click time, it didn’t quite fit my use case. However, I think it's still a useful feature that might help others.
I also want to say a huge thanks for pointing out MouseClickedLastCount - that was exactly the solution I needed.
Here’s what I’m using now:

                        auto &io = ImGui::GetIO();

                        if (object.selected)
                        {
                            if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(0) && drag_item_id == 0)
                            {
                                drag_item_id = object.ptr->id;
                                logTrace("Dragging started (selected) for ID: %llx", drag_item_id);
                            }

                            if (ImGui::IsMouseReleased(0))
                            {
                                if (drag_item_id == object.ptr->id)
                                {
                                    drag_item_id = 0;
                                    logTrace("Dragging ended (selected) for ID: %llx", object.ptr->id);
                                }
                                else if (ImGui::IsItemHovered())
                                {
                                    if (io.MouseClickedLastCount[0] >= 2)
                                    {
                                        object.editing = true;
                                        logTrace("Double click (selected)");
                                    }
                                    else if (io.MouseClickedLastCount[0] == 1)
                                    {
                                        object.selected = true;
                                        logTrace("Single click (selected)");
                                    }
                                }
                            }
                            else if (drag_item_id == object.ptr->id)
                            {
                                logTrace("Dragging (active) for ID: %llx", object.ptr->id);
                            }
                        }
                        else if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0))
                        {
                            object.selected = true;
                            logTrace("Single click (unselected)");
                        }

This behavior is similar to Cinema 4D or Blender, and everything seems to work fine—unless I missed something!

Since my original question was about delayed single click detection, and that’s exactly what you implemented, I think we can close the issue. Great job, and thanks again!

@ocornut
Copy link
Owner

ocornut commented Jan 22, 2025

I really wanted to use your commit in my project, but since it checks the release time instead of the click time, it didn’t quite fit my use case. However, I think it's still a useful feature that might help others.

Reopening as I would like to reevaluate that. Maybe both are useful, maybe ReleasedWithDelay is not even useful.

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

No branches or pull requests

2 participants