-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
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
Comments
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 :( |
I think I'll take a crack at it and post something here for people to critique. |
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):
Source (ImStyle_RAII.cpp):
|
My most used RAII object for Imgui:
|
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
|
@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 :
|
Consider {
ImWindowHelper(str_id, ...);
{
ImChildHelper(str_id, ...);
ImGui::Text("Child 1");
}
} which is less noisy. |
@ice1000 |
It works with clang++-6.0 but I didn't lookup the standard spec. |
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 :
From http://eel.is/c++draft/class.dtor#:destructor,implicit_call :
|
@ice1000 MSVC doesn't like what your doing which suggests to me the standard isn't actually saying
Relevant ImWindowHelper class
And it compiles on MSVC v141 to this as I suspected :
Essentially it gets instantly destructed after it gets constructed, simply doing this :
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. |
@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? |
@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. |
|
Just to illustrate my point, I totally forgot that https://github.com/sethk/imgui/blob/raii/misc/raii/imgui_raii.h |
@sethk Looking good! Some pieces of feedback:
|
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.) |
I'm happy with enforcing C++11 as a requirement for those C++ extension. |
How about
Sounds good, but there is already an |
I could imagine using 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. @ocornut I would love to hear your thoughts on this approach |
@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 |
@sethk Apologies for later answer.
Sure.
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. |
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. |
But that's an issue with both RAII and the unique_ptr no? To be honest I have no idea how this
How about |
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. |
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 I don't see any obvious way of how we could prevent users doing |
Well if it's the same thing then why not stick to the version with no template porn? |
As I said previously
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. |
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 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. |
can I ask why you do not merge ImTreeNode with TreeNodeEx and ImTreeNodeV with ImTreeNodeExV ? |
@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. |
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 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 |
Hi, why not conditionally adds if (ImScoped::Window("name")) {
// codes ...
} |
@edsdev Apparently if you do this, the compiler still considers the call to |
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"))
{ ... } |
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 https://github.com/kfsone/imguiwrap I'm still adding wrappers, and mac/linux builds will be next. |
FYI |
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. |
Hi Friends, |
@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. |
@sethk Thank you. |
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. |
I have changed imgui_sugar to c++11 :) |
I know I'm some years late, but how about if (RaiiWindow _("MyWindow")) // notice the _
// blah blah which at least to me looks acceptable |
The problem with
Is that if you forgot to write the "_" it will compile without warning but won't work:
That is why I prefer the macro defined scope:
It is just a syntactic sugar, generates the same code but ensures that the RAII guard is named. |
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. |
Yes you are right, I just added ref qualifiers to my guards yesterday ;)
Yes it is a matter of user preferences, and i know that macros can be evil, but...
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).
So yes, it is a matter of personal preferences. 👍 |
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);
} |
Is there any information documentation or reference about this? Looking at the imgui_demo, I do not fully understand when the 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;
}; |
Begin and BeginChild are the only inconsistent ones. It is all commented in imgui.h and effectively the demo.
|
Please don't implement RAII guards again and again and again.... There are many attempts already. 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... |
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. |
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 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 |
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.
The text was updated successfully, but these errors were encountered: