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

Generic Box() component #5944

Open
ypujante opened this issue Dec 1, 2022 · 10 comments
Open

Generic Box() component #5944

ypujante opened this issue Dec 1, 2022 · 10 comments

Comments

@ypujante
Copy link
Contributor

ypujante commented Dec 1, 2022

I have created a generic Box component (inspired by jetpack compose) for the purpose of:

  • having padding (top/right/bottom/left)
  • background color
  • border

It works great as can be seen in this image

imgui-box

Here is the code I have:

struct Modifier
{
  ImVec4 fPadding{};
  ImU32 fBackgroundColor{};
  ImU32 fBorderColor{};

  constexpr Modifier &padding(float iTop, float iRight, float iBottom, float iLeft) { fPadding = {iTop, iRight, iBottom, iLeft}; return *this; } // x/y/z/w
  constexpr Modifier &padding(float iPadding) { return padding(iPadding, iPadding, iPadding, iPadding); }
  constexpr Modifier &padding(float iHorizontalPadding, float iVerticalPadding) { return padding(iVerticalPadding, iHorizontalPadding, iVerticalPadding, iHorizontalPadding); }

  constexpr Modifier &backgroundColor(ImU32 iColor) { fBackgroundColor = iColor; return *this; }
  constexpr Modifier &borderColor(ImU32 iColor) { fBorderColor = iColor; return *this; }
};

// note that I use ImVec2 math in this code
void Box(Modifier const &iModifier, std::function<void()> const &iBoxContent)
{
  // split draw list in 2
  ImDrawListSplitter splitter{};
  splitter.Split(ImGui::GetWindowDrawList(), 2);

  // first we draw in channel 1 to render iBoxContent (will be on top)
  splitter.SetCurrentChannel(ImGui::GetWindowDrawList(), 1);

  ImGui::BeginGroup();
  {
    auto position = ImGui::GetCursorPos();
    // account for padding left/top
    ImGui::SetCursorPos(position + ImVec2{iModifier.fPadding.w, iModifier.fPadding.x});
    iBoxContent();
    ImGui::EndGroup();
  }

  auto min = ImGui::GetItemRectMin();
  // account for padding right/bottom
  auto max = ImGui::GetItemRectMax() + ImVec2{iModifier.fPadding.y, iModifier.fPadding.z};

  // second we draw the rectangle and border in channel 0 (will be below)
  splitter.SetCurrentChannel(ImGui::GetWindowDrawList(), 0);

  // draw the background
  if(!ColorIsTransparent(iModifier.fBackgroundColor))
    ImGui::GetWindowDrawList()->AddRectFilled(min, max, iModifier.fBackgroundColor);

  // draw the border
  if(!ColorIsTransparent(iModifier.fBorderColor))
    ImGui::GetWindowDrawList()->AddRect(min, max, iModifier.fBorderColor);

  // merge the 2 draw lists
  splitter.Merge(ImGui::GetWindowDrawList());

  // reposition the cursor (top left) and render a "dummy" box of the correct size so that it occupies
  // the proper amount of space
  ImGui::SetCursorScreenPos(min);
  ImGui::Dummy(max - min);
}

It works great, but of course it is a very different api style from the rest of ImGui. I should have instead a BeginBox(Modifier) and EndBox() api instead. The reason why I was not able to follow the same API is that in the EndBox() call I need information that is provided in the BeginBox call (like the colors and padding). Is there any recommended way on how I could do that (I guess I would need some form of "generic" PushMyVar api)?

@ocornut
Copy link
Owner

ocornut commented Dec 1, 2022

You would need to store that state somewhere.
One approach is to let user instantiate the stare storage (e.g. on stack) and have it passed to BeginBox()/EndBox().

Please note instantiating and using a new Splitter every frame is prohibitively costly. We never do such thing in Dear ImGui codebase, as a rule of thumb Dear ImGui should never allocate on idle frames, only occasionally on growth/opening-something frames.

My plan for e.g. an upcoming BeginSelectable()/EndSelectable() which should hold being drawn thousands of times a frame, is to claim vertices/indices ahead of time and then fill them (or reposition/recolor them) during the End operation. This works easily for AddRectFilled() without-rounding which is a known number of vertices, but more other shapes may not be as easier as vertices count may vary. Perhaps this feature will need to introduce ImDrawFlags to specify drawing techniques which are more deterministic.

@ypujante
Copy link
Contributor Author

ypujante commented Dec 1, 2022

Thank you for the feedback. How would you recommend implementing my box widget without instantiating and using a new Splitter every frame?

@ypujante
Copy link
Contributor Author

ypujante commented Dec 1, 2022

@ocornut Are you suggesting that somehow I can issue the AddRectFilled first with some default dimension, then render the content and somehow get back in the list of commands issued for AddRectFilled and change its dimension after the fact? That would be very clever (I just don't know how I would do that).

@ocornut
Copy link
Owner

ocornut commented Dec 1, 2022

Thank you for the feedback. How would you recommend implementing my box widget without instantiating and using a new Splitter every frame?

Keep a splitter around and reuse it across frames.

Are you suggesting that somehow I can issue the AddRectFilled first with some default dimension, then render the content and somehow get back in the list of commands issued for AddRectFilled and change its dimension after the fact? That would be very clever (I just don't know how I would do that).

Yes that's my suggestion for AddRectFilled().. I suggested it may not be as easy to do for other shapes, but non-rounded AddRect() should also work and use a deterministic number of indices/vertices. The first pass you may record drawlist positions, claim PrimReserve(), second pass sort of seek back into position, add AddXXX primitives, and restore position. That's essentially a super specialized version of splitter. I would need to experiment with that and see if I can actually bundle it into a shared helper.

We already use ShadeVertsLinearUV() and ShadeVertsLinearColorGradientKeepAlpha() to adjust already-submitted vertices but it is a much simpler process than the need to call ->AddXXX primitives as a second pass.

@ypujante
Copy link
Contributor Author

ypujante commented Dec 2, 2022

I took a look at PrimReserve() and it seems doable for AddRectFilled but it starts to be pretty hairy with AddRect (due to the AddPolyline call...).

I think for now, I will simply declare

static ImDrawListSplitter splitter{};

which should solve the "using a new Splitter every frame is prohibitively costly" issue. The side effect is that Boxes are no longer nestable but that is not an issue with my specific use case.

@ypujante
Copy link
Contributor Author

ypujante commented Dec 3, 2022

I wanted to close the loop and provide the "final" code with the following features:

  • support for nesting (provide your own (static) splitter)
  • optimized in the event of no background or border (just padding) (don't use the splitter)
void Box(Modifier const &iModifier, 
         std::function<void()> const &iBoxContent,
         ImDrawListSplitter *iSplitter = nullptr)
{
  // Implementation note: this is made static because of this https://github.com/ocornut/imgui/issues/5944#issuecomment-1333930454
  // "using a new Splitter every frame is prohibitively costly".
  static ImDrawListSplitter kSplitter{};

  auto hasBackground = !ColorIsTransparent(iModifier.fBackgroundColor);
  auto hasBorder = !ColorIsTransparent(iModifier.fBorderColor);

  ImDrawList *drawList{};

  if(hasBackground || hasBorder)
  {
    drawList = ImGui::GetWindowDrawList();
    if(!iSplitter)
      iSplitter = &kSplitter;

    // split draw list in 2
    iSplitter->Split(drawList, 2);

    // first we draw in channel 1 to render iBoxContent (will be on top)
    iSplitter->SetCurrentChannel(drawList, 1);
  }

  auto min = ImGui::GetCursorScreenPos();
  // account for padding left/top
  ImGui::SetCursorScreenPos(min + ImVec2{iModifier.fPadding.w, iModifier.fPadding.x});

  ImGui::BeginGroup();
  {
    iBoxContent();
    ImGui::EndGroup();
  }

  // account for padding right/bottom
  auto max = ImGui::GetItemRectMax() + ImVec2{iModifier.fPadding.y, iModifier.fPadding.z};

  if(drawList)
  {
    // second we draw the rectangle and border in channel 0 (will be below)
    iSplitter->SetCurrentChannel(drawList, 0);

    // draw the background
    if(hasBackground)
      drawList->AddRectFilled(min, max, iModifier.fBackgroundColor);

    // draw the border
    if(hasBorder)
      drawList->AddRect(min, max, iModifier.fBorderColor);

    // merge the 2 draw lists
    iSplitter->Merge(drawList);
  }

  // reposition the cursor (top left) and render a "dummy" box of the correct size so that it occupies
  // the proper amount of space
  ImGui::SetCursorScreenPos(min);
  ImGui::Dummy(max - min);
}

Here is an example of usage (with nesting):

    // constexpr ImU32 kWhiteColor = ...;
    // constexpr ImU32 kBoxColor = ...;
    // constexpr ImU32 kNestedBoxColor = ...;

    Box(Modifier{}.padding(10.0f).backgroundColor(kBoxColor).borderColor(kWhiteColor), []{
      static ImDrawListSplitter kNestedSplitter{}; // note how this is static!
      ImGui::Text("Inside before");
      ReGui::Box(Modifier{}.padding(15.0f).backgroundColor(kNestedBoxColor), [] {
                   ImGui::Text("Nested");
                 },
                 &kNestedSplitter); // passing the splitter to allow for nesting
      ImGui::Text("Inside after");
    });

which renders like this:

Screen Shot 2022-12-03 at 09 30 25

@sukesh-ak
Copy link

Thanks for the snippet @ypujante

I was trying to use this as a Card UI, so had few questions.

  • Where is ColorIsTransparent coming from?
  • If I use separator like below, it extends outside of the box.
Box(Modifier{}.padding(10.0f).backgroundColor(kBoxColor).borderColor(kWhiteColor), [] {
ImGui::Text("Header");
ImGui::Separator(); // <= This extends outside the Box
ImGui::Text("Content");
});

I am relatively new to ImGui so checking if you have any tips to fix it.

@ypujante
Copy link
Contributor Author

ColorIsTransparent is implemented this way

constexpr bool ColorIsTransparent(ImU32 iColor)
{
  return (iColor & IM_COL32_A_MASK) == 0;
}

I think the issue with Separator() is that it always extend to the size of the window itself and I am not sure why ImGui::GetItemRectMax() after the Begin/End Group() which contains the separator does not account for it. I am also not an expert in ImGui.

@sukesh-ak
Copy link

ColorIsTransparent is implemented this way

constexpr bool ColorIsTransparent(ImU32 iColor)
{
  return (iColor & IM_COL32_A_MASK) == 0;
}

I think the issue with Separator() is that it always extend to the size of the window itself and I am not sure why ImGui::GetItemRectMax() after the Begin/End Group() which contains the separator does not account for it. I am also not an expert in ImGui.

Thank you.
Will wait for @ocornut to comment on that then.

@sukesh-ak
Copy link

@ypujante I think this is related, haven't tried yet.
#1643

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

3 participants