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

Pie menu test #434

Open
ocornut opened this issue Dec 11, 2015 · 35 comments
Open

Pie menu test #434

ocornut opened this issue Dec 11, 2015 · 35 comments

Comments

@ocornut
Copy link
Owner

ocornut commented Dec 11, 2015

This is more a proof of concept that a finished api.

pie_menu2

#include <imgui_internal.h>

// Return >= 0 on mouse release
// Optional int* p_selected display and update a currently selected item
int PiePopupSelectMenu(const ImVec2& center, const char* popup_id, const char** items, int items_count, int* p_selected)
{
    int ret = -1;

    // FIXME: Missing a call to query if Popup is open so we can move the PushStyleColor inside the BeginPopupBlock (e.g. IsPopupOpen() in imgui.cpp)
    // FIXME: Our PathFill function only handle convex polygons, so we can't have items spanning an arc too large else inner concave edge artifact is too visible, hence the ImMax(7,items_count)
    ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0,0,0,0));
    ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0,0,0,0));
    if (ImGui::BeginPopup(popup_id))
    {
        const ImVec2 drag_delta = ImVec2(ImGui::GetIO().MousePos.x - center.x, ImGui::GetIO().MousePos.y - center.y);
        const float drag_dist2 = drag_delta.x*drag_delta.x + drag_delta.y*drag_delta.y;

        const ImGuiStyle& style = ImGui::GetStyle();
        const float RADIUS_MIN = 30.0f;
        const float RADIUS_MAX = 120.0f;
        const float RADIUS_INTERACT_MIN = 20.0f;
        const int ITEMS_MIN = 6;

        ImDrawList* draw_list = ImGui::GetWindowDrawList();
        //ImGuiWindow* window = ImGui::GetCurrentWindow();
        draw_list->PushClipRectFullScreen();
        draw_list->PathArcTo(center, (RADIUS_MIN + RADIUS_MAX)*0.5f, 0.0f, IM_PI*2.0f*0.99f, 32);   // FIXME: 0.99f look like full arc with closed thick stroke has a bug now
        draw_list->PathStroke(ImColor(0,0,0), true, RADIUS_MAX - RADIUS_MIN);

        const float item_arc_span = 2*IM_PI / ImMax(ITEMS_MIN, items_count);
        float drag_angle = atan2f(drag_delta.y, drag_delta.x);
        if (drag_angle < -0.5f*item_arc_span)
            drag_angle += 2.0f*IM_PI;
        //ImGui::Text("%f", drag_angle);    // [Debug]

        int item_hovered = -1;
        for (int item_n = 0; item_n < items_count; item_n++)
        {
            const char* item_label = items[item_n];
            const float item_ang_min = item_arc_span * (item_n+0.02f) - item_arc_span*0.5f; // FIXME: Could calculate padding angle based on how many pixels they'll take
            const float item_ang_max = item_arc_span * (item_n+0.98f) - item_arc_span*0.5f;

            bool hovered = false;
            if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN)
            {
                if (drag_angle >= item_ang_min && drag_angle < item_ang_max)
                    hovered = true;
            }
            bool selected = p_selected && (*p_selected == item_n);

            int arc_segments = (int)(32 * item_arc_span / (2*IM_PI)) + 1;
            draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_ang_min, item_ang_max, arc_segments);
            draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_ang_max, item_ang_min, arc_segments);
            //draw_list->PathFill(window->Color(hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg));
            draw_list->PathFill(hovered ? ImColor(100,100,150) : selected ? ImColor(120,120,140) : ImColor(70,70,70));

            ImVec2 text_size = ImGui::GetWindowFont()->CalcTextSizeA(ImGui::GetWindowFontSize(), FLT_MAX, 0.0f, item_label);
            ImVec2 text_pos = ImVec2(
                center.x + cosf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
                center.y + sinf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
            draw_list->AddText(text_pos, ImColor(255,255,255), item_label);

            if (hovered)
                item_hovered = item_n;
        }
        draw_list->PopClipRect();

        if (ImGui::IsMouseReleased(0))
        {
            ImGui::CloseCurrentPopup();
            ret = item_hovered;
            if (p_selected)
                *p_selected = item_hovered;
        }
        ImGui::EndPopup();
    }
    ImGui::PopStyleColor(2);
    return ret;
}
{
static const char* test_data = "Menu";
const char* items[] = { "Orange", "Blue", "Purple", "Gray", "Yellow", "Las Vegas" };
int items_count = sizeof(items)/sizeof(*items);

static int selected = -1;

ImGui::Button(selected >= 0 ? items[selected] : "Menu", ImVec2(50,50));
if (ImGui::IsItemActive())          // Don't wait for button release to activate the pie menu
    ImGui::OpenPopup("##piepopup");

ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);
if (n >= 0)
    printf("returned %d\n", n);
@ghost
Copy link

ghost commented Dec 11, 2015

Great job :-)

@lundmark
Copy link

Looking really good! Will you be adding it to the lib or will it just be a snippet?

@ocornut
Copy link
Owner Author

ocornut commented Dec 13, 2015

Eventually it should be added but it's not complete nor good enough yet. In order to support submenus the API would likely need to be changed?

@lundmark
Copy link

What would submenues within a pie-popup look like?

@ocornut
Copy link
Owner Author

ocornut commented Dec 13, 2015

I'm not sure. Open a second pie-popup over the center of the previous button? That would suggest that merely holding mouse would open the sub-menu (like normal menu), which in turn ask the question of how we can get back to the parent menu.

I haven't spend time to think about it in details, just implemented something as fast as I can for someone, which is essentially why this is a proof of concept rather than a feature.

@lundmark
Copy link

Another idea would perhaps be to expand the pie outwards, presenting a top-down menu. The amount of choices could be increased by increasing the size of the extended pie-slice upwards/downwards?

@ghost
Copy link

ghost commented Dec 14, 2015

It does not work when trying to display in without a window ? I try to display it on a simple OpenGL window ! After a drag & drop... I drop inside a 3D view... and nothing appear !

@Horrowind
Copy link

It always bothered me, that the border between the items increased from the inner ring to the outer, so here is a fix for that (only listing the inner rendering for-loop):

    for (int item_n = 0; item_n < items_count; item_n++)
    {
        const char* item_label = items[item_n];
        const float inner_spacing = style.ItemInnerSpacing.x / RADIUS_MIN / 2;
        const float item_inner_ang_min = item_arc_span * (item_n - 0.5f + inner_spacing);
        const float item_inner_ang_max = item_arc_span * (item_n + 0.5f - inner_spacing);
        const float item_outer_ang_min = item_arc_span * (item_n - 0.5f + inner_spacing * (RADIUS_MIN / RADIUS_MAX));
        const float item_outer_ang_max = item_arc_span * (item_n + 0.5f - inner_spacing * (RADIUS_MIN / RADIUS_MAX));

        bool hovered = false;
        if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN)
        {
            if (drag_angle >= item_inner_ang_min && drag_angle < item_inner_ang_max)
                hovered = true;
        }
        bool selected = p_selected && (*p_selected == item_n);

        int arc_segments = (int)(32 * item_arc_span / (2*IM_PI)) + 1;
        draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_outer_ang_min, item_outer_ang_max, arc_segments);
        draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_inner_ang_max, item_inner_ang_min, arc_segments);
        //draw_list->PathFill(window->Color(hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg));
        draw_list->PathFill(hovered ? ImColor(100,100,150) : selected ? ImColor(120,120,140) : ImColor(70,70,70));

        ImVec2 text_size = ImGui::GetWindowFont()->CalcTextSizeA(ImGui::GetWindowFontSize(), FLT_MAX, 0.0f, item_label);
        ImVec2 text_pos = ImVec2(
            center.x + cosf((item_inner_ang_min + item_inner_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
            center.y + sinf((item_inner_ang_min + item_inner_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
        draw_list->AddText(text_pos, ImColor(255,255,255), item_label);

        if (hovered)
            item_hovered = item_n;
    }

Furthermore here are inspirational videos from other pie menu implementations:

https://www.youtube.com/watch?v=nXh1Tm24kTE&t=50s
the submenu implementation from this one is probably not advisable, but I like how it adds a second ring if there are more then six items.

https://www.youtube.com/watch?v=Job4Rg-sbDo
this is the one in OneNote; it has some kind of submenu implementation, but from the video, I am unable to understand how it works.

@ghost
Copy link

ghost commented Dec 14, 2015

I really like the simple Ubuntu "look", just a simple set of "alpha blended" pies over the screen :-P

@ocornut
Copy link
Owner Author

ocornut commented Dec 14, 2015

Thanks @Horrowind, really useful!

@ocornut
Copy link
Owner Author

ocornut commented Dec 17, 2015

You should be able to, the pie-menu is its own window. Krys please provide more detailed information and standalone repro code with your questions when possible! Lots of your questions are really unclear, if you can't clarify the question then please provide the smallest set of code that demonstrate the issue you have. Thanks!

@jarikomppa
Copy link

It is impossible to gives a small reproduction case, because the
application is huge !

What he means is, start a new, tiny application that just reproduces a
single problem.

On Thu, Dec 17, 2015 at 12:25 PM, krys-spectralpixel <
notifications@github.com> wrote:

I agree but...

It is impossible to gives a small reproduction case, because the
application is huge !

The effect is that nothing appear !

Here is my code, but it is approx. a copy/paste of your code:

        const char* items[] = { "Color", "Specular", "Bump", "Opacity" };
        int items_count = sizeof(items) / sizeof(*items);

        static int selected = -1;
        ImGui::OpenPopup("##piepopup");
        // ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
        ImVec2 pie_menu_center = ImGui::GetMousePos();
        int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);


Reply to this email directly or view it on GitHub
#434 (comment).

@ocornut
Copy link
Owner Author

ocornut commented Dec 17, 2015

It is your job to provide a repro to ensure that your request is well formed and thought up. It also in many cases helps you understand the issue better. Your code above is not a repro I can paste.

Have you noticed that my pie code above close automatically if mouse is released?

This works without a window for me:

        const char* items[] = { "Color", "Specular", "Bump", "Opacity" };
        int items_count = sizeof(items) / sizeof(*items);

        static int selected = -1;
        if (!ImGui::IsMouseHoveringAnyWindow() && ImGui::IsMouseClicked(0))
            ImGui::OpenPopup("##piepopup");
        int n = PiePopupSelectMenu(ImGui::GetIO().MouseClickedPos[0], "##piepopup", items, items_count, &selected);

@ghost
Copy link

ghost commented Dec 28, 2015

I propose to replace:

if (drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN && drag_dist2 < RADIUS_MAX*RADIUS_MAX)

with

if (&& drag_dist2 >= RADIUS_INTERACT_MIN*RADIUS_INTERACT_MIN && drag_dist2 < RADIUS_MAX*RADIUS_MAX)

In order to avoid selection when the mouse is out of the pie menu !

@ocornut
Copy link
Owner Author

ocornut commented Dec 29, 2015

I did that intentionally to avoid false negatives and also considering that pie menus have a "gesture" feel to them. e.g. click and drag left, release, by not worrying about distance travelled you can accomplish faster actions. Maybe it isn't as important with mouse controls. Both ways have legit uses, it could be a global setting of the pie menus potentially.

@ghost
Copy link

ghost commented Dec 29, 2015

Right. Sure there can be plenty of options for this control :-D

@vd3d
Copy link

vd3d commented Feb 12, 2016

Hi,

I have noticed that when I use only 4 "items" in the pie menu I have a line going over the items (See the red arrow on the attached picture please).

Have you ever noticed it, can you reproduce it ?

image

@ocornut
Copy link
Owner Author

ocornut commented Feb 12, 2016

The PathFill function in theory only handle convex fill, so I suppose that's an artefact of this limitation.
Proper support for concave fill would be quite some work.
The cheap workaround would be to split the arc in smaller section (say, divided by 2) and draw it as two parts. It would however show visible artefacts if your fill alpha is < 1.0f

@vd3d
Copy link

vd3d commented Feb 12, 2016

Great explanation !

Thanks for it. You helped me a lot.

@zhouxs1023
Copy link

I compiled pie menu with DX9, after run exe ,I found a black area at the centre of the circle, do not know how to cancel this area?
default

@lapinozz
Copy link

lapinozz commented Oct 9, 2016

@zhouxs1023 this is caused by the popup background, I don't know why it dosen't follow the style pushed but I "fixed" it by adding this code just before if (ImGui::BeginPopup(popup_id))

ImGui::SetNextWindowPos({-100, -100});

@zhouxs1023
Copy link

Thank you very much!

@thennequin
Copy link

A modified version with support of sub menu, it's not finished or optimized and need a new struct for storing values and items label.
@ocornut, is there any chance to have a storage of string someday for avoiding this custom struct? ;)
Or add a way (with macro?) to add custom variables in ImGuiContext.

circularmenu
Can by used like this

if( ImGui::IsWindowHovered() && ImGui::IsMouseClicked( 1 ) )
{
  ImGui::OpenPopup( "PieMenu" );
}

if( BeginPiePopup( "PieMenu", 1 ) )
{
  if( PieMenuItem( "Test1" ) ) { /*TODO*/ }
  if( PieMenuItem( "Test2" ) ) { /*TODO*/ }
  
  if( PieMenuItem( "Test3", false ) )  { /*TODO*/ }
  
  if( BeginPieMenu( "Sub" ) )
  {
    if( BeginPieMenu( "Sub sub\nmenu" ) )
    {
      if( PieMenuItem( "SubSub" ) ) { /*TODO*/ }
      if( PieMenuItem( "SubSub2" ) ) { /*TODO*/ }
      EndPieMenu();
    }
    if( PieMenuItem( "TestSub" ) ) { /*TODO*/ }
    if( PieMenuItem( "TestSub2" ) ) { /*TODO*/ }
    EndPieMenu();
  }
  
  EndPiePopup();
}

Full source here
https://gist.github.com/thennequin/64b4b996ec990c6ddc13a48c6a0ba68c

@ocornut
Copy link
Owner Author

ocornut commented Dec 24, 2017

@thennequin

is there any chance to have a storage of string someday for avoiding this custom struct? ;)

There's a helper ImGuiTextBuffer which is close to what you are doing.

Maybe a stripped down version of https://github.com/ocornut/Str would generally be useful, but if you can use a single buffer like ImGuiTextBuffer it helps.

Or add a way (with macro?) to add custom variables in ImGuiContext.

I've been considering it, perhaps a way for a external subsystem to register a slot instead an array of pointers stored in ImGuiContext, so you can store one PieMenuContext per ImGuiContext. Is that were you were thinking of?

@thennequin
Copy link

thennequin commented Dec 25, 2017

There's a helper ImGuiTextBuffer which is close to what you are doing.

I use an ImVector< char > for storing strings.
Sorry for the misunderstanding of the first question, I just wanted a way to add strings (like bool/int/float/ptr) in the ImGuiStorage for avoiding the use of a new external struct because I store bool/float/int and string.

I've been considering it, perhaps a way for a external subsystem to register a slot instead an array of pointers stored in ImGuiContext, so you can store one PieMenuContext per ImGuiContext. Is that were you were thinking of?

Yes, I want to store one PieMenuContext per ImGuiContext.

@Stanlyhalo
Copy link

@thennequin You probably haven't updated this in a while nor remember how you wrote it, but it's worth the shot. I tried your implementation, and I've seen only 1 issue, and a weird quirk I wanted to see if I can change. The issue is that there is no background color, and yes, I've tried to change the color, didn't budge, and the quirk is that if it goes a little offscreen, it pushes itself away from the border, if I wanted to remove this quirk, how would I go about doing that?

@thennequin
Copy link

I updated the gist to fix the "quirk" (if you talk about the black rectangle) and I fixed the background (wrong UV).

@tmsrise
Copy link

tmsrise commented Jul 14, 2022

does this work with controller navigation?

@ocornut
Copy link
Owner Author

ocornut commented Jul 14, 2022 via email

@tmsrise
Copy link

tmsrise commented Jul 19, 2022

For those of you that want to implement controller support:

  1. There is no ImGuiKey_GamepadLStickX, but there is ImGuiKey_GamepadLStickUp, down, left, right, etc, so you need to subtract.

  2. Implement hovered memory so that releasing the joystick keeps the highlight and subsequent return when you close the menu.

  3. Mind the inner_spacing gaps. For controllers it creates awkward locations inbetween elements that results in no selection. Either remove it or create separate floats for the actual selectable and visual representation of segments if you want a clean look without usability sacrifice.

  4. Implement the controller equivalent to the minimum drag distance (deadzone) so that the kickback from releasing a thumbstick doesn't inadvertently select a different element. I've found |x| or |y| greater than 0.6 works well.

@berthubert
Copy link

This could maybe better live within ImPlot? https://github.com/epezent/implot

@D7ry
Copy link

D7ry commented Jun 3, 2023

Hi! just dropping by to say thanks for the initial idea. I've managed to make sth cool with it

@tmsrise
Copy link

tmsrise commented Jun 3, 2023

Hi! just dropping by to say thanks for the initial idea. I've managed to make sth cool with it

Nice! I used it to make a combat art + prosthetic wheel in Sekiro. Yours seems much more polished though. I was considering doing a rewrite that would fix the bugs, crashes, and terrible spaghetti code. Mind if I yoink parts of that with credit? (if I even get to it lol)

@D7ry
Copy link

D7ry commented Jun 4, 2023

@tmsrise It's MIT licensed so take anything you'd like :)

@ocornut
Copy link
Owner Author

ocornut commented Apr 16, 2024

Posting a minor update to the 2015 version (some code simplification) note however that it is functionally the same.

I reckon Thibault's version may be better suited: #434 (comment)

#include "imgui_internal.h"

// Return >= 0 on mouse release
// Optional int* p_selected display and update a currently selected item
int PiePopupSelectMenu(const ImVec2& center, const char* popup_id, const char** items, int items_count, int* p_selected)
{
    int ret = -1;

    if (ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoDecoration))
    {
        const ImVec2 drag_delta = ImVec2(ImGui::GetIO().MousePos.x - center.x, ImGui::GetIO().MousePos.y - center.y);
        const float drag_dist2 = drag_delta.x * drag_delta.x + drag_delta.y * drag_delta.y;

        const ImGuiStyle& style = ImGui::GetStyle();
        const float RADIUS_MIN = 30.0f;
        const float RADIUS_MAX = 120.0f;
        const float RADIUS_INTERACT_MIN = 20.0f;    // Handle hit testing slightly below RADIUS_MIN
        const int ITEMS_MIN = 6;                    // If they are less than 6 items, we still make each item fill a 1/6 slice.

        // Draw background
        ImDrawList* draw_list = ImGui::GetWindowDrawList();
        draw_list->PushClipRectFullScreen();
        draw_list->PathArcTo(center, (RADIUS_MIN + RADIUS_MAX) * 0.5f, 0.0f, IM_PI * 2.0f);
        draw_list->PathStroke(IM_COL32(0, 0, 0, 255), ImDrawFlags_Closed, RADIUS_MAX - RADIUS_MIN);

        const float item_arc_span = 2 * IM_PI / ImMax(ITEMS_MIN, items_count);
        float drag_angle = ImAtan2(drag_delta.y, drag_delta.x);
        if (drag_angle < -0.5f * item_arc_span)
            drag_angle += 2.0f * IM_PI;
        //ImGui::Text("%f", drag_angle);    // [Debug]

        // Draw items
        int item_hovered = -1;
        for (int item_n = 0; item_n < items_count; item_n++)
        {
            const char* item_label = items[item_n];
            const float item_ang_min = item_arc_span * (item_n + 0.02f) - item_arc_span * 0.5f; // FIXME: Could calculate padding angle based on how many pixels they'll take
            const float item_ang_max = item_arc_span * (item_n + 0.98f) - item_arc_span * 0.5f;

            bool hovered = false;
            if (drag_dist2 >= RADIUS_INTERACT_MIN * RADIUS_INTERACT_MIN)
                if (drag_angle >= item_ang_min && drag_angle < item_ang_max)
                    hovered = true;

            bool selected = p_selected && (*p_selected == item_n);

            draw_list->PathArcTo(center, RADIUS_MAX - style.ItemInnerSpacing.x, item_ang_min, item_ang_max);
            draw_list->PathArcTo(center, RADIUS_MIN + style.ItemInnerSpacing.x, item_ang_max, item_ang_min);
            draw_list->PathFillConvex(ImGui::GetColorU32(hovered ? ImGuiCol_HeaderHovered : selected ? ImGuiCol_HeaderActive : ImGuiCol_Header));

            ImVec2 text_size = ImGui::CalcTextSize(item_label);
            ImVec2 text_pos = ImVec2(
                center.x + cosf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.x * 0.5f,
                center.y + sinf((item_ang_min + item_ang_max) * 0.5f) * (RADIUS_MIN + RADIUS_MAX) * 0.5f - text_size.y * 0.5f);
            draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), item_label);

            if (hovered)
                item_hovered = item_n;
        }
        draw_list->PopClipRect();

        if (ImGui::IsMouseReleased(0))
        {
            ImGui::CloseCurrentPopup();
            ret = item_hovered;
            if (p_selected)
                *p_selected = item_hovered;
        }
        ImGui::EndPopup();
    }
    return ret;
}

Usage:

    static const char* test_data = "Menu";
    const char* items[] = { "Orange", "Blue", "Purple", "Gray", "Yellow", "Las Vegas" };
    int items_count = sizeof(items) / sizeof(*items);

    static int selected = -1;

    ImGui::Button(selected >= 0 ? items[selected] : "Menu", ImVec2(50, 50));
    if (ImGui::IsItemActive())          // Don't wait for button release to activate the pie menu
        ImGui::OpenPopup("##piepopup");

    ImVec2 pie_menu_center = ImGui::GetIO().MouseClickedPos[0];
    int n = PiePopupSelectMenu(pie_menu_center, "##piepopup", items, items_count, &selected);
    if (n >= 0)
        printf("returned %d\n", n);
}

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