I'm actually playing with a message-passing-based UI system right now, mostly
as a prototype; long story below. I'm making it considerably more Erlangy than
yours -- essentially, you have message-passing with a sequential/direct-style
interface on top of it, so one can write code like:

(send-to foo '(:bar 1 "foo"))
(setf val (recv))
(format t "~A~%" val)
(send-to baz `(:thingy ,val))

The basic idea is, you have some set of primitive widgets (e.g. :text, :image,
:text-input), which are "just data," like in HTML. Components, rather than
being objects or functions, are instead actors. Other, non-component, actors
would exist as well, including a compositor actor associated with each window,
which you would be sending messages that look like:

`(:update (:layout-box (:direction :vertical)
(:layout-box (:direction :horizontal)
(:text-input (:layout (= (width :self) (/ (width :parent) 2))
:contents "Hello, world!"))
(:text (:layout (= (width :self) (/ (width :parent) 2)))
"Hello, world!"))
(:include ,other-actor)))

Breaking this down, it creates a layout box (invisible box for layout
purposes), containing the equivalent of the HTML:

<div>
<input style="width: 50%;" value="Hello, world!"></input>
<span style="width: 50%;">Hello, world!</span>
</div>

Below that, it places... whatever OTHER-ACTOR's current render tree is. Other
than having its name, the actor sending this tree doesn't need to call into
OTHER-ACTOR at all.

The compositor is in charge of gluing together the tree fragments,
laying them out (with Cassowary), "legalizing" them into primitives the
current platform supports, and displaying them to the screen. It also sends
back input events to the actors involved, probably with some
CALL-NEXT-METHOD -like primitive to opt into bubbling.

This lets the compositor operate concurrently with state updates to
components, and components to operate concurrently with each other. This is
good in the general case of "new processors getting more cores, not higher
clocks," and especially since I'm going to be using this mainly on ARM
devices, where this is especially true, and being able to split computations
to work with big.LITTLE is a huge win for battery life.

I think this also should help quite a bit with ensuring a user interface
remains interactive while performing lots of work, if I were to implement
preemption (or context switch checks often enough to act like preemption is
occurring, like Go has historically had). The only bottlenecks where the whole
interface can get stuck are in the compositor, and the only expensive thing
there is Cassowary. And if that's fast enough for Apple, it's fast enough for
me.

Once this is in a state where I can take pretty screenshots (okay, considering
my aesthetic taste, not too pretty...), I'll probably submit a blogpost
version of this here with code samples. If you wanna discuss this beyond the
length HN makes convenient, feel free to drop me a line on
https://lists.sr.ht/~remexre/public-inbox (~remexre/public-inbox@lists.sr.ht).

Long story:

I recently got a PinePhone, and I'm experiencing keyboard latency that makes
it incredibly annoying to actually type on it. I only use a small handful of
apps on my phone, most of which have CLI or library equivalents, so I figured
it wouldn't be that much of a loss to reimplement crappy versions, if I at
least had a nice keyboard. (Plus, my old phone isn't actually broken yet, so
instability here isn't as big of a problem as it might be otherwise.)

So, I reflashed it to a non-graphical build of Arch Linux ARM, and set up SBCL
with Swank on it, with the intent of making a new interface to replace Phosh
and the apps that run under it. This has the pretty nice effect that I can
live-edit code that's running on the phone itself from Vim on any of my other
machines (or even several at once, if I want). Plus, Lisp itself has great
performance relative to other languages that provide this level of dynamism.
Lastly, since it's set up as a systemd service, even when I mangle EGL state
enough to get a segfault, it restarts and generally Just Works Right.

I've been meaning to try out this concept, but always meant to implement a
custom language that looks more like Erlang semantically to do it with. Using
Lisp lets me buck Greenspun's tenth rule, though, and forces me to avoid
spending a year implementing my own efficient code generation. Plus, Lisp
already runs great on my phone.

I am probably performing the do-notation transform with a macro, though, to
allow for the green thread implementation I need for large numbers of actors.
Alternately, I could use cl-cont, but I'm not sure that I want continuations
exposed to the programmer.