AllenDang / giu

Cross platform rapid GUI framework for golang based on Dear ImGui.
MIT License
2.39k stars 136 forks source link

lazy drawing (on-demand) of Widgets and their descendants #136

Closed folays closed 3 years ago

folays commented 3 years ago

Problem :

Due to the "declarative" nature of *Widgets.Layout(...) in giu, and the call-chain used to declare them, the whole hierarchy of giu widgets are instantiated at each ui loop. (though only a small struct initialisation for each widget, on the "side" of giu only).

Some Widgets have an internal "open" status (internal to C++ imgui, returned by imgui::Begin*V). For example:

With both C++ imgui and imgui-go paragdim, users of those widgets would code some sort of:

if imgui.BeginWhateverV() == true {
    // draws child widgets
}

When you have (possibly multiples) Widgets with can thus be "unopened", the if in the code above would alleviate the CPU. That could be relevant in either:

Solution :

Currently this can be alleviated using:

        g.TabBar("Tabs").Layout(
            g.TabItem("tab1").Layout(
                g.Label("tab1 child"),
            ),
            g.TabItem("tab2").Layout(
                g.Label("tab2 child"),
                g.Custom(func() {
                    fmt.Printf("custom dynamic on-demand\n")
                    g.Layout{
                        g.Label("custom dynamic on-demand"),
                    }.Build()
                }),
            ),

There above, the func() {} contained in the g.Custom() helps putting a "barrier" between declarative "sections". I use the term "declarative" because all g.*Widget have mainly the effect of declaring a giu.*Widget struct. Their .Build() functions are only invoked indeed when drawing is needed.

The "declarative" instructions inside the func() {} are thus only invoked IF the (*CustomWidget).Build() function is invoked.

So... What's the problem?

The giu paradigm of being Immediate and all declarative with not-so-much ifs could leave users to wonder how they would go about not needlessly executing a long chain of g.WidgetParent().Layout( g.WidgetChild(...) ) when the WidgetParent is not opened.

Maybe the possibility of doing the example above of using g.Custom should be documented.

Alternative :

Or maybe even could giu provide a g.LazyWidget() function?

func Lazy(fn func() g.Widget) g.Widget {
    return g.Custom(func() {
        fn().Build()
    })
}

Which would be used by an user with:

        g.TabBar("Tabs").Layout(
            g.TabItem("tab1").Layout(
                g.Label("tab1 child"),
            ),
            g.TabItem("tab3").Layout(
                lazy(func() g.Widget {
                    return g.Label("tab3 child lazy")
                }),
            ),

g.Label("tab3 child lazy") or whatever other widgets being declared there, this block of code would only be executed if "tab3" is selected.

The only thing achieved by the "alternative" construct above is to alleviate the new user of giu the difficult phase of needing to figure out they could go "lazy/ondemand" by using a g.Layout{ ... }.Build() construct inside a parent Build(...) function customarily provided by g.Custom(). (because beginners should probably not be concerned too soon with the .Build() paradigm of giu, and only take knowledge of it maybe in later advanced stage)

AllenDang commented 3 years ago

@folays Thanks for the proposal. In my mind, this is a advanced trick and could be archived easily by following code.

func buildSomething() g.Layout {
  if someCondition {
    return g.Layout{}  
  }

  // Build complex layout here
  return g.Layout{...}
}

func loop() {
  ...
  g.Child(..).Layout(buildSomething)
  ...
}

I intend to keep the layout gramma as simple as it could be.

folays commented 3 years ago

Thanks for you answer. At the first glance, it made me think that maybe I had missed or overlooked on something.

After a second thought, I think have identified the following:

1) Inside the context of a func buildSomething() g.Widget {}

Of course, your buildSomething function which return a g.Widget can "run" and condition its code path to some someCondition user-defined/user-controlled variables. In your example, those (potentially "build complex layout here") would run only if the code path get to this point.

2) Inside the context of a (*g.Widget).Layout() having an open/unopened state

(Of course, this whole discussion is relevant only for Widget which are not drawn into their "unopened" state, and the topic is to try to skip "declaring" their children hierarchy in the subs-.Layout() when they won't be draw anyway)

"Inside" the Layout() parenthesis characters ((...)) we are not in the context of a "function". When this code path is reached, all .Layout() arguments will be evaluated, no matter if they will be used or not.

Furthermore, we cannot pass (to this .Layout() function) a buildSomething argument, we could only pass buildSomething() argument. And so again buildSomething would also run.

By the way, I think that in your example above, you cannot do (it wouldn't compile):

  g.Child(..).Layout(buildSomething)

But you would be expected to do:

  g.Child(..).Layout(buildSomething()) // <--------- added () there to call the function

And this where my point is, what .Layout() will do about :

So...

It seems that .Layout():

Of couse due to the Go language, the only solution an user has to "act" on some variable, is to insert a "function" somewhere. I guess that buildSomething is not an answer, because really, the buildSomething "function" is /"not"/ used as a fundamental function which purpose would be to be called repeatedly, but rather only once to get its immediate result.

In a declarative chain, nothing really condition calling buildSomething(), which would be called once, and its result and sub-results always being put in the declarative chain.

Besides that, putting in the buildSomething function, a if related to the "opened state" of a known parent Widget.Layout() seems to be nonsensical, because:

Since .Layout() expect to evaluate all its arguments "now", but to run .Build() on them ONLY IF they must be draw, it seems rational that one of the arguments would be needed to be a g.Custom func (really, any function with a .Build() method, which act as a soft of a "dummy" widget only having sub-Layout), which would serve to break the "declarative chain".

Purpose of this issue:

Raising awareness on:

Still

Still, I agree without you that the grammar should be keep as simple as possible. Especially, replacing the signature of the .Layout(...Widgets) API with a .Layout(...func()) would be probably less user-friendly.

Maybe proposing a .LayoutCustom(...func()) ? Which could be implemented such as: (AnyWidget being e.g. *g.TabItemWidget or any other widget having an "openable" state) :

func (any *AnyWidget) LayoutCustom(builder func() Widget) {
    any.layout = g.Custom(func() {
        builder().Build()
    })
}

Which could be used to break the declarative chain with:

g.TabItem("tab2").LayoutCustom(func() g.Widget {
    return g.Layout {
        g.Label("this is tab2 content lazy declared"),
        g.TreeNode("and all sub-children will also be lazy declared").Layout(...),
        ....,
    }
})

Or not proposing it, and assuming that users which would need it, would be able to figure how to do it themselves.

Regards,

AllenDang commented 3 years ago

@folays Basically I think this is a advance trick to optimize. There are many solutions could be done in different scenarios, like but not limited as below.

  1. Generate layout and reserve it outside loop function, change it only when some events were triggered and invoke giu.Update to notice redraw.
  2. Use function to return layout and control the construction inside it.
  3. Cache generated complex layout (like huge number of rows of table) in memory for future reuse.
  4. etc...

To provide a lazy load mechanism is not hard, but it still will increase the complicity to understand what's going on behind it.

One example is the giu.Conditional widget. The reason why it was invented is I want to reduce the chance to use giu.Custom. But after more and more practices, I found out in most cases when I need to conditional build something, it is complicated than just simply, if this show button else show label.

As it is an advanced trick, I think a simple system is easier to optimize.