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

Custom widget - How to properly set hoverID for overlapping widgets? #4055

Closed
gallickgunner opened this issue Apr 18, 2021 · 13 comments
Closed
Labels

Comments

@gallickgunner
Copy link

gallickgunner commented Apr 18, 2021

Version: 1.80
Branch: master
Back-ends: OpenGL3 + GLFW
Operating System: Win10

Hey ocornut, I've been going through the internal code since the day before yesterday. I'm trying to create gradient bar which is editable. When the user clicks on this multi colored gradient bar a circular knob appears which can then be slided. This can happen multiple times.

Since the use case felt similar to Slider widget I read it's internal and have managed to code it fairly well. The only problem is setting hoveredIDs for overlapping widgets (the gradient bar and the circular knob) since in the slider use case, the background frame or bar and the knob don't need separate hoverIDs. Here's what I've coded currently,

bool rectMultiColor(const char* label_id, float height, float width, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left)
{
    const ImGuiStyle& style = ImGui::GetStyle();
    const ImGuiID id = ImGui::GetCurrentWindow()->GetID(label_id);
    ImVec2 cursorPos = ImGui::GetCurrentWindow()->DC.CursorPos;
    
    ImRect total_bb(cursorPos, ImVec2(width + cursorPos.x , height + cursorPos.y));
    ImGui::ItemSize(total_bb, style.FramePadding.y);
    if (!ImGui::ItemAdd(total_bb, id))
        return false;
    
    ImDrawList* draw_list = ImGui::GetCurrentWindow()->DrawList;
    draw_list->AddRectFilledMultiColor(cursorPos, ImVec2(cursorPos.x + width, cursorPos.y + height), col_upr_left, col_upr_right, col_bot_right, col_bot_left);
    return ImGui::ItemHoverable(total_bb, id);
}

Similar snippet goes for drawing the circular knob, just swap that AddRectFilled with AddCircleFilled. Here's how I'm drawing them

if(rectMultiColor("##rectMulti", gradient_height, gradient_width, IM_COL32_BLACK, IM_COL32_WHITE, IM_COL32_WHITE, IM_COL32_BLACK))
{
        if(ImGui::IsMouseClicked(ImGuiMouseButton_Right))
        {
               num_knobs++
               // find the ratio "t" from mousePos and find the value using ScaleValueFromRatioT() and store knob data in a list
        }
}

for(int i = 0; i < num_knobs; i++)
{
       //ImGui::SetItemAllowOverlap();
       ImGui::SliderBehavior()        // get position for the knob based on value found earlier
       if(knob())      // render knobs

}

So the problem is, if I don't use ImGui::SetItemAllowOverlap() the hoveredID is always set to the bottom gradient bar. This poses problems such as clicking once to get the knob. Then clicking again on the knob renders a second knob when this shouldn't happen. If i use ImGui::SetItemAllowOverlap() the hoveredIDs are set correctly but the hovered event is still fired for the gradient bar since I guess at the start of the new frame, the knob isn't rendered yet so we get the hover first on the bar and then the knob.

Any suggestions for this use case? Sorry if I missed a similar issue.

@gallickgunner
Copy link
Author

I'm working around for now by checking if the bottom bar was hovered after the code for rendering the knobs i.e. after the for loop by using if(ImGui::GetHoveredID() == win->GetID("##rectMulti")). This slightly changes when the knobs actually get rendered i.e. current frame vs next frame but it's not effecting my use case so far. Keeping this open so I can find out if there's actually a proper way or any other suggestion by ocornut.

@rokups
Copy link
Contributor

rokups commented Apr 21, 2021

I do not quite understand what you are trying to achieve, but it seem knobs should be part of rectMultiColor(), right? If so - render knobs manually inside rectMultiColor() and do not give them separate IDs. Your problems will magically go away. This is how slider widget works as well - knob is merely a rect rendered at appropriate position.

@gallickgunner
Copy link
Author

gallickgunner commented Apr 21, 2021

I do not quite understand what you are trying to achieve, but it seem knobs should be part of rectMultiColor(), right? If so - render knobs manually inside rectMultiColor() and do not give them separate IDs. Your problems will magically go away. This is how slider widget works as well - knob is merely a rect rendered at appropriate position.

That's how the original slider works. I want to differentiate when I'm hovering over the slider (the actual background frame or rect) and the actual knob ( the grab rect or circle in my case). I need this because I need to render multiple knobs when clicked on the background rect. So I want a way for the knob or the grab rect to override the hoverID of the background frame rect which ImGui::SetAllowItemOverlap() does but due to the way things get rendered (bg frame rect first, then the knobs) the frame fires the hover event because the knobs aren't rendered by then.

I dont know if this should be a thing provided by ImGui or it should be handled by the programmer manually like I did. IF this was handled by ImGui then it'd need to remember that the Knobs are rendered over the background frame (the z order) and that the knobs were being hovered on the previous frame so in the current frame we the background frame rect shouldn't fire the hover event. Which seems quite messy, I admit. Just wanted to share my use case and get some suggestions.

@rokups
Copy link
Contributor

rokups commented Apr 21, 2021

I want to differentiate when I'm hovering over the slider (the actual background frame or rect) and the actual knob ( the grab rect or circle in my case).

You know your slider rect, knob location, knob size - you can test if mouse position is within circle of knob. Basically it is all manual work calculating and testing various coordinates. I think you try to optimize it a bit, offloading certain behaviors for imgui to handle. While usually it is a good thing to do, when writing custom widgets this is suboptimal as you already discovered.

@gallickgunner
Copy link
Author

I want to differentiate when I'm hovering over the slider (the actual background frame or rect) and the actual knob ( the grab rect or circle in my case).

You know your slider rect, knob location, knob size - you can test if mouse position is within circle of knob. Basically it is all manual work calculating and testing various coordinates. I think you try to optimize it a bit, offloading certain behaviors for imgui to handle. While usually it is a good thing to do, when writing custom widgets this is suboptimal as you already discovered.

Yes another way is to calculate the hovering of the background rect manually. Imgui already provides ItemHoverable internal func for calculating hovering but it simply checks whether mouse cursor is inside rect or not. The problem is when you are hovering over the knob you are also hovering over the rect unless as you said we implement the checking manually and ignore for cases where the mouse is hovering over the knotBB as well.

@rokups
Copy link
Contributor

rokups commented Apr 22, 2021

The problem is when you are hovering over the knob you are also hovering over the rect unless as you said we implement the checking manually and ignore for cases where the mouse is hovering over the knotBB as well.

Not much of a problem i would say. What you should do is first handle all your knobs, and if no knob was hovered - handle hovering widget rect. You can use ImDrawListSplitter if you need rendering order different from execution order (render knobs into channel 1 and background into channel 0).

@gallickgunner
Copy link
Author

gallickgunner commented Apr 22, 2021

ImDrawListSplitter

Now this is something I didn't know. I'll try it out. Thanks for the info.

@gallickgunner
Copy link
Author

gallickgunner commented Apr 22, 2021

@rokups - totally diff topic but asking it here since I'd have to make another issue. Do you know how I can cause my custom widgets to get effected by scrolling? Here are 2 simple images to demonstrate the problem

Here as you can see the button is rendered beneath my gradient bar. All good.
Untitled

What happens when the window size gets smaller and I have to scroll to the bottom. Apparently my custom gradient bar which is RectMulti (code above) doesn't go up causing the button to get render on top of it. I've tried searching but I can't find what's the thing that's missing.
Untitled2

I think it has to do something with clipRects but those are calculated by the window correctly. I tried finding occurrences to Push/PopClipRect and the only thing that's missing in my custom widgets and most of the library ones is a call to renderNavHighlight which uses these. However I don't think I need this call here. I did try to place it tho but nothing happened.

@rokups
Copy link
Contributor

rokups commented Apr 22, 2021

I can't tell only by looking at that bit of code. Although i do have a small advice: use total_bb.Min, total_bb.Max in AddRectFilledMultiColor() call. It wont fix your issue but will reduce space for errors.

@gallickgunner
Copy link
Author

gallickgunner commented Apr 22, 2021

I can't tell only by looking at that bit of code. Although i do have a small advice: use total_bb.Min, total_bb.Max in AddRectFilledMultiColor() call. It wont fix your issue but will reduce space for errors.

Yeah I already did that :) Thanks for pointing that out tho. That's the core code that draws the multicolor rect. Simply speaking I'm just doing

ImGui::Begin()
rectMultiColor(....)  -> function code above
ImGui::Button("asd")
ImGui::End()

Any ideas what cause scrolling issues?
@ocornut - any pointers brother?

Btw your drawListSplitter suggestion works. So I guess the original problem is solved. Thanks for that as well.

@gallickgunner
Copy link
Author

Okay, I managed to solve the issue. And f me. I forgot to that I had changed the code I posted above. In order to wrap around the mess of absolute coordinates and relative coordinates what I did was pass relative coordinates to all the functions drawing through draw_list and convert from relative to absolute there so the main function is free from conversion. But I was missing the window->scroll factor in the conversion and only adding windowPosition to the relative coordinate which caused the issue. Thanks for the help guys.

@ocornut
Copy link
Owner

ocornut commented Apr 23, 2021

I'm trying to create gradient bar which is editable.

In case it may be useful, have you seen projects such as ImGradient?
https://github.com/ocornut/imgui/wiki/Useful-Widgets#curves-animations-gradients

I haven't understood this whole thread but some TL;DR;

  • Hit testing and hover testing by default are done on a first-come first-served basis, which generally matches the back-to-front order.
  • SetItemAllowOverlap() attempt to provide a wait to invert this order. It is currently poorly defined and designed, but I attempted to provide a better explanation of it here: Seeking something like "IsAnyOtherItemHovered()" to fix interaction with multiple overlapping elements #3909 (comment) In particular, there are cases were we want the widget "under" to be highlighted visually as hovered but not react to action (allowing the widget "over" to react), and cases were we don't want the widget "under" to even highlights. Both use cases are valid and should be reflected in an improved rework the "allow overlap" system.
  • Many problems are solved locally by careful ordering of rendering and hit tests/submissions.

@gallickgunner
Copy link
Author

I'm trying to create gradient bar which is editable.

In case it may be useful, have you seen projects such as ImGradient?
https://github.com/ocornut/imgui/wiki/Useful-Widgets#curves-animations-gradients

I haven't understood this whole thread but some TL;DR;

  • Hit testing and hover testing by default are done on a first-come first-served basis, which generally matches the back-to-front order.
  • SetItemAllowOverlap() attempt to provide a wait to invert this order. It is currently poorly defined and designed, but I attempted to provide a better explanation of it here: #3909 (comment) In particular, there are cases were we want the widget "under" to be highlighted visually as hovered but not react to action (allowing the widget "over" to react), and cases were we don't want the widget "under" to even highlights. Both use cases are valid and should be reflected in an improved rework the "allow overlap" system.
  • Many problems are solved locally by careful ordering of rendering and hit tests/submissions.

Thanks for the tips. I actually had seen that but I didn't got to see the pics of a gradient editor on his github, though the code does exist. Since this was for a very specific usecase (transfer functions in volume rendering) I decided to make my own. I'm also currently working on rendering cubic splines. Someone already did work for the bezier curves but I didn't see any for simple cubic splines. Hopefully will share the results so you can add it to the useful links if anyone needs them.

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

3 participants