4coder's GUI: Preparing for Customizability

Allen Webster  —  1 year, 1 month ago
Last week I discussed all the goals of the 4coder GUI system I have been developing. Now I will look into detail on how I plan to achieve the first big goal: giving the entire GUI to the custom layer and making it as easy as possible.

Any talk about ease of use and GUI will wind up being a talk about how great immediate mode is, and I have already talked about this on a previous blog post. In the 4coder GUI the API design is immediate mode. But because of all the other constraints on the 4coder GUI system, some aspects of it do not look like a typical immediate mode system. I will discuss those points more in future blog posts.

What I want to talk about today is the difference between a library and an engine. 4coder's customization API is not a library, it is an engine. The main difference being that the user of an engine writes code that does not have one controlling stack frame, the user is always expected to return back to the engine. This fact alone does not actually change the principles of API design very much. In an engine it is very easy to convince yourself that everything has to be retained mode. Because it seems controlling code will have the authoritative state and the user is doomed to just asking about that state and issuing commands to change to the state. But this doesn't need to be true, the problem is just how to allow the user's code to have some stable state so that they can have the authoritative state.

There are a few obvious ways to do this. The user can rely on global memory, or you can establish a user data system that allows the user to get some heap memory. After taking that step, they can store any state they want and then all you have to do is be sure to provide an entry point to the user's code where they declare the GUI according to whatever state and rules they want, and make sure this entry point is called frequently enough for the GUI to stay correct. In the case of 4coder it is not hard to guarantee the GUI will refresh frequently enough because the 100% of the GUI declaration code is already contained in one place that can easily be replaced with a call into the custom layer.

So that is one option, and as I said, I feel like this is the "obvious" option.

But there is still an issue with this, which is that the controlling state can now be changed while the GUI control code is not executing. For instance, in 4coder a command might be called, which wants to change from viewing a buffer to a file list. Making such systems work is usually more difficult. A disciplined user can set aside memory that is "only" for the GUI control code, but the temptation to reach in and pull some neat trick is usually non-zero in such a system. Plus always getting all the state stored you actually need can be difficult by itself.

To solve this some of this, I could say that the dispatch to commands is controlled by the GUI controlling code as well, and in fact I plan to do this and go even further with giving control of the command system to the user. But, there are also multiple views who will all need to keep track of their own GUI control code, and as I said last week in my list, I am not willing to merge them all into one control structure. So the issue is pretty much unsolved.

A similar issue made it really hard to implement commands like search, goto-line, and replace in 4coder for a while. These are commands that would want more interaction than simply being called and they involved showing something back to the user. I originally wrote them by having the command be called repeatedly with new input until the command reported that it completed, and it was up to the user to make sure every piece of state that needed to be repeated was stored in a non-local memory. These systems were bug prone, and actually I never released a version with working replace commands when I was trying to make those commands with this system.

There I ran a little experiment, which I still believe was one of the greatest success's of the experimental side of 4coder work so far. I decided all commands should be run in a "coroutine". A coroutine is like a thread that never runs in parallel. You have to explicitly say "I am done for now come back to me later" from inside the coroutine, and the calling code has to later say "okay you should continue now". A lot of people try to use them in conjunction with their fancy multi-threading systems, but from my point of view the entire advantage of the coroutine is as a flow control mechanism for when both sides of an API need long term local storage. Attached to each "yield" and "resume" coroutines and their callers can pass information back and forth.

So the 4coder customization API got the call: "app->get_user_input". This call yields the currently executing command and says to the caller "I am still going, I need more information from the user". When 4coder get's more input from the user it passes it back to the command's coroutine. Now the command can store all of it's state in a local stack frame, where the only way it can be changed by someone else is if you gave them a pointer to the local memory, or if they have a really wild invalid pointer bug.

This change made the complex control flow of the interactive commands a lot more natural. But the command must still exit and relinquish this little island of control that was established.

Based on the success of that experiment I plan to try the same thing for the view control code itself. Each view will run in a coroutine, and it will yield by asking for more messages from the application side. The big difference being that these coroutine's are expected to never terminate, so that the user has a permanent base of local storage. To make the change complete, all other entry points into the custom layer will be removed, so that the user knows exactly where flow control will continue every time their code is invoked by the engine.

So that is it, the entire plan for the future of 4coder customization. I should mention that this coroutine business is the only part of the system I have not implemented yet, so I am only arguing that I think it will be neat, I do not yet have the results of this experiment. However, that has been the plan that everything else, which I will discuss in future weeks, is building towards.

Until then, thanks for following everyone!
#7035 Andrew Chronister  —  1 year, 1 month ago
Hmm, I'm interested to see where this goes...

As always thanks, for the update, Allen!
Log in to comment