ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
60.95k stars 10.29k forks source link

DragXYZ mouse wrap around (warping) #228

Open extrawurst opened 9 years ago

extrawurst commented 9 years ago

a spinoff from here: https://github.com/ocornut/imgui/issues/180#issuecomment-93978306

what do you think about implementing this using a callback in the IO-structure? unfortunately imgui has to trigger the mouse warping itself to be able to adjust the DragXYZ values accordingly..

ocornut commented 9 years ago

When this is desired, would the user want to hide the cursor or not ? It's unclear what the wrap around specs should be, based on (0.0f->DisplaySize) or another set of coordinates? From a given distance (for a horizontal drag widget) ? It's hard to design that without a set of clear use cases.

extrawurst commented 9 years ago

well i know what the use case in unity3D is and i guess it could be called a well known one: leaving the window left boundary with the visible cursor while dragging a value (DragFloat e.g.) warps the cursor to the right window boundary so enable seamless dragging along the value...

ocornut commented 8 years ago

Quick note to mention that the gamepad/keyboard navigation branch #323 introduces a .WantMoveMouse output to request the backend to move the mouse cursor, which could be used for that sort of interaction here.

GloriousPtr commented 2 years ago

Edit: It's not wrapping but locking, but wrapping can be achieved the same way.

My implementation:

static bool pressed = false;
if (ImGui::IsMouseDown(ImGuiMouseButton_Right) && m_ViewportHovered)
{
    ImGui::SetMouseCursor(ImGuiMouseCursor_None); // Hide cursor
    ImVec2 newMousePosition = ImGui::GetMousePos();

    if (!pressed)
    {
        pressed = true;
        m_LockedMousePosition = newMousePosition;
    }
    Input::SetMousePosition(m_LockedMousePosition); // Call to glfwSetCursorPos();

    // use the new mouse position and old one (m_LockedMousePosition) if desired.
    // I use it for my Viewport
    const ImVec2 change = (newMousePosition - m_LockedMousePosition) * m_MouseSensitivity;
    yaw += change.x;
    pitch = glm::clamp(pitch - change.y, -89.9f, 89.9f);
}

But there is one problem here. As ImGui doesn't provide an API for SetMousePosition, I had to implement it the glfw way and with Multi viewports, glfw doesn't pass the events or handle input from any viewport outside of Main Window.

Video attached for reference. The behavior breaks when I undock the window and drag it outside of the main window as the glfwSetCursorPos() fails.

https://user-images.githubusercontent.com/29519295/190253913-50875ac3-42b5-4869-a361-6dc357598a5e.mp4

Edit 2: Fixed it by removing these two lines in GLFW/src/input.c Method: GLFWAPI void glfwSetCursorPos(GLFWwindow* handle, double xpos, double ypos)

// Remove or comment these lines
if (!_glfw.platform.windowFocused(window))
    return;
ocornut commented 2 years ago

But there is one problem here. As ImGui doesn't provide an API for SetMousePosition,

It does.

ocornut commented 2 years ago

I don't think you should be using mouse global wrapping for a FPS camera view. GLFW_CURSOR_DISABLED may be more appropriate.

GloriousPtr commented 2 years ago

Didn't knew it existed, Thanks for the info, switched to it and works like charm.

I don't think you should be using mouse global wrapping for a FPS camera view. GLFW_CURSOR_DISABLED may be more appropriate.

It's not wrapping, it's locking, mouse cursor is being tracked, the change/direction is passed further and then setting a new position on mouse cursor is called. Hence mouse cursor stays where it is and change/direction is consumed. Also glfw suddenly stopped taking input for me when used GLFW_CURSOR_DISABLED, that's why I switched to this implementation after I updated the current docking branch. I can try to repro this after 8hrs or something

dimateos commented 1 year ago

Just wanted to add that some apps move the mouse back to where it was (on click release). E.g. Unity does not do it, but Blender does and moves it exactly to where is was clicked down. It's a nice addition when you have to tweak a bunch of stacked drags. So if there were to be warping maybe there could be a flag for that too.

2023-04-28_(190222)_blender

elanhickler commented 1 year ago

I will add another use case. In audio plugins, sometimes there are knobs that go from 0 to 1 where 1 and 0 produce the same result, and some audio hardware interfaces have continuous knobs that may control values in software, so in other words, certain values can be dragged infinitively.

ocornut commented 1 year ago

Here are proof of concept function relying on backend supporting io.WantSetMousePos signal + with fix just pushed (#6837).

Usage, e.g. in DragScalar():

if (g.ActiveId == id)
    WrapMousePos(1 << ImGuiAxis_X);

Would be interested in feedback on multi-monitors with multi-viewports enabled, and in particular when there's monitor gaps.

My intuition is that this counter-intuitively V2 may behave better in more situations:

(V1)

void WrapMousePosEx(int axises_mask, const ImRect& wrap_rect)
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(axises_mask == 1 || axises_mask == 2 || axises_mask == (1 | 2));
    ImVec2 p_mouse = g.IO.MousePos;
    for (int axis = 0; axis < 2; axis++)
    {
        if ((axises_mask & (1 << axis)) == 0)
            continue;
        float size = wrap_rect.Max[axis] - wrap_rect.Min[axis];
        while (p_mouse[axis] >= wrap_rect.Max[axis])
            p_mouse[axis] -= size - 1.0f;
        while (p_mouse[axis] <= wrap_rect.Min[axis])
            p_mouse[axis] += size - 1.0f;
    }
    if (p_mouse.x != g.IO.MousePos.x || p_mouse.y != g.IO.MousePos.y)
        ImGui::TeleportMousePos(p_mouse);
}

// When multi-viewports are disabled: wrap in main viewport.
// When multi-viewports are enabled: wrap in monitor.
// FIXME: Experimental: not sure how this behaves with multi-monitor and monitor coordinates gaps.
void WrapMousePos(int axises_mask)
{
    ImGuiContext& g = *GImGui;
#ifdef IMGUI_HAS_DOCK
    if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        const ImGuiPlatformMonitor* monitor = GetViewportPlatformMonitor(g.MouseViewport);
        WrapMousePosEx(axises_mask, ImRect(monitor->MainPos, monitor->MainPos + monitor->MainSize));
    }
    else
#endif
    {
        ImGuiViewport* viewport = GetMainViewport();
        WrapMousePosEx(axises_mask, ImRect(viewport->Pos, viewport->Pos + viewport->Size));
    }
}

(V2)

// When multi-viewports are disabled: wrap in main viewport.
// When multi-viewports are enabled: wrap in monitor.
// FIXME: Experimental: not sure how this behaves with multi-monitor and monitor coordinates gaps.
void ImGui::WrapMousePos(int axises_mask)
{
    ImGuiContext& g = *GImGui;
#ifdef IMGUI_HAS_DOCK
    if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        const ImGuiPlatformMonitor* monitor = GetViewportPlatformMonitor(g.MouseViewport);
        WrapMousePosEx(axises_mask, ImRect(monitor->MainPos, monitor->MainPos + monitor->MainSize));
    }
    else
#endif
    {
        ImGuiViewport* viewport = GetMainViewport();
        WrapMousePosEx(axises_mask, ImRect(viewport->Pos, viewport->Pos + viewport->Size));
    }
}

void ImGui::WrapMousePosEx(int axises_mask, const ImRect& wrap_rect)
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(axises_mask == 1 || axises_mask == 2 || axises_mask == (1 | 2));
    ImVec2 p_mouse = g.IO.MousePos;
    for (int axis = 0; axis < 2; axis++)
    {
        if ((axises_mask & (1 << axis)) == 0)
            continue;
        float size = wrap_rect.Max[axis] - wrap_rect.Min[axis];
        if (p_mouse[axis] >= wrap_rect.Max[axis])
            p_mouse[axis] = wrap_rect.Min[axis] + 1.0f;
        else if (p_mouse[axis] <= wrap_rect.Min[axis])
            p_mouse[axis] = wrap_rect.Max[axis] - 1.0f;
    }
    if (p_mouse.x != g.IO.MousePos.x || p_mouse.y != g.IO.MousePos.y)
        TeleportMousePos(p_mouse);
}
RT2Code commented 11 months ago

I used your functions to create a cursor lock, akin to what Blender does. However, the wrapping was only working on the negative side of each axis. This was due to the cursor position being 0-based and unable to reach the screen size, so I had to subtract 1 from the screen size to fix it.

void WrapMousePosEx(int axises_mask, const ImRect& wrap_rect)
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(axises_mask == 1 || axises_mask == 2 || axises_mask == (1 | 2));
    ImVec2 p_mouse = g.IO.MousePos;
    for (int axis = 0; axis < 2; axis++)
    {
        if ((axises_mask & (1 << axis)) == 0)
            continue;
        if (p_mouse[axis] >= wrap_rect.Max[axis])
            p_mouse[axis] = wrap_rect.Min[axis] + 1.0f;
        else if (p_mouse[axis] <= wrap_rect.Min[axis])
            p_mouse[axis] = wrap_rect.Max[axis] - 1.0f;
    }
    if (p_mouse.x != g.IO.MousePos.x || p_mouse.y != g.IO.MousePos.y)
        TeleportMousePos(p_mouse);
}

void WrapMousePos(int axises_mask)
{
    ImGuiContext& g = *GImGui;
#ifdef IMGUI_HAS_DOCK
    if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        const ImGuiPlatformMonitor* monitor = GetViewportPlatformMonitor(g.MouseViewport);
        WrapMousePosEx(axises_mask, ImRect(monitor->MainPos, monitor->MainPos + monitor->MainSize - ImVec2(1.0f, 1.0f)));
    }
    else
#endif
    {
        ImGuiViewport* viewport = GetMainViewport();
        WrapMousePosEx(axises_mask, ImRect(viewport->Pos, viewport->Pos + viewport->Size - ImVec2(1.0f, 1.0f)));
    }
}

void ActiveItemLockMousePos()
{
    static ImGuiID target_item = 0;
    static ImVec2  prev_mous_pos;

    ImGuiContext& g = *GImGui;
    ImGuiID id = GetItemID();

    if (IsItemActive() && (!GetInputTextState(id) || g.InputTextDeactivatedState.ID == id))
    {
        if (target_item == 0)
        {
            target_item = id;
            prev_mous_pos = GetMousePos();
        }
        WrapMousePos(1 << ImGuiAxis_X);
        SetMouseCursor(ImGuiMouseCursor_None);
    }
    else if (target_item > 0 && target_item == id && (IsItemDeactivated() || g.InputTextDeactivatedState.ID != id))
    {
        TeleportMousePos(prev_mous_pos);
        target_item = 0;
    }
}

This function hide the cursor when an item is activated and use your wrapping function to allow infinite scrolling. When the item is deactivated, the cursor is shown again and reset to its position at the time the widget was initially clicked. I tried to handle widgets with support for both dragging an text input editing (e.g. DragFloat) since we don't want to lock the cursor for the latter. It seems to work well for my use case, although there's probably a lot of room for improvements.

Animation

Unfortunately, I can't test your wrapping function with multiples monitors as I only own one.

JulesFouchy commented 9 months ago

Both versions worked equally well for me with two monitors and multi-viewport enabled (I was not able to properly test with coordinates gap between monitors though). But I had to tweak WrapMousePos to prevent the mouse from escaping to my other monitor instead of wrapping :

void ImGui::WrapMousePos(int axes_mask)
{
    ImGuiContext& g = *GImGui;
#ifdef IMGUI_HAS_DOCK
    if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
            // Changed from here ...
            const int monitor_index = FindPlatformMonitorForPos(g.IO.MousePosPrev);
            if (monitor_index == -1)
                return;
        const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[monitor_index];
        WrapMousePosEx(axes_mask, ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize - ImVec2(1.0f, 1.0f)));
            // ... to here
    }
    else
#endif
    {
        ImGuiViewport* viewport = GetMainViewport();
        WrapMousePosEx(axes_mask, ImRect(viewport->Pos, viewport->Pos + viewport->Size - ImVec2(1.0f, 1.0f)));
    }
}

Without my change: Animation With my change: Animation2

My fork of Dear Imgui for reference: https://github.com/CoolLibs/imgui/blob/docking/imgui.cpp#L9679