A Common Lisp UI Library
It’s been a while since I wrote about Mesha. I haven’t given up on the idea of having my own productivity tool that I can tailor to my needs. Last time, I mentioned wanting to do a vertical slice of the tool and that was in Jan 2023. My side projects took a back seat for a while as I focused on balancing work and a newborn. My son is now approaching 3 years old and I have had some more time to work on my project. I have also been leveraging some of the new LLM based tools to accelerate my work.
So, what have I been up to since proposing my vertical slice? I failed at the vertical slice, or at least I haven’t reached it yet. The thing I struggled with the most was writing UI in a way I enjoyed. The landscape of UI tech is very unappealing to me. I do not enjoy writing HTML/CSS/JS and I do not want my UI code to be littered with “objects” that I have to maintain and figure out lifetimes for when using most native toolkits. I much prefer and “immediate mode” API for UI à la Dear IMGUI.
So I tried writing bindings for Dear IMGUI for Common Lisp, and I hated it. Dealing with C strings and Dear IMGUI expecting a persistent pointer that it can treat as your object ID was all inconvenient. I realized at this point that what I wanted was a Common Lisp native immediate mode UI library. What about GC pressure though?
I was not sure how I would deal with this. Initially I thought I would actually have to implement most of the rendering in a C++ library and have the CL API just encode a command buffer as needed to update the UI.
Then I came across this post on Hacker News and I knew this was the answer I was looking for. I did not get around to actually playing with SBCL arenas till 2025 and once I did, the design for my UI library quickly came into place.
Here’s what code for a counter UI looks like
(defpackage #:demo-counter
(:use #:cl)
(:import-from #:trivia #:match)
(:export #:init
#:update
#:subscriptions
#:layout
#:counter))
(in-package #:demo-counter)
(defclass counter ()
((value :initarg :value)))
(defun init ()
(make-instance ‘counter :value 0))
(defun update (model message)
(let ((tag (mesha.core:message-route-pop message)))
(match tag
(’increment (incf (slot-value model ‘value)))
(’decrement (decf (slot-value model ‘value))))))
(defun subscriptions (model)
(declare (ignore model))
(list
(input:click ‘increment-button ‘increment)
(input:click ‘decrement-button ‘decrement)))
(defun layout (counter)
(layout:in ((layout:rect ‘main-content
:sizing ‘(:x :grow :y :grow)
:direction :top-to-bottom
:color :slate-900
:child-gap 8.0))
(layout:in ((layout:rect ‘title-container
:sizing ‘(:x :grow :y :fit)
:direction :left-to-right
:color :slate-800
:padding 8.0))
(layout:text ‘title “Counter Demo” :color :slate-50 :font-size 32.0))
(layout:in ((layout:rect ‘counter-display
:sizing ‘(:x :grow :y :fit)
:direction :left-to-right
:color :slate-800
:padding 8.0))
(layout:text ‘counter-value
(format nil “Count: ~D” (slot-value counter ‘value))
:color :slate-50
:font-size 32.0))
(layout:in ((layout:rect ‘button-container
:sizing ‘(:x :grow :y :fit)
:direction :left-to-right
:child-gap 8.0
:padding 8.0))
(layout:in ((layout:rect ‘decrement-button
:sizing ‘(:x (:fixed 120.0) :y (:fixed 80.0))
:color :red-500
:padding 8.0))
(layout:text ‘decrement-label “-” :color :slate-50 :font-size 48.0))
(layout:in ((layout:rect ‘increment-button
:sizing ‘(:x (:fixed 120.0) :y (:fixed 80.0))
:color :emerald-500
:padding 8.0))
(layout:text ‘increment-label “+” :color :slate-50 :font-size 48.0)))))
Hopefully this is pretty readable to anyone familiar with Common Lisp and some UI programming. My core API is fairly low-level and I expect “widgets” (buttons, labels, checkboxes etc) to be defined at the application level. The implementation is still fairly basic and I’m fleshing it out as I build Mesha.
The API is inspired by Elm and Clay. It diverges from both in meaningful ways and I expect it will evolve more as I design out parts of the actual tool. I’m very happy with my workflow using this design so far.
Here’s a video of the current state of the application
What are the dependencies?
At this point, the most significant one is SDL3 via cl-sdl3.
“What about CPU usage though?”
If you are familiar with immediate mode UIs but have not done any significant work with them, you are probably aware of their reputation of “updating every frame”. This is true of a lot of implementations and is not problematic for a lot of typical uses of IMGUIs, namely games and real-time applications. Updating every frame is not a requirement for IMGUIs though, and my design is explicitly to avoid having to do that.
I have a proof of concept implementation in C++ where the application does what a typical retained mode toolkit would do in the case of the application just idling, simply re-draw the cached layout (not even generating draw commands if the window hasn’t been interacted with in any way). There is minor CPU ( order of 0.1ms) overhead compared to how retained mode toolkits handle this but I expect that won’t be a problem, and maybe I will figure out how to do away with that as well at some point.
“What about accessibility?”
Short answer is I don’t know yet. IMGUIs typically don’t have support for accessibility. Since this is primarily intended for my personal use, I haven’t given this much thought. I would like to explore it in the future though. My assumption is that having a cached layout tree will lend itself well to screen-reader support but I don’t even know what other features I would need to support.
“Where’s the code?”
It’s still in a private repo. It is intended to be FOSS but I want a bigger proof of concept that I can use to battle-test my API before I want other folks’ opinions. I definitely don’t want to support other folks using it for anything at this point.
What’s next?
I need to flesh out the API some more before I can achieve the vertical slice from my earlier post but I like my odds at this point. Wish me luck.
