EntriesAbout

A Logic GUI

logui_drawing.png

Well, it’s been a while since my session at Recurse (see my previous post) finished and I wanted to write some more about my “graphics in Prolog” experiment went.

Getting Graphic

At the end of the last post, I was able to open an SDL window with Prolog and fill it with a solid colour. From there, I’d worked on extending my library (which has the “persisted-temporary-placeholder” name “Logui”), adding the ability to handle click events and draw things – initially just by placing more rectangles filled with different colours. My first approach had a pretty strong “Clojure accent” though; it was superficially Prolog, using a DCG to write my view components, but it wound up constructing a series of nested dictionaries, then walking the resulting tree to render it.

root_window -->
    id(root),
    size(800, 800),
    background(rgb(0, 0, 0)),
    hide_cursor(true),

    child(( id(main_bg),
            background(rgb(128, 24, 200)),
            rect(0, 0, 800, 800) )),

    app_state(State),

    when(get_dict(mouse_position, State, pos(X, Y)),
         child(cursor_display(pos(X, Y)))
    ).

I was able to build a simple “paint” app (drawing lines by just drawing lots of little rectangles, so lines would lose coherence the faster the mouse moved), but I was not very happy with the architecture.

I found myself having to add lots of special-cases for things like mouse clicks, handling some events specially and squirrelling state away to be able to make interactivity work. My whole reason for building my own thing rather than using the existing XPCE graphics interface was because I wanted to make something that felt idiomatically Prolog and this was already failing that. If the API already didn’t feel very nice or orthogonal this early on, something needed to change.

The inspiration for how to change came from my friend Raf. While his familiarity with Prolog is limited to using Datalog in Clojure and years of me ranting about how great it is, he immediately asked if an ECS architecture might be a better fit for the project. His reasoning was that ECS is more like querying a database than walking a tree, which seems to map better to his understanding of the language. I thought that sounded like a good idea, but didn’t know too much about ECS, so went on a little diversion to learn more about that.

A Little Diversion

“ECS” stands for “entity-component-system”, which is an architecture generally associated with games. You have “entities”, which are your “things” (e.g. the player, an enemy, a wall), “components”, which are kind of like attributes or capabilities (e.g. “moveable”, “has physics”, “has health”), and systems, which act on entities that have given components. That was my very high-level understanding of ECS, but I’d never really used such a system in anger before. I’d previously started looking at a tutorial for the Rust game framework “Bevy”, which uses ECS, but hadn’t gone further than that.

I went back to the Bevy tutorial, finished it, then read through the source of their ECS implementation. It was pleasantly simple and free of excessive abstraction and I found it pretty easy to understand. Generally, entities are identifiers, components are more-or-less structs that have (among whatever other fields) a field to link each instance said identifier for their entity, then “systems” are functions that operate on collections of associated components. It seemed simple but powerful and, more saliently, very straightforward indeed to bring to Prolog.

ECS in Prolog

Armed with my new approach, I started reimplementing the Prolog side of the “logui” library. I very quickly realized how much nicer this approach was. I wrote a few macros (using term_expansion/2) to make defining components and systems pleasant (which was fun on its own) and dove into rebuilding the “drawing” demo (which you can now see a screenshot of at the top of this very post).

In the way that Bevy “systems” are “just functions”, I realized that in Prolog systems are “just predicates”, but even more so! While Bevy systems use the Rust type system to declare what components they act on and the framework does the work to run that query and find the arguments with which to call the function, in Logui the query itself is just Prolog code, since we use the dynamic database to store components! The ecs:system macro does a bit of rearranging on top of that, but all the logic of finding which components a given system acts upon happens for free, by the nature of Prolog! That realization really vindicated Raf’s suggestion that this architecture would be a good fit.

Writing the demo app was so much more pleasant this time, for a few reasons. For one, when building things as more of a functional “tree” structure, it was natural to want to add behaviours & interactivity by putting callbacks on certain nodes (e.g. “on click”). However, that doesn’t work very well with Prolog, since it doesn’t really have the notion of functions in that way, leading to workarounds of varying ugliness. With this ECS system though, it’s very easy to write a predicate that queries for active events and the corresponding components that might want to act on them. No special-casing required!

% Component used for the colour selection -- clicking on such a
% component sets the canvas' draw colour to whatever it has set
ecs:component(set_current_draw_colour_view, colour).
% component just to mark which entity is the canvas that gets drawn in
ecs:component(canvas).
% component to store the current draw colour
ecs:component(draw_colour, colour).

% The `set_current_draw_colour_click` system
% it queries for a `mouse_button_down` event (which is itself asserted
% by a system defined in the core logui module), finds the canvas entity,
% then for each `set_current_draw_colour_view` component, checks if it
% was clicked (via point_inside/2) and if so updates the `draw_colour`
% to the colour that was clicked on
ecs:system(set_current_draw_colour_click, ( event(mouse_button_down, ClickState),
                                            canvas(CanvasEntity),
                                            set_current_draw_colour_view(Entity, SetColour),
                                            point_inside(ClickState, Entity)
                                          )) -->
    update_draw_colour(CanvasEntity, SetColour).
A bit more on that example

To prove how simple the ecs:system macros is, here’s what that expands to!


?- listing(set_current_draw_colour_click).
drawing:set_current_draw_colour_click(A, B) :-
    dcg_high_order:foreach((length(C, 0), []=C, drawing:(event(mouse_button_down, D), canvas(E), set_current_draw_colour_view(F, G), point_inside(D, F))), logui_ecs:optional(drawing:update_draw_colour(E, G)), A, B).

Cleaning up the formatting a bit and undoing the DCG expansion, that’s something like this:

drawing:set_current_draw_colour_click -->
    dcg_high_order:foreach(
       ( length(C, 0), % ignore this, it's just to deal
         [] = C, % with extra free variables (none here)
         drawing:event(mouse_button_down, D),
         drawing:canvas(E),
         drawing:set_current_draw_colour_view(F, G),
         point_inside(D, F)),
       logui_ecs:optional(drawing:update_draw_colour(E, G))).

Which we can see is essentially just looping over the results of of that query for events & components. A paper-thin layer!

So little special-casing was required in fact, that I was able to fully separate the core ECS stuff from the graphics bits. There’s now a logui_ecs module that defines the basics, then the logui module just defines a bunch of components and systems that do the SDL stuff.

I had a lot of fun continuing to build out the drawing demo and add features. I eventually made it draw proper lines by using an SDL_Surface that can have arbitrary pixels drawn on it, to get rid of the ugly artifacts that drawing with a bunch of little rectangles brings. That was a fair amount of mucking around on the C++ interface side of things, but on the Prolog side, everything stayed nice and clean. Indeed, I was very happy to find that for the most part as I went to add new features, it was all pretty “linear”: I would write how I wanted to the API to work from the “consumer” side in the demo code, add whatever was needed to implement that in the library, and for the most part, that was it. There were far few times when I realized that to do something this way, I’d need to add extra book-keeping, or change how something was stored, or whatever kind of nonsense had been happening with the prior approach.

Diving Deeper

After getting the drawing app to a nice proof-of-concept stage, I built another little demo, one a bit more special-purpose: A UDDF viewer. UDDF (Universal Dive Data Format) is the standard XML-based format used to export logs from a dive computer for scuba diving. Being the sort of person that I am, rather than keeping my dive logs in a normal logbook app, I export them as UDDF into a git repo, which I then view & edit with tools of my own creation. I’ve already written one viewer in Clojurescript and one in Rust, but am not completely happy with either, so I thought this would be a good project to try to test out Logui. Eventually, I would like to make a full-on UDDF viewer/editor, but I decided to start with the dive graph.

Just drawing the lines was pretty straight-forward, given the stuff I’d already written for the drawing demo. However, I also wanted to display text, which is not built in to SDL. It proved easy enough to add SDL_ttf (after going on another side-quest to actually learn CMake and not just use cargo-culted CMakeList.txt), wire it up, and then I had a bare-bones but servicable viewer!

uddf_demo.png
Figure 1: UDDF viewer showing a dive log (depth (in metres) in blue, temperature (Celcius) in red)

I was pretty pleased about how concise the code for this is too – as of the time of writing, only 131 lines, including all the imports and whitespace.

I look forward to continuing to work on both my dive viewer and the Logui project itself. It’s been really satisfying to develop, rewarding to learn new things (SDL itself, Prolog’s C++ interface, CMake, and the ECS architecture), and I hope that some sunny day soon I’ll have the time to build this out into a library that other people can make use of too (and maybe, someday, return to my original dream and make a game?!).

If anyone else out there would like to work on this too or just finds it interesting, please let me know!