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

Windows should allow dragging to scroll their contents #3379

Open
JMS55 opened this issue Aug 4, 2020 · 20 comments
Open

Windows should allow dragging to scroll their contents #3379

JMS55 opened this issue Aug 4, 2020 · 20 comments

Comments

@JMS55
Copy link

JMS55 commented Aug 4, 2020

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
Copy link
Owner

ocornut commented Aug 4, 2020

EDIT See last version from 2023-08-15: #3379 (comment)

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 added a commit that referenced this issue Aug 5, 2020
…on the edge of scroll limits. (#3379) + Demo: Rename "Layout" to "Layout & Scrolling".
@ocornut
Copy link
Owner

ocornut commented Aug 5, 2020

EDIT See last version from 2023-08-15: #3379 (comment)

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
Copy link

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
Copy link
Owner

ocornut commented Sep 30, 2020

@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
Copy link

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
Copy link

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
Copy link

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

scribam added a commit to scribam/flycast that referenced this issue Nov 10, 2021
TODO: refactor scrollWhenDraggingOnVoid and windowDragScroll functions
ocornut/imgui#3379
@Aarkham
Copy link

Aarkham commented May 6, 2023

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
Copy link

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
Copy link
Owner

ocornut commented Aug 15, 2023

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 #3379 (comment) 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
Copy link

Aarkham commented Aug 16, 2023

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

ocornut commented Aug 16, 2023

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

@Aarkham
Copy link

Aarkham commented Aug 18, 2023

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
Copy link

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
Copy link
Owner

ocornut commented Aug 31, 2023

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
Copy link
Owner

ocornut commented Aug 31, 2023

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
Copy link

ZimM-LostPolygon commented Aug 31, 2023

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
Copy link
Owner

ocornut commented Aug 31, 2023

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
Copy link

@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
Copy link

@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

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

10 participants