Oh, yeah, the Stack Overflow post especially seems to talk about very similar problems to what I have been grappling with. Thanks for the pointer! The code is pretty opaque to me, though; it's been well over a decade since I've last had any interaction with the WINAPI programming style, Hungarian notation and all.
I wonder why he arrives at the conclusion that he needs a full-fledged DSL for what he is doing. I remember that at the time I was working on this, the impression I had was that a lot of my problems would go away if only there were some unique way to identify every distinct invocation of a function (so I could use data along the lines of "you are currently in the 3rd call of Button() in something.cpp"). __FILE__ and __LINE__ get close but don't disambiguate between multiple calls on the same line (and anyhow would need to be baked into the invocations with macro hackery).
Every immediate-mode UI deals with the id namespace issue. What they usually have in common is a hierarchical id namespace where you have an id stack and a child id is derived from the parent id via hashing, child_id = hash(parent_id, widget_type, subid). [1] The subid can be derived from widget arguments that are likely to be unique and stable from frame to frame, e.g. a text edit box's buffer pointer. But there always needs to be a way to provide an explicit subid since the implicit method doesn't always cut it. Alternatively, as long as the default subid is stable you can always make it unique by wrapping the widget (and other nearby related widgets) with an explicit pair of push_id/pop_id calls. You can see an example here: https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#.... The defaults work most of the time but this is definitely an aspect of immediate-mode UIs that can't just be treated as a hidden implementation detail.
[1] The prevailing use of hashing is a consequence of the popular immediate-mode UI libraries being focused on minimal state per widget. If you already plan to maintain significant state for every widget (as you would need in an immediate-mode interface to win32 controls) you would just do hierarchical interning with sequential id assignment (i.e. the first time a new subid is used with a given parent, it is assigned a sequential global id and put in a table so the association can be memoized across frames) and then hash collisions won't cause id collisions.
Yeah, I essentially copied imgui's ID stack approach for my experiments too. (I've been using imgui for some other projects to great success.) It still seems like a hack; I'm quite surprised that no programming language (I'm aware of) makes it possible to uniquely identify callsites like that. Maybe it hints at a more general blind spot/free real estate in PL design :)
(On the off chance you're curious, I just pushed some previously unpushed updates to that experiment I had sitting around, so now it has labels and text entry too. I guess the real test of the architecture would still be making an alternative "rendering backend" based on win32 widgets or something.)
Focusing too much on the call site is probably misleading, which is why most production-quality immediate-mode UI libraries generally don't rely on __FILE__/__LINE or stack walks or anything else like that. The issue isn't just loops. As soon as you wrap code in a function for reuse, the proximate call site no longer has anything to do with the widget ID; in the extreme case, your entire immediate-mode UI is data driven and there's nothing in the code paths that indicate anything at all about the widget IDs.
The real hack is implicit IDs, not the ID stack (which is just a way of implementing a hierarchical namespace like file system paths or URLs). Implicit IDs just work 99% of the time and rarely require manual intervention, so seeking a 100% solution is a tempting siren song. But once you actually start writing UIs like this, the 99% solution is just fine.
> As soon as you wrap code in a function for reuse, the proximate call site no longer has anything to do with the widget ID.
That's a good point.
(a) hash the entire call stack (though that might produce false negatives, i.e. consider two UI elements that should be the same distinct?)?
(b) put the burden on the reusable function to mark itself as such by pushing/popping an identifier of its own call site on the ID stack?
> The real hack is implicit IDs, not the ID stack (which is just a way of implementing a hierarchical namespace like file system paths or URLs). The fact that implicit IDs just work 99% of the time and only require manual intervention 1% of the time is a false siren song into letting you believe a 100% solution is desirable (you have to consider the marginal cost of what it would entail).
Well, this is just the standard problem of library design, isn't it? You always have to figure out the appropriate tradeoff between supporting rare cases and making common ones easy. (Of course, you can often do both; in this case, you probably could both give "explicit ID" and "call site ID" versions of each UI element API.)
Yeah, a mixture of implicit and explicit IDs is fine and what everyone does in one form or another. That's what I had in mind as the 99% solution. You don't want the default implicit ID scheme to be too clever or opaque so the programmer can easily diagnose what went wrong when it inevitably does. I was just cautioning against relying too much on ever fancier implicit schemes because of those failure modes; I went down that path a few times when I first started experimenting with immediate-mode UIs years ago.
Yes, I think he finds a DSL useful for that reason: each place where flow of code diverges (FOR, IF) is recorded and serves as the identity of the entire execution path.
The differential execution approach is to "diff" the GUI by "diff"ing the program's control flow. As opposed to something like React where we're diffing the virtual-DOM output of the render().
During a GUI update the program is run comparing it's new control flow with the prior run. So for example on an IF statement, if the prior execution took branch A but the new run takes branch B, then we need to run branch A again in "erase" mode to erase everything created by branch A, then run branch B to create the new state.
I wonder why he arrives at the conclusion that he needs a full-fledged DSL for what he is doing. I remember that at the time I was working on this, the impression I had was that a lot of my problems would go away if only there were some unique way to identify every distinct invocation of a function (so I could use data along the lines of "you are currently in the 3rd call of Button() in something.cpp"). __FILE__ and __LINE__ get close but don't disambiguate between multiple calls on the same line (and anyhow would need to be baked into the invocations with macro hackery).