[ANN] CImGui.jl - A Wrapper for Bloat-free Immediate Mode Graphical User interface(Dear ImGui)

hi @piever, I have no experience of Interact.jl-based GUI before, so please correct me if I’m wrong. After reading the docs, I feel like the underlying design between ImGui and Interact are quite different.

ImGui is immediate mode UI that doesn’t hold any hidden states, which means it manipulates the value and does “drawing” stuff (emits vertices, textures, drawing cmds, etc to drawing data buffer/window stack) simultaneously. For example, what this line of code CImGui.ColorEdit3("color", col1) does is “drawing” the slider widget and immediately set col1 to current color/value selected by users. Unlike retained mode UI, all of the widgets get updated every frame regardless of whether the states changed or not(the idea is to update widgets and the states every frame instead of storing hidden states and calculating which widget should be re-rendering). As a result, widgets in ImGui do not return outputs, so current unify widget syntax may not fit.

Considering the following example:

import Colors
using Plots

function mycolorpicker()
    r = slider(0:255, label = "red")
    g = slider(0:255, label = "green")
    b = slider(0:255, label = "blue")
    output = Interact.@map Colors.RGB(&r/255, &g/255, &b/255)
    plt = Interact.@map plot(sin, color = &output)
    wdg = Widget(["r" => r, "g" => g, "b" => b], output = output)
    @layout! wdg hbox(plt, vbox(:r, :g, :b)) ## custom layout: by default things are stacked vertically
end

In ImGui, one might write:

# this is a fake Julia code snippet, but you may already get the idea.
let
r, g, b = 0, 0, 0 # ImGui won't handle states for us, we need to find a way to store them ourselves.
function mycolorpicker()
    # Interact.@map magic should happen here, but is it still necessary?
    slider(&r, 0, 255, label = "red")
    slider(&g, 0, 255, label = "green")
    slider(&b, 0, 255, label = "blue")
    # emit textures immediately to drawing buffer
    plot(sin, color = Colors.RGB(r/255, g/255, b/255))
    SameLine()/NoSameLine() # layouts are not a part of widgets in ImGui
end
end

My intuition is that the Widget struct would be a wrapper for the CImGui widgets. For example, in case of a slider, one would create something where the output is an Observable corresponding to the value of the slider and the “layout” (i.e. the thing that is actually rendered would be the CImGui slider), so something like:

function Widgets.slider(::CImGuiBackend, range; label = label)
  imslider = slider(range)
  imoutput = # observable corresponding to slider output
  Widget{:slider}(output = imoutput, layout = _ -> hbox(imslider, label))
end

The tricky bit is to have an observable that carries the value of the slider (which was already mentioned above as "supporting the Observables framework). The easiest is probably to initialize the observable to the same value as the slider and at every frame if the slider is changed, update the observable.

1 Like

Just want to say that going from an ] add CImGui CSyntax, to being able to run julia demo.jl and have everything work extremely well (and look beautiful and minimalistic to boot) is just absolutely amazing. Thanks for putting this wrapper together @Gnimuc! I look forward to using this for all sorts of stuff. :slightly_smiling_face:

1 Like

Unfortunately I’ve noticed an important downside of Dear ImGui (not specifically the Julia wrapper). When we launch the demo window for instances the GPU activity raises above 35%, even if we don’t touch any window. This is quite bad for the durability of battery charges on laptops.

2 Likes

not quite fits ImGui’s design, but it’s good for Widget API consistency, so makes sense to me. :slight_smile:

That’s probably due to the implementation of OpenGL backend is not very efficient. It queries a lot OpenGL global states in the rendering loop:

This implementation is copied from Dear ImGui which maybe a trade-off bwteen correctness and performance.

I don’t think the blame is on you :grinning: In the Dear ImGui site they have a zip file with pre-compiled binaries and both the OpenGL & Vulcan ones show the same GPU usage behavior. So the issue is more fundamental in its origins.

1 Like

Nicolas Guillemot addresses the power inefficiency in his C++ lightning talk on Dear imgui. It is an immediate mode renderer that recreates the entire gui every time through the event loop. It is very responsive, but power hungry.

Had seen references to it but didn’t realize the effects of that continuous recreation were so drastic.

Just gave this a try combined with ImPlot.jl and I’m really impressed with how responsive it is. Also, it seems I can run the loop on a background thread. I wrapped the examples in https://github.com/wsphillips/ImPlot.jl/blob/master/demo/example_plots.jl in a function and called it with Threads.@spawn and it all worked like magic. This seems like a real advantage over other plotting / UI libraries for Julia.

4 Likes

Could you share the example code? I got a crash on my machine. I guess I just did something wrong when using Threads.@spawn.

 2020-08-30 10:40:44.470 julia[42621:627679] *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1677.104/Foundation/Misc.subproj/NSUndoManager.m:363
2020-08-30 10:40:44.471 julia[42621:627679] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.'
*** First throw call stack:
(
)
libc++abi.dylib: terminating with uncaught exception of type NSException

signal (6): Abort trap: 6
in expression starting at none:0
__pthread_kill at /usr/lib/system/libsystem_kernel.dylib (unknown line)
Allocations: 7965270 (Pool: 7963320; Big: 1950); GC: 7
[1]    42621 abort      julia

Sure. It might be platform dependent.

You can see what I tried in this gist which I adapted from the ImPlot.jl example.

I tested on Julia 1.5.0 on Windows 10 (20H2).

If that doesn’t work for you, you could use @async instead and put yeild() at the end of the event loop. Not multi-threading, but at least it runs in the background.

Looks like this doesn’t work on macOS. Maybe this is due to CImGui.jl isn’t thread-safe, will try to fix it later.

update: it turns out to be a GLFW limitation: Which Thread For Handling OpenGL's Window? - #2 by mmozeiko - support - GLFW

1 Like

Works fine in Julia 1.5.0 on Ubuntu. Very impressive!

1 Like

Here’s how my oscilloscope GUI works so far. I’m very happy with CImGui and ImPlot!
ezgif-5-3afeb5688cfc

10 Likes

Wow, I have been thinking of trying this for a long time! Is this via serial or GPIB?

https://github.com/iuliancioarca/TIVM
You can find the code here. The gui is somehow separate from the instrument driver, I still have to tidy things up.
The gif above is the gui actually connected to a Lecroy HDO6054 via USB( not the TDS2002 in the repo).
The Lecroy setup has decent refresh rate, depending on the size of the waveform( this one was tested with only 5k samples).
I’m struggling however with the TDS2002(also USB), which has 100ms delay for every command I send. Fetching a waveform (2500 pts) for one channel takes 1 second and it’s driving me crazy. I don’t know if it’s the scope that is slow or if I have a problem with the computer( it’s a different computer than the Lecroy setup, some old core duo laptop), but other instruments exhibit this behavior as well…

2 Likes

Many thanks! I misread the oscilloscope type and thought it’s older series, of course USB nowadays.

This is because on macOS the GUI must run on the application main thread, I hit the same problem in QML.jl, and presumably Gtk.jl would be similarly affected. Maybe we should figure out a way to keep the main thread free for this kind of application?

I guess at least we should notify those Mac users to always run GUI code on the main thread. Is it feasible to define a function that will throw the warning when called with Threads.@spawn?