imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Windows should allow dragging to scroll their contents

Open JMS55 opened this issue 4 years ago • 20 comments

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.

JMS55 avatar Aug 04 '20 00:08 JMS55

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 avatar Aug 04 '20 09:08 ocornut

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.

ocornut avatar Aug 05 '20 15:08 ocornut

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?

YuDang1024 avatar Sep 28 '20 03:09 YuDang1024

@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.

ocornut avatar Sep 30 '20 12:09 ocornut

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 avatar Jul 14 '21 13:07 PentagramPro

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);

PentagramPro avatar Jul 15 '21 07:07 PentagramPro

@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.

v-atamanenko avatar Nov 01 '21 23:11 v-atamanenko

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);

Aarkham avatar May 06 '23 18:05 Aarkham

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?

zcfearns avatar Aug 14 '23 21:08 zcfearns

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:

  • Internals: calling ButtonBehavior() without calling ItemAdd() now requires a KeepAliveID() call. This is because the KeepAliveID() call was moved from GetID() to ItemAdd(). (#5181)

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())

ocornut avatar Aug 15 '23 10:08 ocornut

@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.

Aarkham avatar Aug 16 '23 12:08 Aarkham

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

ocornut avatar Aug 16 '23 12:08 ocornut

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.

Aarkham avatar Aug 18 '23 16:08 Aarkham

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?

ZimM-LostPolygon avatar Aug 31 '23 19:08 ZimM-LostPolygon

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 avatar Aug 31 '23 20:08 ocornut

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.

ocornut avatar Aug 31 '23 20:08 ocornut

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.)

ZimM-LostPolygon avatar Aug 31 '23 21:08 ZimM-LostPolygon

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.

ocornut avatar Aug 31 '23 22:08 ocornut

@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.

lailoken avatar Oct 30 '23 14:10 lailoken

@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

shayded-exe avatar Feb 11 '24 06:02 shayded-exe