Rendering with 'Dear, ImGui'
Dear, ImGui
`Dear, ImGui’ is a direct-mode gui library that can be rendered using OpenGL, DirectX and other 3d apis. direct-mode means that everything on screen is re-created every frame. This means that there is no need of a view-model diff/patch library to minimize changes to a retained view - such as /react/ or /snabbdom/ in the browser.
The result is very simple descriptive code to generate the view and easy composition of ux controls into a 3D scene.
view
Collect the renderers
vector<observable<shared_ptr<Model>>> renderers;
Render once per frame, using latest ViewModel
with_latest_from()
tracks the latestViewModel
and calls thetake_at
function to extract theViewModel
during each frame.
// render analysis
renderers.push_back(
frames |
with_latest_from(rxu::take_at<1>(), viewModels) |
// ...
Verify that the rendering is on the main thread
auto renderthreadid = this_thread::get_id();
if (mainthreadid != renderthreadid) {
cerr << "render on wrong thread!" << endl;
terminate();
}
Create a Window with a default size and position. When moved and resized the position is stored in imgui.ini
and that stored value is used the next time the app is opened
EDIT: This tweet caused me to update to the pattern below. The UNWIND handles cleanup on exception. but is
dismiss()
ed when there is no exception. I tried a couple of different patterns, but I do not like any of them.
ImGui::SetNextWindowSize(ImVec2(200,100), ImGuiSetCond_FirstUseEver);
if (ImGui::Begin("Live Analysis")) {
RXCPP_UNWIND(End, [](){
ImGui::End();
});
// draw window contents
End.dismiss();
}
ImGui::End();
Draw two labels in the window
ImGui::TextWrapped("url: %s", URL.c_str());
ImGui::Text("Now: %s, Total Tweets: %d", utctextfrom().c_str(), m.total);
Create a collapsing section in the window and open it by default
if (ImGui::CollapsingHeader("Tweets Per Minute (windowed by arrival time)",
ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen))
{
// ...
}
Convert the history of overlapping tweets-per-minute groups into a vector of floats using Range-v3.
vector<float> tpm;
// Range-v3
tpm = m.tweetsperminute |
ranges::view::transform([](int count){return static_cast<float>(count);});
Plot the floats in a chart to visualize tweets-per-minute over time.
ImVec2 plotextent(ImGui::GetContentRegionAvailWidth(),100);
ImGui::PlotLines("", &tpm[0], tpm.size(), 0, nullptr, 0.0f, fltmax, plotextent);
the complete expression
// render analysis
renderers.push_back(
frames |
with_latest_from(rxu::take_at<1>(), viewModels) |
tap([=](const ViewModel& vm){
auto renderthreadid = this_thread::get_id();
if (mainthreadid != renderthreadid) {
cerr << "render on wrong thread!" << endl;
terminate();
}
auto& m = *vm.m;
auto& words = vm.words;
ImGui::SetNextWindowSize(ImVec2(200,100), ImGuiSetCond_FirstUseEver);
if (ImGui::Begin("Live Analysis")) {
RXCPP_UNWIND(End, [](){
ImGui::End();
});
ImGui::TextWrapped("url: %s", URL.c_str());
ImGui::Text("Now: %s, Total Tweets: %d", utctextfrom().c_str(), m.total);
// by window
if (ImGui::CollapsingHeader("Tweets Per Minute (windowed by arrival time)", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen))
{
vector<float> tpm;
tpm = m.tweetsperminute |
ranges::view::transform([](int count){return static_cast<float>(count);});
ImVec2 plotextent(ImGui::GetContentRegionAvailWidth(),100);
ImGui::PlotLines("", &tpm[0], tpm.size(), 0, nullptr, 0.0f, fltmax, plotextent);
}
End.dismiss();
}
ImGui::End();
}) |
reportandrepeat());
combine views
rxcpp is lazy. lambda’s are lazy. auto greet = []{cout << "Hello.";}
does nothing until called - greet();
. All the rxcpp expressions we have defined thus far are dormant, waiting for a call to subscribe()
.
Since all the expressions have been connected in a graph from rxcurl -> parsetweets -> reducers -> models -> viewmodels -> views, the only subscribe()
required is to the merged views. This subscribe()
will propagate to every observable in the expression.
// subscribe to everything!
iterate(renderers) |
merge(mainthread) |
subscribe<shared_ptr<Model>>([](const shared_ptr<Model>&){});
main loop
The main loop merely pumps the mainthread
rxcpp scheduler (named rl
for runloop) and triggers the renderer with_latest_from()
expressions by calling sendframe()
// main loop
while(lifetime.is_subscribed()) {
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSdlGL3_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
lifetime.unsubscribe();
break;
}
}
if (!lifetime.is_subscribed()) {
break;
}
ImGui_ImplSdlGL3_NewFrame(window);
while (!rl.empty() && rl.peek().when < rl.now()) {
rl.dispatch();
}
sendframe();
while (!rl.empty() && rl.peek().when < rl.now()) {
rl.dispatch();
}
// Rendering
glViewport(0, 0, (int)ImGui::GetIO().DisplaySize.x, (int)ImGui::GetIO().DisplaySize.y);
glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui::Render();
SDL_GL_SwapWindow(window);
}