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

It would be nice to use RAII for pushing/popping #2096

Open
sethk opened this issue Sep 21, 2018 · 80 comments
Open

It would be nice to use RAII for pushing/popping #2096

sethk opened this issue Sep 21, 2018 · 80 comments

Comments

@sethk
Copy link

sethk commented Sep 21, 2018

This is especially true because some things need to be popped even when they aren't open (e.g. child regions), but it's difficult to remember that.

@ocornut
Copy link
Owner

ocornut commented Sep 21, 2018

You can create C++ helpers for doing just that, they would be a few lines to implement. I am open the idea of providing an official .h file with those helpers if they are designed carefully.

Begin/BeginChild are inconsistent with other API for historical reasons unfortunately :(

@sethk
Copy link
Author

sethk commented Sep 21, 2018

I think I'll take a crack at it and post something here for people to critique.

@maxkunes
Copy link

Something I put together a while back, only handles PushStyleVar and PushStyleColor, would probably be nice to improve it to handle other ImGui push/pop methods.

Header (ImStyle_RAII.h):

#pragma once
#include "imgui.h"

class ImStyle_RAII {
public:

	ImStyle_RAII(ImGuiStyleVar idx, const ImVec2& val);

	ImStyle_RAII(ImGuiStyleVar idx, const float& val);

	ImStyle_RAII(ImGuiCol idx, const ImU32& col);

	~ImStyle_RAII();

private:
	bool bClr;
};

Source (ImStyle_RAII.cpp):

#include "ImStyle_RAII.h"

ImStyle_RAII::ImStyle_RAII(ImGuiStyleVar idx, const ImVec2& val)
{
	bClr = false;
	ImGui::PushStyleVar(idx, val);
}

ImStyle_RAII::ImStyle_RAII(ImGuiStyleVar idx, const float& val)
{
	bClr = false;
	ImGui::PushStyleVar(idx, val);
}

ImStyle_RAII::ImStyle_RAII(ImGuiCol idx, const ImU32 & col)
{
	bClr = true;
	ImGui::PushStyleColor(idx, col);
}

ImStyle_RAII::~ImStyle_RAII()
{
	if (bClr)
		ImGui::PopStyleColor();
	else
		ImGui::PopStyleVar();
}

@meshula
Copy link

meshula commented Sep 23, 2018

My most used RAII object for Imgui:

class SetFont
{
public:
	SetFont(ImFont* f) { ImGui::PushFont(f); }
	~SetFont() { ImGui::PopFont(); }
};

@sethk
Copy link
Author

sethk commented Sep 24, 2018

Looks like there's some demand for this! Here's what I'm working with so far:

The only part I can imagine being controversial is that I'm providing operator bool() so that you can say for instance ImWindow window("Blah"); if (window) ....

#include "imgui.h"

#pragma once

class ImWindow
{
public:
    bool IsOpen;

    ImWindow(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0) { IsOpen = ImGui::Begin(name, p_open, flags); }
    ~ImWindow() { if (IsOpen) ImGui::End(); }

    operator bool() { return IsOpen; }
};

class ImPushID
{
public:
    ImPushID(const char* str_id) { ImGui::PushID(str_id); }
    ImPushID(const char* str_id_begin, const char* str_id_end) { ImGui::PushID(str_id_begin, str_id_end); }
    ImPushID(const void* ptr_id) { ImGui::PushID(ptr_id); }
    ImPushID(int int_id) { ImGui::PushID(int_id); }
    ~ImPushID() { ImGui::PopID(); }
};

class ImTreeNode
{
public:
    bool IsOpen;

    ImTreeNode(const char* label) { IsOpen = ImGui::TreeNode(label); }
    ImTreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(str_id, fmt, ap); va_end(ap); }
    ImTreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(3) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeV(ptr_id, fmt, ap); va_end(ap); }
    ~ImTreeNode() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeV
{
public:
    bool IsOpen;

    ImTreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(str_id, fmt, args); }
    ImTreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(3) { IsOpen = ImGui::TreeNodeV(ptr_id, fmt, args); }
    ~ImTreeNodeV() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeEx
{
public:
    bool IsOpen;

    ImTreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0) { IsOpen = ImGui::TreeNodeEx(label, flags); }
    ImTreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, ap); va_end(ap); }
    ImTreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(4) { va_list ap; va_start(ap, fmt); IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, ap); va_end(ap); }
    ~ImTreeNodeEx() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

class ImTreeNodeExV
{
public:
    bool IsOpen;

    ImTreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(str_id, flags, fmt, args); }
    ImTreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(4) { IsOpen = ImGui::TreeNodeExV(ptr_id, flags, fmt, args); }
    ~ImTreeNodeExV() { if (IsOpen) ImGui::TreePop(); }

    operator bool() { return IsOpen; }
};

@maxkunes
Copy link

maxkunes commented Sep 24, 2018

@sethk Interesting. I have wrapped ImGui children and windows in a similar fashion but without RAII. I'm currently using lamda's for this use.

ImWindowHelper and ImChildHelper implicitly calls begin/end accordingly.

Ex Pseudo :

ImWindowHelper(str_id ..., [&]() {

    ImChildHelper(str_id, size, ..., [&]() {
        ImGui::Text("Child 1");
    });

    ImChildHelper(str_id, size, ..., [&]() {
        ImGui::Text("Child 2");
    });

});

@ice1000
Copy link
Contributor

ice1000 commented Sep 24, 2018

Consider

{
  ImWindowHelper(str_id, ...);
  {
    ImChildHelper(str_id, ...);
    ImGui::Text("Child 1");
  }
}

which is less noisy.

@maxkunes
Copy link

maxkunes commented Sep 24, 2018

@ice1000
Are the destructors garenteed to happen after that call to Text? I'm quite sure you would need to store those raii instances in an actual variable for that code to work correctly. Not sure the standard spec on that. That is the reason I chose the lamda method as I don't need to worry much about scope and how the compiler may treat the code.

@ice1000
Copy link
Contributor

ice1000 commented Sep 24, 2018

It works with clang++-6.0 but I didn't lookup the standard spec.

@ice1000
Copy link
Contributor

ice1000 commented Sep 24, 2018

Confirmed: if you don't throw exceptions in the constructor, it's destructed at the time as we expected in my code snippet above.

From https://en.cppreference.com/w/cpp/language/destructor :

The destructor is called whenever an object's lifetime ends, which includes

end of scope, for objects with automatic storage duration and for temporaries whose life was extended by binding to a reference

From http://eel.is/c++draft/class.dtor#:destructor,implicit_call :

A destructor is invoked implicitly

for a constructed object with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl]),

@maxkunes
Copy link

maxkunes commented Sep 25, 2018

Confirmed: if you don't throw exceptions in the constructor, it's destructed at the time as we expected in my code snippet above.

From https://en.cppreference.com/w/cpp/language/destructor :

The destructor is called whenever an object's lifetime ends, which includes
end of scope, for objects with automatic storage duration and for temporaries whose life was extended by binding to a reference

From http://eel.is/c++draft/class.dtor#:destructor,implicit_call :

A destructor is invoked implicitly
for a constructed object with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl]),

@ice1000 MSVC doesn't like what your doing which suggests to me the standard isn't actually saying
what you think it is, that is unless I'm making a mistake or MSVC isn't following the standard although I believe the former is more likely.

{
		ImWindowHelper("BaseMenu", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings);

		BeginChildLamda("Menu", menuSize, false, false, [&]() {
			renderTabs();
			renderContent();
			renderBanner();
		});

}

Relevant ImWindowHelper class

ImWindowHelper::ImWindowHelper(const char* name, bool* p_open, ImGuiWindowFlags flags)
{
	ImGui::Begin(name, p_open, flags);
}

ImWindowHelper::~ImWindowHelper()
{
	ImGui::End();
}

And it compiles on MSVC v141 to this as I suspected :

ImWindowHelper::ImWindowHelper((ImWindowHelper *)&_This.tabWidthPercentage + 3, "BaseMenu", 0, 259);
  ImWindowHelper::~ImWindowHelper((ImWindowHelper *)&_This.tabWidthPercentage + 3);

Essentially it gets instantly destructed after it gets constructed, simply doing this :

{
		auto baseMenuWin = ImWindowHelper("BaseMenu", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings);

		BeginChildLamda("Menu", menuSize, false, false, [&]() {
			renderTabs();
			renderContent();
			renderBanner();
		});
//baseMenuWin will be destructed here.
}

Would work as expected and would be destructed where you want it to be. This is precisely why I chose lamdas, because you can definately ensure what code gets called when.

@sethk
Copy link
Author

sethk commented Sep 25, 2018

@ice1000 That syntax reminds me of something I saw in a Facebook C++ talk about some very insidious bugs they had when RAII initializations were missing variable names and thus turned into side-effect free declarations. The compactness is nice if it works, but it loses the ability to bypass code when windows are collapsed or clipped, etc. In any case, that syntax should be possible with my wrappers, if it works for you. What C++ standard are you targetting with Clang?

@maxkunes I like the idea of lambdas as well. Are there any drawbacks? I haven't used C++ lambdas much. Is there any runtime overhead when they're used as coroutines like this?

@maxkunes
Copy link

maxkunes commented Sep 25, 2018

@sethk Regarding the question about RAII you referenced @ice1000 I would direct you to my post above yours where I show that at the very least, MSVC will not work as he expects, my guess is as good as yours if it works other places.

EDIT: Through more research from https://godbolt.org/ seems many compilers will call the destructor instantly after constructor when not allocated locally.

Regarding lamdas, to my knowledge, they don't have many drawbacks, but I heard somewhere that std::function has a little bit of overhead (should fact check that), but for example, if you use function pointers with lamdas, I don't think there is any overhead.

@meshula
Copy link

meshula commented Sep 26, 2018

class RAII {
public:
    RAII() { printf("A");}
    ~RAII() { printf("~A");}
};
class RAII2 {
    public:
    RAII2() { printf("B");}
    ~RAII2() { printf("~B");}
};

void bad() { RAII(); RAII2(); } // destruct instantly because scope is itself
void good() { RAII a; RAII2 b; } // destruct according to containing scope

@sethk
Copy link
Author

sethk commented Oct 5, 2018

Just to illustrate my point, I totally forgot that ImGui::End() needs to be called regardless of the return value of ImGui::Begin(), because that's how you handle the case where the window is open but collapsed. Anyway, if you want to play along at home I'll be gradually updating this header in this branch as I test it:

https://github.com/sethk/imgui/blob/raii/misc/raii/imgui_raii.h

@ocornut
Copy link
Owner

ocornut commented Oct 5, 2018

@sethk Looking good!

Some pieces of feedback:

  • Because some people would want to avoid <string> but may want to use this, this should probably be kept in a separate file than the existing imgui_stl.h. I will rename the existing folder to misc/cpp to prepare for the possibility to adding more files in there.
  • IsExpanded should be IsContentsVisible to be more accurate and explanatory.
  • Even though it is practical, it is a incorrect using the Im prefix instead of ImGui here. In the codebase, Im is reserved for things that don't sit on top of ImGui functions or context.
  • Since everything in your class is public, you could use struct and this is what we do everywhere in imgui codebase (I also like it because it challenges C++ dogmas a little :)

ocornut added a commit that referenced this issue Oct 12, 2018
@jdumas
Copy link
Contributor

jdumas commented Oct 12, 2018

This looks good! Another minor comment I have is: we should probably remove the copy/move constructors of those classes, even though it's unlikely someone will make the mistake of copying those guys. A simple macro can facilitate the job for that:

#define IMGUI_DELETE_MOVE_COPY(Base)     \
    Base(Base&&) = delete;                 \
    Base& operator=(Base&&) = delete;      \
    Base(const Base&) = delete;            \
    Base& operator=(const Base&) = delete; \

(If you want to stay pre-C++11 you can declare them private and not define them instead of deleting them.)

@ocornut
Copy link
Owner

ocornut commented Oct 12, 2018

If you want to stay pre-C++11

I'm happy with enforcing C++11 as a requirement for those C++ extension.
(In fact, I don't rule out doing the same for core imgui maybe next year.. will see.)

@sethk
Copy link
Author

sethk commented Oct 18, 2018

  • IsExpanded should be IsContentsVisible to be more accurate and explanatory.

How about IsContentVisible? I'm only objecting to the grammar...

  • Even though it is practical, it is a incorrect using the Im prefix instead of ImGui here. In the codebase, Im is reserved for things that don't sit on top of ImGui functions or context.

Sounds good, but there is already an ImGuiWindow struct in the global namespace due to imgui_internal.h. What name should I use for a Begin()/End() wrapper?

@oberrich
Copy link

oberrich commented Oct 30, 2018

I could imagine using std::unique_ptr to create so-called finally-actions rather than writing a class for each function pair. It makes it a lot more trivial to implement the individual guards.

See this proof of concept for reference

// Original 'finally' function from https://softwareengineering.stackexchange.com/a/308747
template<typename F>
[[nodiscard]] auto im_finally(F f, bool active = true) noexcept(noexcept(F(std::move(f)))) {
  auto x = [f = std::move(f)](void*){ f(); };
  return std::unique_ptr<void, decltype(x)>((void*)(active), std::move(x));
}

[[nodiscard]] inline auto im_window(char const *name, bool *open = nullptr, ImGuiWindowFlags flags = 0) {
  return im_finally([]{ ImGui::End(); }, ImGui::Begin(name, open, flags));
}

// Usage

  if (auto window = im_window("cpp_im_gui", ...)) {

    // add buttons or whatever

  } // ImGui::Begin == true -> unique_ptr.get() != nullptr -> call deleter
    // ImGui::Begin == false -> unique_ptr.get() == nullptr -> dont call deleter

Here is a working demo of the concept https://ideone.com/KDJz25.
More elaborate tests here https://godbolt.org/z/KPcgP8

@ocornut I would love to hear your thoughts on this approach

@meshula
Copy link

meshula commented Oct 30, 2018

@obermayrrichard Using unique_ptr like that is clever! For reference, I use the finally in Microsoft's GSL implementation - https://github.com/Microsoft/GSL/blob/master/include/gsl/gsl_util

@ocornut
Copy link
Owner

ocornut commented Oct 30, 2018

@sethk Apologies for later answer.

How about IsContentVisible? I'm only objecting to the grammar...

Sure.

Sounds good, but there is already an ImGuiWindow struct in the global namespace due to imgui_internal.h. What name should I use for a Begin()/End() wrapper?

Good question, I don't have the answer to this unfortunately. Let us think about it .. or I wonder if instead we could opt for a specific prefix to denote the type of functions we are discussing here..

@obermayrrichard tbh I find this solution really unnecessary and overkill - harder to understand, harder to debug/step into, probably slower in non-optimized builds, it raises the user C++ minimum proficiency requirements, drag unnecessary dependencies; and none of those things are in the spirit of dear imgui. I see no value in it considering the solution proposed initially is so simple and obvious to write and maintain.

@ratchetfreak
Copy link

And the solution breaks down a bit if you do:

auto win1 = im_window("cpp_im_gui", ...);
if(win1){

}

//win1 isn't closed at this point
auto win2 = im_window("cpp_im_gui", ...);
if(win2){

}

because win1 doesn't get destroyed before win2 gets created.

Also if the user doesn't create the variable it gets immediately destroyed after the condition is checked with no warning.

if(im_window("cpp_im_gui", ...)){
    //window is already closed
}

IOW a bit too fragile in normal use for my tastes.

@jdumas
Copy link
Contributor

jdumas commented Oct 30, 2018

But that's an issue with both RAII and the unique_ptr no? To be honest I have no idea how this unique_ptr solution works, and I've been coding in C++ for 10 years...

Good question, I don't have the answer to this unfortunately. Let us think about it .. or I wonder if instead we could opt for a specific prefix to denote the type of functions we are discussing here..

How about Impp, or ImGuipp or something like this, to denote the C++ side of it. Or maybe ImStdlib like the filename? Or use its own namespace to be more C++y?

@ratchetfreak
Copy link

Actually it's more similar how a scope_guard works for a mutex. You need to create a local variable for the guard and make sure it goes out of scope at the correct time.

With std::unique_ptr it's less of an issue because the unique_ptr itself is your handle to the resource it guards so you are not going to forget making a variable for it.

@oberrich
Copy link

oberrich commented Oct 30, 2018

Writing a scope-guard class and using a finally action like this is roughly equivalent, imo it's just less code and less painful to implement the individual.

Both writing an RAII class and finally have the same drawbacks in terms of pitfalls users can run into.
They are both RAII classes in the end, the one being written manually, the other one taking unique_ptrs existing code, abusing the internal pointer as our bool variable and moving the deleter into it which gets called by its dtor if ptr/our bool is zero.

I don't see any obvious way of how we could prevent users doing if (scope_guard{}).
I have tried doing [[nodiscard]] operator bool() {...}.
The only way to explicitly disallow this sort of behavior I can think of is removing the bool conversion operator altogether.

@jdumas
Copy link
Contributor

jdumas commented Oct 30, 2018

Well if it's the same thing then why not stick to the version with no template porn?

@oberrich
Copy link

oberrich commented Oct 30, 2018

Well if it's the same thing then why not stick to the version with no template porn?

As I said previously

imo it's just less code and less painful to implement the individual [function pairs].

In the end it comes down to preference. I don't really care which version will be implemented since I'm not actively involved in imgui's development.
I don't think it's worth discussing my proposal any further since it will only end in a pointless discussion like tabs vs. spaces.

@sethk
Copy link
Author

sethk commented Jan 13, 2019

I would want it for something like this.

{
    style_object mySuperCoolStyle( ... );
    // a bunch of code here
    if( cond ) mySuperCoolStyle.dismiss();
    // a bunch more code
    if( otherCond ) mySuperCoolStyle.dismiss();
    // ...
} // if it wan't popped or if an exception was thrown, it will be popped in the destructor

Similarly to what @jdumas says, if we allow a style object to be popped in the middle of a block, then we've broken the connection between the style's lifetime and the block, meaning that there's no guarantee we're popping the correct style when dismiss() is called.

In general, my intent was not to achieve exception safety with RAII, but to solve a similar problem of forgetting to call the appropriate pop method manually. In order to have exception safety, you would need to use RAII for every possible push/pop of any type of context, and it seems like at that point you might as well start maintaining stateful UI elements instead of using immediate mode.

@psocolovsky
Copy link

can I ask why you do not merge ImTreeNode with TreeNodeEx and ImTreeNodeV with ImTreeNodeExV ?
I don't see why having multiple classes when the constructors are different. the API is supposed to be a helper, the simpler the better I guess...

@sethk
Copy link
Author

sethk commented May 19, 2019

@psocolovsky Are you sure the constructors are unambiguous? If so, why are they different functions within the ImGui namespace instead of being overloaded? I guess it could just be a design choice. I actually didn't put much thought into this, and generated the wrappers with a script, so it didn't seem obvious that they should be combined.

@psocolovsky
Copy link

Hi sethk, if you look in the source code both TreeNode and TreeNodeEx end up calling the TreeNodeExV version. the V version is useless in the RAII wrapper, all of the calls made using your wrapper will be the TreeNode and TreeNodeEx versions, and if someone is using the variadic function ever is probably working at low level and RAII is unlikely to be desireable.

The TreeNode and TreeNodeEx have different function names probably because it is not rare to play around with a function and then change from
ImGui::TreeNodeEx( x, ImGuiTreeNodeFlags_Selected, "hello");
to
ImGui::TreeNodeEx( x, 0, "hello");

if TreeNodeEx is overloaded with TreeNode signature the compiler may either complain about ambiguity or call the wrong function passing 0 to const char* fmt triggering an assert failure or crashing at first string pointer dereference
I think this is the most reasonable explanation of why having two different calls, however you should ask ocornut... maybe he just like it like that :D

@daiyousei-qz
Copy link

daiyousei-qz commented May 25, 2019

Hi, why not conditionally adds [[nodiscard]] attribute to those RAII wrapper structs for c++17 compatible compilers to pitfalls aforementioned.

if (ImScoped::Window("name")) {
    // codes ...
}

@sethk
Copy link
Author

sethk commented May 25, 2019

@edsdev Apparently if you do this, the compiler still considers the call to operator bool() as consuming the result of the initialization. See oberrich's comment above: #2096 (comment)

@oberrich
Copy link

oberrich commented May 25, 2019

@edsdev You would need a nodiscard on the constructor which isn't possible unfortunately.
Alternatively you would have to make functions returning an RAII wrapper and mark them nodiscard but iirc there are problems with that as well (probably what @sethk is referring to)

@gotocoffee1
Copy link

gotocoffee1 commented Jul 5, 2019

i find this macro (i know we all hate macros ;) ) quite useful:

#define CONCAT_(x,y) x##y
#define CONCAT(x,y) CONCAT_(x,y)
#define GUI(x, ...) ImScoped::x CONCAT(_gui_element, __COUNTER__) {__VA_ARGS__}

if (GUI(Window, "Test Window"))
{ ... }

@kfsone
Copy link
Contributor

kfsone commented May 15, 2021

I've done something like this in my own CMake/C++ bindings for ImGui:

#include "imguiwrap.dear.h"

void windowFn()
{
  dear::Begin{"My Window"} && [] {
    dear::TabBar{"##TabBar"} && [] {
      dear::TabItem{"About"} && [] {
        ImGui::Text("About what?");
      };
      dear::TabItem("Help"} && [] {
        ImGui::Text("Abandon all hope");
      };
    };
  };
}

for the cmake part, I've made it trivial to introduce imgui to a new project using FetchContent or CPM; I configure the targets properly so that as long as you do target_link_libraries(myprogram PUBLIC imguiwrapper) you'll have everything you need configured.

https://github.com/kfsone/imguiwrap

I'm still adding wrappers, and mac/linux builds will be next.

@ocornut
Copy link
Owner

ocornut commented May 18, 2021

FYI
dear::Begin{"My Window"} && [] {
This is currently not the right way to use Begin() (it is known to be inconsistent with other API, we have the fix but it's a rather painful transition tho we have helpers for it)

@kfsone
Copy link
Contributor

kfsone commented May 18, 2021

That's one of the things I wanted to encompass in the abstraction; imguiwrap handles Begin and BeginChild correctly (it will always call End, as opposed to say MenuBar, which will only call EndMenuBar if BMB returned true), and Group where BeginGroup is group, so it behaves as though BeginGroup returns true.

@mnesarco
Copy link

mnesarco commented Jun 3, 2021

Hi Friends,
I like the ideas from @sethk, but the link is broken (https://github.com/sethk/imgui/blob/raii/misc/raii/imgui_raii.h), how can i get it for testing?

@maxkunes
Copy link

maxkunes commented Jun 3, 2021

@mnesarco no clue if this is up to date, but this is mentioned in the readme for imgui/misc/cpp

#2197

@sethk
Copy link
Author

sethk commented Jun 4, 2021

@mnesarco Try here: https://github.com/sethk/imgui/blob/raii/misc/cpp/imgui_scoped.h

May be a bit out of date because the branch is a few years old now.

@mnesarco
Copy link

mnesarco commented Jun 4, 2021

@sethk Thank you.

@mnesarco
Copy link

mnesarco commented Jun 4, 2021

@sethk @ocornut

Hi Friends, I have learned a lot from this thread, and in the spirit of sharing my approach, i have published my own version :D. It would be great to hear your opinions.

This is not a proposal at all. I am just sharing my personal approach, I know that c++17 is almost banned here :D and macros should be hated to death, but this is just another view for the discussion.

https://github.com/mnesarco/imgui_sugar

Cheers.

@mnesarco
Copy link

mnesarco commented Jun 5, 2021

I have changed imgui_sugar to c++11 :)

@TerensTare
Copy link

TerensTare commented Jun 6, 2021

Good question, I don't have the answer to this unfortunately. Let us think about it .. or I wonder if instead we could opt for a specific prefix to denote the type of functions we are discussing here..

I know I'm some years late, but how about RaiiWindow/ScopedWindow and such?
As for the naming issue, I think we can safely write

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

which at least to me looks acceptable

@mnesarco
Copy link

mnesarco commented Jun 6, 2021

The problem with

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

Is that if you forgot to write the "_" it will compile without warning but won't work:

if (RaiiWindow ("MyWindow")) // notice the missing _
// blah blah

That is why I prefer the macro defined scope:

with_Window("My Window") {
  // blah blah
}

It is just a syntactic sugar, generates the same code but ensures that the RAII guard is named.

@TerensTare
Copy link

The problem with

if (RaiiWindow _("MyWindow")) // notice the _
// blah blah

Is that if you forgot to write the "_" it will compile without warning but won't work:

Right, that's a valid issue, but can be fixed by adding ref-qualifiers to the conversion operator. Please see https://godbolt.org/z/PeGGvqn6G.

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

@mnesarco
Copy link

mnesarco commented Jun 7, 2021

Right, that's a valid issue, but can be fixed by adding ref-qualifiers to the conversion operator. Please see https://godbolt.org/z/PeGGvqn6G.

Yes you are right, I just added ref qualifiers to my guards yesterday ;)

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

Yes it is a matter of user preferences, and i know that macros can be evil, but...

  1. The macros used are very innocent:
#define with_Whatever(...) if (Raii _ = Raii(Whatever(__VA_ARGS__)))

Internally I used other macros just to not repeat the same for every function. So i get a very small (<200 lines of code) header (including license and comments).

  1. The generated code is exactly the same
if (Raii _ = Raii(Whatever(args...))) 
  // blah blah
  1. The syntax is very terse
with_Window("Test") 
{
  with_MenuBar 
  {
    with_Menu("File") 
    {
        with_MenuItem("Edit")
            // blah blah
        with_MenuItem("Save")
           // blah blah
    }
  }
}
  1. I feel that the pros overcomes the cons, but that's my personal opinion of course .

So yes, it is a matter of personal preferences. 👍

@kfsone
Copy link
Contributor

kfsone commented Jun 9, 2021

The solution you propose is really nice (in syntax and hard-to-misuse terms) but I personally prefer avoiding macros, since the rest of the functions avoid macros.

Because ImGui is written in pseudo-C, it's very easy to forget how the language you're trying to work in actually works. Unless your constraints preclude the use of more modern C++, you can entirely avoid macros and just rely on good-old C++ object lifetimes by using method-on-a-temporary

https://gcc.godbolt.org/z/1xq737d1Y

and for the scopes, lambdas. This is how I was able to get pure RAII scoping with https://github.com/kfsone/imguiwrap

#include "imguiwrap.dear.h"
#include <array>
#include <string>

ImGuiWrapperReturnType
render_fn()  // opt-in functionality that gets called in a loop.
{
  bool quitting { false };
  dear::Begin("Window 1") && [&quitting](){   // or make quitting a static so capture not required.
    dear::Text("This is window 1");
    dear::Selectable("Click me to quit", &quitting);
  };
  if (quitting)
    return 0;

  dear::Begin("Window 2", nullptr, ImGuiWindowFlags_AlwaysAutoResize) && [](){
    static constexpr size_t boarddim = 3;
    static std::array<std::string, boarddim * boarddim> board { "X", "O", "O", "O", "X", "O", "O", "X", " " };
    dear::Table("0s and Xs", 3, ImGuiTableFlags_Borders) && [](){
      for (const auto& box : board) {
        ImGui::TableNextColumn();
        dear::Text(box);
      }
    };
  };
}

int main(int argc, const char* argv[])
{
    return imgui_main(argv, argv, my_render_fn);
}

@GasimGasimzada
Copy link

You can create C++ helpers for doing just that, they would be a few lines to implement. I am open the idea of providing an official .h file with those helpers if they are designed carefully.

Begin/BeginChild are inconsistent with other API for historical reasons unfortunately :(

Is there any information documentation or reference about this? Looking at the imgui_demo, I do not fully understand when the End statements need to be inside the Begin condition scope vs outside of it:

if (ImGui::Begin(...)) {

}

// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L2479
ImGui::End();

if (ImGui::BeginChild(...)) {

}

// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L3107
ImGui::EndChild();

if (ImGui::BeginTable(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L553
  ImGui::EndTable();
}

if (ImGui::MenuBar(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L2977
  ImGui::EndMenuBar();
}

if (ImGui::MainMenuBar(...)) {

  // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6421
  ImGui::EndMainMenuBar();
}

I am thinking of implementing my own RAII implementation and these two implementations matter in terms of when to apply this:

class Window {
public:
   Window(...) { mOpen = ImGui::BeginWindow(...); }

   ~Window() { ImGui::EndWindow(); }
private:
  bool mOpen = false;
};

class MenuBar {
public:
   MenuBar(...) { mOpen = ImGui::BeginMenuBar(...); }

   ~MenuBar() {
     if (mOpen)
       ImGui::EndMenuBar();
    }
private:
  bool mOpen = false;
};

@ocornut
Copy link
Owner

ocornut commented Aug 8, 2022 via email

@mnesarco
Copy link

Please don't implement RAII guards again and again and again.... There are many attempts already.
I implemented one of them myself, but not much people use it:

https://github.com/mnesarco/imgui_sugar

#include <imgui/imgui.h>
#include <imgui_sugar.hpp>

// ...

    static int left = 0, right = 0;
    ImGui::SetNextWindowPos(ImVec2(30, 50), ImGuiCond_FirstUseEver);
    set_StyleColor(ImGuiCol_WindowBg, ImVec4{0.88f, 0.88f, 0.88f, 1.0f});        
    set_StyleColor(ImGuiCol_Text, 0xff000000);

    with_Window("Test Window", nullptr, ImGuiWindowFlags_AlwaysAutoResize) {

        ImGui::Text("Hello");
        
        with_Group {
            ImGui::Text("Left %d", left);
            if (ImGui::Button("Incr Left"))
                ++left;
        }
        
        ImGui::SameLine();
        
        with_Group {
            set_StyleColor(ImGuiCol_Text, 0xffff0000);
        
            ImGui::Text("Right %d", right);
        
            if (ImGui::Button("Incr Right"))
                ++right;
        
            with_Child("##scrolling", ImVec2(200, 80)) {

                ImGui::Text("More text ...");
                ImGui::Text("More text ...");
                ImGui::Text("More text ...");
                
                with_StyleColor(ImGuiCol_Text, ImVec4{ 0, 0.5f, 0, 1.0f })
                    ImGui::Text("More text ...");
                
                ImGui::Text("More text ...");
                
                with_StyleColor(ImGuiCol_Text, ImVec4{ 0.5f, 0.0f, 0, 1.0f }) {
                    ImGui::Text("More text ...");
                    ImGui::Text("More text ...");
                }
            }
        }

        ImGui::Text("Bye...");
    }    


// ...

There are different approaches, Lambdas, Macros, ... Use one of the existent projects...

@GasimGasimzada
Copy link

I have created my own RAII implementation that I am really happy with. It works in the following way:

if (auto table = Table("MyTable", 3)) {
  // covers 95% of the use-cases for table
  // for me
  table.row("I am a text", glm::vec3(2.5f), 25.0f);

  // Custom implementation
  ImGui::TableNextRow();
  ImGui::TableNextColumn();
  ImGui::Button("I want a button inside this column");
  // ..other cols as well
}

if (auto _ = Window("Window title", open)) {
  ImGui::Text("Inside the window");
}

I may create a Macro around the API to make it cleaner but it is not important to me at the moment and it has already fixed one of the error-prone things for me, which was mismatching ends, especially when you have lots of UI code and it is easy to miss.

@sugrob9000
Copy link
Contributor

sugrob9000 commented Mar 10, 2023

In C++17 (I think), you can get C++ to generate most of the above API for you like so:

namespace ImScoped {
namespace detail {
template <auto Begin, auto End, bool UnconditionalEnd = false> class Widget {
    bool shown;
public:
    explicit Widget (auto&&... a)
        : shown{
            [] (auto&&... aa) {
                return Begin(std::forward<decltype(aa)>(aa)...);
            } (std::forward<decltype(a)>(a)...)
        } {}
    ~Widget () { if (UnconditionalEnd || shown) End(); }
    explicit operator bool () const& { return shown; }
    explicit operator bool () && = delete;
};
} // namespace detail

using Window = detail::Widget<ImGui::Begin, ImGui::End, true>;
using TabBar = detail::Widget<ImGui::BeginTabBar, ImGui::EndTabBar>;
using TabItem = detail::Widget<ImGui::BeginTabItem, ImGui::EndTabItem>;
using Table = detail::Widget<ImGui::BeginTable, ImGui::EndTable>;
// etc.
} // namespace ImScoped

Deleting the rvalue-ref overload of operator bool protects you from accidentally writing

if (ImScoped::Window(...)) // Wrong! the temporary would be destroyed immediately

If you wish to add methods, you can use

struct Table: detail::Widget<ImGui::Begin, ImGui::End> {
    using Widget::Widget;
    void row ();
    // etc.
};

Regular parameter pack forwarding fails when omitting defaulted parameters, but wrapping it another time through a variadic lambda works.

One downside is that, ironically, this approach fails for when the function has actual overloads, because you can't bind the non-type template parameter Begin to an overload set. You can make it compile by disambiguating the overload set with static_cast, but that yet again loses information about defaulted arguments. As far as I can see, only BeginChild is out.

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