ocornut / imgui

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

Windows should allow dragging to scroll their contents #3379

Open JMS55 opened 4 years ago

JMS55 commented 4 years ago

In addition to having a scrollbar, add methods to a Window to allow dragging to scroll contents, as if you were using a browser on mobile.

ocornut commented 4 years ago

EDIT See last version from 2023-08-15: https://github.com/ocornut/imgui/issues/3379#issuecomment-1678718752

Hello, I think we can include this in more general "touch screen" support for dear imgui.

Right now a possible workaround on a per-window basis:

#include "imgui_internal.h"
void ScrollWhenDraggingOnVoid(const ImVec2& delta)
{
    ImGuiContext& g = *ImGui::GetCurrentContext();
    ImGuiWindow* window = g.CurrentWindow;
    bool hovered = false;
    bool held = false;
    if (g.HoveredId == 0) // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!)
        ImGui::ButtonBehavior(window->Rect(), window->GetID("##scrolldraggingoverlay"), &hovered, &held, ImGuiButtonFlags_MouseButtonLeft);
    if (held)
    {
        window->Scroll.x += delta.x;
        window->Scroll.y += delta.y;
    }
}

Call before End(), e.g. only scroll vertically:

ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
ScrollWhenDraggingOnVoid(ImVec2(0.0f, -mouse_delta.y));

[[ Note for myself: there's currently an issue if you tried to call SetScrollX to queue a scroll request instead of poking directly in Scroll value: CalcNextScrollFromScrollTargetAndClamp() is having some undesirable snapping on top/bottom in the (0..WindowPadding.y) region - which makes sense for some higher-level function calls but not most - and this is making it stuck on edge if not initially moving fast enough. That's an issue I've been aiming to fix separately. Reducing WindowPadding or adding a workaround to clamp the value submitted with ImClamp(new_scroll, window->WindowPadding.y, window->ScrollMax.y - window->WindowPadding.y) should fix it.

if (delta.y != 0.0f)
{
    float new_scroll_y = window->Scroll.y + delta.y;
    // FIXME: Workaround to bypass WindowPadding Scroll snapping (should fix in core)
    if (delta.y < 0.0f)
        new_scroll_y = ImMin(new_scroll_y, window->ScrollMax.y - window->WindowPadding.y - 1.0f);
    else
        new_scroll_y = ImMax(new_scroll_y, window->WindowPadding.y + 1.0f);
    ImGui::SetScrollY(window, new_scroll_y);
}

]] Will post here and other related issue when I get to finish the work to remove this snapping.

ocornut commented 4 years ago

EDIT See last version from 2023-08-15: https://github.com/ocornut/imgui/issues/3379#issuecomment-1678718752

FYI have fixed to snapped issue mentioned above, so I can confirm that this work-around will work:

#include "imgui_internal.h"
void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button)
{
    ImGuiContext& g = *ImGui::GetCurrentContext();
    ImGuiWindow* window = g.CurrentWindow;
    bool hovered = false;
    bool held = false;
    ImGuiButtonFlags button_flags = (mouse_button == 0) ? ImGuiButtonFlags_MouseButtonLeft : (mouse_button == 1) ? ImGuiButtonFlags_MouseButtonRight : ImGuiButtonFlags_MouseButtonMiddle;
    if (g.HoveredId == 0) // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!)
        ImGui::ButtonBehavior(window->Rect(), window->GetID("##scrolldraggingoverlay"), &hovered, &held, button_flags);
    if (held && delta.x != 0.0f)
        ImGui::SetScrollX(window, window->Scroll.x + delta.x);
    if (held && delta.y != 0.0f)
        ImGui::SetScrollY(window, window->Scroll.y + delta.y);
}

Needs to be called JUST before ImGui::End().

ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
ScrollWhenDraggingOnVoid(ImVec2(0.0f, -mouse_delta.y), ImGuiMouseButton_Middle);
ImGui::End();

I think we can probably easily implement something globally as desired, it's mostly a matter of design. If we have config options to scroll this way it may hinder other inputs. I'll have to think of what is the safest way to design that option.

YuDang1024 commented 3 years ago

Is this method only available in the root window? I seem to be unable to use this method to drag and drop the child window in the child window?

ocornut commented 3 years ago

@YuDang1024 I have confirmed that the code in #3379 works perfectly with child window. Make sure you call it JUST before EndChild(), see details above.

PentagramPro commented 3 years ago

The code doesn't work with touch screens since hover never happens. As a result, scroll jumps back to original position every time I touch the window.

PentagramPro commented 3 years ago

My workaround:

        ImGuiContext& g = *ImGui::GetCurrentContext();
    ImGuiWindow* window = g.CurrentWindow;
    const auto lastHeldStateId = window->GetID("##lastheldstate");
    const bool lastHeld = window->DC.StateStorage->GetBool(lastHeldStateId, false);
    bool hovered = false;
    bool held = false;

    if (g.HoveredId == 0)  // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!)
        ImGui::ButtonBehavior(window->Rect(), window->GetID("##scrolldraggingoverlay"), &hovered, &held, mouseButton);
    if (lastHeld) {
        if (held && delta.x != 0.0f) {
            ImGui::SetScrollX(window, window->Scroll.x + delta.x);
        }
        if (held && delta.y != 0.0f) {
            ImGui::SetScrollY(window, window->Scroll.y + delta.y);
        }
    }
    window->DC.StateStorage->SetBool(lastHeldStateId, held);
v-atamanenko commented 2 years ago

@ocornut trying this on a touchscreen, your workaround works but only using two fingers. (Using mouseButtonLeft). How can I make it work with one finger?

@PentagramPro 's version, on the other hand, didn't work for me at all.

Aarkham commented 1 year ago

Hello

I'm using this version:

void ScrollWhenDragging(const ImVec2& aDeltaMult,ImGuiMouseButton aMouseButton)
{
  ImGuiContext& g = *ImGui::GetCurrentContext();

  if(g.MovingWindow!=nullptr)
    {
      return;
    }

  ImGuiWindow* window=g.CurrentWindow;
  if(!window->ScrollbarX && !window->ScrollbarY) // Nothing to scroll
    {
      return;
    }

  ImGuiIO& im_io=ImGui::GetIO();

  bool hovered = false;
  bool held = false;

  const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow;
  bool window_highlight =  (window_to_highlight && (window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight || (window->DockNode && window->DockNode == window_to_highlight->DockNode)));

  ImGuiButtonFlags button_flags = (aMouseButton==0) ? ImGuiButtonFlags_MouseButtonLeft : (aMouseButton==1) ? ImGuiButtonFlags_MouseButtonRight : ImGuiButtonFlags_MouseButtonMiddle;
  if(   g.HoveredId==0                  // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!)
     && im_io.MouseDown[aMouseButton]   // Mouse pressed
     && window_highlight                // Window active
    )
    {
      ImGui::ButtonBehavior(window->InnerClipRect,window->GetID("##scrolldraggingoverlay"),&hovered,&held,button_flags);

      if((window->InnerClipRect.Contains(im_io.MousePos)))
        {
          held=true;
        }
      else if(window->InnerClipRect.Contains(im_io.MouseClickedPos[aMouseButton]) ) // If mouse has moved outside window, check if click was inside
        {
          held=true;
        }
      else
        {
          held=false;
        }
    }

  if (held && aDeltaMult.x != 0.0f)
      ImGui::SetScrollX(window, window->Scroll.x+ aDeltaMult.x*im_io.MouseDelta.x);
  if (held && aDeltaMult.y != 0.0f)
      ImGui::SetScrollY(window, window->Scroll.y+ aDeltaMult.y*im_io.MouseDelta.y);
}

And calling

ImGuiX::ScrollWhenDragging(ImVec2(0.0f,-1.0f), 0);

just before End() or EndChild().

I tried it in PC, Android and iOS with a custom back end.

For my use case works perfectly but in general you need a little care if you start dragging in a button or something similar. It would be perfect that ImGui had an option to disable interaction temporarily while the content is scrolling.

@PentagramPro I also had this problem, but it is fixed in imgui 1.89.5 calling

ImGui::GetIO().AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);

zcfearns commented 1 year ago

I've found myself needing this touch support for scrolling - particularly inside of combo boxes since the slider width can be very small.

The scrolling is being extremely temperamental, particularly at high FPS - the held value is resetting to 0 even if the mouse-button is continually held down, thus stopping scrolling entirely.

This has been done using 1.89.4.

Each mouse button "press and hold" registers as held = 1 for two frames and then resets to 0. Verified that "out-held" is being replaced by held which defaults to 'false'.

@ocornut has this been deprecated by further updates - or are we missing a particular flag in the solution?

ocornut commented 1 year ago

For my use case works perfectly but in general you need a little care if you start dragging in a button or something similar. It would be perfect that ImGui had an option to disable interaction temporarily while the content is scrolling.

I don't understand. Your code checks if (g.HoveredId) and then call ButtonBehavior() which should take g.ActiveId + key/button ownership so it should be impossible to interact with other things while scrolling.

I've found myself needing this touch support for scrolling - particularly inside of combo boxes since the slider width can be very small.

@zcfearns You can and should probably increase ScrollbarWidth on a touch device.

The scrolling is being extremely temperamental, particularly at high FPS - the held value is resetting to 0 even if the mouse-button is continually held down, thus stopping scrolling entirely. This has been done using 1.89.4. Each mouse button "press and hold" registers as held = 1 for two frames and then resets to 0. Verified that "out-held" is being replaced by held which defaults to 'false'.

I used "Debug Log->ActiveId" to investigate this:

[01739] SetActiveID() old:0x00000000 (window "") -> new:0x1C3EFF00 (window "Dear ImGui Demo")
[01741] NewFrame(): ClearActiveID() because it isn't marked alive anymore!
[01741] SetActiveID() old:0x1C3EFF00 (window "Dear ImGui Demo") -> new:0x00000000 (window "")

In 1.88 had this breaking change:

I have fixed the snippet from https://github.com/ocornut/imgui/issues/3379#issuecomment-669259429 by changing it to:

#include "imgui_internal.h"
void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button)
{
    ImGuiContext& g = *ImGui::GetCurrentContext();
    ImGuiWindow* window = g.CurrentWindow;
    bool hovered = false;
    bool held = false;
    ImGuiID id = window->GetID("##scrolldraggingoverlay");
    ImGui::KeepAliveID(id);
    ImGuiButtonFlags button_flags = (mouse_button == 0) ? ImGuiButtonFlags_MouseButtonLeft : (mouse_button == 1) ? ImGuiButtonFlags_MouseButtonRight : ImGuiButtonFlags_MouseButtonMiddle;
    if (g.HoveredId == 0) // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!)
        ImGui::ButtonBehavior(window->Rect(), id, &hovered, &held, button_flags);
    if (held && delta.x != 0.0f)
        ImGui::SetScrollX(window, window->Scroll.x + delta.x);
    if (held && delta.y != 0.0f)
        ImGui::SetScrollY(window, window->Scroll.y + delta.y);
}

Usage

ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
ScrollWhenDraggingOnVoid(ImVec2(0.0f, -mouse_delta.y), ImGuiMouseButton_Middle);
ImGui::End(); (or EndChild())
Aarkham commented 1 year ago

@zcfearns I did not test my version with combo, but it does work Ok, It has the problem that it also scrolls the parent window, I have to look at it. I have only tested it in Windows, Android should be the same.

Combo

@ocornut the issue is that if I start scrolling in a widget and end also in it, then the interaction happens. What I wanted to say is that if I start scrolling, when I stop that should not happen. I have recorded a gif to show the issue. I start scrolling dragging in a check box, if I stop dragging in that check box its value is changed.

check

I'm using 1.89.6 WIP 18953.

ocornut commented 1 year ago

Are you sure you are using the code I posted? My code shouldn’t do that, you cannot start dragging over a checkbox.

Aarkham commented 1 year ago

No, I was using the code I posted on May 6. The version posted at that time did not work for me.

I have tested with the new version and it works fine. As you said, it does not allow to start draggin over a checkbox (or butoo, tree text....) but sometimes this behaviour is more clunsy as you start dragging and nothing happens and you have to reposition the mouse and start again. For example, in the case of the combo box list it does not scroll because everything is a widget.

combo2

With the version I'm using you can start dragging and it works. The problem is that you can end draggin over the same widget and activate it.

For me, the ideal behaviour would be that you press anywhere and if you drag more that a certaint amount, then the widgets do not react to the end dragging. Don't know if this can be done easily.

ZimM-LostPolygon commented 1 year ago

Is there any way to achieve this without using any internal stuff? Since cimgui doesn't generate the wrappers for internals, it seems like this solution is only usable from C++, otherwise you're basically screwed?

ocornut commented 1 year ago

The solution is that your binding should generate internal stuff. AFAIK cimgui does it with an option, but it ihmo very awkwardly includes the output in the same cimgui.h file instead of eg a cimgui_internal.h. I find that rather bad and it is one of the reason that prompted us to develop “dear bindings”, although it doesn’t support internal yet.

ocornut commented 1 year ago

That said you can perfectly manually include that code in a C++ source file and make it accessible to your C sources. In other words if you can link with cimgui you can perfectly generate one more function manually.

ZimM-LostPolygon commented 1 year ago

That's true, unfortunately though, for a more clean solution, cimgui is only half of the puzzle, since there is yet another binding from cimgui-generated C code to the target language. In my case, it's ImGui.NET, which doesn't really support internals either. Which is, obviously, a problem completely outside of the Dear ImGui domain, but this is just to say - using even basic internal stuff from outside C++ is a major PITA, involving vendoring multiple packages, hacking stuff together, and then maintaining all that, which is something no one is really going to do unless it's some absolutely critical functionality. For most non-C++ projects, internals are an automatic no-go, so it's often not a solution... At least until most bindings support internals.

(P.S. The output of 'dear bindings' looks sooo clean! I'm getting a strong itch to start making a new wrapper for .NET based on that.)

ocornut commented 1 year ago

At least until most bindings support internals.

That’s the direction to head toward. It makes me want to add more cool stuff in internals until bindings and pipelines catch up :)

i find it really regrettable that eg updating/building imgui.net yourself is not an obvious/trivial task, but if more people head into doing that it it’s more likely for that path to be improved and streamlined until it becomes trivial.

lailoken commented 10 months ago

@ocornut , I'm using the code you posted and it's working wonderfully. However, it seems to disable table right-click context menus (ImGuiTableFlags_ContextMenuInBody) in empty cells even though I'm calling scrolling with the left mouse button: ScrollWhenDraggingOnVoid(ImVec2(0.0f, -mouse_delta.y), ImGuiMouseButton_Left);.

My solution was to add an extra if (ImGui::IsMouseDown(mouse_button)) inside that function to protect against this.

shayded-exe commented 7 months ago

@ZimM-LostPolygon FYI adding new cimgui externs to C# is actually pretty easy now with [LibraryImport].

You can see this project I'm working on for an example: https://github.com/b-effort/b_led/blob/main/b_led/Interop/cimgui.cs