<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fluff.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fluff.blog/" rel="alternate" type="text/html" /><updated>2026-09-16T00:03:13+00:00</updated><id>https://fluff.blog/feed.xml</id><title type="html">Technical Fluff</title><subtitle>Dan&apos;s notes on software, design, maths or whatever.</subtitle><entry><title type="html">The UI frame loop, and why input handling is UI</title><link href="https://fluff.blog/2026/09/15/the-ui-frame-loop-and-why-input-handling-is-ui.html" rel="alternate" type="text/html" title="The UI frame loop, and why input handling is UI" /><published>2026-09-15T00:00:00+00:00</published><updated>2026-09-15T00:00:00+00:00</updated><id>https://fluff.blog/2026/09/15/the-ui-frame-loop-and-why-input-handling-is-ui</id><content type="html" xml:base="https://fluff.blog/2026/09/15/the-ui-frame-loop-and-why-input-handling-is-ui.html"><![CDATA[<p><em>This is Chapter 3 of <a href="https://fluff.blog/2026/09/13/i-made-my-perfect-ui-library.html">a 3-part blog post series</a> discussing “Perfection”, a greenfield UI library I built for personal use.</em></p>

<hr />

<p>Over the last two blog posts, we’ve built up a whole UI system from first principles, and uncovered a fundamentally simpler way of organising the shapes in our UI that allows us to use function composition instead of a strict UI hierarchy, as well as tackle difficult layout problems with ease.</p>

<p>In this final blog post, we’ll round it out with a broader understanding of where our UI procedure sits within the wider frame loop, and how it can be extended to handle interactions from the user in order to forward semantic events to the game engine.</p>

<hr />

<h2 id="hey-just-query-the-mouse-position">…hey, just… query the mouse position?</h2>

<p>Now that we’re presenting the user with shapes on the screen, the next natural step is to get responses <em>back</em> from the user and make the UI interactive.</p>

<p>To start, let’s whip up a button with our freshly discovered presentation primitives. The button will have different colours when it’s being hovered or pressed down.</p>

<p>The code should be readable to you by now:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// We'll wire this up in a second.</span>
	<span class="k">let</span> <span class="n">state</span> <span class="o">=</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span><span class="p">;</span>
	
	<span class="c1">// Using the hover/press state, decide on button colours.</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="c1">// Emit the button, ready to be included elsewhere.</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s start by trying to make these hover effects work. We’ll accept a <code class="language-plaintext highlighter-rouge">mouse_pressed: bool</code> directly, and let’s assume we have access to a <code class="language-plaintext highlighter-rouge">mouse_position: Vec2</code> argument, to compute whether the mouse is in bounds for hovering.</p>

<p>This requires a little bit of rearrangement, but you can in theory make it work. Here’s what the rearrangement looks like:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">mouse_position</span><span class="p">:</span> <span class="n">Vec2</span><span class="p">,</span>
	<span class="n">mouse_pressed</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// Render the text first so we can measure the button bounds</span>
	<span class="c1">// for hit testing. Punt the text colour; it has separate dependencies.</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>

	<span class="c1">// Don't respond to the cursor if it's somewhere else.</span>
	<span class="c1">// Otherwise, set the state based on what the mouse is doing.</span>
	<span class="k">let</span> <span class="n">state</span> <span class="o">=</span> 
		<span class="k">if</span> <span class="o">!</span><span class="n">button_bounds</span><span class="nf">.contains</span><span class="p">(</span><span class="n">mouse_position</span><span class="p">)</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="p">}</span>
		<span class="k">else</span> <span class="k">if</span> <span class="n">mouse_pressed</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="p">}</span>
		<span class="k">else</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="p">};</span>

	<span class="c1">// This is where we punted the colour computation to.</span>
	<span class="c1">// We now compute it separately from text layout.</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span> <span class="c1">// New operation!</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Okay, so this <em>seems</em> reasonable, except… what mouse position do you pass to this function?</p>

<p>Remember - back when we were dealing with layout, we posited that the button’s layout doesn’t depend on where it’s positioned. That meant that we intentionally constructed the button outside of the normal coordinate system, with the understanding that the button would be moved into place by the caller at some point.</p>

<p>That means the caller would need to somehow pass in a mouse position in this button’s own coordinate system. But the caller won’t even know where that bounding box ends up <em>itself</em> - the final position of things isn’t fixed until the end of <code class="language-plaintext highlighter-rouge">render_ui()</code> as a whole.</p>

<p>Just like in Chapter 2, we have a cyclic dependency - except this time, the way the component renders is cyclically dependent on <em>the whole render process</em>.</p>

<h2 id="breaking-the-dependency-cycle">Breaking the dependency cycle</h2>

<p>To try and move past this, we’ll employ the same trick we did for those layout cycles last chapter; duplicate the logic, and figure out what wants to happen first, and what wants to happen later. That means two <code class="language-plaintext highlighter-rouge">render_ui()</code> calls for now.</p>

<p>First: tracking where our button ends up between the two calls. We’ll add a new “hit region” to our composition to represent the interactive area, that’ll flow along with all the rest of our shapes. Importantly, it’ll have a stable key from our <code class="language-plaintext highlighter-rouge">UiContext</code> so we can refer to it across runs.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="n">region</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"region"</span><span class="p">),</span> <span class="n">button_bounds</span><span class="p">);</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">region</span><span class="py">.state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">region</span><span class="p">,</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, all we need to do is a first-run of the UI which is inert and collects all the hit region positions/sizes, then we can do a second run that updates <code class="language-plaintext highlighter-rouge">region.state</code> for all of the hit regions based on where they landed in the first run:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">ctx</span> <span class="o">=</span> <span class="nn">UiContext</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>

<span class="k">loop</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="nf">.next_frame</span><span class="p">();</span>
	<span class="c1">// Lay out all the hit regions, being careful not to cause side effects.</span>
	<span class="k">let</span> <span class="n">first_shapes</span> <span class="o">=</span> <span class="nf">render_ui</span><span class="p">(</span><span class="n">ctx</span><span class="p">);</span>
	<span class="n">ctx</span><span class="nf">.reposition_hit_regions</span><span class="p">(</span><span class="n">first_shapes</span><span class="p">);</span>
	<span class="c1">// Update the states of the hit regions as-laid-out based on the mouse</span>
	<span class="c1">// position and pressed state.</span>
	<span class="n">ctx</span><span class="nf">.update_hit_region_states</span><span class="p">(</span><span class="n">mouse_position</span><span class="p">,</span> <span class="n">mouse_pressed</span><span class="p">);</span>
	<span class="c1">// Do the final layout of the frame, ready for rendering.</span>
	<span class="k">let</span> <span class="n">second_shapes</span> <span class="o">=</span> <span class="nf">render_ui</span><span class="p">(</span><span class="n">ctx</span><span class="p">);</span>
	<span class="nf">render_to_screen</span><span class="p">(</span><span class="n">second_shapes</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This would work! Assuming your render functions were deterministic and any side effects were kept under control, you’d be able to precisely evaluate which hit regions were hovered or pressed.</p>

<p>That said, rendering the same UI twice for some hover effects is pretty unfortunate. Clearly, we need the layout of the hit regions <em>before</em> we can build the rest of this UI.</p>

<p>What’s <em>not</em> clear is <em>how</em> you would do that. Layout is a screen-wide process, so our component doesn’t own it. Inputs could be blocked by other components we’re not aware of that overlap us. Plus, layout and presentation are tightly woven together in a lot of cases, so it’s not clear that you can even clearly make a separation between them.</p>

<p>So, instead of fighting against our UI system trying to rationalise a split inside of the procedure, let’s instead perform a lateral move along a different dimension.</p>

<h2 id="frames-live-between-renders">Frames live between renders</h2>

<p>In theory, we present the user with a rendered frame, then they move their mouse somewhere and click in response. That response is processed at the end of the frame, ready to feed forward into the next rendered frame, and into the wider game engine.</p>

<p>In other words, renders are transient reconciliations between frames, where the trailing edge of the past frame is closed, and the leading edge of the next frame is opened.</p>

<p>So, we should be measuring how a user <em>responds</em> to the hit regions we have shown last frame. However, what our two-pass system does in practice is set up a bunch of hit regions, then immediately measure where the cursor intersects them in a second pass. We’re measuring the user’s input against UI we haven’t yet shown them - and in fact, haven’t even finished constructing. That’s where our dependency cycle is coming from.</p>

<p>This is one of the few places where depending on the UI’s history is actually a good thing. A user being able to interact with a button they’ve never seen on the screen is a failure mode, but it’s exactly what a history-less system like ours would offer. (Some platforms like Android go as far as to artificially sink inputs to buttons when they’ve <em>just</em> appeared at a location, so this kind of observation isn’t new.)</p>

<p>So, to break the cycle, let’s set up the hit regions on frame N, then measure the user’s interaction with those hit regions as we build frame N + 1. Comparing those regions with the mouse pointer’s location tells us where the mouse ultimately landed on the last presented frame that a user <em>actually saw</em>.</p>

<p>This works well for almost all cases; for a button sitting on the screen idle, last frame’s hit regions are an exact match for this frame’s hit regions, making them behave identically.</p>

<p>The only behaviour change comes if anything interactive is changing (e.g. dialogs appearing, buttons moving etc.) - they have to <em>first</em> render to the screen before responding to inputs on the next frame. That can be a source of visual discrepancy, but in those cases, you’d likely want to sink any interactions happening across those state changes anyway, as Android does.</p>

<p>Here’s what the change looks like in the main loop; notice how <code class="language-plaintext highlighter-rouge">reposition_hit_regions</code> now sits next to <code class="language-plaintext highlighter-rouge">render_to_screen</code> - both are now per-frame “committed” outputs, presented to the user together for the next period of time.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">ctx</span> <span class="o">=</span> <span class="nn">UiContext</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>

<span class="k">loop</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="nf">.next_frame</span><span class="p">();</span>
	<span class="c1">// Use the latest mouse position to reduce input latency.</span>
	<span class="n">ctx</span><span class="nf">.update_hit_region_states</span><span class="p">(</span><span class="n">mouse_position</span><span class="p">,</span> <span class="n">mouse_pressed</span><span class="p">);</span>
	<span class="c1">// Render this frame, using the previous hit region positions to inform</span>
	<span class="c1">// what our hover and press states are.</span>
	<span class="k">let</span> <span class="n">shapes</span> <span class="o">=</span> <span class="nf">render_ui</span><span class="p">(</span><span class="n">ctx</span><span class="p">);</span>
	<span class="c1">// Set up the hit region positions for next frame.</span>
	<span class="n">ctx</span><span class="nf">.reposition_hit_regions</span><span class="p">(</span><span class="n">shapes</span><span class="p">);</span>
	<span class="c1">// Render to screen.</span>
	<span class="nf">render_to_screen</span><span class="p">(</span><span class="n">shapes</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="clicking-things-in-place">Clicking things in place</h2>

<p>Now that we’ve established whether the mouse is over our button, the natural next step is to detect clicks that occur over our button, and take action accordingly.</p>

<p>Our current system already gives us a lot of nice properties for free. For example, since we update hit region states globally, it’s trivial to implement hit region “occlusion” where hit regions can absorb hovers that’d otherwise pass to hit regions behind them. Compared to pulling messages from an input queue <em>during</em> rendering, a single pass over all hit regions makes it easy to reason about which hit regions sink which events, rather than having to reason about implicit nonlocal ordering of how events are pulled during rendering.</p>

<p>As a second example, having a stable key for every hit region <em>also</em> makes it trivial to implement proper press/release handling, where a button only “clicks” if the press and release happens over the same region. Since regions can be tracked across frames with stable identity, it’s easy to abandon a click that ends with the mouse located elsewhere.</p>

<p>That leaves us with one question: what do you do when someone clicks a region? Where does that get handled?</p>

<p>A classic imgui approach is to return the clicked state as a result of rendering a clickable object. For example, in classic imgui, a clickable button would have this shape:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Returns true if the button has just been clicked.</span>
<span class="k">if</span> <span class="nf">button</span><span class="p">(</span><span class="s">"Hello, world"</span><span class="p">)</span> <span class="p">{</span>
	<span class="nf">do_click_action</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We actually already do this with our hover state; the only difference is that it’s accessed <em>through</em> a returned object, rather than being the returned value directly.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ... snip ...</span>
<span class="k">let</span> <span class="n">region</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"region"</span><span class="p">),</span> <span class="n">button_bounds</span><span class="p">);</span>
<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">region</span><span class="py">.state</span> <span class="p">{</span>
<span class="c1">// ... snip ...</span>
</code></pre></div></div>

<p>So, let’s do it again, but this time with a <code class="language-plaintext highlighter-rouge">was_clicked</code> state. To match the shape of things, let’s also try to pass that state out of the button so our caller can do something in response.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="p">(</span><span class="k">impl</span> <span class="n">Paint</span><span class="p">,</span> <span class="nb">bool</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="n">region</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"region"</span><span class="p">),</span> <span class="n">button_bounds</span><span class="p">);</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">region</span><span class="py">.state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	
	<span class="c1">// New: return both the composition and the state.</span>
	<span class="k">let</span> <span class="n">shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">region</span><span class="p">,</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">);</span>
	
	<span class="p">(</span><span class="n">shapes</span><span class="p">,</span> <span class="n">region</span><span class="py">.was_clicked</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But, this is awkward. Anyone using the button now has to deconstruct or index into it, meaning it doesn’t slot neatly into constructs like <code class="language-plaintext highlighter-rouge">compose!</code> any more. You can find yourself shuttling state around in higher-up functions, and the button’s actions and presentation can get visibly distant on the page for no good reason.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">GameState</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// Render the buttons.</span>
	<span class="k">let</span> <span class="n">resume</span> <span class="o">=</span> <span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">achievements</span> <span class="o">=</span> <span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">options</span> <span class="o">=</span> <span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">quit</span> <span class="o">=</span> <span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">);</span>
	
	<span class="c1">// Enact any required actions.</span>
	<span class="k">if</span> <span class="n">resume</span><span class="na">.1</span> <span class="p">{</span>
		<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nb">None</span><span class="p">;</span> 
		<span class="n">game_state</span><span class="py">.paused</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">achievements</span><span class="na">.1</span> <span class="p">{</span>
		<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">options</span><span class="na">.1</span> <span class="p">{</span>
		<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">quit</span><span class="na">.1</span> <span class="p">{</span>
		<span class="n">game_state</span><span class="py">.loop_state</span> <span class="o">=</span> <span class="nn">GameLoopState</span><span class="p">::</span><span class="n">Exit</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="c1">// Compose them into the final shape stream.</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span><span class="n">resume</span><span class="na">.0</span><span class="p">,</span> <span class="n">achievements</span><span class="na">.0</span><span class="p">,</span> <span class="n">options</span><span class="na">.0</span><span class="p">,</span> <span class="n">quit</span><span class="na">.0</span><span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ideally, adding interactivity to a component wouldn’t alter its fundamental shape, so altering its return value like this isn’t going to scale well as components are refactored or take on different responsibilities.</p>

<p>So, instead of returning anything different at all, let’s change <code class="language-plaintext highlighter-rouge">render_button</code> to accept a closure, specifying what to do when the button is clicked.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
	<span class="n">on_activate</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">FnOnce</span><span class="p">()</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="n">region</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"region"</span><span class="p">),</span> <span class="n">button_bounds</span><span class="p">);</span>
	<span class="c1">// Pass through to on_activate() right away if we can.</span>
	<span class="k">if</span> <span class="n">region</span><span class="py">.was_clicked</span> <span class="p">{</span> <span class="nf">on_activate</span><span class="p">();</span> <span class="p">}</span>
	
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">region</span><span class="py">.state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	
	<span class="c1">// The return value is now completely unaffected.</span>
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">region</span><span class="p">,</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">GameState</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// Now, the buttons can be directly composed, and no state</span>
	<span class="c1">// needs to be shuttled around at all.</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nb">None</span><span class="p">;</span>
			<span class="n">game_state</span><span class="py">.paused</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">);</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">);</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="n">game_state</span><span class="py">.loop_state</span> <span class="o">=</span> <span class="nn">GameLoopState</span><span class="p">::</span><span class="n">Exit</span><span class="p">;</span>
		<span class="p">})</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this change, what a button does always lives with the rest of the button’s definition, preserving local reasoning and making code more concise without losing meaning.</p>

<h2 id="its-not-time-to-act">It’s (not) time to act</h2>

<p>There’s a more subtle timing concern to walk through with our code as-written.</p>

<p>Our intention with this code is to let the user switch views and unpause the game when they press the button, so that’s exactly what we wrote in the closures. But, consider the <em>effect</em> of those closures on higher-up UI code:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_game_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">GameState</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">current_view</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"current_view"</span><span class="p">)</span><span class="nf">.once</span><span class="p">(||</span> <span class="nb">None</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="c1">// If a view is open, add cinematic black bars over the world.</span>
		<span class="n">current_view</span><span class="nf">.is_some</span><span class="p">()</span><span class="nf">.then</span><span class="p">(||</span> <span class="p">{</span>
			<span class="nd">compose!</span> <span class="p">[</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
			<span class="p">]</span>
		<span class="p">}),</span>
	
		<span class="c1">// If we're paused, render the pause menu.</span>
		<span class="n">game_state</span><span class="py">.paused</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_pause_menu</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"pause_menu"</span><span class="p">),</span>
			<span class="n">screen_bounds</span><span class="p">,</span>
			<span class="o">&amp;</span><span class="k">mut</span> <span class="n">current_view</span><span class="p">,</span> <span class="c1">// could be mutated by this call!</span>
			<span class="n">game_state</span>         <span class="c1">// could be mutated by this call!</span>
		<span class="p">)),</span>
		<span class="c1">// Otherwise, if we're unpaused, render the in-game HUD.</span>
		<span class="p">(</span><span class="o">!</span><span class="n">game_state</span><span class="py">.paused</span><span class="p">)</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_hud</span><span class="p">(</span> <span class="o">...</span> <span class="p">)),</span>
		
		<span class="c1">// Render the view that's currently being shown, if any.</span>
		<span class="n">current_view</span><span class="nf">.map</span><span class="p">(|</span><span class="n">view</span><span class="p">|</span> <span class="k">match</span> <span class="n">view</span> <span class="p">{</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span> <span class="k">=&gt;</span> <span class="nf">render_achievements</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Options</span> <span class="k">=&gt;</span> <span class="nf">render_options</span><span class="p">(</span> <span class="o">...</span> <span class="p">)</span>
		<span class="p">}),</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Because we’re mutating inline with our UI builder code, clicking the buttons leaves us in a ‘torn’ state. Consider these two scenarios:</p>

<ol>
  <li>Clicking the “resume” button, changing <code class="language-plaintext highlighter-rouge">game_state.paused</code> halfway through.</li>
  <li>Clicking the “options” button, changing <code class="language-plaintext highlighter-rouge">current_view</code> halfway through.</li>
</ol>

<p>In the case of “resume”, the pause menu is rendered due to <code class="language-plaintext highlighter-rouge">paused.then</code> executing, then <code class="language-plaintext highlighter-rouge">paused</code> is changed to <code class="language-plaintext highlighter-rouge">false</code>, which allows <code class="language-plaintext highlighter-rouge">(!paused).then</code> to execute <em>in the same frame</em>. As a result, you see the in-game HUD rendered over the pause menu unintentionally.</p>

<p>In the case of “options”, the black bars can be skipped if no view is open, but once <code class="language-plaintext highlighter-rouge">render_pause_menu</code> sets <code class="language-plaintext highlighter-rouge">current_view</code>, the options view is rendered next, without the black bars that are supposed to render behind all views.</p>

<p>This points to a general truth: generally, you <em>don’t</em> want to change anything while the UI is in the middle of being built. Once you enter <code class="language-plaintext highlighter-rouge">render_ui()</code>, anything you use to determine what’s rendered needs to be frozen in place until you’re done - never read your writes. That means any changes you make in <code class="language-plaintext highlighter-rouge">render_ui()</code> must be <em>deferred</em> to a later point.</p>

<p>So, how do you defer those changes?</p>

<h2 id="deriving-a-correct-defer-abstraction">Deriving a correct defer abstraction</h2>

<p>Your first thought might be to somehow defer the <em>closures</em>; if setting <code class="language-plaintext highlighter-rouge">paused = true</code> is the problem, running the code that sets <code class="language-plaintext highlighter-rouge">paused</code> later seems like the obvious approach, given it barely impacts the code footprint:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Pseudocode.</span>
<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="nf">run_after_frame</span><span class="p">(||</span> <span class="p">{</span>
	<span class="o">*</span><span class="n">current_view</span> <span class="o">=</span> <span class="nb">None</span><span class="p">;</span>
	<span class="n">game_state</span><span class="py">.paused</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
<span class="p">})),</span>
</code></pre></div></div>

<p>However, there’s a key problem with this approach: because control flow is allowed to return from the component between construction and deferral, there’s no longer any guarantee that the values you locally saw are up to date, or even exist at all - especially if other UI touches them.</p>

<p>This manifests in two ways:</p>

<ul>
  <li>Values you read from within the closure are being sampled at an unpredictable point in the future, possibly <em>after</em> other closures have mutated the values you’re reading; effectful closures are combinatorially harder to reason about.</li>
  <li>Rust can’t guarantee that it’s memory safe. Our closures here refer to <code class="language-plaintext highlighter-rouge">game_state</code> mutably, which means nobody else is allowed to refer to <code class="language-plaintext highlighter-rouge">game_state</code> in the meantime, but <code class="language-plaintext highlighter-rouge">game_state</code>  is a <em>widely-used struct</em>. That will instantly turn into a compilation failure unless you pay for heavier state sharing methods.</li>
</ul>

<p>In general, deferring closures is a bad idea. Code that lives in the component should run inside the component, not just for ease of editing, but because it’s required to enforce a consistent, unmoving view of the world that can be reasoned about thoroughly.</p>

<p>The real problem here is that, in these closures, we’re mutating the state our UI depends on while we’re halfway through the build. A better solution - that solves <em>both</em> for tearing <em>and</em> for ensuring closures sample unmutated, predictable state - is to run all the closures immediately, but only read from <em>this frame’s</em> value and only write to <em>next frame’s</em> value.</p>

<p>Let’s start by tackling <code class="language-plaintext highlighter-rouge">current_view</code>, which we currently define as a basic (and <em>slightly</em> fictional) “mutable storage place”. It’s initialised once using <code class="language-plaintext highlighter-rouge">.once()</code>, then you write to that variable directly from there on out. Since it’s mutable, it can change halfway through the procedure as we pointed out.</p>

<p>For things like <code class="language-plaintext highlighter-rouge">current_view</code> which are stored directly by our UI code, we already built a solution back in Chapter 1: we store a flip cache in <code class="language-plaintext highlighter-rouge">UiContext</code> that lets us read the old state while writing the new one.</p>

<p>So, let’s make <code class="language-plaintext highlighter-rouge">UiContext</code> sprout a new <code class="language-plaintext highlighter-rouge">.state()</code> utility that lets us store our in-UI state directly in the flip cache, using the same stable keys we’ve used everywhere else. When we call it, it’ll give the current value, as well as a function to set next frame’s value. This’ll replace our (fictionalised) usage of <code class="language-plaintext highlighter-rouge">.once()</code> and fix that tearing:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_game_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">GameState</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// Now, current_view isn't `mut`, so we literally can't change it.</span>
	<span class="c1">// set_next_view also doesn't need `&amp;mut` because it can't mutate anything</span>
	<span class="c1">// that will be read here; the mutations are kept internal to UiContext.</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">current_view</span><span class="p">,</span> <span class="n">set_next_view</span><span class="p">)</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"current_view"</span><span class="p">)</span><span class="nf">.state</span><span class="p">(||</span> <span class="nb">None</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="c1">// Just a plain value, so every operation on it works!</span>
		<span class="n">current_view</span><span class="nf">.is_some</span><span class="p">()</span><span class="nf">.then</span><span class="p">(||</span> <span class="p">{</span>
			<span class="nd">compose!</span> <span class="p">[</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
			<span class="p">]</span>
		<span class="p">}),</span>
	
		<span class="c1">// .paused will still tear - we'll get back to it soon...</span>
		<span class="n">game_state</span><span class="py">.paused</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_pause_menu</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"pause_menu"</span><span class="p">),</span>
			<span class="n">screen_bounds</span><span class="p">,</span>
			<span class="n">current_view</span><span class="p">,</span>
			<span class="n">set_next_view</span><span class="p">,</span> <span class="c1">// Grant permission to set next frame's view.</span>
			<span class="n">game_state</span>
		<span class="p">)),</span>
		<span class="p">(</span><span class="o">!</span><span class="n">game_state</span><span class="py">.paused</span><span class="p">)</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_hud</span><span class="p">(</span> <span class="o">...</span> <span class="p">)),</span>
		
		
		<span class="c1">// Always reads the original value, so current_view can</span>
		<span class="c1">// never cause a tear here (even when the pause menu acts!)</span>
		<span class="n">current_view</span><span class="nf">.map</span><span class="p">(|</span><span class="n">view</span><span class="p">|</span> <span class="k">match</span> <span class="n">view</span> <span class="p">{</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span> <span class="k">=&gt;</span> <span class="nf">render_achievements</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Options</span> <span class="k">=&gt;</span> <span class="nf">render_options</span><span class="p">(</span> <span class="o">...</span> <span class="p">)</span>
		<span class="p">}),</span>
	<span class="p">]</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">set_next_view</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">),</span> <span class="c1">// New setter!</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">GameState</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nb">None</span><span class="p">);</span> <span class="c1">// No longer takes effect immediately.</span>
			<span class="n">game_state</span><span class="py">.paused</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">));</span> <span class="c1">// Same here.</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">));</span> <span class="c1">// Same here.</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="n">game_state</span><span class="py">.loop_state</span> <span class="o">=</span> <span class="nn">GameLoopState</span><span class="p">::</span><span class="n">Exit</span><span class="p">;</span>
		<span class="p">})</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>(As a happy note for any Fusion users in the audience, we have exactly replayed the <a href="https://elttob.uk/Fusion/0.3/tutorials/best-practices/state/#top-down-control">top-down control principle</a> where we pair read-only state with a callback that can change it! In general, the <code class="language-plaintext highlighter-rouge">let (value, setter_for_value) = state();</code> shape should feel familiar to anyone who has worked in modern reactive frameworks recently.)</p>

<p>So, now that <code class="language-plaintext highlighter-rouge">current_view</code> is behaving properly, that only leaves <code class="language-plaintext highlighter-rouge">game_state.paused</code> to deal with. Since that’s <em>not</em> owned by the UI, it can’t use a lightweight flip cache as its source of truth, so we’re going to have to dig further.</p>

<p>The tearing caused by <code class="language-plaintext highlighter-rouge">game_state.paused</code> fundamentally comes from the same place as it did with <code class="language-plaintext highlighter-rouge">current_view</code> - <em>anything</em> <code class="language-plaintext highlighter-rouge">mut</code> is dangerous, because it’s capable of changing between reads. That’s why we got rid of <code class="language-plaintext highlighter-rouge">.once()</code>, and it’s why we’re <em>now</em> going to get rid of <code class="language-plaintext highlighter-rouge">&amp;mut GameState</code>, replacing it with a downgraded read-only <code class="language-plaintext highlighter-rouge">&amp;GameState</code>.</p>

<p>Which leaves only one question: how do you enact an unpause or quit action, if you can’t mutate the game state to make that so?</p>

<p>Well, hey - we’re pragmatists here, so let’s employ a “temporary” solution, and create a dedicated callback for sending actions like unpause or quit.  On the other side of the callback, we can collect those actions into a list as we go, and once we reach the end of the frame, we can just manually loop through the list and make the changes we intended to.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_game_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="c1">// No longer mutable, so we know for the duration of this function,</span>
	<span class="c1">// the state we read from here isn't going to change.</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span>
	<span class="c1">// Similar to set_next_view, this will change what the game state is</span>
	<span class="c1">// only after this frame has finished being built.</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">current_view</span><span class="p">,</span> <span class="n">set_next_view</span><span class="p">)</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"current_view"</span><span class="p">)</span><span class="nf">.state</span><span class="p">(||</span> <span class="nb">None</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="n">current_view</span><span class="nf">.is_some</span><span class="p">()</span><span class="nf">.then</span><span class="p">(||</span> <span class="p">{</span>
			<span class="nd">compose!</span> <span class="p">[</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
				<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="nn">PxRect</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">),</span>
			<span class="p">]</span>
		<span class="p">}),</span>
	
		<span class="c1">// These two usages of .paused can never disagree, because</span>
		<span class="c1">// render_pause_menu can't change .paused mid-function anymore.</span>
		<span class="n">game_state</span><span class="py">.paused</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_pause_menu</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"pause_menu"</span><span class="p">),</span>
			<span class="n">screen_bounds</span><span class="p">,</span>
			<span class="n">current_view</span><span class="p">,</span>
			<span class="n">set_next_view</span><span class="p">,</span>
			<span class="n">game_state</span>
		<span class="p">)),</span>
		<span class="p">(</span><span class="o">!</span><span class="n">game_state</span><span class="py">.paused</span><span class="p">)</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_hud</span><span class="p">(</span> <span class="o">...</span> <span class="p">)),</span>
		
		<span class="n">current_view</span><span class="nf">.map</span><span class="p">(|</span><span class="n">view</span><span class="p">|</span> <span class="k">match</span> <span class="n">view</span> <span class="p">{</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span> <span class="k">=&gt;</span> <span class="nf">render_achievements</span><span class="p">(</span> <span class="o">...</span> <span class="p">),</span>
			<span class="nn">Views</span><span class="p">::</span><span class="n">Options</span> <span class="k">=&gt;</span> <span class="nf">render_options</span><span class="p">(</span> <span class="o">...</span> <span class="p">)</span>
		<span class="p">}),</span>
	<span class="p">]</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">set_next_view</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">),</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span> <span class="c1">// No longer mutable!</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span> <span class="c1">// Newly tasked with external state changes.</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nb">None</span><span class="p">);</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="nf">SetPaused</span><span class="p">(</span><span class="k">false</span><span class="p">));</span> <span class="c1">// No longer immediate.</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="n">TerminateLoop</span><span class="p">);</span> <span class="c1">// Also no longer immediate.</span>
		<span class="p">})</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By definition, this solution works, but maybe we don’t feel so good about the idea of everything having to become an action. Perhaps it recalls memories of giant global reducers from some older UI frameworks, which we already know don’t work.</p>

<p>But, we arrived here from first principles, so maybe there’s something we can learn. Let’s continue with this for now, and see if it develops into something more nuanced later, once we’ve exposed it to the real world.</p>

<h2 id="touching-on-something-input-ant">Touching on something input-ant</h2>

<p>Cavey ships on a Steam Deck, which puts alternative inputs front and centre. We can’t assume keyboard and mouse as the “best” input method. For example, someone may want to play Cavey using a <em>touch screen</em>.</p>

<p>So, let’s consider the case of touch controls, which allow a player without any physical input types to still control the game. We’ll focus on a single “attack” button to keep the code examples short.</p>

<p>When the player pushes the button to attack, we can’t set the player’s inputs directly in-line - that’d be <code class="language-plaintext highlighter-rouge">&amp;mut GameState</code> - so we’ll turn this state mutation into a queued action.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_hud</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">attack</span> <span class="o">=</span> <span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"attack"</span><span class="p">),</span> <span class="s">"Attack"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
		<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="n">Attack</span><span class="p">);</span> <span class="c1">// using our new defer vocabulary</span>
	<span class="p">});</span>
	<span class="k">let</span> <span class="n">attack</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">attack</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">attack</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">attack</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">1.0</span><span class="p">));</span>
	<span class="n">attack</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This may raise an eyebrow. This <em>kind of</em> looks like we’re dispatching an <em>input</em>, right?</p>

<p>Let’s talk about that.</p>

<p>Typically, input systems for games map key codes to “semantic” events like Attack, Jump, Walk and Look. Let’s take Cavey’s input system as a quick example - I won’t get <em>too</em> detailed about it, but I’ll share some snippets as visual aids instead.</p>

<p>Here are some of Cavey’s existing definitions from its existing input system, which separately define the <em>semantic</em> information about an input (what kind of input is it?) and the actual events from the OS that will drive them (what devices write this input?):</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span>
	<span class="c1">// "Attack" is a one-dimensional button-style input ...</span>
	<span class="n">OutputDescription1D</span> <span class="p">{</span>
		<span class="n">output_id</span><span class="p">:</span> <span class="nn">output_ids</span><span class="p">::</span><span class="n">ATTACK</span><span class="p">,</span>
		<span class="n">title</span><span class="p">:</span> <span class="s">"Attack"</span><span class="p">,</span>
		<span class="n">constraint</span><span class="p">:</span> <span class="nn">Constraint1D</span><span class="p">::</span><span class="n">Button</span>
	<span class="p">},</span>
	<span class="c1">// ... which is controlled by the left mouse button.</span>
	<span class="n">Writer1D</span> <span class="p">{</span>
		<span class="n">input</span><span class="p">:</span> <span class="nn">Input1D</span><span class="p">::</span><span class="nf">Mouse</span><span class="p">(</span><span class="nn">MouseButton</span><span class="p">::</span><span class="nb">Left</span><span class="p">),</span>
		<span class="n">mode</span><span class="p">:</span> <span class="nn">WriterMode</span><span class="p">::</span><span class="n">Overwrite</span><span class="p">,</span>
	<span class="p">}</span>
<span class="p">),</span>

<span class="p">(</span>
	<span class="c1">// "Walk" is a two-dimensional joystick-style input ...</span>
	<span class="n">OutputDescription2D</span> <span class="p">{</span>
		<span class="n">output_id</span><span class="p">:</span> <span class="nn">output_ids</span><span class="p">::</span><span class="n">WALK</span><span class="p">,</span>
		<span class="n">title</span><span class="p">:</span> <span class="s">"Walk"</span><span class="p">,</span>
		<span class="n">constraint</span><span class="p">:</span> <span class="nn">Constraint2D</span><span class="p">::</span><span class="n">Joystick</span>
	<span class="p">},</span>
	<span class="c1">// ... which is controlled by four keys on the keyboard.</span>
	<span class="n">Writer2D</span> <span class="p">{</span>
		<span class="n">input</span><span class="p">:</span> <span class="nn">Input2D</span><span class="p">::</span><span class="n">Keyboard</span> <span class="p">{</span>
			<span class="n">up</span><span class="p">:</span> <span class="nn">KeyCode</span><span class="p">::</span><span class="n">KeyW</span><span class="p">,</span>
			<span class="n">down</span><span class="p">:</span> <span class="nn">KeyCode</span><span class="p">::</span><span class="n">KeyS</span><span class="p">,</span>
			<span class="n">left</span><span class="p">:</span> <span class="nn">KeyCode</span><span class="p">::</span><span class="n">KeyA</span><span class="p">,</span>
			<span class="n">right</span><span class="p">:</span> <span class="nn">KeyCode</span><span class="p">::</span><span class="n">KeyD</span>
		<span class="p">},</span>
		<span class="n">mode</span><span class="p">:</span> <span class="nn">WriterMode</span><span class="p">::</span><span class="n">Overwrite</span>
	<span class="p">}</span>
<span class="p">),</span>
</code></pre></div></div>

<p>Cavey even has some more <em>complex</em> input types which accumulate over time with their own internal state, for example, the look direction is stored in the input system so that incoming mouse moves can directly change the camera angle with zero latency.</p>

<p>You could almost think of “Look” like a physical gimbal, which the mouse “pushes around” statefully:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span>
	<span class="c1">// "Look" is a two-dimensional gimbal-style input ...</span>
	<span class="n">OutputDescription2D</span> <span class="p">{</span>
		<span class="n">output_id</span><span class="p">:</span> <span class="nn">output_ids</span><span class="p">::</span><span class="n">LOOK</span><span class="p">,</span>
		<span class="n">title</span><span class="p">:</span> <span class="s">"Look"</span><span class="p">,</span>
		<span class="n">constraint</span><span class="p">:</span> <span class="nn">Constraint2D</span><span class="p">::</span><span class="n">Gimbal</span>
	<span class="p">},</span>
	<span class="c1">// ... which is statefully added to by mouse movements.</span>
	<span class="n">Writer2D</span> <span class="p">{</span>
		<span class="n">input</span><span class="p">:</span> <span class="nn">Input2D</span><span class="p">::</span><span class="n">Mouse</span> <span class="p">{</span>
			<span class="n">pixels_per_unit</span><span class="p">:</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">ONE</span> <span class="o">*</span> <span class="n">MOUSE_DPI</span> <span class="o">/</span> <span class="mf">5.0</span>
		<span class="p">},</span>
		<span class="n">mode</span><span class="p">:</span> <span class="nn">WriterMode</span><span class="p">::</span><span class="n">Delta</span> <span class="c1">// not overwriting!</span>
	<span class="p">}</span>
<span class="p">),</span>
</code></pre></div></div>

<p>This solution itself was motivated from first principles: one of Cavey’s goals is to support remappable inputs. If you don’t like the keys and input styles that were chosen for you, then you should be able to configure them to something else that <em>does</em> work for you. But input handling is complex, so the system grew to capture all that complexity and state management.</p>

<p>As a result, all the game engine has to worry about, is reading off some semantic values without worrying about provenance at all:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Player physics doesn't care about where these come from.</span>
<span class="c1">// The code path continues without branching on device types.</span>
<span class="k">let</span> <span class="n">walk_input</span> <span class="o">=</span> <span class="n">input_snapshot</span><span class="py">.states_2d</span><span class="nf">.get</span><span class="p">(</span><span class="nn">output_ids</span><span class="p">::</span><span class="n">WALK</span><span class="p">);</span>
<span class="k">let</span> <span class="n">jump_input</span> <span class="o">=</span> <span class="n">input_snapshot</span><span class="py">.states_1d</span><span class="nf">.get</span><span class="p">(</span><span class="nn">output_ids</span><span class="p">::</span><span class="n">JUMP</span><span class="p">);</span>
<span class="k">let</span> <span class="n">crouch_input</span> <span class="o">=</span> <span class="n">input_snapshot</span><span class="py">.states_1d</span><span class="nf">.get</span><span class="p">(</span><span class="nn">output_ids</span><span class="p">::</span><span class="n">CROUCH</span><span class="p">);</span>

<span class="k">let</span> <span class="n">forward_vector</span> <span class="o">=</span> <span class="nf">flat_forward_vec</span><span class="p">(</span><span class="n">yaw</span><span class="p">);</span>
<span class="k">let</span> <span class="n">right_vector</span> <span class="o">=</span> <span class="nf">flat_right_vec</span><span class="p">(</span><span class="n">yaw</span><span class="p">);</span>
<span class="k">let</span> <span class="n">walk_vector</span> <span class="o">=</span> <span class="n">right_vector</span> <span class="o">*</span> <span class="n">walk_input</span><span class="py">.x</span> <span class="o">+</span> <span class="n">forward_vector</span> <span class="o">*</span> <span class="n">walk_input</span><span class="py">.y</span><span class="p">;</span>

<span class="k">let</span> <span class="n">perform_jump</span> <span class="o">=</span> <span class="o">*</span><span class="n">jump_input</span> <span class="o">&gt;</span> <span class="mf">0.5</span><span class="p">;</span>
<span class="k">let</span> <span class="n">perform_crouch</span> <span class="o">=</span> <span class="o">*</span><span class="n">crouch_input</span> <span class="o">&gt;</span> <span class="mf">0.5</span><span class="p">;</span>

<span class="c1">// ... rest of locomotion code ...</span>
</code></pre></div></div>

<p>Tying this back to our touch controls example, you’d <em>almost</em> want to write a definition like this, where you declared that an on-screen button would drive the input:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span>
	<span class="c1">// "Attack" is a one-dimensional button-style input ...</span>
	<span class="n">OutputDescription1D</span> <span class="p">{</span>
		<span class="n">output_id</span><span class="p">:</span> <span class="nn">output_ids</span><span class="p">::</span><span class="n">ATTACK</span><span class="p">,</span>
		<span class="n">title</span><span class="p">:</span> <span class="s">"Attack"</span><span class="p">,</span>
		<span class="n">constraint</span><span class="p">:</span> <span class="nn">Constraint1D</span><span class="p">::</span><span class="n">Button</span>
	<span class="p">},</span>
	<span class="c1">// ... which is controlled by creating an on-screen touch control.</span>
	<span class="n">Writer1D</span> <span class="p">{</span>
		<span class="n">input</span><span class="p">:</span> <span class="nn">Input1D</span><span class="p">::</span><span class="nf">OnScreen</span><span class="p">(</span><span class="s">"Attack"</span><span class="p">),</span>
		<span class="n">mode</span><span class="p">:</span> <span class="nn">WriterMode</span><span class="p">::</span><span class="n">Overwrite</span><span class="p">,</span>
	<span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>That’s <em>two</em> places in our code base that want to care about on-screen controls. The longer you look at them, the more you start to see the similarities emerge:</p>

<ul>
  <li><em>Both</em> systems, at some times, need to show controls to the user on the screen.</li>
  <li><em>Both</em> systems listen to incoming OS events to figure out when interactions occur.</li>
  <li><em>Both</em> systems have complex state interactions that need to be managed.</li>
  <li><em>Both</em> systems emit a series of actions to be interpreted by the game engine.</li>
</ul>

<p>What’s more: these systems need to be aware of each other’s state. Touch controls need to disappear when you’re in a dialog or menu. Drags and joystick inputs need to be redirected or have their inputs sunk when the game world isn’t the focus. Gamepads need on-screen hints to accurately reflect the face buttons on the controller model being used.</p>

<p>So we’ve established these two systems broadly <em>do the same thing</em>, and are <em>highly coupled</em>.</p>

<p>Let me plant a thought in your head…</p>

<h2 id="input-handling-is-ui">Input handling <em>is</em> UI</h2>

<p>To justify this, let’s dig into the nature of the actions being emitted.</p>

<p>Both systems attempt to be <em>semantic</em> to some degree - we don’t want the game engine to care about specific OS events, devices, and such. <code class="language-plaintext highlighter-rouge">Action::Mouse1Clicked</code> would be bad for obvious reasons; it’s not portable to different input modalities like gamepad and makes input remapping much harder.</p>

<p>But what about the grey areas? <code class="language-plaintext highlighter-rouge">Action::OpenSettingsMenu</code> would be input-agnostic, and it’s the sort of thing Redux-style apps encode. But - as we saw with closures before - it doesn’t make a lot of architectural sense to export local UI concerns to a global queue just to mutate some local state again. It complicates the control flow of the UI’s business logic by splitting it across a boundary. So, we shouldn’t be sending these actions over to the game engine side at all.</p>

<p>Instead, <strong>the queue’s vocabulary should just be defined by what the game engine needs to know.</strong> The engine has no need for settings menus or scrollbars, but it <em>does</em> have a use for knowing when the player is AFK, or when they’re walking and looking around, or when they drop an item on the ground.</p>

<p>This line is exactly the same one most <em>input handling</em> systems draw today. The reason we define verbs like Jump, Attack and Walk is because that’s what our character controllers and gameplay systems need to know. What’s more, the Redux problem is avoided by ensuring UI concerns are managed <em>locally</em> using our <code class="language-plaintext highlighter-rouge">state()</code> constructs instead of being carved up across the action boundary.</p>

<p>The case for unifying the systems is strengthened by considering the frame loop as a whole; both input and UI want a low-latency path through simulation/response logic directly into a newly rendered frame. Since they both consume OS events and both emit game engine vocabulary, they are forced to occupy the same region of the frame loop; the game can’t simulate or respond without having the completed action queue, and can’t render until that simulation or response has completed.</p>

<p>Under all of these constraints, and given the deep inter-relation between the two systems demonstrated by touch controls, gamepad hints and input sinking, it’s ultimately far more reasonable to construct them as one unit instead of two.</p>

<h2 id="completing-the-interaction-model">Completing the interaction model</h2>

<p>Let’s use this insight to finish up our input handling. As an easy first win, we can finally codify our understanding of what our pause menu is <em>actually</em> doing in our mental model, and rename our actions:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">set_next_view</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">),</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nb">None</span><span class="p">);</span>
			 <span class="c1">// Notify the game engine we want to resume gameplay.</span>
			 <span class="c1">// We're now using the game engine's _own_ vocabulary for this.</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="nf">Idle</span><span class="p">(</span><span class="k">false</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="c1">// Same here - now refers to the game, not the loop it controls.</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="n">ExitGame</span><span class="p">);</span>
		<span class="p">})</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now for our first-person input handling. Since we don’t need to query a specific spot on the screen for keys, mouse or gamepad inputs, we can use a hit region that has unlimited bounds to capture the inputs we care about.</p>

<p>Other controls can then be composed on top of this hit region to occlude certain inputs, for example, touch controls can sink presses before they reach our pitch/yaw or attack/interact inputs.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_game_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">current_view</span><span class="p">,</span> <span class="n">set_next_view</span><span class="p">)</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"current_view"</span><span class="p">)</span><span class="nf">.state</span><span class="p">(||</span> <span class="nb">None</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="c1">// ... rest of render_game_ui function ...</span>
		
		<span class="c1">// Process player inputs / show on-screen controls</span>
		<span class="c1">// only when no other UI is active right now.</span>
		<span class="n">current_view</span><span class="nf">.is_none</span><span class="p">()</span><span class="nf">.then</span><span class="p">(||</span> <span class="nf">render_inputs</span><span class="p">(</span>
			<span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"inputs"</span><span class="p">),</span> 
			<span class="n">screen_bounds</span><span class="p">,</span> 
			<span class="n">game_state</span><span class="p">,</span> 
			<span class="n">queue_action</span>
		<span class="p">))</span>
	<span class="p">]</span>
	
<span class="p">}</span>

<span class="c1">// Because it implements Paint, it reserves the right to</span>
<span class="c1">// return any on-screen visual elements that we may want,</span>
<span class="c1">// for example touch controls.</span>
<span class="k">fn</span> <span class="nf">render_inputs</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">inputs</span> <span class="o">=</span> <span class="n">InputSnapshot</span> <span class="p">{</span>
		<span class="n">walk_vector</span><span class="p">:</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">ZERO</span><span class="p">,</span>
		<span class="n">pitch_yaw</span><span class="p">:</span> <span class="n">game_state</span><span class="py">.local_player.pitch_yaw</span><span class="p">,</span>
		<span class="n">jump</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">crouch</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">attack</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">interact</span><span class="p">:</span> <span class="mf">0.0</span>
	<span class="p">};</span>
	
	<span class="c1">// Define an input listener which takes up infinite 2D space,</span>
	<span class="c1">// but which still has a "z-index" in our occlusion stack.</span>
	<span class="k">let</span> <span class="n">hit</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"hit"</span><span class="p">),</span> <span class="nn">PxRect</span><span class="p">::</span><span class="n">INF</span><span class="p">)</span><span class="nf">.mouse_locked</span><span class="p">();</span>
	
	<span class="c1">// Direct inputs for keyboard, taken from the hit region.</span>
	<span class="c1">// Sampled at the frame end as continuous input.</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_is_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">W</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.walk_vector</span> <span class="o">+=</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">Y</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_is_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">S</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.walk_vector</span> <span class="o">-=</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">Y</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_is_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">A</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.walk_vector</span> <span class="o">-=</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">X</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_is_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">D</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.walk_vector</span> <span class="o">+=</span> <span class="nn">DVec2</span><span class="p">::</span><span class="n">X</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_is_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">LeftShift</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.crouch</span> <span class="o">+=</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span>
	
	<span class="c1">// Direct inputs sampled across the whole frame interval,</span>
	<span class="c1">// *not* point sampled at the end.</span>
	<span class="c1">// This ensures presses aren't missed.</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.key_has_been_held</span><span class="p">(</span><span class="nn">KeyCode</span><span class="p">::</span><span class="n">Space</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.jump</span> <span class="o">+=</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.mouse_has_been_held</span><span class="p">(</span><span class="nn">MouseButton</span><span class="p">::</span><span class="nb">Left</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.attack</span> <span class="o">+=</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span>
	<span class="k">if</span> <span class="n">hit</span><span class="nf">.mouse_has_been_held</span><span class="p">(</span><span class="nn">MouseButton</span><span class="p">::</span><span class="nb">Right</span><span class="p">)</span> <span class="p">{</span> <span class="n">inputs</span><span class="py">.interact</span> <span class="o">+=</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span>
	
	<span class="c1">// Mouse delta between last render and this render,</span>
	<span class="c1">// which we integrate into pitch_yaw.</span>
	<span class="k">const</span> <span class="n">PIXELS_PER_RADIAN</span><span class="p">:</span> <span class="nb">f64</span> <span class="o">=</span> <span class="mf">6000.0</span><span class="p">;</span>
	<span class="n">inputs</span><span class="py">.pitch_yaw</span> <span class="o">+=</span> <span class="n">hit</span><span class="nf">.mouse_delta</span><span class="p">()</span> <span class="o">/</span> <span class="n">PIXELS_PER_RADIAN</span><span class="p">;</span>
	
	<span class="c1">// A few touch controls to show on the screen.</span>
	<span class="c1">// These are overlaid on top of `hit` so they take over inputs</span>
	<span class="c1">// on their own part of the screen only.</span>
	<span class="k">let</span> <span class="n">on_screen_controls</span> <span class="o">=</span> <span class="p">{</span>
		<span class="c1">// Same as render_button, but fires the callback continuously.</span>
		<span class="k">let</span> <span class="n">attack</span> <span class="o">=</span> <span class="nf">render_button_has_been_held</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"touch_attack"</span><span class="p">),</span> <span class="s">"Attack"</span><span class="p">,</span> 
			<span class="p">||</span> <span class="n">inputs</span><span class="py">.attack</span> <span class="o">+=</span> <span class="mf">1.0</span>
		<span class="p">);</span>
		<span class="k">let</span> <span class="n">interact</span> <span class="o">=</span> <span class="nf">render_button_has_been_held</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"touch_interact"</span><span class="p">),</span> <span class="s">"Interact"</span><span class="p">,</span>
			<span class="p">||</span> <span class="n">inputs</span><span class="py">.interact</span> <span class="o">+=</span> <span class="mf">1.0</span>
		<span class="p">);</span>
		<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">hstack</span><span class="p">(</span><span class="o">&amp;</span><span class="p">[</span><span class="n">attack</span><span class="p">,</span> <span class="n">interact</span><span class="p">],</span> <span class="mi">4</span><span class="p">);</span> <span class="c1">// New operation; vstack, but horizontal</span>
		<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="nf">pad</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">1.0</span><span class="p">));</span>
		<span class="n">buttons</span>
	<span class="p">};</span>
	
	<span class="c1">// ... gamepad, trackpads, VR 6DOF, normalisation, etc ...</span>
	
	<span class="c1">// Send it to the engine.</span>
	<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="nf">PlayerInputs</span><span class="p">(</span><span class="n">inputs</span><span class="p">));</span>
	
	<span class="c1">// `hit` - our input receiving layer - comes before our</span>
	<span class="c1">// on screen controls, so touch controls sink inputs before</span>
	<span class="c1">// they get to our other controls.</span>
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="n">hit</span><span class="p">,</span>
		<span class="n">on_screen_controls</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, the code can look just as simple as querying inputs every frame, but now with the convenience of being trivially gated by the UI’s <code class="language-plaintext highlighter-rouge">current_view</code> state, and being able to render on-screen buttons in-line with the rest of the input logic: we get exactly our desired “on-screen” declaration in line with our input handling that we previously fantasised about under the old divorced system.</p>

<p>What’s more, these controls are rendered using a variant of our existing <code class="language-plaintext highlighter-rouge">render_button</code> that fires continuously when held, showing how non-special touch controls are under this system - almost all of the code is shared without ceremony, and we could easily reuse this for a non-touch-control UI that needs similar behaviour.</p>

<p>With that, we’ve built a fully-motivated minimal UI interaction stack that covers all the bases needed to build various kinds of game UI for a wide range of input modalities, while avoiding all the common pitfalls, and all without introducing any complex book-keeping, asynchronous primitives, or unwieldy cross-system coupling.</p>

<p>From here, it’s theoretically easy to extend to remappable inputs, or even to unique input devices like analogue keyboard switches, 6DOF tracked VR, or custom driving wheels, joysticks and even MIDI instruments - all with one mental model.</p>

<p>All input styles <em>are</em> some kind of UI - it’s just more visible with some than others.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>With that, I hope I’ve now demonstrated to you the three core principles of my new UI library, and why I selected them:</p>

<ol>
  <li>Immediate mode &amp; flip caches, and why I chose them for their lighter weight and leaner cache interactions.</li>
  <li>Operating on shapes as data, and why it’s more powerful, expressive &amp; animatable than a box model.</li>
  <li>UI as the single procedure that consumes OS events, and how I think about event timing between frames.</li>
</ol>

<p>I’m currently actively implementing and playing with these three ideas in the context of <a href="https://caveygame.com/">Cavey</a>, my casual cave spelunking game. When I have the time, I’d love to show off some cool example projects or develop these ideas further to show just how powerful a fully-composable shape-centric UI stack can be.</p>

<p>I haven’t touched on ergonomics too much in this post, instead choosing to focus on fundamentals. As an example, the function parameters in these render methods could get harder to manage over time! However, most programming languages have decent tools for managing these things, such as builder patterns or ways of doing dependency injection, but I wanted to keep this blog post series clean and focused on the core. These posts cumulatively are hitting north of 15,000 words as-is!</p>

<p>I figured I would get these out in the world instead of waiting forever. I hope some of these ideas inspire you to think about UI more deeply in the future :)</p>]]></content><author><name></name></author><summary type="html"><![CDATA[In this final blog post, we'll round out with a broader understanding of where our UI procedure sits within the wider frame loop, and how it can be extended to handle interactions from the user in order to forward semantic events to the game engine.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/the-ui-frame-loop-and-why-input-handling-is-ui/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/the-ui-frame-loop-and-why-input-handling-is-ui/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Just draw it: you don’t need a box model</title><link href="https://fluff.blog/2026/09/14/just-draw-it-you-dont-need-a-box-model.html" rel="alternate" type="text/html" title="Just draw it: you don’t need a box model" /><published>2026-09-14T00:00:00+00:00</published><updated>2026-09-14T00:00:00+00:00</updated><id>https://fluff.blog/2026/09/14/just-draw-it-you-dont-need-a-box-model</id><content type="html" xml:base="https://fluff.blog/2026/09/14/just-draw-it-you-dont-need-a-box-model.html"><![CDATA[<p><em>This is Chapter 2 of <a href="https://fluff.blog/2026/09/13/i-made-my-perfect-ui-library.html">a 3-part blog post series</a> discussing “Perfection”, a greenfield UI library I built for personal use.</em></p>

<hr />

<p>In the last blog post, we discovered an equivalence between immediate mode and reactive signal UI styles, and how one is mechanically translatable into the other. At the end of that post, I left you with a teaser for our very first UI presentation problem we would run into.</p>

<p>In this blog post, instead of running away from those problems and imposing abstractions, we’re going to plow right through the heart of the problem, and discover a very useful shape algebra along the way.</p>

<hr />
<h2 id="hey-lets-just-draw-stuff">Hey, let’s just draw stuff!</h2>

<p>So - we’re doing immediate mode. Let’s bring it back to <a href="https://www.youtube.com/watch?v=DYWTw19_8r4">that Clay introduction video</a>, and pretend like we’re beginners and just wrote a <code class="language-plaintext highlighter-rouge">render_ui()</code> function with a bunch of calls to directly draw things on the GPU, all with hardcoded coordinates.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// Render a nice background.</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">104</span><span class="p">,</span> <span class="mi">120</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	<span class="c1">// Then, a bunch of labels stacked vertically.</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"File"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"New"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Open"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">52</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Save"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">76</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Upload"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As Nic predicted in the video, we can see this isn’t going to work with all these numbers to remember and keep in sync. But instead of bailing out to a library like Clay, let’s actually make an effort to do all those calculations ourselves and see how bad it gets.</p>

<p>Since we’re just stacking things on top of each other, we could make it a <em>little</em> less impossible-to-modify by keeping a running total of our position and adding onto it. We’ll also throw in a few other variable names, too.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">item_height</span> <span class="o">=</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">104</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">120</span><span class="p">;</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"File"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"New"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Open"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Save"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Upload"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And hey, that sure is a bunch of duplication. Maybe we should use a loop.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">item_height</span> <span class="o">=</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">104</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">120</span><span class="p">;</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="nf">draw_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Okay, so the code <em>is</em> a little cleaner now. Some people, like Casey, do actually write some UIs pretty much like this - using incrementing variables and drawing as-you-go.</p>

<p>But by now, you can probably feel what Nic was saying. There’s still hardcoded stuff here, and we can’t fix that without solving some circular logic puzzles.</p>

<p>For example, it’s great that we implemented this automatic for-loop for the items, but we’ve still hardcoded the background. We <em>have</em> to draw the background before the text, so we need to know the width and height ahead of time. But <em>also</em> - we want the background to fit around the width and height of the text, which we haven’t rendered yet!</p>

<p>This circular logic happens because we have mixed up a couple of concepts in this code, which really want to separate out into different layers. If we keep pushing forward, you’ll start to see the layers unmix, a bit like oil coming out of water.</p>

<h2 id="unmixing-layout-from-rendering">Unmixing layout from rendering</h2>

<p>Here’s the first move we’ll take to unpick this tangle. We’ll take some duplicate measurements in a second loop at the start of the function so that we can calculate a sufficient menu size.</p>

<p>This doesn’t feel as nice now, because we have to lay out the text once for measuring it, then we have to lay it out again to draw it, but it <em>does</em> make the background sizing work, and it <em>does</em> mean we can base the item heights on the actual text bounds, rather than a hardcoded 16 pixels.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">laid_out_text</span> <span class="o">=</span> <span class="nf">lay_out_text</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">laid_out_text</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">laid_out_text</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">laid_out_text</span> <span class="o">=</span> <span class="nf">draw_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">laid_out_text</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To feel less bad, let’s save that perfectly good <code class="language-plaintext highlighter-rouge">lay_out_text()</code> result and pass it into <code class="language-plaintext highlighter-rouge">render_text()</code>, reusing all that work.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">laid_out_texts</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">laid_out_text</span> <span class="o">=</span> <span class="nf">lay_out_text</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">laid_out_text</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">laid_out_text</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">laid_out_texts</span><span class="nf">.push</span><span class="p">(</span><span class="n">laid_out_text</span><span class="p">);</span>
	<span class="p">}</span>
	
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">laid_out_text</span> <span class="k">in</span> <span class="n">laid_out_texts</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
		<span class="nf">draw_text</span><span class="p">(</span><span class="n">laid_out_text</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">laid_out_text</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s move the positioning logic up there as well, so positions and sizes stay together. We’ll also move the text colour up there so that text drawing becomes completely mechanical and parameter-free.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">laid_out_texts</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">laid_out_text</span> <span class="o">=</span> <span class="nf">lay_out_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">laid_out_text</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">laid_out_text</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">laid_out_texts</span><span class="nf">.push</span><span class="p">(</span><span class="n">laid_out_text</span><span class="p">);</span>
	<span class="p">}</span>
	
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	<span class="k">for</span> <span class="n">laid_out_text</span> <span class="k">in</span> <span class="n">laid_out_texts</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
		<span class="nf">draw_text</span><span class="p">(</span><span class="n">laid_out_text</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By separating out <em>layout</em> as a separate stage before rendering, you’re free to compute things in whatever order you need. The choice of what appears in-front or behind becomes orthogonal - you just change the order of the render operations at the end, not any of your logic, leaving you free to encode the order of evaluation as dependencies require you to.</p>

<h2 id="introducing-shapes">Introducing shapes</h2>

<p>Let’s now talk about the fact we’re calling draw methods at all. If you think back to Chapter 1, you’d remember our caching mechanism allows us to skip running logic on some frames.</p>

<p>However, you can’t do that for a draw instruction. If it doesn’t run, the shape doesn’t show up.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Bad! Caching stops draw instructions from running.</span>
<span class="n">ctx</span><span class="nf">.expensive</span><span class="p">(|</span><span class="n">get</span><span class="p">|</span> <span class="p">{</span>
	<span class="k">move</span> <span class="p">||</span> <span class="nf">draw_text</span><span class="p">(</span><span class="n">laid_out_text</span><span class="p">);</span>
<span class="p">})</span>
</code></pre></div></div>

<p>So, instead of doing the drawing ourselves, we’ll just return a list of shapes - same data, different delivery mechanism. We can kick out the drawing of that list to a different place.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">text_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">composition</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="n">composition</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">));</span>
	<span class="k">for</span> <span class="n">text_shape</span> <span class="k">in</span> <span class="n">text_shapes</span> <span class="p">{</span>
		<span class="n">composition</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="n">composition</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, the UI code is completely pure, and shapes are now cacheable data, so they can live inside of caches whenever they need to. That’s meaningfully more powerful, bought for very little mental cost!</p>

<p>For convenience, while we’re here and talking about mental cost, why not define a trivial <code class="language-plaintext highlighter-rouge">compose!</code> helper which does the final merge-down into a single flat list? This makes the paint ordering much simpler to see; it’ll be ordered how the arguments are ordered.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">text_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>
	
	<span class="c1">// Same logic as previously, just nicer to write.</span>
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">text_shapes</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="operating-on-shapes">Operating on shapes</h2>

<p>Let’s throw a curveball; up until now, we’ve been rendering relative to <code class="language-plaintext highlighter-rouge">0, 0</code>. That is to say, we have a top-left-aligned menu.</p>

<p>How would you centre this UI so that the background quad is nicely aligned on the screen?</p>

<p>Your first instinct may be to derive some <code class="language-plaintext highlighter-rouge">centre_x</code> and <code class="language-plaintext highlighter-rouge">centre_y</code> variables, and feed them into the positions of <code class="language-plaintext highlighter-rouge">render_text</code> and <code class="language-plaintext highlighter-rouge">Quad::solid</code>, so let’s try that and see where we end up.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="c1">// Try and calculate the top-left corner of the menu.</span>
	<span class="c1">// But this depends on data we haven't computed yet...</span>
	<span class="k">let</span> <span class="n">menu_x</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.width</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_width</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_y</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.height</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_height</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">menu_x</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_y</span> <span class="o">+</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">text_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">menu_x</span><span class="p">,</span> <span class="n">menu_y</span><span class="p">,</span> <span class="n">menu_x</span> <span class="o">+</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_y</span> <span class="o">+</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">text_shapes</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Just like last time, we’ve found another cyclic dependency. We want to position the contents of the menu relative to its top-left corner, but we don’t know where the menu’s top-left corner is until we’ve finished rendering the menu.</p>

<p>We <em>could</em> compute everything twice - after all, we know it’s possible to get the menu width. Let’s stomach that for a second and see where it gets us.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="c1">// Compute the width of the menu _first_.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="c1">// Create a throw-away shape to measure the width.</span>
		<span class="k">let</span> <span class="n">dummy_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">dummy_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">dummy_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="c1">// Now we can compute the top-left corner!</span>
	<span class="k">let</span> <span class="n">menu_x</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.width</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_width</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_y</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.height</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_height</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="c1">// Restart from the top so we can start rendering the text for real.</span>
	<span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">menu_x</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_y</span> <span class="o">+</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">menu_x</span><span class="p">,</span> <span class="n">menu_y</span><span class="p">,</span> <span class="n">menu_x</span> <span class="o">+</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_y</span> <span class="o">+</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">text_shapes</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Digesting this for a second, let’s notice we’re generating the same text shapes twice:</p>

<ol>
  <li>The first shape has its size measured, then is discarded completely.</li>
  <li>The second shape has its position set, then gets returned for drawing.</li>
</ol>

<p>This looks very similar to the move we made before; something wants to be computed earlier, and it’s trying to separate out. Specifically, the <em>text layout loop</em> is trying to separate upwards into a pre-calculation, and the <em>centring logic</em> is trying to separate downwards into a post-process.</p>

<p>This should actually make a lot of sense if you think about it! This exactly mirrors the way that most box models or layout engines work; they lay out the contents of children <em>first</em> in a position-invariant way. Once they know what the child looks like, the child can be moved to wherever it belongs in the interface. We’ve discovered this from first principles by following the dependencies.</p>

<p>So, let’s return to our original menu logic. Let’s treat this as our “child layout” process - we’ll create the entire menu relative to <code class="language-plaintext highlighter-rouge">0, 0</code>, like it’s in “menu space”. Then, we can calculate where the pre-laid-out menu needs to go, and move it there.</p>

<p>And it turns out, one of our previous moves helps us do this <em>super easily</em>. Since shapes are data instead of immediate draw calls, we can drag the shapes around however we need to, before returning them to be drawn.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="c1">// Render the menu in "menu space".</span>
	<span class="c1">// Since shapes are just data, we can safely do this</span>
	<span class="c1">// without anything showing up on-screen as a side effect.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">text_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">text_shapes</span>
	<span class="p">];</span>
	
	<span class="c1">// Work out where the top-left of the menu belongs,</span>
	<span class="c1">// then offset the shapes so the top-left lands there.</span>
	<span class="k">let</span> <span class="n">menu_x</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.width</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_width</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_y</span> <span class="o">=</span> <span class="p">(</span><span class="n">screen_bounds</span><span class="nf">.height</span><span class="p">()</span> <span class="o">-</span> <span class="n">menu_height</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nf">offset</span><span class="p">(</span><span class="n">menu_shapes</span><span class="p">,</span> <span class="n">menu_x</span><span class="p">,</span> <span class="n">menu_y</span><span class="p">);</span>
	
	<span class="n">menu_shapes</span> <span class="c1">// The final centred menu shapes are returned.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In one motion, we’ve landed on the core idea behind layout in Perfection: you can operate on UI that’s already been “rendered”. That isn’t possible in a naive renderer, because you emit a draw instruction and it’s committed to the frame right away, so computing information about something is coupled with showing it on the screen.</p>

<p>There are two very specific reasons why this motion is interesting.</p>

<h2 id="the-golden-unlock">The golden unlock</h2>

<p>Most systems - ours included - end up with ways to represent UI as a pile of atomic “building blocks”, represented in data and able to be passed around, operated upon and queried.</p>

<p>However, notice something critical: our shape “atoms” are output-side - the by-products of a layout evaluation that has already happened. Our shapes have known positions and sizes.</p>

<p>Most UI abstractions (the DOM, Clay’s box model, etc) are input-side - a raw non-evaluated description of how something <em>will</em> be laid out. HTML or Clay elements can have styling rules on them, but they require some other fixed subsystem to evaluate them <em>for us</em> before we can learn anything about them. As an example, asking for a particular DOM element’s width may force the layout engine to re-run to give you the answer.</p>

<p>What we’ve shown from first principles then, is that output-side versus input-side is actually a <em>choice you can make</em>. There is no fact of the universe forcing you to <em>describe</em> layouts in order to compute them. You can just compute them and operate on the results after the fact - <strong>layout as the middle of a computation, not the terminal of one.</strong></p>

<p>That’s the golden unlock of this system: the moment you can write layout as a plain computation, it follows all the normal rules of plain code:</p>

<ul>
  <li>It directly encodes what the computer will <em>definitely</em> do, meaning no hidden performance cliffs from a dynamic subsystem’s decision - the procedure is fixed.</li>
  <li>It can be composed together from reusable sub-procedures, meaning you’re free to build abstractions as you find convenient, or abandon them as they inconvenience you.</li>
  <li>It has no restrictions on what data or state will be accepted as input, meaning you can base layouts on <em>any</em> positions, sizes, or other metrics you have access to.</li>
  <li>It has no restrictions on what can consume it, meaning you can even perform operations on whole layouts at a time; for example, interpolating rectangles between two disjoint layouts as a way to animate across a reflow.</li>
</ul>

<p>This is the “layout and placement data flow” that I envisioned while writing <a href="https://fluff.blog/2023/03/16/ui-layout-hypotheses.html">the 2023 blog post from before.</a> It is the common core of every further single-frame layout paradigm you would want to write. Pretty neat, right?</p>

<h2 id="a-closed-algebra-of-shapes">A closed algebra of shapes</h2>

<p>Now for the second reason this is interesting; there’s something about the <em>way</em> we operated on the whole menu UI at once - moving the layout and composition into the centre - that points to a really nice structural truth about operations on shapes.</p>

<p>I’ll demonstrate by pushing the code example a little further. First things first, we can compose a new <code class="language-plaintext highlighter-rouge">align</code> utility from a bunch of the offset logic we just wrote:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text_shape</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">menu_height</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">text_shape</span><span class="nf">.width</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="n">text_shape</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">text_shapes</span>
	<span class="p">];</span>
	
	<span class="c1">// Pull out the offset/alignment logic into a slightly more</span>
	<span class="c1">// general utility. Some(0.5) means "align around the 50% mark".</span>
	<span class="c1">// We could pass Some(0.0) to start-align, Some(1.0) to end-align,</span>
	<span class="c1">// or None to leave that axis alone.</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">menu_shapes</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="n">menu_shapes</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can also now revisit our text layout logic, too. Instead of hardcoding the placement of each text shape in the list as we go, let’s try and reformulate it as a series of small operations operating on the text shapes themselves.</p>

<p>It’s a little more verbose for now, but it will be worth it:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	
	<span class="c1">// Create the text shapes.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">));</span>
	<span class="p">}</span>
	<span class="c1">// Apply the offset that comes from the text padding.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">padded_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">shape</span> <span class="k">in</span> <span class="n">text_shapes</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">padded_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="nf">offset</span><span class="p">(</span><span class="n">shape</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">));</span>
	<span class="p">}</span>
	<span class="c1">// Stack them up vertically.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">stacked_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">shape</span> <span class="k">in</span> <span class="n">padded_shapes</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">stacked_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="nf">offset</span><span class="p">(</span><span class="n">shape</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">));</span>
		<span class="n">y</span> <span class="o">+=</span> <span class="n">shape</span><span class="nf">.height</span><span class="p">()</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="c1">// Compute the size of the menu.</span>
	<span class="k">let</span> <span class="p">(</span><span class="k">mut</span> <span class="n">menu_width</span><span class="p">,</span> <span class="k">mut</span> <span class="n">menu_height</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
	<span class="k">for</span> <span class="n">shape</span> <span class="k">in</span> <span class="n">stacked_shapes</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
		<span class="n">menu_width</span> <span class="o">=</span> <span class="n">menu_width</span><span class="nf">.max</span><span class="p">(</span><span class="n">shape</span><span class="py">.max_x</span><span class="p">);</span>
		<span class="n">menu_height</span> <span class="o">=</span> <span class="n">menu_height</span><span class="nf">.max</span><span class="p">(</span><span class="n">shape</span><span class="py">.max_y</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="n">menu_width</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
	<span class="n">menu_height</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
	
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">stacked_shapes</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">menu_shapes</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="n">menu_shapes</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice a few related facts about the direction our code has been heading up to this point:</p>

<ul>
  <li>Our existing <code class="language-plaintext highlighter-rouge">align()</code> and <code class="language-plaintext highlighter-rouge">offset()</code> usages, in addition to the <code class="language-plaintext highlighter-rouge">padded_shapes</code> case we’ve just uncovered, are all use cases for applying single-shape operations to whole <strong>groups of shapes</strong> all at once.</li>
  <li>The <code class="language-plaintext highlighter-rouge">stacked_shapes</code> code looks like the perfect candidate for a specialised <code class="language-plaintext highlighter-rouge">vstack()</code> abstraction, one that stacks <strong>nested groups of shapes</strong> on top of each other.</li>
  <li>The <code class="language-plaintext highlighter-rouge">menu_width</code> / <code class="language-plaintext highlighter-rouge">menu_height</code> code is begging for a <code class="language-plaintext highlighter-rouge">bounds_of()</code> utility to synthesise what the bounding box of a <strong>group of shapes</strong> should be.</li>
</ul>

<p>The latent fact we’re hitting on here is that - at least conceptually - a group of shapes is <em>itself</em> a shape. Our <code class="language-plaintext highlighter-rouge">compose!</code> abstraction already leans this way; passing in lists to <code class="language-plaintext highlighter-rouge">compose!</code> is not just a convenience, but a reflection that our shape algebra wants to be <em>closed</em> and <em>easily composable</em>.</p>

<p>Being specific, groups of shapes satisfy a few key criteria:</p>

<ul>
  <li>A group is a pure composition of other sub-shapes, meaning all of its contents can be uniformly operated on as shapes. (This is why you can <code class="language-plaintext highlighter-rouge">compose!</code> them, for example).</li>
  <li>You can draw useful* bounds around the group, meaning the group has a single source of truth for how it’s measured and how it takes part in layout. (I’ll get back to that asterisk.)</li>
</ul>

<p>So, just like we can manipulate shapes after they’re drawn, we should be able to manipulate groups of shapes too. That’s the formal motivation for adding <code class="language-plaintext highlighter-rouge">offset()</code>, <code class="language-plaintext highlighter-rouge">vstack()</code> and <code class="language-plaintext highlighter-rouge">bounds_of()</code> to simplify this code.</p>

<p>Let’s implement those now.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	
	<span class="c1">// Create the text shapes.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="c1">// No need for text to be positioned if we can assume</span>
		<span class="c1">// it is rendered at 0, 0 - it can be implied</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">));</span>
	<span class="p">}</span>
	<span class="c1">// Apply the offset that comes from the text padding.</span>
	<span class="k">let</span> <span class="n">padded_shapes</span> <span class="o">=</span> <span class="nf">offset</span><span class="p">(</span><span class="n">text_shapes</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">);</span>
	<span class="c1">// Stack them up vertically.</span>
	<span class="k">let</span> <span class="n">stacked_shapes</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">padded_shapes</span><span class="p">,</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
	<span class="c1">// Compute the size of the menu.</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_bounds</span> <span class="o">=</span> <span class="nf">bounds_of</span><span class="p">(</span><span class="n">stacked_shapes</span><span class="p">);</span>
	<span class="n">menu_bounds</span><span class="py">.min_x</span> <span class="o">-=</span> <span class="n">text_pad</span><span class="p">;</span>
	<span class="n">menu_bounds</span><span class="py">.min_y</span> <span class="o">-=</span> <span class="n">text_pad</span><span class="p">;</span>
	<span class="n">menu_bounds</span><span class="py">.max_x</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
	<span class="n">menu_bounds</span><span class="py">.max_y</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
	
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="c1">// Construct our background directly from the menu's size.</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">menu_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">stacked_shapes</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">menu_shapes</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="n">menu_shapes</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Greatly simplified! We’re almost at a point where you could call this code “clean”.</p>

<p>Thanks to that golden unlock, we’ve built a straightforward higher-order abstraction where you can compose new shapes by combining others, and where those groups are nouns that behave just like any primitive shape would.</p>

<p>When combined with our immediate mode caching scheme (which is compatible with this whole algebra), and when combined with <code class="language-plaintext highlighter-rouge">compose!</code> (which can flatten everything down to a single ordered list to be drawn), you end up with a combinatorially rich and uniquely flexible model of UI. If you need to do a specific manoeuvre, no matter where you are or what you’re working on, you can just do it.</p>

<p>Everything is data, everything is computation, everything is composition.</p>

<h2 id="now-about-that-asterisk">Now, about that asterisk…</h2>

<p>I promised you I’d get back to it before we’re done. Remember this assertion about groups of shapes?</p>

<blockquote>
  <p>You can draw useful* bounds around the group</p>
</blockquote>

<p>If you read <a href="https://fluff.blog/2024/07/24/understanding-roblox-typography.html">my 2024 post about typographic bounding boxes</a>, you might have detected where things were going. The slightly messy truth is that <em>there isn’t one single “useful” way to draw bounds around a group!</em></p>

<p>Consider the example of a play button in a video preview UI. If you’re thinking like a programmer, you might take the vertices of the triangle, find the rectangle that tightly fits around all three vertices, and call it a day. Bounding box achieved, job done, time to pass it onwards to the layout functions.</p>

<p>Your designer will wince. For <em>those</em> shapes, you generally want to use a <em>bounding circle</em> as your alignment guide, because it will look more <em>optically aligned</em> than the mathematical bounding box will. (Ask your friendly neighbourhood designer about it.)</p>

<p>So, maybe you would actually draw a slightly larger, <em>mathematically off-centre</em> bounding box for it to ensure that the bounding circle is neatly aligned with the rest of the UI, even if the vertices have slightly biased x-coordinates.</p>

<p>But that’s the <em>easy</em> case. The hard case - as I alluded to - is typography. In that case, your bounding boxes might not even bound the painted “ink” of the shapes at all. It’s good typographic practice to align lines of text using various <em>baselines</em>; most commonly, the alphabetic baseline, the x-height baseline, or the cap-height baseline. (Though, alternatives are frequently used for Devanagari or ideographic text.)</p>

<p>Oftentimes, the bounding box formed between those baselines is <em>substantially smaller</em> than the quads you’re drawing to show the glyphs, and it doesn’t even necessarily relate to anything geometric or mathematical <em>at all</em> - it’s visually tuned by a font designer in a studio by hand. Disrespecting that bounding box could mean things like a centred text label shifting vertically whenever a “p” or “b” shows up in the text.</p>

<p>This all points towards a surprisingly rarely implemented idea: you need the latitude to decouple rendered geometry <strong>completely</strong> from the bounding boxes used for layout. Whether it’s for optically aligning non-rectangular shapes, or for keeping Latin and Devanagari spans of text aligned with each other, there are many, <em>many</em> occurrences of the two perspectives not agreeing.</p>

<p>Unfortunately, most UI systems today pretty much just special case text layout, forcing you into the framework’s opinion on how text should be aligned (usually relative to the em box, which has no guaranteed relation to anything useful), and leaving you high and dry for anything requiring optical alignment.</p>

<p>In our system… well, text is just a group of shapes. So, <code class="language-plaintext highlighter-rouge">render_text()</code> can just return a group of laid-out glyphs or rasterised quads, and <em>declare</em> where the typographically useful bounds are, enhancing the appearance of text everywhere throughout our UI system practically for free - no special case needed.</p>

<p>In Perfection, that’s done by calling <code class="language-plaintext highlighter-rouge">.bounded_by()</code> on a shape (or composition of shapes), which attaches a bounding box that’s independent of any rendered content:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
	<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">menu_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
	<span class="n">stacked_shapes</span>
<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">menu_bounds</span><span class="p">);</span> <span class="c1">// Align these shapes using the menu's outer bounds.</span>
</code></pre></div></div>

<p>There’s another benefit to decoupling the layout bounds from the rendered shapes: you can use it for <em>more</em> than just optical alignments. Did you notice all that <code class="language-plaintext highlighter-rouge">text_pad</code> in our code?</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Pretend that our text has a padded bounding box.</span>
<span class="k">let</span> <span class="n">padded_shapes</span> <span class="o">=</span> <span class="nf">offset</span><span class="p">(</span><span class="n">text_shapes</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">);</span>
<span class="c1">// Stack them up vertically, manually maintaining the illusion</span>
<span class="c1">// that the bounding boxes are padded by giving double space.</span>
<span class="k">let</span> <span class="n">stacked_shapes</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">padded_shapes</span><span class="p">,</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">);</span>
<span class="c1">// Manually add the padding to the menu bounds so that the</span>
<span class="c1">// quad incorporates it.</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_bounds</span> <span class="o">=</span> <span class="nf">bounds_of</span><span class="p">(</span><span class="n">stacked_shapes</span><span class="p">);</span>
<span class="n">menu_bounds</span><span class="py">.min_x</span> <span class="o">-=</span> <span class="n">text_pad</span><span class="p">;</span>
<span class="n">menu_bounds</span><span class="py">.min_y</span> <span class="o">-=</span> <span class="n">text_pad</span><span class="p">;</span>
<span class="n">menu_bounds</span><span class="py">.max_x</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
<span class="n">menu_bounds</span><span class="py">.max_y</span> <span class="o">+=</span> <span class="n">text_pad</span><span class="p">;</span>
</code></pre></div></div>

<p>Instead of pretending to pad the bounding box of the text… we can recognise that text is a group of shapes, which has a bounding box attached. So, you can just <em>pad the bounds</em>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">text_shapes</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="c1">// Pad out the text's bounding box, so the group</span>
		<span class="c1">// builds in space for the text to sit in.</span>
		<span class="n">text_shapes</span><span class="nf">.push</span><span class="p">(</span><span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">));</span>
	<span class="p">}</span>
	<span class="c1">// Now the text can be stacked directly.</span>
	<span class="k">let</span> <span class="n">stacked_shapes</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">text_shapes</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
	<span class="c1">// And the menu bounds can be used unmodified!</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">menu_bounds</span> <span class="o">=</span> <span class="nf">bounds_of</span><span class="p">(</span><span class="n">stacked_shapes</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">menu_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">stacked_shapes</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">menu_bounds</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">menu_shapes</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">menu_shapes</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="n">menu_shapes</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Bingo. Text padding has a single source of truth, and the rest of the function is better for it: a whole <code class="language-plaintext highlighter-rouge">offset()</code> loop skipped, a more semantically truthful <code class="language-plaintext highlighter-rouge">vstack()</code>, and a dead simple <code class="language-plaintext highlighter-rouge">bounds_of()</code> query for the menu background.</p>

<p>With that, we’ve arrived at the end state of our whole journey through presentation.</p>

<p>Let’s look back at where we came from near the start of the chapter. If you were drawing UI as you went - like we discussed earlier - this is about as far as you might get while holding to a realistic standard of code quality:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui_old</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">item_height</span> <span class="o">=</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">104</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">120</span><span class="p">;</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="nf">draw_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But, with everything we’ve built up, we can beat it on length while making it semantically even <em>more</em> powerful. Thanks to our efforts, we have a concise, consistent vocabulary for expressing our intended layout moves. Plus, our move to make everything into plain computations on data means we get to use Rust’s powerful iterators to go fully declarative. And hey - we’re not even hardcoding menu dimensions anymore, and it’s trivial to re-align if we so choose.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui_new</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">items</span> <span class="o">=</span> <span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span><span class="nf">.into_iter</span><span class="p">()</span>
		<span class="nf">.map</span><span class="p">(|</span><span class="n">item</span><span class="p">|</span> <span class="nf">pad</span><span class="p">(</span><span class="nf">render_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span> <span class="mi">4</span><span class="p">))</span>
		<span class="nf">.collect_vec</span><span class="p">();</span>
	<span class="k">let</span> <span class="n">items</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">items</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">bounds</span> <span class="o">=</span> <span class="nf">bounds_of</span><span class="p">(</span><span class="n">items</span><span class="p">);</span>
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">),</span>
		<span class="n">items</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>How’s that for a semantically rich yet syntax-efficient system? :)</p>

<hr />

<h2 id="a-teaser-for-interaction-handling">A teaser for interaction handling</h2>

<p>Now that we’re presenting the user with shapes on the screen, the next natural step is to get responses <em>back</em> from the user and make the UI interactive.</p>

<p>To start, let’s whip up a button with our freshly discovered presentation primitives. The button will have different colours when it’s being hovered or pressed down.</p>

<p>The code should be readable to you by now:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// We'll wire this up in a second.</span>
	<span class="k">let</span> <span class="n">state</span> <span class="o">=</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span><span class="p">;</span>
	
	<span class="c1">// Using the hover/press state, decide on button colours.</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="c1">// Emit the button, ready to be included elsewhere.</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s start by trying to make these hover effects work. We’ll accept a <code class="language-plaintext highlighter-rouge">mouse_pressed: bool</code> directly, and let’s assume we have access to a <code class="language-plaintext highlighter-rouge">mouse_position: Vec2</code> argument, to compute whether the mouse is in bounds for hovering.</p>

<p>This requires a little bit of rearrangement, but you can in theory make it work. Here’s what the rearrangement looks like:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">mouse_position</span><span class="p">:</span> <span class="n">Vec2</span><span class="p">,</span>
	<span class="n">mouse_pressed</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// Render the text first so we can measure the button bounds</span>
	<span class="c1">// for hit testing. Punt the text colour; it has separate dependencies.</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>

	<span class="c1">// Don't respond to the cursor if it's somewhere else.</span>
	<span class="c1">// Otherwise, set the state based on what the mouse is doing.</span>
	<span class="k">let</span> <span class="n">state</span> <span class="o">=</span> 
		<span class="k">if</span> <span class="o">!</span><span class="n">button_bounds</span><span class="nf">.contains</span><span class="p">(</span><span class="n">mouse_position</span><span class="p">)</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="p">}</span>
		<span class="k">else</span> <span class="k">if</span> <span class="n">mouse_pressed</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="p">}</span>
		<span class="k">else</span> <span class="p">{</span> <span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="p">};</span>

	<span class="c1">// This is where we punted the colour computation to.</span>
	<span class="c1">// We now compute it separately from text layout.</span>
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span> <span class="c1">// New operation!</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Okay, so this <em>seems</em> reasonable, except… what mouse position do you pass to this function?</p>

<p>Remember - back when we were dealing with layout, we posited that the button’s layout doesn’t depend on where it’s positioned. That meant that we intentionally constructed the button outside of the normal coordinate system, with the understanding that the button would be moved into place by the caller at some point.</p>

<p>That means the caller would need to somehow pass in a mouse position in this button’s own coordinate system. But the caller won’t even know where that bounding box ends up <em>itself</em> - the final position of things isn’t fixed until the end of <code class="language-plaintext highlighter-rouge">render_ui()</code> as a whole.</p>

<p>Just like in Chapter 2, we have a cyclic dependency - except this time, the way the component renders is cyclically dependent on <em>the whole render process</em>.</p>

<p>Join me in <a href="https://fluff.blog/2026/09/15/the-ui-frame-loop-and-why-input-handling-is-ui.html">part 3 of this blog post series</a>, and we’ll start unpicking exactly how our UI system <em>ought to</em> handle inputs, and draw some surprising parallels to game engine systems you might already have built yourself.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Instead of running away from presentation problems and imposing abstractions, we're going to plow right through the heart of the them, and discover a very useful shape algebra along the way.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/just-draw-it-you-dont-need-a-box-model/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/just-draw-it-you-dont-need-a-box-model/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">I made my perfect UI library</title><link href="https://fluff.blog/2026/09/13/i-made-my-perfect-ui-library.html" rel="alternate" type="text/html" title="I made my perfect UI library" /><published>2026-09-13T00:00:00+00:00</published><updated>2026-09-13T00:00:00+00:00</updated><id>https://fluff.blog/2026/09/13/i-made-my-perfect-ui-library</id><content type="html" xml:base="https://fluff.blog/2026/09/13/i-made-my-perfect-ui-library.html"><![CDATA[<p><em>This is Chapter 1 of a 3-part blog post series discussing “Perfection”, a greenfield UI library I built for personal use.</em></p>

<hr />

<p>For the past decade, I’ve been fighting against <em>clever</em> UI frameworks. Ones that try and be helpful, provide features and conveniences, build in all the features you need. That is to say, ones that tie your hands behind your back the moment you escape their mental model.</p>

<p>Perhaps it’s the panel you want to animate from one part of the screen to the other. That “dead-simple” layout engine can’t handle it because it’s a change of parent.</p>

<p>Perhaps it’s the button you <em>just</em> want to place a few pixels over. The “effortless” box model complains because that space was taken by another box.</p>

<p>Or perhaps, splitting a dynamically sized container into fractions. Or, computing multiple layouts and picking them based on your own fitness function. Or, tweening to an element’s automatic size. <em>Or, like, just properly typographically centring text in a button - shouldn’t that be simple?</em> I could go on and on and on.</p>

<p>So all I ask for, is a thin, unclever, unopinionated, first-principles library that can build <em>whatever</em> I want, and <em>doesn’t fight me back</em>.</p>

<p>I built that library for myself, and I’ll show you here how it’s put together.</p>

<hr />

<h2 id="the-original-thesis">The original thesis</h2>

<p>You know what they say: perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. So the basis for my new UI library, “Perfection”, revolved around a reductive core philosophy: <strong>everything is data, everything is computation, everything is composition.</strong></p>

<p>The main UI entry point would return a single list of quads. That list of quads would be rendered first element first, last element last - literal Painter’s algorithm. Everything between your application state and what textured quads show up on the screen would be a pipeline you control. Freedom from the tyranny of conveniences at long last.</p>

<p>Want to render a string of text? You call into a function which transforms the string into textured, laid-out quads. Want to stack those strings of text? Pass those rendered quads into a function which puts each group of quads on top of the other. Centre align everything? Pass the whole assembly of quads into a function that moves all of them to the centre of the screen’s bounding box.</p>

<p>Everything in the UI code would have the same shape; every primitive would be a transformation from input data to output data. Layout reuse and parameterisation looks exactly the same as UI reuse and parameterisation.</p>

<p>Most importantly, it would satisfy <a href="https://fluff.blog/2023/03/16/ui-layout-hypotheses.html">some constraints I first articulated a few years back</a>: it’d be a unidirectional data flow from state, through to layout, through to deciding where quads were placed, and finally through to rendering - no feedback loops, and no forced hierarchies or hardcoded layout inputs like you’d get with a box model.</p>

<p>But it sounds insane, right? For the past three decades, we adopted DOMs, stylesheets, box models and flex layouts for a reason, right? After all, I remembered watching the <a href="https://www.youtube.com/watch?v=DYWTw19_8r4">introduction video for Clay</a> - a popular UI library. It literally starts with examples of how emitting plain draw instructions falls apart quickly.</p>

<p><img src="/assets/posts/i-made-my-perfect-ui-library/clay.jpg" alt="The Clay presentation showing draw instruction bugs." /></p>

<p>This introduction mentions you’d have to adjust everything by hand whenever you input any new data or needed to fit new children - it’d be a bunch of micromanagement for limited gain. But the jump to “flexible boxes” felt ill-motivated; instead of trying some of the fixes, it felt like a post-hoc justification for a particular mental model.</p>

<p>Furthermore, people like Casey Muratori - whatever you think of them - can seemingly produce whole game UIs in side-effectful per-frame renderer code without even creating any UI objects or layout structures.</p>

<p>It made me wonder if most people had <em>truly, seriously</em> tried to proceed without jumping straight for objectful UI frameworks. Perhaps most of us just assumed that all these indirections were doing us some service, and that they were an established natural solution.</p>

<p>I had a suspicion they weren’t, so I threw them out.</p>

<p>And you know what? After spending a few days prototyping it out roughly, I got to a point where I could write code every bit as ergonomic as modern reactive code, with the same compositional beauty and straightforward reasoning about state. It fully satisfies the type checker, it isn’t coloured by any particularly crazy syntax, and there’s no large data structures being constructed anywhere.</p>

<p>Here’s how it looks in practice. Unlike a box model, where every part of the UI is organised into parent/child relationships, a UI built in Perfection is instead a <em>data flow</em>, where a DAG is formed by dependencies of operations, not modelled as an explicit hierarchy concept.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">label</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
	<span class="n">on_activate</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">FnOnce</span><span class="p">()</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">render_text</span><span class="p">(</span><span class="n">label</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">button_bounds</span> <span class="o">=</span> <span class="nf">pad</span><span class="p">(</span><span class="n">text</span><span class="nf">.bounds</span><span class="p">(),</span> <span class="mi">4</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="n">region</span> <span class="o">=</span> <span class="nf">hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"region"</span><span class="p">),</span> <span class="n">button_bounds</span><span class="p">);</span>
	<span class="k">if</span> <span class="n">region</span><span class="py">.was_clicked</span> <span class="p">{</span> <span class="nf">on_activate</span><span class="p">();</span> <span class="p">}</span>
	
	<span class="k">let</span> <span class="p">(</span><span class="n">bg_colour</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">)</span> <span class="o">=</span> <span class="k">match</span> <span class="n">region</span><span class="py">.state</span> <span class="p">{</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Rest</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Hover</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">YELLOW</span><span class="p">),</span>
		<span class="nn">PointerState</span><span class="p">::</span><span class="n">Press</span> <span class="k">=&gt;</span> <span class="p">(</span><span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="p">)</span>
	<span class="p">};</span>
	<span class="k">let</span> <span class="n">text</span> <span class="o">=</span> <span class="nf">recolour</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">fg_colour</span><span class="p">);</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">,</span> <span class="n">bg_colour</span><span class="p">),</span>
		<span class="n">region</span><span class="p">,</span>
		<span class="n">text</span>
	<span class="p">]</span><span class="nf">.bounded_by</span><span class="p">(</span><span class="n">button_bounds</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span>
	<span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">,</span>
	<span class="n">screen_bounds</span><span class="p">:</span> <span class="n">PxRect</span><span class="p">,</span>
	<span class="n">current_view</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">set_next_view</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="nb">Option</span><span class="o">&lt;</span><span class="n">Views</span><span class="o">&gt;</span><span class="p">),</span>
	<span class="n">game_state</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">GameState</span><span class="p">,</span>
	<span class="n">queue_action</span><span class="p">:</span> <span class="k">impl</span> <span class="nf">Fn</span><span class="p">(</span><span class="n">Action</span><span class="p">)</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="k">impl</span> <span class="n">Paint</span> <span class="p">{</span>
	<span class="c1">// The buttons can be directly composed, and no state</span>
	<span class="c1">// needs to be shuttled around at all.</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="o">&amp;</span><span class="p">[</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"resume"</span><span class="p">),</span> <span class="s">"Resume the game"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nb">None</span><span class="p">);</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="nf">Idle</span><span class="p">(</span><span class="k">false</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"achieve"</span><span class="p">),</span> <span class="s">"View achievements"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Achievements</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"options"</span><span class="p">),</span> <span class="s">"Game options"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">set_next_view</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Views</span><span class="p">::</span><span class="n">Options</span><span class="p">));</span>
		<span class="p">}),</span>
		<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"quit"</span><span class="p">),</span> <span class="s">"Quit to desktop"</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
			<span class="nf">queue_action</span><span class="p">(</span><span class="nn">Action</span><span class="p">::</span><span class="n">ExitGame</span><span class="p">);</span>
		<span class="p">})</span>
	<span class="p">];</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">vstack</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">buttons</span> <span class="o">=</span> <span class="nf">align</span><span class="p">(</span><span class="n">buttons</span><span class="p">,</span> <span class="n">screen_bounds</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">),</span> <span class="nf">Some</span><span class="p">(</span><span class="mf">0.5</span><span class="p">));</span>
	
	<span class="nd">compose!</span> <span class="p">[</span>
		<span class="nn">Quad</span><span class="p">::</span><span class="nf">solid</span><span class="p">(</span><span class="n">screen_bounds</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">BLACK</span><span class="nf">.opacity</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)),</span>
		<span class="n">buttons</span>
	<span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is my perfect UI library. It outsources almost everything to abstractions above it. There’s nothing left to take away.</p>

<p>Over the next three blog posts, I’m going to explore how I got there, justifying these three key pillars:</p>

<ul>
  <li><strong>Part 1 (this post):</strong> Using immediate mode to model an ultralight, object-free reactive cache invalidation scheme.</li>
  <li><strong>Part 2:</strong> “Just draw it” - a composable shape algebra that’s simpler &amp; more powerful than a strict UI hierarchy.</li>
  <li><strong>Part 3:</strong> Unifying all OS event handling through the UI layer, emitting semantic inputs to the game engine.</li>
</ul>

<hr />

<h2 id="but-wait-you-cant-do-all-that-every-frame">But wait, you can’t do all that every frame!</h2>

<p>It’s the most common argument against immediate-mode UI; that the performance is irredeemable. It’s only suitable for game engines, it must magically drain battery more, it’s some renderer hack, and all that. If you’re building “real UI”, you just need a traditional retained-mode UI framework, like a reactive signals library. At least, people say.</p>

<p>Let’s test that theory. First, let’s go over how each approach can cache data under the hood, because <em>both</em> architectures can be made to stably retain information over multiple frames.</p>

<p>Reactive signals are simple enough; they’re objects that are constructed once when the UI function is run, and then continue existing to respond to events thereafter. The library can store caches and book-keeping internally on the object:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="c1">-- How Fusion constructs a simple reactive signal object.</span>
<span class="c1">-- Slightly simplified. The object stores everything.</span>
<span class="kd">local</span> <span class="k">function</span> <span class="nf">Value</span><span class="p">(</span><span class="n">initialValue</span><span class="p">)</span>
	<span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="nb">setmetatable</span><span class="p">(</span>
		<span class="p">{</span>
			<span class="n">createdAt</span> <span class="o">=</span> <span class="nb">os.clock</span><span class="p">(),</span>
			<span class="n">dependentSet</span> <span class="o">=</span> <span class="p">{},</span>
			<span class="n">lastChange</span> <span class="o">=</span> <span class="nb">os.clock</span><span class="p">(),</span>
			<span class="n">validity</span> <span class="o">=</span> <span class="s2">"valid"</span><span class="p">,</span>
			<span class="n">value</span> <span class="o">=</span> <span class="n">initialValue</span>
		<span class="p">},</span> 
		<span class="n">METATABLE</span>
	<span class="p">)</span>
	<span class="k">return</span> <span class="n">self</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That’s all there is to that. As long as the object stays live (foreshadowing), the state can be retained.</p>

<p>On the other hand, in immediate mode UI, the UI code runs all the time, so it’s not a stable place to store or remember things. The UI function itself can’t retain data on its own in this context.</p>

<p>Instead, the main loop of the program manually carries over a “context” object from one frame to the next, as the explicit owner of multi-frame state.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">let</span> <span class="n">ctx</span> <span class="o">=</span> <span class="nn">UiContext</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>

<span class="k">loop</span> <span class="p">{</span>
	<span class="n">ctx</span><span class="nf">.next_frame</span><span class="p">();</span>
	<span class="nf">render_ui</span><span class="p">(</span><span class="n">ctx</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then, each frame, you can refer to stable named storage locations within that context object. In the case of my library, Perfection, that’s done by building up a “path” to the location using <code class="language-plaintext highlighter-rouge">ctx.key()</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// We use .key() to name the storage location first.</span>
	<span class="c1">// Then, we can do things at that location.</span>
	<span class="k">let</span> <span class="n">my_number</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"my_thing"</span><span class="p">)</span><span class="nf">.once</span><span class="p">(||</span> <span class="p">{</span>
		<span class="nd">print!</span><span class="p">(</span><span class="s">"I only run once!"</span><span class="p">);</span>
		<span class="k">return</span> <span class="mi">42</span><span class="p">;</span>
	<span class="p">});</span>
	
	<span class="nd">dbg!</span><span class="p">(</span><span class="n">my_number</span><span class="p">)</span> <span class="c1">// prints: 42</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.key()</code> actually returns a new “nested” <code class="language-plaintext highlighter-rouge">UiContext</code>, so you can give each sub-function its own clean named <code class="language-plaintext highlighter-rouge">UiContext</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">fn</span> <span class="nf">render_pause_menu</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// path: /option_resume/</span>
	<span class="nf">render_option</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"option_resume"</span><span class="p">));</span>
	<span class="nf">render_option</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"option_exit"</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_option</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// path: /option_resume/button/</span>
	<span class="nf">render_button</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"button"</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">render_button</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// path: /option_resume/button/hit_region/</span>
	<span class="nf">render_hit_region</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"hit_region"</span><span class="p">));</span> 
<span class="p">}</span>
</code></pre></div></div>

<p>Internally in the <code class="language-plaintext highlighter-rouge">UiContext</code>, you have two maps from keys to stored values. On frame N, you can write these named locations into one of the maps. Then, on frame N + 1, you flip the maps over - now you can refer to last frame’s map while building up next frame’s map, copying over anything you want to retain, and leaving out what you want to purge.</p>

<p>These are what I call “flip caches”. <a href="https://fluff.blog/2026/01/07/flip-caches-and-information-loss.html">I discuss them more in my January post on them</a>, as well as some extra background on a concept called “information loss” which motivates the choice of reactive signals and immediate mode as the starting points here. (It’s not central to anything in this post, happily.)</p>

<p>This is similar to what projects like React have done in the past, except React often leans on implicit keys based on call order; if you know about the “Rules of Hooks”, that’s why. Perfection is the same idea without the overcomplication the implicit rules provide: if it needs to be remembered, it deserves a key - no attempt to hide it.</p>

<p>So cool, each approach has a way of caching state if it needs to. That’s important to establish; immediate mode UI does not <em>have</em> to throw everything away every frame!</p>

<h2 id="two-sides-of-one-coin">Two sides of one coin</h2>

<p>So, back to this claim:</p>

<blockquote>
  <p>If you’re building “real UI”, you just need something retained, like a reactive signals library.</p>
</blockquote>

<p>Having built both a reactive signal library and an immediate mode one, <strong>I disagree.</strong> Both are perfectly acceptable and on even footing with each other when you’re not strawmanning one or the other.</p>

<p>More foundationally, both approaches are two sides of the exact same coin, assuming both approaches are implemented properly and professionally. Both approaches try to treat UI as a function of state, and in fact, both try and keep your logic stateless, divorced of history. One is mechanically translatable to the other, and the distinction is entirely in the caching approach.</p>

<p>Let’s expose this fact by writing the same example code snippet in both.</p>

<p>Every derived reactive signal you write contains little islands of immediate-mode logic. The computation is always stateless and you expect it to always hold true. The framework just chooses to skip the computation when the inputs haven’t changed.</p>

<p>We do this because it makes it easy to reason about the chain of events that lead to a final value, without having to hold the whole history of the UI and all the state transitions in your head.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="kd">local</span> <span class="n">itemName</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="s2">"Carrots"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">itemPrice</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="mi">150</span><span class="p">)</span>

<span class="kd">local</span> <span class="n">message</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Computed</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="c1">-- Collect inputs...</span>
	<span class="kd">local</span> <span class="n">name</span> <span class="o">=</span> <span class="n">use</span><span class="p">(</span><span class="n">itemName</span><span class="p">)</span>
	<span class="kd">local</span> <span class="n">price</span> <span class="o">=</span> <span class="n">use</span><span class="p">(</span><span class="n">itemPrice</span><span class="p">)</span>
	<span class="c1">-- Run this when the inputs notify you of a change.</span>
	<span class="k">return</span> <span class="s2">"Buy "</span> <span class="o">..</span> <span class="n">name</span> <span class="o">..</span> <span class="s2">" for "</span> <span class="o">..</span> <span class="n">price</span> <span class="o">..</span> <span class="s2">" coins"</span>
<span class="k">end</span><span class="p">)</span>

<span class="nb">print</span><span class="p">(</span><span class="n">Fusion</span><span class="p">.</span><span class="n">peek</span><span class="p">(</span><span class="n">message</span><span class="p">))</span> <span class="c1">-- prints: Buy Carrots for 150 coins</span>
</code></pre></div></div>

<p>The way these reactive frameworks operate under the hood, is that they wire up objects to each other as you <code class="language-plaintext highlighter-rouge">use()</code> them. Those pathways are used to communicate change events, which are the things that cause the computation callback to re-run. As a result, you can avoid running the computation any more frequently than that.</p>

<p>As it turns out, an immediate-mode library can express exactly the same kind of cached computation. Look, you can even pattern-match it with your eyes.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">let</span> <span class="n">item_name</span> <span class="o">=</span> <span class="s">"Carrots"</span><span class="p">;</span>
<span class="k">let</span> <span class="n">item_price</span> <span class="o">=</span> <span class="mi">150</span><span class="p">;</span>

<span class="c1">// This is a utility function that makes caching</span>
<span class="c1">// nice and pleasant to write for us, just like Fusion.</span>
<span class="k">let</span> <span class="n">message</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.expensive</span><span class="p">(|</span><span class="n">get</span><span class="p">|</span> <span class="p">{</span>
	<span class="c1">// Collect inputs...</span>
	<span class="k">let</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">item_name</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">price</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">item_price</span><span class="p">);</span>
	<span class="c1">// Run this when the inputs appear to have changed.</span>
	<span class="k">move</span> <span class="p">|</span><span class="n">ctx</span><span class="p">|</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"Buy {name} for {price} coins"</span><span class="p">)</span>
<span class="p">});</span>

<span class="nd">dbg!</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="c1">// prints: Buy Carrots for 150 coins</span>
</code></pre></div></div>

<p>There are a few nuances worth digging into here, including a few things that will probably surprise the uninitiated - the first of which being that the inputs (<code class="language-plaintext highlighter-rouge">item_name</code> and <code class="language-plaintext highlighter-rouge">item_price</code>) are <em>not</em> objects, but are just plain data.</p>

<p>The reason why reactive frameworks wrap everything in objects, is to form that change-notification network we discussed before - a “reactive graph”. That’s how you know when to re-run the expensive computation.</p>

<p>Perfection doesn’t do this. Instead, it splits expensive computations into two halves; the first callback is called every frame to collect inputs with <code class="language-plaintext highlighter-rouge">get()</code> (allowing for dynamic/conditional gets), then the expensive part is returned as a <em>second</em> callback which may or may not be executed based on what you got.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Expanded version of the above code snippet.</span>
<span class="c1">// You wouldn't write it this way for realsies,</span>
<span class="c1">// but it's useful for explanation.</span>

<span class="c1">// The callback for the "cheap" part.</span>
<span class="c1">// Runs every time the UI is evaluated.</span>
<span class="k">let</span> <span class="n">cheap_part</span> <span class="o">=</span> <span class="p">|</span><span class="n">get</span><span class="p">|</span> <span class="p">{</span>
	<span class="c1">// Collecting all the inputs is super cheap.</span>
	<span class="c1">// If we get the same number of inputs, and they have</span>
	<span class="c1">// the same values, then assuming our expensive</span>
	<span class="c1">// computation is pure and deterministic, it must be</span>
	<span class="c1">// identical too.</span>
	<span class="k">let</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">item_name</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">price</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">item_price</span><span class="p">);</span>
	
	<span class="c1">// The callback for the "expensive" part.</span>
	<span class="c1">// If all the get() stuff is equal, we don't run it,</span>
	<span class="c1">// and instead fetch the last result from memory.</span>
	<span class="k">let</span> <span class="n">expensive_part</span> <span class="o">=</span> <span class="k">move</span> <span class="p">|</span><span class="n">ctx</span><span class="p">|</span> <span class="p">{</span>
		<span class="nd">format!</span><span class="p">(</span><span class="s">"Buy {name} for {price} coins"</span><span class="p">)</span>
	<span class="p">};</span>
	
	<span class="n">expensive_part</span> <span class="c1">// Always returned.</span>
<span class="p">};</span>

<span class="k">let</span> <span class="n">message</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.expensive</span><span class="p">(</span><span class="n">cheap_part</span><span class="p">);</span>
</code></pre></div></div>

<p>Rust makes this very cheap to do, and presumably you’d only be caching things that are so expensive that any overhead from this split would be atomically small in comparison. You save the overhead from not doing object allocations, too.</p>

<p>So yeah, you don’t need a reactive graph or a bunch of objects in order to get something that feels <em>exactly like</em> lazy reactive signals. Immediate mode and reactive signals are implemented differently, but capable of expressing the exact same concepts.</p>

<p>I would actually argue on syntax alone that immediate mode makes these ideas much nicer to express, since it solves the biggest problems of reactive signals: since there are no objects, there’s no lifetime wrangling, and reactivity isn’t a viral annotation that spreads through your codebase like wildfire - it’s all plain inputs and outputs, no object wrappers. It’s why the example code I showed earlier looks so clean.</p>

<p>There’s a more subtle and under-discussed aspect, though.</p>

<h3 id="ui-lifecycle-and-reconstruction">UI lifecycle and reconstruction</h3>

<p>A common consequence of reactive frameworks relying on constructed, region-allocated signal objects, is that re-running UI code means re-constructing all the objects. For example, consider this <em>correct</em> Fusion code:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="c1">-- Create a timer that smoothly increases from</span>
<span class="c1">-- the start of the program.</span>
<span class="kd">local</span> <span class="n">timer</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">everyFrame</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">dt</span><span class="p">)</span>
	<span class="n">timer</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="n">Fusion</span><span class="p">.</span><span class="n">peek</span><span class="p">(</span><span class="n">timer</span><span class="p">)</span> <span class="o">+</span> <span class="n">dt</span><span class="p">)</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1">-- Drive a sine wave using the timer.</span>
<span class="kd">local</span> <span class="n">wobble</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Computed</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="k">return</span> <span class="nb">math.sin</span><span class="p">(</span><span class="n">use</span><span class="p">(</span><span class="n">timer</span><span class="p">))</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1">-- Define a physically simulated follower that</span>
<span class="c1">-- will lag behind the sine wave with some inertia.</span>
<span class="c1">-- (i.e. this will depend on the object's own history)</span>
<span class="kd">local</span> <span class="n">laggard</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Spring</span><span class="p">(</span><span class="n">wobble</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Value</code>, <code class="language-plaintext highlighter-rouge">Computed</code> and <code class="language-plaintext highlighter-rouge">Spring</code> objects are constructed once, which is good - the <code class="language-plaintext highlighter-rouge">Spring</code> in particular is initialised to <code class="language-plaintext highlighter-rouge">wobble</code> at first, but then over time can refer to its own internal history to keep track of its own position and velocity.</p>

<p>Now, consider why this <em>very similar</em> Fusion code does <em>not</em> work:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="c1">-- Same as above.</span>
<span class="kd">local</span> <span class="n">timer</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">everyFrame</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">dt</span><span class="p">)</span>
	<span class="n">timer</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="n">Fusion</span><span class="p">.</span><span class="n">peek</span><span class="p">(</span><span class="n">timer</span><span class="p">)</span> <span class="o">+</span> <span class="n">dt</span><span class="p">)</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1">-- Same as above.</span>
<span class="kd">local</span> <span class="n">wobble</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Computed</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="k">return</span> <span class="nb">math.sin</span><span class="p">(</span><span class="n">use</span><span class="p">(</span><span class="n">timer</span><span class="p">))</span>
<span class="k">end</span><span class="p">)</span>

<span class="c1">-- All we did was wrap this in a Computed.</span>
<span class="kd">local</span> <span class="n">laggard</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Computed</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="c1">-- Every time `wobble` changes, this internal Spring</span>
	<span class="c1">-- object is re-created, so it has no memory of where</span>
	<span class="c1">-- it has been. So, it doesn't animate!</span>
	<span class="k">return</span> <span class="n">scope</span><span class="p">:</span><span class="n">Spring</span><span class="p">(</span><span class="n">use</span><span class="p">(</span><span class="n">wobble</span><span class="p">))</span>
<span class="k">end</span><span class="p">)</span>
</code></pre></div></div>

<p>This is a perfect illustration of how Fusion’s <code class="language-plaintext highlighter-rouge">Computed</code> objects break <a href="https://gafter.blogspot.com/2006/08/tennents-correspondence-principle-and.html">Tennent’s Correspondence Principle</a>. By wrapping an expression in a cache, we unintuitively cause it to wipe all knowledge of its own memory. This is a direct artifact of tying all history state to the lifetime of particular objects. At a deeper level, it’s a conflation between the lifecycle of objects in the UI, and the lifecycle of the function that renders them - Fusion is only designed for UI functions to run once.</p>

<p>Of course, Perfection - being an immediate mode framework - is designed to run its UI functions more than once. This forces a decoupling of those two lifecycles, so instead of storing state in fragile objects, it <em>has</em> to be stored at a stable named location. So this example fully works, with or without the <code class="language-plaintext highlighter-rouge">expensive</code> cache - it’s totally transparent.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="c1">// No need to manage this example timer ourselves,</span>
<span class="c1">// we are called every frame, so it just works.</span>
<span class="k">let</span> <span class="n">timer</span> <span class="o">=</span> <span class="n">current_frame</span><span class="py">.timer</span><span class="p">;</span>

<span class="c1">// Same wobble logic - just a sine wave.</span>
<span class="k">let</span> <span class="n">wobble</span> <span class="o">=</span> <span class="n">timer</span><span class="nf">.sin</span><span class="p">();</span>

<span class="c1">// Option 1: without the cache</span>
<span class="c1">// Name a storage location "my_spring",</span>
<span class="c1">// and save a spring there.</span>
<span class="k">let</span> <span class="n">laggard</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"my_spring"</span><span class="p">)</span><span class="nf">.spring</span><span class="p">(</span><span class="n">wobble</span><span class="p">);</span>

<span class="c1">// Option 2: with the cache</span>
<span class="c1">// It works exactly the same, because storage</span>
<span class="c1">// is decoupled completely from runs.</span>
<span class="k">let</span> <span class="n">laggard</span> <span class="o">=</span> <span class="n">ctx</span><span class="nf">.expensive</span><span class="p">(|</span><span class="n">get</span><span class="p">|</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">wobble</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">wobble</span><span class="p">);</span>
	<span class="k">move</span> <span class="p">|</span><span class="n">ctx</span><span class="p">|</span> <span class="n">ctx</span><span class="nf">.key</span><span class="p">(</span><span class="s">"my_spring"</span><span class="p">)</span><span class="nf">.spring</span><span class="p">(</span><span class="n">wobble</span><span class="p">)</span>
<span class="p">});</span>
</code></pre></div></div>

<p>(It’s worth mentioning that reactive signal frameworks could choose to do this too. As mentioned before, they’re two sides of the same coin. They just don’t, because it’s not the norm to name stable storage locations - they just use the object wrappers as storage.)</p>

<p>This property makes Perfection code a lot more fearless - instead of being precious about re-runs as a matter of correctness, it’s just a matter of performance. That frees you up to think about bigger issues, like…</p>

<h2 id="the-memory-tragedy-of-the-commons">The memory tragedy of the commons</h2>

<p>Ever since the 90s, computation has outpaced the speed of memory accesses. To smooth over this pressure, modern CPUs tend to have caches that are physically closer to the chip. Any cold memory accesses that miss the cache take on the order of thousands of times longer than a “usual” ALU operation operating directly on available registers.</p>

<p>Of course, that’s a slight simplification - there’s actually multiple caches (L1, L2, L3…) at various distances from the chip. Closer caches like L1 are much faster, but also much smaller.</p>

<p>As a result of all this, you <em>really</em> want to be economical with CPU cache lines. Anything that loads new things in from memory, by necessity, will evict older cached items - first down to lower caches like L1-&gt;L2 or L2-&gt;L3, but eventually it’ll evict it entirely.</p>

<p>This is why caching things on modern computers isn’t always a win. If the computation is sufficiently trivial, or the memory sufficiently cold, you can end up in situations where recomputing the thing from scratch is many times faster than waiting for the previously computed answer to be retrieved from memory.</p>

<p>That’s the obvious takeaway, anyway. There’s actually a more subtle angle you should consider too, which is that <em>even if something is locally faster to cache</em>, you might not want to, because <em>something else</em> benefits from the cache more than you do. That is: the more things that do reads from memory, the more things that may be evicted from the caches. By the time you get to some <em>critical</em> cached computation, you may end up with a fully wiped-out cache, meaning that super-important cache read takes orders of magnitude longer.</p>

<p>By caching more things to get <em>real, measurable local speedups</em>, the pressure imposed by the whole procedure worsens, and <em>raises the memory latency floor overall</em>. It’s a tragedy of the commons. (I’ll call it TOTC.)</p>

<p>The worst thing about this kind of TOTC is that it is basically un-attributable by normal profiling done on a per-function basis. Your function can measurably get faster, it can measurably take less frame time, whatever. But your profiler would be lying by omission to you; silently, in the background, the whole procedure quietly ticks up in overall execution time, most nefariously in functions you aren’t even explicitly profiling, or which you assume take “constant time”.</p>

<p>This TOTC is why it’s so important to choose what parts of your UI you want to cache, and why it’s so important for UI libraries to have thin, straightforward caching logic. It’s also exactly the place where Fusion fails with its reactive signals; the common practice is to wrap all derived values in <code class="language-plaintext highlighter-rouge">Computed</code>, which carries a whole bunch of bulky caching logic and environment setup logic with it.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="kd">local</span> <span class="n">itemName</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="s2">"Carrots"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">itemPrice</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="mi">150</span><span class="p">)</span>

<span class="c1">-- There is no reason to cache such a simple inline</span>
<span class="c1">-- computation, but Fusion will cache it for us anyway.</span>
<span class="kd">local</span> <span class="n">message</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Computed</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="k">return</span> <span class="s2">"Buy "</span> <span class="o">..</span> <span class="n">use</span><span class="p">(</span><span class="n">itemName</span><span class="p">)</span> <span class="o">..</span> <span class="s2">" for "</span> <span class="o">..</span> <span class="n">use</span><span class="p">(</span><span class="n">itemPrice</span><span class="p">)</span> <span class="o">..</span> <span class="s2">" coins"</span>
<span class="k">end</span><span class="p">)</span>
</code></pre></div></div>

<p>The only way to avoid it is to replace it with a plain closure, which still imposes some inherent overhead in Luau, which means you’re still competing for resources (though fewer).</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Written in Fusion, my old reactive signals framework</span>

<span class="kd">local</span> <span class="n">itemName</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="s2">"Carrots"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">itemPrice</span> <span class="o">=</span> <span class="n">scope</span><span class="p">:</span><span class="n">Value</span><span class="p">(</span><span class="mi">150</span><span class="p">)</span>

<span class="c1">-- You can't remove the closure, because you need</span>
<span class="c1">-- to keep the use() calls in case it ends up in</span>
<span class="c1">-- a higher-up `Computed` somewhere.</span>
<span class="kd">local</span> <span class="n">message</span> <span class="o">=</span> <span class="k">function</span><span class="p">(</span><span class="n">use</span><span class="p">)</span>
	<span class="k">return</span> <span class="s2">"Buy "</span> <span class="o">..</span> <span class="n">use</span><span class="p">(</span><span class="n">itemName</span><span class="p">)</span> <span class="o">..</span> <span class="s2">" for "</span> <span class="o">..</span> <span class="n">use</span><span class="p">(</span><span class="n">itemPrice</span><span class="p">)</span> <span class="o">..</span> <span class="s2">" coins"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And of course, Fusion’s various UI lifecycle problems only compound this, forcing you to use <code class="language-plaintext highlighter-rouge">Computed</code> and its relatives in very particular ways to achieve correct behaviour.</p>

<p>By comparison, Perfection is a breath of fresh air. If you don’t want to spend limited cache resources on something cheap to recompute… just recompute it. It’s immediate mode. There are no viral annotations.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Written in Perfection, my new immediate-mode UI library</span>

<span class="k">let</span> <span class="n">item_name</span> <span class="o">=</span> <span class="s">"Carrots"</span><span class="p">;</span>
<span class="k">let</span> <span class="n">item_price</span> <span class="o">=</span> <span class="mi">150</span><span class="p">;</span>
<span class="k">let</span> <span class="n">message</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"Buy {item_name} for {item_price} coins"</span><span class="p">);</span>
</code></pre></div></div>

<p>This gets towards one of the interesting things about the difference between reactive signals and immediate mode. The choice not to use objects in the latter isn’t just me wanting to avoid OOP, nor is it a debate about syntax. It’s really about a fundamental truth of code that adds these layers of indirection, and the way it behaves on the metal.</p>

<h2 id="proof-in-the-pudding">Proof in the pudding</h2>

<p>I would hope by this point, I’ve shown mechanically that the distinction between retained-mode signals and immediate-mode flip caches are negligible. Any computation that can be cached in one, can be cached in the other. Any efficiency or inefficiency follows from norms, not from an inherent limitation of the possibility space.</p>

<p>But if you remain unconvinced, here are some of the most performance-sensitive production projects which use immediate mode, and experience none of the conjectured issues.</p>

<h3 id="file-pilot"><a href="https://filepilot.tech/">File Pilot</a></h3>

<p>It’d be impossible not to lead with this. Using this file manager myself was a religious experience; I’ve never used a program so fast that hovering the buttons felt truly connected to my hand as I moved my mouse. It lets you scroll through <em>your whole C drive</em> as if it’s nothing.</p>

<p>It’s so fast that <a href="https://xcancel.com/vkrajacic/status/2048853124211752987">an internal Microsoft source claims it’s the new benchmark the in-box File Explorer is being measured against.</a> That’s a glowing review for a UI that’s built with - supposedly - the “slower” option.</p>

<h3 id="blick"><a href="https://blickeditor.com/">Blick</a></h3>

<p>This one’s a new one, demoed at Better Software Conference, but it’s staggeringly performant and well-put-together. Funnily enough for this post, it’s even got endorsements from both <a href="https://xcancel.com/cmuratori/status/2081778849885684020">Casey Muratori</a> (who coined imgui) and <a href="https://xcancel.com/nicbarkeragain/status/2081647191727493523">Nic Barker</a> (who made Clay)!</p>

<p><a href="https://xcancel.com/Blick_Editor/status/2082456045943689595#m">Just check out this clip of it absolutely eating up 30,000 clips for breakfast.</a></p>

<p>Video editors are some of the hardest pieces of software to make truly performant, so the dedication to fluidity is remarkable here. To say it’s competitive with Premiere and friends, is to give Premiere way too much credit.</p>

<h3 id="nomad-sculpt"><a href="https://nomadsculpt.com/">Nomad Sculpt</a></h3>

<p>This one’s notable because it runs on mobile hardware, which gives us a reference point for a complex bit of kit that’s not plugged into a wall. As it turns out; battery drain just isn’t a problem if you build it well.</p>

<p>It’s listed on <a href="https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui">Dear Imgui’s list of disclosed users</a> and is generally a really cool app. <a href="https://nomadsculpt.com/demo/">There’s even a web demo if you’re curious.</a></p>

<h3 id="blender"><a href="https://www.blender.org/">Blender</a></h3>

<p>Following the same train of thought, Blender has a huge presence in the 3D modelling world too, and it’s shown to be perfectly capable even for people just getting into modelling without workstation-level hardware.</p>

<p>In fact, it has a unique blend of immediate mode with damage redrawing, showing that immediate mode can be a perfectly principled solution even if you want to retain final pixels instead of refreshing them 144 times a second. Immediate mode logic can just be about solving lifecycle problems and keeping code simple!</p>

<h3 id="tracy"><a href="https://github.com/wolfpld/tracy">Tracy</a></h3>

<p>Profiles are one of the most data-dense UIs in existence, so it means something that one of the most popular and most performant ones runs on Dear ImGui. It renders timelines with <em>millions</em> of profiling zones at full framerate, completely smoothly. If ever there was proof that “running every frame” can scale incredibly well, well, here you go.</p>

<p>I use Tracy actively in the development of my voxel game <a href="https://caveygame.com/">Cavey</a> and I have never, <em>ever</em> looked back. It’s brilliant and I wouldn’t ever skip an opportunity to sing its praises.</p>

<h3 id="remedybg"><a href="https://remedybg.itch.io/remedybg">RemedyBG</a></h3>

<p>Staying in the world of high-performance dev tools for a second; this commercial Windows debugger sells itself as a super-fast alternative to more traditional tools like the Visual Studio debugger. It refreshes nearly instantly despite being composed of dense data fields and tons of text to be truncated all the time.</p>

<hr />
<h2 id="a-teaser-for-presentation">A teaser for presentation</h2>

<p>So - we’re doing immediate mode style UI. That means drawing the UI every frame. I want to introduce you to what the initial problem space looks like here, ahead of the next blog post.</p>

<p>Let’s bring it back to <a href="https://www.youtube.com/watch?v=DYWTw19_8r4">that Clay introduction video</a>, and pretend like we’re beginners and just wrote a <code class="language-plaintext highlighter-rouge">render_ui()</code> function with a bunch of calls to directly draw things on the GPU, all with hardcoded coordinates.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// Render a nice background.</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">104</span><span class="p">,</span> <span class="mi">120</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	<span class="c1">// Then, a bunch of labels stacked vertically.</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"File"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"New"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Open"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">52</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Save"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">76</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Upload"</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As Nic predicted in the video, we can see this isn’t going to work with all these numbers to remember and keep in sync. But instead of bailing out to a library like Clay, let’s actually make an effort to do all those calculations ourselves and see how bad it gets.</p>

<p>Since we’re just stacking things on top of each other, we could make it a <em>little</em> less impossible-to-modify by keeping a running total of our position and adding onto it. We’ll also throw in a few other variable names, too.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">item_height</span> <span class="o">=</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">104</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">120</span><span class="p">;</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"File"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"New"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Open"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Save"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
	<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="nf">draw_text</span><span class="p">(</span><span class="s">"Upload"</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And hey, that sure is a bunch of duplication. Maybe we should use a loop.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">render_ui</span><span class="p">(</span><span class="k">mut</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">UiContext</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">text_pad</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">item_height</span> <span class="o">=</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">text_pad</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_width</span> <span class="o">=</span> <span class="mi">104</span><span class="p">;</span>
	<span class="k">let</span> <span class="n">menu_height</span> <span class="o">=</span> <span class="mi">120</span><span class="p">;</span>
	<span class="nf">draw_quad</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">menu_width</span><span class="p">,</span> <span class="n">menu_height</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">DARK_BLUE</span><span class="p">);</span>
	
	<span class="k">let</span> <span class="k">mut</span> <span class="n">top_of_item</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="o">&amp;</span><span class="p">[</span><span class="s">"File"</span><span class="p">,</span> <span class="s">"New"</span><span class="p">,</span> <span class="s">"Open"</span><span class="p">,</span> <span class="s">"Save"</span><span class="p">,</span> <span class="s">"Upload"</span><span class="p">]</span> <span class="p">{</span>
		<span class="nf">draw_text</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">text_pad</span><span class="p">,</span> <span class="n">top_of_item</span> <span class="o">+</span> <span class="n">text_pad</span><span class="p">,</span> <span class="nn">Colour</span><span class="p">::</span><span class="n">WHITE</span><span class="p">);</span>
		<span class="n">top_of_item</span> <span class="o">+=</span> <span class="n">item_height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Okay, so the code <em>is</em> a little cleaner now. Some people, like Casey, do actually write some UIs pretty much like this - using incrementing variables and drawing as-you-go.</p>

<p>But by now, you can probably feel what Nic was saying. There’s still hardcoded stuff here, and we can’t fix that without solving some circular logic puzzles.</p>

<p>For example, it’s great that we implemented this automatic for-loop for the items, but we’ve still hardcoded the background. We <em>have</em> to draw the background before the text, so we need to know the width and height ahead of time. But <em>also</em> - we want the background to fit around the width and height of the text, which we haven’t rendered yet!</p>

<p>This is where most immediate mode UI schemes can start to feel a little weird or constrained, but it doesn’t have to be that way.</p>

<p>Join me in <a href="https://fluff.blog/2026/09/14/just-draw-it-you-dont-need-a-box-model.html">the next blog post</a>, where we’ll start plowing directly through this problem and uncover a satisfyingly insightful shape algebra that lets us render arbitrarily complex immediate mode UIs in practice.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Over the next three blog posts, I'm going to explore how I got there, justifying three key pillars.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/i-made-my-perfect-ui-library/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/i-made-my-perfect-ui-library/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">~200$ in, here’s what I think about Claude</title><link href="https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude.html" rel="alternate" type="text/html" title="~200$ in, here’s what I think about Claude" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude</id><content type="html" xml:base="https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude.html"><![CDATA[<p>I was first introduced to Claude at work (because I work for a Big Tech Company). Originally, I was pretty sceptical of anything to do with AI, and generally leaned negative on it due to ethical and moral concerns around the conduct of the companies involved.</p>

<p>To this day, I still have my reservations, though my position on IP law as a whole is relatively conflicted as I’m both opposed to its existence but also opposed to how it’s been trampled to hurt artists.</p>

<p>Anyway, that’s a topic for another post. I’m going to need to simmer for longer on that one.</p>

<p>Something strange happened last year. As people around me started seemingly using these systems <em>effectively</em> - something I didn’t know how they could even do - I was interested in learning more about how to use these systems for myself, so that I properly understood how people can evangelise these things so thoroughly, when from my perspective, they were mostly just semi-useful stochastic parrots.</p>

<p>So over the last week, I invested about $200 USD into creating a reference Vulkan renderer for <a href="https://caveygame.com/">Cavey</a>, my voxel game I’m working on for fun. There’s going to be no hyperbole and no glaze in this post; I sunk my own earned money into this thing, and in exchange for that money I expect results.</p>

<p>Let’s see how Claude did on my personal, army-of-one project. Interspersed between the anecdotes, I’ll share my thoughts on various parts of the process.</p>
<h2 id="baby-steps-porting-a-webgpu-renderer-to-vulkan">Baby steps: porting a WebGPU renderer to Vulkan</h2>

<p>If you know me, you know exactly how disillusioned I am about WebGPU. It’s a great <em>idea</em>, and certainly good enough for <em>many</em> projects, but it’s a very slow-moving graphics library with lowest-common-denominator features and a few painfully design-by-committee choices, like the endless annoyance of not using an industry-standard shader format - for strangely secretive reasons?</p>

<p>I had had enough. WebGPU wasn’t going to cut it for me or for this project, so it was time to move on. The obvious choice for my target spec device - the Steam Deck OLED - would be Vulkan, which natively exposes all of the features of that device (including HDR rendering, which I was particularly excited about). It’s more general while still being widely supported, but unfortunately also more complex as it foists more responsibility onto the programmer (with dubious payoff).</p>

<p>To start weaning Cavey off of WebGPU and towards Vulkan, I had built version 3 of Cavey’s high-level rendering abstractions to be <em>completely opaque</em>. Anything built on this renderer wouldn’t know what graphics library it was interfacing with. I had already set up some basic-enough plumbing to get a splash screen rendering under WebGPU, but nothing more:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/splash-screen.png" alt="Splash screen" /></p>

<p>Under the hood, the frontend API was simple and declarative; it’s a static render-graph-style API, just minimally flexible enough for a renderer that’d be GPU driven. Very much YAGNI-style:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span>
	<span class="n">frontend</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">RenderFrontend</span><span class="o">&lt;</span><span class="n">ChosenBackend</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">window_size</span><span class="p">:</span> <span class="n">PhysicalSize</span><span class="o">&lt;</span><span class="nb">u32</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span> <span class="p">{</span>
	<span class="nd">profiler_span!</span><span class="p">(</span><span class="s">"SplashScreenRenderer::new"</span><span class="p">);</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">g</span> <span class="o">=</span> <span class="nn">RenderGraph</span><span class="p">::</span><span class="nf">empty</span><span class="p">();</span>

	<span class="k">let</span> <span class="n">sync_logo</span> <span class="o">=</span> <span class="nn">SyncSource</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nf">load_splash_logo</span><span class="p">()</span><span class="o">?</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">logo</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.texture</span><span class="p">(</span><span class="nn">Texture</span><span class="p">::</span><span class="n">Uninit</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen Logo"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">size</span><span class="p">:</span> <span class="nn">TextureSize</span><span class="p">::</span><span class="n">D2</span> <span class="p">{</span> <span class="n">size</span><span class="p">:</span> <span class="k">Self</span><span class="p">::</span><span class="n">SPLASH_SIZE</span><span class="nf">.as_u16vec2</span><span class="p">(),</span> <span class="n">array</span><span class="p">:</span> <span class="mi">0</span> <span class="p">},</span>
		<span class="n">data_type</span><span class="p">:</span> <span class="nn">TextureDataType</span><span class="p">::</span><span class="n">Unorm8x4</span>
	<span class="p">});</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">SyncTexture</span> <span class="p">{</span> <span class="n">texture</span><span class="p">:</span> <span class="n">logo</span><span class="p">,</span> <span class="n">source</span><span class="p">:</span> <span class="n">sync_logo</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">sync_uniforms</span> <span class="o">=</span> <span class="nn">SyncSource</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nf">create_uniforms</span><span class="p">(</span><span class="n">window_size</span><span class="p">));</span>
	<span class="k">let</span> <span class="n">uniforms</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.buffer</span><span class="p">(</span><span class="n">Buffer</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen Uniforms"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">initial_content</span><span class="p">:</span> <span class="nn">BufferData</span><span class="p">::</span><span class="nn">zeroed</span><span class="p">::</span><span class="o">&lt;</span><span class="n">SplashScreenUniforms</span><span class="o">&gt;</span><span class="p">()</span>
	<span class="p">});</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">SyncBuffer</span> <span class="p">{</span> <span class="n">buffer</span><span class="p">:</span> <span class="n">uniforms</span><span class="p">,</span> <span class="n">source</span><span class="p">:</span> <span class="n">sync_uniforms</span><span class="nf">.clone</span><span class="p">()</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">surface</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.texture</span><span class="p">(</span><span class="nn">Texture</span><span class="p">::</span><span class="n">Surface</span><span class="p">);</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">Raster</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">binds</span><span class="p">:</span> <span class="nd">hashmap!</span> <span class="p">{</span>
			<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">RasterBind</span><span class="p">::</span><span class="n">UniformBuffer</span> <span class="p">{</span> <span class="n">buffer_id</span><span class="p">:</span> <span class="n">uniforms</span> <span class="p">},</span>
			<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">RasterBind</span><span class="p">::</span><span class="n">SampledTexture</span> <span class="p">{</span> <span class="n">texture_id</span><span class="p">:</span> <span class="n">logo</span> <span class="p">}</span>
		<span class="p">},</span>
		<span class="n">push</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">vertices</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">assembly</span><span class="p">:</span> <span class="nn">RasterAssembly</span><span class="p">::</span><span class="n">Triangles</span><span class="p">,</span>
		<span class="n">cull_back</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span>
		<span class="n">shader</span><span class="p">:</span> <span class="nn">RasterShader</span><span class="p">::</span><span class="nf">bundled</span><span class="p">(</span><span class="s">"splash_screen::vertex"</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="s">"splash_screen::fragment"</span><span class="p">)),</span>
		<span class="n">colour</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="n">RasterColour</span> <span class="p">{</span>
			<span class="n">target</span><span class="p">:</span> <span class="n">surface</span><span class="p">,</span>
			<span class="n">blend</span><span class="p">:</span> <span class="nb">None</span>
		<span class="p">}),</span>
		<span class="n">depth</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">draw</span><span class="p">:</span> <span class="nn">RasterDraw</span><span class="p">::</span><span class="n">FullScreenQuad</span>
	<span class="p">});</span>
	<span class="k">let</span> <span class="n">act_present</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.executable_act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">Present</span> <span class="p">{</span> <span class="n">surface</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">proc</span> <span class="o">=</span> <span class="n">frontend</span><span class="nf">.compile</span><span class="p">(</span><span class="n">g</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
	<span class="nf">Ok</span><span class="p">(</span><span class="k">Self</span> <span class="p">{</span> <span class="n">proc</span><span class="p">,</span> <span class="n">sync_uniforms</span><span class="p">,</span> <span class="n">act_present</span> <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Claude’s job here was to write a second <em>backend</em> for this API that would do <em>exactly</em> the same thing that the WebGPU backend did. This would not be a 1:1 translation however; Claude would have to reason about more than what the WebGPU code provided, for example handling the synchronisation between host and device, managing the swapchain itself, and handling resource state transitions, to name a few things.</p>

<p>It started on the 13th of March with a technical doc, written as a collaboration between myself and Claude on my laptop. I didn’t have a working desk setup as I had only just moved across the Atlantic:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-doc.png" alt="Migration doc" /></p>

<p>By the 25th of March, I had my desktop in basic working order and could hit the ground running. I put Claude in the driver’s seat and paid a finite amount of attention to what it was doing. My job was to specify what Claude should do and to direct high-level design decisions; I wasn’t particularly interested in specifics of how Claude chose to set up the device or other minutiae - those aren’t the bits worth caring about. Essentially, I was a principal engineer pair programming with a digital intern.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-claude.png" alt="Migration claude" /></p>

<p>The next day, we together had successfully burned through a massive number of the tasks, and by the end of the day, had recreated the splash screen fully, with some back-and-forth to resolve some of the bugs and edge cases that cropped up. All of our plans were documented in Markdown files and we ticked off each set of steps as we completed them.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-burndown.png" alt="Migration burndown" /></p>

<p>For context: this Vulkan transition is a leap I had put off for <em>months</em>, not just because I had to move countries, but because it was a mentally tiring / overwhelming task. I knew the <em>outline</em> of what needed to be done. I could do it if I bothered to, but I didn’t have the executive function to do it when there was always <em>something else</em> to be thinking about.</p>

<p>That said, working it out with Claude made all the difference, because of the way you have to use these bots to get value out of them.</p>

<p>Claude is not boundlessly superintelligent. I’d compare it to an adequately capable university student. If you ask Claude to synthesise whole codebases from nothing, it’ll do a <em>serviceable</em> job, but probably not a <em>good</em> job. Tick the boxes, hand in the assignment, leave it to bitrot.</p>

<p>Whenever I’ve worked with Claude at my day job, I’ve figured out that really, <em>you’re still the software architect, because Claude can’t be.</em> At the end of the day, the latent space of possible code generations is far more populated with bad solutions than good ones as a pure statistical fact (there are more ways to screw up code than there are ways to write it correctly), so your job is to bias the code generation in a direction that lands you in a good part of the latent space, for what your definition of “good code” is.</p>

<p>So, I’ve learned to essentially “code in English” by precisely and comprehensively specifying what the software should do, chunking it down into tiny bite-sized steps, and getting Claude to <em>just do them</em>, with no grand architecture or plan in mind - essentially, doing <a href="https://caseymuratori.com/blog_0015">Semantic Compression</a> but with a robot.</p>

<p>It turns out this approach is good for both humans and for AI. By breaking down the problem into manageable chunks, I could better understand what steps needed to be taken, and Claude could expose areas where I didn’t specify enough at the desired level of detail. For those gaps, I would go out and find lectures on YouTube, or blog posts on the internet, and synthesise a strategy to come back to Claude with and finish the plan.</p>

<p>It’s a kind of synergy where Claude points out the problems, I figure out the solutions, and Claude fixes the problem with my solution.</p>

<p>It worked really well. I got the thing ported in a day. I would happily use Claude for this again.</p>

<h2 id="extending-further-building-a-slang-voxel-ray-tracer">Extending further: building a Slang voxel ray tracer</h2>

<p>I wasn’t about to build out the whole render graph API with no project to apply it to. Instead, I was fully intending to build the railroad as the train was moving.</p>

<p>So, I dove straight into Claude’s main assignment: <em>building out a new reference ray-tracing renderer for my voxel game.</em></p>

<p>I took a similar approach; asymmetric pair programming with comprehensively planned documents prepared upfront. However, I did something slightly different this time; I <em>also</em> got Claude to do a round of research specifically into 64-trees and fast voxel ray tracing, so that I could come up with a good acceleration structure using them. I specifically asked for evidence to back each technique and approach it researched, which yielded good results out of the gate:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/render_hq_spec.png" alt="Render HQ spec" /></p>

<p>By April Fool’s Day, we had set up a simple ray-traced scene, ready for integrating these ideas into. I didn’t have to recall any of my memorised equations or look up the Ray Tracing books online; Claude is one of the best math equation recallers in the world, even if it struggles to actually do the math. (Just like a uni student, one might say…)</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/ray-traced-scene.png" alt="Ray traced scene" /></p>

<p>The next day, I had it implement a routine for tracing a single 64-bit 4x4x4 chunk, and then had it extend to a whole contiguous world. At this stage, it was still completely separate from the rest of the game, so this terrain wasn’t “real”, but it was good enough to test the ray tracing approaches we had co-designed. Performance was great:</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/PBa2MQ3WkQc?si=MYuW6V8CgwAxDe1s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mikyaoducs2n" data-bluesky-cid="bafyreidirktjnl5ugvojdwmst3sg4xtfvwnfazrdiq4e5rz3peutqi4siy" data-bluesky-embed-color-mode="system"><p lang="en">Runs at 90fps on Deck, pretty goooood<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mikyaoducs2n?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mikyaoducs2n?ref_src=embed">2 April 2026 at 22:26</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>By the very next day, we had planned and executed on the steps necessary to wire in actual voxel data from the rest of the game. At this point, I identified some latent bugs in the code and struggled a little bit to get Claude to fix them.</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/U_wN9_yTFiE?si=vvF4PfiIAUIAmACH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Here’s where we talk about Claude’s tendency to go off the rails sometimes. Much like a naïve uni student, it will often try and hack its way around a problem rather than taking the principled path, and oftentimes will run with a completely unjustified diagnosis that can often end up being completely wrong.</p>

<p>That is to say, you should not trust these things to be correct. They’re very productive idiots.</p>

<p>Throughout this process, I have spent a good amount of time intervening in what Claude is doing. I’m quick to interrupt and quick to correct, and if I feel like something has gone far off the tracks, I know how to rewind to an earlier point in the conversation and nuke the code changes.</p>

<p>I’ve often had to remind Claude that we’re allowed to build out the <code class="language-plaintext highlighter-rouge">rengraph</code> API, or that things are meant to happen on the GPU and should not be synced back to the CPU, or that recompiling the graph every frame defeats the point of having a static compiled graph and that it should prefer declarative solutions.</p>

<p>Some of that is wisdom, some of that is taste. Claude needs it to stay on the rails.</p>

<p>This is why I say that you can’t let Claude be completely in the driver’s seat. Left alone, it will make counterproductive choices that don’t fit into a larger vision of the system, especially across chat compaction boundaries or across sessions. Pretty much: enjoy Claude’s memory of what it did while it remembers because it will be gone in an hour’s time.</p>

<h2 id="end-game-a-beautiful-reference-renderer">End game: a beautiful reference renderer</h2>

<p>Anyhow, with the core ray tracing routine set up, it was time to start decorating the voxel scene and working towards something with the graphical fidelity needed to produce reference renders for assisting in future rendering decisions.</p>

<p>On the 4th of April, we started experimenting with porting the old renderer’s UV mapping code to render albedo colours. There were bumps in the road because Claude hadn’t kept track of the different coordinate spaces, so for a while, the UVs were all camera relative:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/uv-bug.jpg" alt="UV bug" /></p>

<p>The bug was quickly identified and fixed, and not long after, we had incorporated the normal and AO maps alongside albedo to get a more complete-looking scene with very little difficulty.</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mipqg3hoic2k" data-bluesky-cid="bafyreiezqmjd7xe2jjwmcnpisufekgrjvzo76clqqmxuusmhofngyip2ye" data-bluesky-embed-color-mode="system"><p lang="en">Now rendering albedo, normal and combined AO/emissive maps in the ray tracing path! Still no water pass as that&#x27;s more complex to pull off.

Next up: going to start setting up a proper tonemapping pipeline and get some antialiasing in there.<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mipqg3hoic2k?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mipqg3hoic2k?ref_src=embed">4 April 2026 at 19:49</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>I also got Claude to port over the “smart bevels” code that would bend the normals of texels that lie along the exposed edges of blocks. This worked flawlessly first try with no planning needed.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/smart-bevels.png" alt="Smart bevels" /></p>

<p>From there, I turned my attention to tonemapping. I had Claude do a bunch of research into which swapchain format should be used for HDR, and how to query parameters like peak display brightness.</p>

<p>I often find myself having extended conversations with Claude about technical decisions like these. At first, Claude advocated for the use of PQ with physically-based luminance values, which I respected, but ultimately didn’t see much of a point in pursuing as I had less use for declaring absolute nit values for my renderer. Instead, I pushed back and advocated to Claude that the scene should internally be rendered with physically sensible lighting intensities, but that the output doesn’t have to be calibrated for a reference display; it just needs to look good on people’s real-world devices, which meant having a properly calibrated paper-white in the scene.</p>

<p>We ended up settling on scRGB with the Uchimura tonemapping curve applied per-channel for simplicity, parameterised by the properties of the display so that HDR-capable devices like the Steam Deck could punch above paper-white while SDR displays would look reasonably consistent, minus the shine.</p>

<p>Simultaneously, I took the opportunity to tell Claude to implement a jittered accumulation buffer for some antialiasing, as an input to the tonemapping pipeline:</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mirgvfvl3c2o" data-bluesky-cid="bafyreihwfum52nk7xkqueilmpqakoh2y5xbknxuz4544pxel2zqty624fi" data-bluesky-embed-color-mode="system"><p lang="en">Accumulating multiple samples per pixel in the ray traced path now. (Not TAA, no reprojection - just to get cleaner reference stills.)<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mirgvfvl3c2o?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mirgvfvl3c2o?ref_src=embed">5 April 2026 at 12:04</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>Up until this point, it had all been smooth sailing. Now for the biggest challenge so far; building out a physically based lighting model.</p>

<p>My end goal here was not necessarily photorealism, but instead physical plausibility. I wanted to avoid lighting “hacks” as much as possible in my renderer, so that it would be easy to progressively enhance or swap the lighting techniques later down the line without interfering with content authored for older lighting systems. Having a physically plausible renderer would also aid in handing off assets between programs; for example, allowing me to author the PBR textures externally.</p>

<p>The goal then was to implement a Monte Carlo integrator that would sample the whole hemisphere of lighting, combine it with a physically based material to attenuate incoming light rays, then customise it to taste with a variety of stylistic tweaks, most notably a pixelation effect to match the art style of the game.</p>

<p>I started by getting Claude to implement random hemisphere sampling to capture the effect of sky light, plus a sample towards the sun for clear directional shadows. When discussing the physical correctness of this, Claude was able to justify it as a kind of Next-Event Estimation. At this point, we were firmly leaving the realm of full-framerate rendering and moving towards “interactive offline rendering”.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/simple-rt-sunlight.png" alt="Simple RT sunlight" /></p>

<p>Once combined with a physically based material, this already produced some very pleasant effects; for example, by lowering material roughness, you could get nice wet-looking surfaces with smooth specular effects. Claude handled the microfacet equations here; I asked it to review the old PBR implementation and it pointed out some deficiencies that could then be trivially fixed. The results look good to my eyes, but I didn’t inspect quite so carefully.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/shiny-stone.png" alt="Shiny stone" /></p>

<p>At this point, I started hitting my stride with Claude, going through the plan and execute loop at great speed (and expense). I regularly hit the daily limit, and just started spending into extra usage to keep the velocity up. I think it’s worth talking about what happened there.</p>

<p>When it comes to all of these lighting concepts, I’m more than familiar with them. I’ve spent a very long time thinking about lighting models for Cavey, and more generally have been interested in light transport for a long time now; coming up on half a decade.</p>

<p>However, I always found that ray traced lighting techniques quickly escape from the realm of easy intuiting into the realm of abstract maths and more difficult reasoning. You quickly start running into integrals and clever sampling/reuse tricks that ultimately suck a lot of the accessibility out of these lighting methods, except to those who sink a lot of time attempting to implement them and get familiar.</p>

<p>This is where I found Claude really helps. Because Claude has a bunch of knowledge baked into itself, it can easily execute on any lighting technique you know to ask about, and since you can ask Claude clarifying questions, it can also patiently explain the mechanics of how they work. I found this invaluable.</p>

<p>What’s more, since there’s pretty reasonably clear answers on how to build up these kinds of “physically correct” lighting models, Claude got a whole lot better at doing what it was supposed to do first try, so long as I kept it on the rails architecture-wise. The region of latent space associated with malfunctioning PBR equations seems to be smaller than the region associated with badly-structured code.</p>

<p>Back to the results. By sampling the block hit by the hemisphere ray for emissive colour and doing a secondary sunlight check, a single bounce of sunlight could be trivially added, which helped to fill in a lot of missing ambient light. Claude did this on my behalf with no trouble.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/sun-bounce.png" alt="Sun bounce" /></p>

<p>I then told Claude to port over the physically-based Hillaire sky model I had previously implemented for the old renderer, but with a twist; it should adjust the constants so that they match the new physically-correct lighting values in this renderer. It got it right on its second try; the first try made the sky a few hundred times too bright, which I had to report to it diligently.</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mislfitxys27" data-bluesky-cid="bafyreiew26cefvxg4kfzf2e3szz3lp34pokchb6xzpvkaiiryia6fgbndm" data-bluesky-embed-color-mode="system"><p lang="en">the new reference renderer is so good<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mislfitxys27?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mislfitxys27?ref_src=embed">5 April 2026 at 22:57</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>That pixelated sun effect was the result of me and Claude going back-and-forth for about an hour debating what the best way of pixelating the sun disc would be. The general idea was established from the start; sample at multiple points, tonemap to predict what the on-screen colour would be, average to get antialiasing, then inverse tonemap to return back to physically-accurate values. Claude went down a rabbit hole with it and started introducing overcomplex tuning factors, but eventually I just told it to stop and reason from first principles. That pretty much directly led to the current zero-parameter solution, which looks the best and preserves its antialiased look no matter how intense the sun is.</p>

<p>I also briefly mentioned that sunsets were too bright, at which point it was able to immediately identify that the Mie scattering coefficient was too high. With approximately no effort on my part, sunsets immediately skyrocketed in visual quality to now looking the best they ever have. This is code that I will for-sure be reusing for the full-performance renderer.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/sunset.png" alt="Sunset" /></p>

<p>Auto-exposure and physically based bloom were next in line. These required a lot of manual tweaking by myself in order to get right, and will still require yet more tweaking over time. However, Claude was able to get functioning versions of these right off the bat, even if they were not initially tuned well.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/bloom-exposure.png" alt="Bloom &amp; exposure" /></p>

<p>And finally, right as I was approaching my monthly spend limit I had set for this project, I implemented atmospheric fog, which was the icing on the cake. Claude executed on this flawlessly, and I did yet more manual tuning after the fact just to tone down the effect to my liking.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/volumetrics.png" alt="Volumetrics" /></p>

<p>As a small disclaimer; since the purpose of this project was to produce a reference renderer, these last steps (especially volumetrics) really sent the performance over the edge for the Steam Deck. Even my 4090 in my desktop could only muster around 20 frames a second here, so I wouldn’t take this as Claude revolutionising real-time graphics or anything. However, for the purposes which I set out to utilise it for, this is now a highly competent and tuned reference renderer which I can use to set up further visual testing.</p>

<h2 id="conclusions">Conclusions</h2>

<p>My feelings on AI coding agents is no longer that they are incapable of producing good work. My questions are now primarily about what exactly the speed-up is, practically, for realistic teams.</p>

<p>For individuals like myself with clear direction and autonomy, it seems pretty clear that these tools let you work at the speed of design, rather than the speed of code. For these kinds of small teams without much bureaucracy, I have no doubt that code is some degree of bottleneck, and these tools do realistically offer good speedups, even if you don’t succumb to the “vibe-coding exponentials” and instead pursue a pair programming approach.</p>

<p>That said, I’m still unconvinced that code is truly the bottleneck at many organisations. I can see value in Claude being able to deliver cheaper prototypes more quickly as discussion artifacts when getting alignment across teams, but I’m not sure how much of this translates to faster release processes. How does Claude speed up a sign-off from Legal?</p>

<p>Overall though, this experience has shown me that I need to use the tools that I have an opinion about. There’s two groups of people who I think are about to be badly hurt by the progression of this technology. One of those groups is obviously the investor-hype types that grandiosely overstate what this tech can do. But the other group belong to the staunchly moralistic anti-AI school of thought, denying outright that this tech can do anything useful at all.</p>

<p>I understand the moral, ethical and legal reservations, and do still share them. I’m not running to this tech, exactly. That said, I don’t see any fundamental reason why there couldn’t be a morally, ethically and legally <em>legitimate</em> version of this tech if it were developed and trained by authors that properly (and auditably) handled training data, and which developed it as a public good rather than as a tool of profit.</p>

<p>This is not technology I would bet against. Say what you want about the companies (and I have a lot of choice words for them), but there is some sense in which coding has changed. Just maybe don’t listen to the hype lords, either - go use this tech for yourself and find your own voice.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I was first introduced to Claude at work (because I work for a Big Tech Company). Originally, I was pretty sceptical of anything to do with AI, and generally leaned negative on it due to ethical and moral concerns around the conduct of the companies involved.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Flip caches &amp;amp; information loss</title><link href="https://fluff.blog/2026/01/07/flip-caches-and-information-loss.html" rel="alternate" type="text/html" title="Flip caches &amp;amp; information loss" /><published>2026-01-07T00:00:00+00:00</published><updated>2026-01-07T00:00:00+00:00</updated><id>https://fluff.blog/2026/01/07/flip-caches-and-information-loss</id><content type="html" xml:base="https://fluff.blog/2026/01/07/flip-caches-and-information-loss.html"><![CDATA[<p>I’ve been exploring ways of introducing caching and incremental recomputation to immediate-mode UI systems, in line with a new information loss principle I’m thinking about.</p>

<h2 id="vdom">VDOM</h2>

<p>For a decade now, I haven’t been a fan of UI approaches that involve comparing trees of elements. Often, these are referred to as “virtual DOMs” or VDOMs (this is terminology inherited from the web).</p>

<p>My main criticism of these approaches is that they throw away granularity information that exists at the time of building the VDOM. By design, the VDOM assumes that everything can change between re-renders of UI, and so must compare everything to retrieve the actual minimal set of changes desired.</p>

<p>If the original builder of the VDOM specified the minimal change they wanted, this work is totally avoidable. In its absence, even a simple property change can balloon into a lot of work done by the CPU just to re-infer the information.</p>

<h2 id="fine-grained-reactivity">Fine-grained reactivity</h2>

<p>My goal when building Fusion (my fine-grained reactivity framework) five years ago was to eliminate this re-inference step. Instead of comparing trees of elements, my idea was to explicitly allow bindings to be made between properties of elements and objects in the code. If the object receives a change, the property is updated.</p>

<p>The key innovation of the system was to allow objects to bind to other objects. You could define computations that take the current state of some objects and returns a newly bindable state. When used in a UI, this established a granular chain of cause-and-effect through the UI, and allowed for changes to be applied directly without re-inferring anything.</p>

<p>However, this still didn’t satisfy me completely. The most obvious issue is a colouring problem; code that is written “reactively” has more ceremony than code which is written “traditionally”. The cost to the programmer of managing a fleet of objects required supporting concepts in memory management and helper utilities for improving ergonomics.</p>

<p>There’s also a more subtle cost in frameworks like this; they tend to over-memoise information by their nature. Since every object is a binding, every object has a state that needs to be synchronised to its users. This typically means the object is a state container. As a result, you effectively memoise every causal node in the system, which often means memoising every piece of dynamic state, even trivially inferrable ones. The cost of this memory has been shown to be notable in practice - it would be more ideal to create a scheme that more naturally encourages recomputation of trivial states rather than universal memoisation.</p>

<h2 id="the-principle-of-information-loss">The principle of information loss</h2>

<p>From my experience, using fine-grained reactivity has led to almost universally more predictable and favourable performance compared to VDOM systems. Many of the highest-performance frameworks today adopt similar principles.</p>

<p>But why is it better? In my eyes, it’s because it moves memoisation work closer to the subject being memoised.</p>

<p>Since the memoisation done by the reactive objects can take advantage of information available in the local context, they’re able to track changes granularly without re-inferring any missing information. The bindings and objects involved are directly and trivially known.</p>

<p>Attempting to apply this principle, I would extrapolate that the best memoisation techniques are likely ones which deduplicate calculations “as they happen” - parts of the computation which wish to reuse historical data can elect to do so on their own, in the same path of computation that returns the result of the computation. To the end user, the computation simply provides a result - caching details are encapsulated in some kind of context side-effect.</p>

<p>That’s what I’ve been exploring next, as part of a reductive approach I call “flip caches”.</p>

<h2 id="flip-caches">Flip caches</h2>

<p>Let’s consider an example of a gallery app. We want to show a bunch of photos with different widths and heights in a list view, similar to Google Images.</p>

<p>Our state may look like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Photo</span> <span class="p">{</span>
	<span class="n">width</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>
	<span class="n">height</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>
	<span class="n">image_content</span><span class="p">:</span> <span class="n">Texture</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">UIState</span> <span class="p">{</span>
	<span class="n">photos</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s write a naive immediate-mode function that implements a wrapping layout.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	 <span class="c1">// Break photos onto rows bounded by screen width</span>
	<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="p">{</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>

		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
			<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
				<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="p">}</span>
			<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
		<span class="p">}</span>
		<span class="n">photo_rows</span>
	<span class="p">}</span>

	<span class="c1">// Render rows of photos scaled to fill screen width</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
		<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every redraw event, we compute the layout in full, then directly send instructions to the renderer to draw textures where we want them. This is stateless and does not memoise, which makes it very easy to work with, but discards a lot of information. That’s especially bad when working with expensive layouts that involve many elements.</p>

<p>Let’s assume for demonstration that the <code class="language-plaintext highlighter-rouge">photo_rows</code> computation is a hotspot (perhaps it’s allocating too much, don’t ask!). Instead of immediately reaching for a VDOM or fine-grained reactivity, we could instead keep a cache between rebuilds. Maybe it looks like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">PhotoRows</span><span class="p">(</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;&gt;</span><span class="p">);</span>

<span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">,</span> <span class="n">cache</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Cache</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	 <span class="c1">// Break photos onto rows bounded by screen width</span>
	<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="n">cache</span><span class="py">.get_or_init</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoRows</span><span class="o">&gt;</span><span class="p">(||</span> <span class="p">{</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
			<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
				<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="p">}</span>
			<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
		<span class="p">}</span>
		<span class="nf">PhotoRows</span><span class="p">(</span><span class="n">photo_rows</span><span class="p">)</span>
	<span class="p">});</span>

	<span class="c1">// Render rows of photos scaled to fill screen width</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
		<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The idea here is that <code class="language-plaintext highlighter-rouge">cache.get_or_init::&lt;T&gt;</code> only runs the inner closure once, then it saves the returned <code class="language-plaintext highlighter-rouge">T</code> and uses it for all subsequent calls. (The type exists to allow for multiple different pieces of data to be independently cached in a type-inferable way.)</p>

<p>This cache solves our memoisation problem for this expensive layout computation without requiring a deep structural change to any code. Since the mechanism is generic, it can encapsulate as much or as little of the code as profiling indicates is necessary.</p>

<p>For example, if constructing commands for the renderer was also expensive, that could be memoised instead:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">PhotoDrawInstructions</span><span class="p">(</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Draw</span><span class="o">&gt;</span><span class="p">);</span>

<span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">,</span> <span class="n">cache</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Cache</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	<span class="k">let</span> <span class="n">instructions</span> <span class="o">=</span> <span class="n">cache</span><span class="py">.get_or_init</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoDrawInstructions</span><span class="o">&gt;</span><span class="p">(||</span> <span class="p">{</span>
		<span class="c1">// Break photos onto rows bounded by screen width</span>
		<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="p">{</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>

			<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
				<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
				<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
				<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
					<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
					<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
				<span class="p">}</span>
				<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
				<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="p">}</span>
			<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
			<span class="p">}</span>
			<span class="n">photo_rows</span>
		<span class="p">}</span>

		<span class="c1">// Render rows of photos scaled to fill screen width</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">instructions</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
			<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
			<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
				<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
				<span class="n">instructions</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
				<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="p">}</span>
			<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="nf">PhotoDrawInstructions</span><span class="p">(</span><span class="n">instructions</span><span class="p">)</span>
	<span class="p">});</span>

	<span class="k">for</span> <span class="n">inst</span> <span class="k">in</span> <span class="n">instructions</span> <span class="p">{</span>
		<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="n">inst</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The desirable property of this solution is that it obeys Tennant’s Correspondence Principle: memoising a passage of code does not require restructuring as fine-grained reactivity does. The default assumption made is that computations are trivial (which they often are).</p>

<p>Of course, the problem now is cache invalidation, but we have existing techniques to address this.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">UIState</span> <span class="p">{</span>
	<span class="k">fn</span> <span class="nf">set_photos</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">photos</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">self</span><span class="py">.photos</span> <span class="o">=</span> <span class="n">photos</span><span class="p">;</span>
		<span class="k">self</span><span class="py">.photos_changed</span> <span class="o">=</span> <span class="k">true</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="c1">// ... later ...</span>

<span class="k">if</span> <span class="k">self</span><span class="py">.photos_changed</span> <span class="p">{</span>
	<span class="n">cache</span><span class="py">.clear</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoDrawInstructions</span><span class="o">&gt;</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To implement such a cache, I would use a “flipping” method to allow read access to the previous version of the cache while allowing write access to a fresh version of the cache. This is why I use “flip caches” to describe this technique; it only requires space for two <code class="language-plaintext highlighter-rouge">T</code> allocations, plus a little book-keeping in the cache object itself, and flips between these allocations for predictable performance.</p>

<h2 id="why-i-think-this-could-be-useful">Why I think this could be useful</h2>

<p>I haven’t yet implemented a fuller UI system using flip caches, but I like the solution because it essentially filters rebuild events for closures which still rebuild in an immediate-mode fashion. There is no colouring problem like fine-grained reactive signals have.</p>

<p>Additionally, flip caches avoid the information loss problem of VDOM techniques; since the caching happens “inline” in the build function, diffing is unnecessary - the renderer receives the draw instructions it needs directly.</p>

<p>Of course, there’s one caveat to all of this, which is that re-constructing trees of components every rebuild could become expensive; a forest of small components can add up in a way that isn’t true of fine-grained reactive systems, which construct their trees once.</p>

<p>I’m interested to explore this system further to see whether higher-level abstractions can be built from this, to reach similar ergonomics to fine-grained reactive systems. In particular, I’m interested in orthogonal techniques to do with cache invalidation patterns, and how these caches could perhaps carry incidental UI state such as animation timelines.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve been exploring ways of introducing caching and incremental recomputation to immediate-mode UI systems, in line with a new information loss principle I’m thinking about.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/flip-caches-and-information-loss/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/flip-caches-and-information-loss/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Solving memory management</title><link href="https://fluff.blog/2025/10/17/solving-memory-management.html" rel="alternate" type="text/html" title="Solving memory management" /><published>2025-10-17T00:00:00+00:00</published><updated>2025-10-17T00:00:00+00:00</updated><id>https://fluff.blog/2025/10/17/solving-memory-management</id><content type="html" xml:base="https://fluff.blog/2025/10/17/solving-memory-management.html"><![CDATA[<p>I have devised a pretty exciting memory model for <a href="https://wolf.phfox.net/">Wolf</a>. If it works, it should be provably safe, scale close-to-optimally, and add zero language complexity.</p>

<p>I recommend you <a href="https://www.rfleury.com/p/enter-the-arena-talk">go watch this revelatory talk by Ryan Fleury.</a> The chain of logic they describe in that talk reflects pretty much my entire history with memory management up to today. I’ll continue this post assuming you know what I’m talking about.</p>

<h2 id="the-joy-of-arenas">The joy of arenas</h2>

<p>Back in 2023, while working on Fusion, <a href="https://fluff.blog/2023/08/30/the-next-ten-years-beyond-maids.html">I rediscovered arena allocation independently in a GC language.</a> Without anyone else ever having explained the concept to me, I immediately saw just how much more powerful structured memory management is, and how it leads to programs that are both trivial to reason about, and also fully deterministic.</p>

<p>It’s so utterly powerful that I managed to implement (what Fusion calls) <em>time-travelling checks</em> to predict use-after-frees, which have been a revelation for catching edge cases that I otherwise would have missed.</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/QTTRtzZxew4?si=lDU5XbiDLXRWNVKH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>It turns out these ideas are not original in the slightest! Ryan Fleury, Casey Muratori, Johnathan Blow and the rest of the gang have been out there for years singing the praises on arena allocators, and my lived experience dealing with all of this is all the proof I need that these ideas are every bit as good as each one of them said they were.</p>

<p>I love arena allocators. They’re everything I’ve loved about “maids” in the Roblox ecosystem for years, and they’re almost word-for-word identical to “scopes” (which are really just properly-done arenas that run destructors instead of freeing memory).</p>

<h2 id="the-rub">The rub</h2>

<p>There’s only one problem with arena allocators. It’s not even really <em>that much</em> of a problem, but it’s still worth mentioning IMO.</p>

<p>Arenas are still visible in your code. (Boo hoo, right? <em>Unusable.</em>)</p>

<p>When I designed “scopes” for Fusion, that was a feature - it declared a code path’s intent to allocate, and allowed you to declare their lifetime explicitly in your code, preventing whole classes of bugs. For any language that exists today, I think it’s the right approach.</p>

<p>But really, I’ve been looking for a way to make code <em>look</em> every bit as clean as garbage collection, while using a deterministic structured approach to memory management that can be analysed ahead of time and trivially checked for safety. I don’t know if today’s languages are up to that task without doing some gross things that I’d never recommend over simply <em>writing the damn thing</em>.</p>

<h2 id="wait-im-writing-a-language-already">Wait, I’m writing a language already</h2>

<p>Yeah, turns out I’m the benevolent dictator of my own still-in-flux language. And, turns out I’ve already been stewing on this problem in a different <em>context</em>.</p>

<p>One of the top features I’ve wanted in Wolf (my language) are implicit function parameters, aka <em>context</em>, so that you don’t have to do “prop drilling” to transiently pass relevant wide-area information down to deeply nested functions. I got the idea from my frontend UI experience, ironically enough - it’s a feature I implemented and loved in Fusion, which itself was somewhat stolen from React. (Hey - React’s useful for something!)</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/dOkoXYCpxMM?si=KCSyNQ2fVZeIASlH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>My <em>original</em> line of thinking here was that it’d be <em>really nice</em> if you could contextually define an arena allocator to use inside of a function, a bit like how Jai (Johnathan Blow’s language) lets you do the same thing. That would make it a lot more ergonomic to have dynamically-sized literal constructors in Wolf - no need for any special syntax to tie them to a specific arena allocator. It also makes all functions generic over allocation strategy by default, which is <em>rad</em>.</p>

<p>However, I realised something pretty nice about Wolf. Since it’s a highly declarative programming language without things like mutation, pointers or references baked deeply into it, everything is lexically scoped. In theory, that makes the whole language pretty compatible with stack allocation, as you might find by default in C. It’s not a 100% solution, but a good 80% solution.</p>

<p>I was… half satisfied. Then I thought a little harder about what stack allocation <em>really</em> is.</p>

<h2 id="oops-all-arenas">Oops! All arenas</h2>

<p>Yeah, turns out stack allocators are just mini little arena allocators, aren’t they?</p>

<p>This is about when I realised that Wolf could have a really nice generic allocation strategy that does the same thing as a stack allocator, but doesn’t prescribe use of a stack per se. You just implicitly create a scratch allocator for each block, and if it passes any values out of itself, then you implicitly parameterise the block with an allocator to use for those values.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-- Pseudocode
fn [.return_alloc : allocator] (
	let local_alloc = allocator
	-- Allocated temporarily for the duration of the block
	let needs_memory = malloc [.name "steve", .age 27] in local_alloc
	-- Important: use the return allocator here
	let will_return_this = malloc [.name "baxter", .age 5] in return_alloc
	free local_alloc -&gt; will_return_this
)
</code></pre></div></div>

<p>Via induction, since all Wolf programs are composed from blocks, you can now guarantee that all memory is allocated and deallocated correctly (if sometimes conservatively, and with some extra nuances/implementation details that aren’t relevant here).</p>

<p>There’s even more nice things where that came from, too. You don’t strictly have to use one allocator per block; you could use as many as you like, wherever you like, because none of those details actually appear anywhere in the syntax.</p>

<p>For example, you can determine when the last time a value from an allocator is used in a block, and at that point, you can choose to drop the allocation right away. Alternatively, you could choose <em>not to</em> if you locally know it’ll get dropped by an allocator in a higher lexical scope. In other words, you can fit your arena allocator lifetimes more tightly or loosely to your program depending on context, all completely automatically.</p>

<p>What’s more, you could decide to use a right-sized allocator just for the parts of the block where the sizes of values are statically known, and use a more flexible allocator for the unbounded dynamically-sized things like user strings. (The fact that Wolf intentionally tries to be as compile-time-analysable as possible helps massively here.)</p>

<p>Oh right, flexible allocators for dynamically sized things… shoot.</p>

<h2 id="who-manages-the-manager">Who manages the manager?</h2>

<p>This model of memory management in Wolf works perfectly well <em>in theory</em>, when you have arenas that are <em>assumed to be</em> growable and flexible, and when you have infinite memory space.</p>

<p>In reality, you get a finite amount of virtual memory address space, and of that, a dismal fraction that’s available as physical memory.</p>

<p>At this point, I was thinking about allocation strategies for arena allocators that needing to be dynamically growable, or perhaps having a dedicated allocator <em>just</em> for growable arrays of some kind, which could then be used as a springboard for implementing dynamically sized arena allocators.</p>

<p>I was decently happy with that idea. That’s about when I discovered Ryan’s talk.</p>

<h2 id="theres-hardware-for-that-dummy">There’s hardware for that, dummy</h2>

<p>I had overcomplicated the whole thing because I wasn’t thinking about how the OS actually manages virtual address space. You can use the hardware’s MMU to just… reserve tons of contiguous memory and forget about it. It’s not like it’s going to be backed by any physical memory until you actually use it, so it’s free real estate.</p>

<p>So, the way Ryan describes the technique: make every arena allocator gigantic in virtual memory, then just forget about it. Sure, you’ll have a maximum arena size, but it’ll be something like 64 GB. No need to sweat over it when it’s orders of magnitude larger than the data your program is dealing with, and when you have 256 TB of virtual address space to carve up for arenas. Plus, this is where statically knowing the size of things can really come in clutch.</p>

<p>Ryan says that this gives you ~O(1) allocation and deallocation, all super close to the metal. I’m inclined to believe it, though I’m not about to repeat it without citing Ryan as I haven’t run through the whole analysis myself.</p>

<p>Zero overhead allocation and deallocation with fully implicit memory management that’s as clean as garbage collected code. I’m dreaming, right?</p>

<h2 id="remaining-considerations">Remaining considerations</h2>

<p>So obviously, I’m super jazzed about this. From my very brief searches online, I haven’t really found a language that does all of this together, in the way I’m describing here. Wolf may well be (one of) the first languages setting foot in this territory.</p>

<p>However, I still have a few lingering thoughts on my mind that I haven’t yet cleared up:</p>

<ul>
  <li>I suspect that I shouldn’t <em>fully</em> rely on the MMU to reserve gobs of virtual address space. I might implement a hybrid solution that chains <em>large-enough</em> blocks of contiguous space instead (something like 1GB chunks?) which might play better on platforms that don’t have as many bits of virtual address space.</li>
  <li>I’m also not entirely convinced yet that I can escape doing allocator management this way. I’m still thinking about ways of potentially carving up the address space in a way that can be used for allocating arenas of various sizes, just in case. I don’t really know.</li>
  <li>I’m also clear-eyed that people may still want to implement more specialised allocation techniques. My idea is that arena allocators are a perfectly good implicit language feature, and that through arena allocation, it should be trivial to manage the lifetime of custom allocators and their reserved memory. So I’m not thinking about them too hard right now.</li>
</ul>

<p>Overall though? I might have just <em>solved memory management</em>.</p>

<p>I’ve defeated that damn garbage collector.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have devised a pretty exciting memory model for Wolf. If it works, it should be provably safe, scale close-to-optimally, and add zero language complexity.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/solving-memory-management/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/solving-memory-management/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Isn’t there someone you forgot to ask?</title><link href="https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask.html" rel="alternate" type="text/html" title="Isn’t there someone you forgot to ask?" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask</id><content type="html" xml:base="https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask.html"><![CDATA[<p>I was motivated to write about my philosophy towards other people.</p>

<h2 id="the-golden-rule">The golden rule</h2>

<p>Simply: I don’t get to control what you think. I don’t get to make demands of you. If it is within the law, you are allowed to do that thing if you really want to. I won’t stop you if you’re determined. You are a human with autonomy and reasons for being the way you are.</p>

<p>Will I still have an opinion on you doing certain things? Sure thing. It’s not illegal to think about whether I would do what other people are doing. But you don’t have to ask for permission from me, you live your best free life and I’ll sit over here allowing you to do that.</p>

<p>I grade people primarily on one axis only: whether they treat others as humans or not. I want to live in a world where we see the humanity in each other.</p>

<h2 id="why-this-is-my-rule">Why this is my rule</h2>

<p>I’m a highly non-prescriptive person in general. I don’t want to be involved in what people around me do in their private lives or their private time, unless we agree to be involved in something. If all parties are consenting, it’s not my business, and I don’t feel inclined to know. That’s one reason.</p>

<p>But more broadly - I understand that we have a process for outlining things that shouldn’t be done, and that process is democracy. If I wanted to stop people from doing something because I was concerned about its impact, that’s the path I would pursue.</p>

<p>Systemic problems demand systemic solutions, not individual ones. The idea that a society of individual choices are responsible for the world’s largest problems isn’t sound to me; governments are bottlenecks through which all legal behaviour must pass. It’s the clear place to target when trying to solve problems, especially ones where responsibility is amorphous and hard to pin down. Make it blameless instead - institute a system that outlines what shouldn’t be done. If you fail to do that, then find out how to do it next time.</p>

<p>I ultimately think the behaviour of individuals should be driven by what they think they should do, not what they’re told to do by others. If I want to reduce the amount of meat in my diet, that’s wonderful and I should go do that. But there’s no logical reason why it’s the solution to a systemic problem; people don’t work like that.</p>

<p>So, if I want others to eat less meat, I can offer a <em>recommendation</em> or a <em>reason</em>, but if they say no, then that’s their choice and it would be improper to override it. It would be especially improper to treat them badly over it, publicly shame them, make people feel bad for associating with them… you get the idea.</p>

<p>It’s almost always the case that there’s a reason people are compelled to be a certain way. Their reasoning may not be <em>sound</em> or <em>rational</em>, but people don’t generally get out of bed in the morning to make the world a more miserable place. I often say that there’s a nugget of truth in every opinion, you just have to work through the dressing to find it.</p>

<p>This generally leads towards my big idea of how we should make change in the world. It’s not by dictating to others what is good for them, regardless of how obvious I think it is. It is instead by listening first, understanding what they think and believe, identifying those nuggets of truth, and figuring out where to go from there. That’s how you build a base around an idea and transform that into change.</p>

<p>That’s true even of the people I disagree with the most vigorously today. I don’t begrudge anyone, because people are complex mosaics of attributes, not single dimensional beings.</p>

<h2 id="why-im-concerned">Why I’m concerned</h2>

<p>It feels like a fair few people don’t do this anymore because they don’t believe in these ideas anymore. It’s mistaken for ideas that aren’t helpful, like centrism or conformism. I think that’s a dying shame that the difference isn’t known better.</p>

<p>It often feels like people demand others to do things for them, or be cast out. Believe in the same ideas we do. Conform to what we tell you to do. They attend gatherings where important speakers present romantic visions of a world where dissent is quieted. To some small extent, perhaps they even pride themselves on how they treat some others badly.</p>

<p>That’s not a phenomenon that’s boxed into a certain sect of society, by the way. There’s endless talks about extremes on a spectrum, but society is far more higher-dimensional than that. It’s not about simple politics or social causes, it runs deep into even the most niche topics where there isn’t unanimity. I’ve seen it in enthusiast spaces where the stakes are supposedly zero.</p>

<p>We have forgotten how to talk about our significant differences in <em>any</em> space on <em>any</em> topic. The moment I realised this, I saw it everywhere. It changed the way I view the world.</p>

<h2 id="what-im-doing">What I’m doing</h2>

<p>As you may have seen, I’m not on social media, except for YouTube. I don’t think I need to explain how it relates to this concept; it’s far too easy for people to walk in the door and shit on your plate. I don’t want to be part of the global town square if that’s what’s on offer.</p>

<p>I much prefer tighter, closer connection. When I get to know you, and we become friends, and we do more things together, I think that motivates people to place more of a value on humanity. When my friends come out with things I can’t accept, being close to them places a cost on rejection and encourages me towards listening. The same logic works when my friends disagree with me, too.</p>

<p>In general, this has made my life <em>much</em> more pleasant. By taking this stance, I haven’t compromised on how I think about the world at all. Instead, the opposite has happened: I feel like I have more conviction in my view of the world, as I understand more about the other people who live in it. I’ve become softer and less demanding of others, and it’s brought me closer to a lot of people in my life.</p>

<p>It feels like it’s been a long road of personal development to understand this. There was a time where I was insufferable and made myself miserable for no reason.</p>

<p>Writing this is a bit like documenting what I’ve learned since then, I suppose. It turns out life can be sunnier if you find the humanity in others. It’s not hard, you just have to believe a little.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I was motivated to write about my philosophy towards other people.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/isnt-there-someone-you-forgot-to-ask/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/isnt-there-someone-you-forgot-to-ask/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why I’m writing a programming language</title><link href="https://fluff.blog/2025/09/17/why-im-writing-a-programming-language.html" rel="alternate" type="text/html" title="Why I’m writing a programming language" /><published>2025-09-17T00:00:00+00:00</published><updated>2025-09-17T00:00:00+00:00</updated><id>https://fluff.blog/2025/09/17/why-im-writing-a-programming-language</id><content type="html" xml:base="https://fluff.blog/2025/09/17/why-im-writing-a-programming-language.html"><![CDATA[<p>I’m doing the thing people tell you not to do. Wolf is my small, smart programming language.</p>

<p>You’ll likely start with a very valid question…</p>

<h2 id="why">Why?</h2>

<p>I don’t believe we’re at the end of programming language history. We haven’t found the perfect language yet (does that even exist?) so why not spend time exploring more of the space? Sure, I’m not an academic, and absolutely daft, but I figure there’s still fun to be had in the process of exploring the possibility space.</p>

<p>In particular, there’s a few ideas that excite me terribly:</p>

<h3 id="true-consistency">True consistency</h3>

<p>Most programming languages are at least influenced by the syntax and semantics of C.</p>

<p>I think that C is a perfectly fine programming language! Basic perhaps, but somewhat fine. However, I think that most languages designed in C-style aren’t really <em>consistently designed</em> languages. Things that are ostensibly similar look different on the page, and things that are ostensibly different look similar on the page.</p>

<p>After a decade+ of working with the Lua language, I know how much I love a simple mental model. I wonder what that could look like when applied to a language’s syntax.</p>

<p>I want a language where you can point to language features <em>visually</em>, by pattern matching with your eyes.  Instead of building towering complexes of features, you should be able to hold a model of the whole language in your head, and it should map 1:1 with each syntax item.</p>

<h3 id="incremental-recomputation">Incremental recomputation</h3>

<p>This is probably the headline thing I think about these days. After years working on reactive signal libraries, I’m desperately tired of all the extra syntax cruft.</p>

<p>I often think about immediate-mode programming. Conceptually, it’s far and away the best choice for expressing logic clearly. The only drawback of it is that blowing away a bunch of computation just to do it all again can be incredibly wasteful, especially when you need to perform more complex layout reflow for example.</p>

<p>Why can’t a compiler optimise that?</p>

<p>Of course, the answer defies simple explanation - side effects, mutation, blah blah. But that’s just why we can’t do it <em>in existing languages</em>. What would a language built for that look like?</p>

<p>Imagine you didn’t just run through a whole function every time you needed to update the result of a previous calculation. What if you could just re-run the part that’s actually relevant?</p>

<p>Right now, I fully believe that something like this is the bridge we need between immediate-mode and retained-mode logic.</p>

<h3 id="memory-management">Memory management</h3>

<p>After implementing scopes in Fusion, I’m pretty furious we ever spent so much time on garbage collection. Structured memory management is so obviously superior it’s not even funny.</p>

<p>The problem is that doing structured memory management in Lua is also so obviously against the grain. Lua has no facilities for doing it ergonomically, because it’s built on the philosophy that garbage collection will handle almost everything for you.</p>

<p>Simultaneously, I’m a pretty happy Rust user, but I’m not about to defend Rust’s memory management approach either. Lifetimes are hell, and the language as a whole is confusing and unintuitive to learn if you’re not familiar with its concepts.</p>

<p>When I listen to Graydon Hoare and Casey Muratori, I wonder just how many trivial memory management techniques we’re missing when we think in terms of RAII and lifetimes.</p>

<p>Could you make a language that trivialises memory management by taking a broader view of the problem?</p>

<h3 id="open-objects">Open objects</h3>

<p>It seems really common these days to define objects as rigid nominal types with <em>only</em> a defined set of data on them.</p>

<p>Perhaps you’re making a 3D modelling program and you want to store some vertex data:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">vertices</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vertex</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">Vertex</span> <span class="p">{</span>
	<span class="n">position</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">normal</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">uv</span><span class="p">:</span> <span class="n">Vec2</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then perhaps, the tool has a vertex painting mode, where you want each vertex to <em>additionally</em> have a vertex colour and alpha.</p>

<p>These things logically live on the vertex, so you <em>could</em> put them there:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Vertex</span> <span class="p">{</span>
	<span class="n">position</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">normal</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">uv</span><span class="p">:</span> <span class="n">Vec2</span><span class="p">,</span>
	<span class="n">colour</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">alpha</span><span class="p">:</span> <span class="nb">f32</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But what if you wanted that vertex colour and alpha to come from an interchangeable “vertex colour map”? You’d have to store them externally:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">VertexColourMap</span> <span class="p">{</span>
	<span class="n">vertices</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">VertexColour</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">VertexColour</span> <span class="p">{</span>
	<span class="n">colour</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">alpha</span><span class="p">:</span> <span class="nb">f32</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’m going to apply a somewhat-arbitrary transformation to this code. I’m going to switch the data around to be stored in SOA format, rather than AOS.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span>
	<span class="n">normals</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">uvs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec2</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">VertexColourMap</span> <span class="p">{</span>
	<span class="n">colours</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">alphas</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">f32</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice that, by performing this switch-around, we’ve actually flattened down one of the dimensions of this problem. No matter whether we store the data in <code class="language-plaintext highlighter-rouge">Mesh</code> or <code class="language-plaintext highlighter-rouge">VertexColourMap</code>, we do not need to change the data structure at all:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The data is stored the same way (!)</span>
<span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span>
	<span class="n">normals</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">uvs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec2</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">colours</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">alphas</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">f32</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This led me to a <em>really cool</em> insight - what if you could build objects by joining together fields like this?</p>

<p>Instead of having a “mesh struct” with a rigid structure, what if an object could be formed from any fields you wanted to view together? In a system like that, you could easily define extra fields for an object in your own namespace, and transparently associate them with data that comes from elsewhere in the codebase.</p>

<p>Instead of “closed objects” that can’t be extended by others, you’d have “open objects” that are just syntax sugar for array accesses anywhere in memory.</p>

<p>I view this as the ultimate expression of the open-closed principle, or perhaps just the logical conclusion of entities from ECS. Objects become mere IDs - things you use to access fields.</p>

<p>What would a language built around “open objects” look like? Could open objects be made efficient? Where would they fall short?</p>

<h2 id="introducing-wolf">Introducing Wolf</h2>

<p>To pursue these questions, late last year I decided to pull the trigger and push the first commit to a repository called <code class="language-plaintext highlighter-rouge">wolf</code>.</p>

<p><img src="/assets/posts/why-im-writing-a-programming-language/initial-commit.png" alt="The initial commit for the Wolf repository." /></p>

<p>Back then, I didn’t intend to actually implement the language I was designing. All I wanted was to <em>explore</em> what me and my good friend Trey thought “our ideal language” could look like.</p>

<p>In that time though, I’ve ruthlessly scrapped and rethought a ton of ideas. In the time that some people may have completed a whole traditional language, I’ve just barely figured out how the basic constructs of the language should even work, and even then I’m not entirely sure.</p>

<p><img src="/assets/posts/why-im-writing-a-programming-language/i-havent-written-much.png" alt="The &quot;Design&quot; page on the Wolf website. It only covers basic language features" /></p>

<p>That said, beyond the pages, there’s a few truths I seem to have landed on in my head:</p>

<h3 id="wolf-works-like-maths">Wolf works like maths</h3>

<p>Wolf is designed along the grain of mathematics. Equals is equals - there’s no assignment or mutation by default.</p>

<p>In that sense, Wolf is firmly placed in the camp of immutable functional languages built atop the lambda calculus, but not for any of the traditional reasons - I didn’t even know what the lambda calculus was before I started.</p>

<p>I don’t necessarily agree that mutation and assignment are fundamental operations that should appear in normal logic. I think most logic should be stateless, and when you’re writing stateless logic, you <em>shouldn’t</em> need mutation in theory. (The lambda calculus backs this up!)</p>

<p>The real reason I think we reach for mutation is because functional syntax sucks. Recursion is not an ergonomic replacement for a <code class="language-plaintext highlighter-rouge">for</code> loop in <em>many</em> cases. Why haven’t we tried to close that gap?</p>

<p>So, to the maximal extent possible, Wolf should let you express stateless functions the same way you express maths - as substitutable expressions.</p>

<h3 id="wolf-mutates-but-carefully">Wolf mutates, but carefully</h3>

<p>The last section may have given you the impression that I’m a Haskell enjoyer (more of an F# guy tbh), but I’ve omitted most of the story.</p>

<p>You see, I don’t think mutation is something to be avoided. After all, our computers are <em>basically</em> just overgrown Turing machines. To deny mutation in a programming language is to invent a new alternate reality.</p>

<p>Instead, I simply view things in reverse to most languages. In my head, mathematics is the base of logic, and mutation/imperative style is an <em>extension</em> to basic logic that allows it to interact with an event-based model of the outside world.</p>

<p>From that angle, I think it is perfectly rational for Wolf to support mutable dynamic objects, because they’re a reality of programming a Turing machine. The only key difference is that I think it should be tightly managed, so that both humans and machines can figure out how these dynamic objects and mutation events affect the execution of the program.</p>

<p>One particular mistake I’m not keen to re-enact is the idea of allowing unmanaged memory allocations and frees. I see no reason for there to be a “global allocator”, and have no desire to implement things like borrow checking or RAII.</p>

<p>Why are we trying so hard to make objects manage themselves internally, when we could just manage them better from the outside? We already know about concepts like arenas, object pools and slab allocators. These things trivialise memory management, but I don’t blame people for not using them when many vogue languages don’t elevate them to a prime position.</p>

<p>In Wolf, I want all dynamic features to route through structured memory management techniques like these, rather than random vibes-based resource allocations or garbage collectors.</p>

<p>But importantly, it shouldn’t feel like you’re managing memory at all - it should be transparent, almost implicit in what you’re writing. After all, it’s maths, right?</p>

<h3 id="wolf-is-ruthlessly-smart">Wolf is ruthlessly smart</h3>

<p>I think static analysis is non-negotiable in this day and age for good autocomplete and program optimisation. I don’t want another dynamic scripting language.</p>

<p>So, Wolf is designed to be 100% statically analysed, type-checked and optimised. The language avoids features that would require dynamic or run-time analysis to check the correctness of, or which lead to optimisation footguns.</p>

<p>Instead of methods, Wolf prefers pipelining and freestanding functions. When you can pipe an object into a function, you don’t need methods anymore. I already write most of my code using freestanding functions today in all programming languages I work in, and it makes modularity so much easier to achieve because it obeys the open-closed principle naturally.</p>

<p>Instead of polymorphism, Wolf prefers switching based on statically-available type information. This sort of thing should work a lot better in Wolf, because it’s built without a lot of the concepts that make indirection necessary in other languages. We know that random memory access is especially expensive when factoring in things like cache misses, so why depend so heavily on it?</p>

<p>Instead of dynamic types, Wolf prefers inferred types. If you define a minimum amount of information about your interfaces, that the compiler should be able to propagate through your implementations. Inference should be a forwards process, since code is written forwards, and that unlocks the best possible autocomplete and error messages for unfinished code.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Wolf is still extremely early in its life. It doesn’t even have a reference implementation, or even a completed design. Mostly, it’s just ideas, but I have some conviction that these ideas are good to pursue further.</p>

<p>Long-term, I want to bridge the gap between lightweight scripting languages and powerful systems languages - a kind of systems-language Lua that threads the needle between expressivity and performance.</p>

<p>I don’t make any promises that this project goes anywhere, but if you see me making commits, then you know what I’m up to. Wish me luck on my misguided programming language adventures :)</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m doing the thing people tell you not to do. Wolf is my small, smart programming language.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/why-im-writing-a-programming-language/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/why-im-writing-a-programming-language/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Towards dedicated Luau development</title><link href="https://fluff.blog/2025/05/02/towards-dedicated-luau-development.html" rel="alternate" type="text/html" title="Towards dedicated Luau development" /><published>2025-05-02T00:00:00+00:00</published><updated>2025-05-02T00:00:00+00:00</updated><id>https://fluff.blog/2025/05/02/towards-dedicated-luau-development</id><content type="html" xml:base="https://fluff.blog/2025/05/02/towards-dedicated-luau-development.html"><![CDATA[<p>I’m deciding to refocus my personal time on projects in the broader Luau ecosystem and my personal projects, rather than Roblox.</p>

<p>This is a strange post to write - Roblox tool development has served me incredibly well. I’ve been lucky enough to make a great thing out of plugin and package development; it’s covered my rent every month and it’s been consistent for me. If I were a ruthless capitalist, perhaps I’d ride out the income until it dried up.</p>

<p>But I respect you more than that.</p>

<p>The truth of the matter is that, since starting my day job last summer, I’ve found myself immersed in Roblox day in and day out. It’s great work! I enjoy it massively. But I don’t wish to take it back home with me.</p>

<p>Instead, I’ve found greater inspiration in the world of things that live outside of the engine. Whether that’s learning about voxel engines with Daydream, or thinking about the future of UI and declarative programming with Fusion, I’ve realised that’s what I want to spend my time on.</p>

<p>So I’m going to quit while I’m ahead. My Roblox projects continue to be massively successful, but I’m happy with where they’ve gone and what they’ve inspired in others. I like to say that “enough” is the most powerful word in the world; I think I have found “enough”.</p>

<h2 id="what-happens-next">What happens next</h2>

<p>I plan to wind down Studio Elttob, which has been the brand under which all of my Roblox-only projects have lived.</p>

<p>This will involve opening up most of its products, cancelling in-progress projects, and rehousing some others.</p>

<ul>
  <li><strong>Cancelled:</strong> The Elttob Atmos and Elttob InCommand plugins have been cancelled.
    <ul>
      <li>There was so much in the in-progress builds of Atmos that I was genuinely proud of! - but I don’t feel compelled to work on these projects at this stage.</li>
    </ul>
  </li>
  <li><strong>Cancelled:</strong> The VOLUMIKA 2 project has been cancelled.
    <ul>
      <li>While this was already somewhat functional, I struggled with performance issues for a long time, and those haven’t been resolved.</li>
    </ul>
  </li>
  <li><strong>Opened up:</strong> The following paid products will enjoy extended support until <strong>1 September 2025</strong>, at which point they’ll be opened up to the community so that everyone can benefit from them. This includes:
    <ul>
      <li>Elttob Relight</li>
      <li>Elttob Reclass</li>
      <li>All Classic Suite plugins</li>
      <li>VOLUMIKA</li>
    </ul>
  </li>
  <li><strong>Continuing:</strong> Fusion will continue development, but:
    <ul>
      <li>Studio Elttob branding will be removed from the project.</li>
      <li>The FusionKit for Roblox project has been cancelled.</li>
      <li>Fusion’s Roblox APIs will continue to be supported, but will be modularised over time to allow Fusion to be used for general purpose Luau development.</li>
    </ul>
  </li>
  <li><strong>Opened up:</strong> Vanilla 4 for Roblox Studio will be opened up:
    <ul>
      <li>Vanilla 4 for Roblox Studio will be opened up to the community, but new icons won’t be designed.</li>
      <li>The Vanilla compiler, previously internal and proprietary, will be open sourced.</li>
    </ul>
  </li>
  <li><strong>Going private:</strong> Vanilla itself will be taken private:
    <ul>
      <li>Plans for a commercial Vanilla icon set have been dropped.</li>
      <li>The Vanilla icons will return to being a personal icon set for my own projects.</li>
    </ul>
  </li>
</ul>

<h2 id="my-new-priorities">My new priorities</h2>

<p>With Studio Elttob going away, I can sharpen my focus on my remaining projects.</p>

<h3 id="front-burner-daydream">Front burner: Daydream</h3>

<p>Daydream can be traced all the way back to some of my earliest Roblox projects back in 2012/13. Since then, it’s enjoyed multiple transformations:</p>

<ul>
  <li>Becoming the first infinite voxel engine on Roblox capable of Minecraft-scale worlds in 2016</li>
  <li>Having its technology repurposed for the monumentally successful Blox in 2019</li>
  <li>Developing into a highly-optimised engine for Blox Survival in 2021</li>
  <li>Being spun off of Roblox into the standalone Project Stockholm, now called Daydream, where it is today!</li>
</ul>

<p>In many ways, Daydream is the one project I’ve wanted to work on for the longest. It’s also the most educational project I’ve ever worked on; originally used to learn about the basics of voxel engines, then teaching me so much more about high-performance Luau and game optimisation, and now serving as a vehicle for me to learn computer graphics, GPU programming, and low-level engine development.</p>

<p>I’m more motivated to work on Daydream now than I’ve ever been before. I love it so much.</p>

<p>So, I’m now focusing my efforts on Daydream first and foremost, to give it the time of day that it deserves. I’ll continue to work on the Daydream YouTube series so that I can educate more people about the myriad of interesting computer science problems that arise from it. Perhaps one day, it’ll even be something you can play! But I’m not thinking that far ahead yet.</p>

<h3 id="back-burner-fusion">Back burner: Fusion</h3>

<p>I still believe in Fusion as the ultimate companion for Luau development and remain committed to providing support for it. I also still intend to take Fusion further than where it is today, though I can’t say that I’ll do so on an expedited schedule.</p>

<p>I think Fusion is overall in a good place after the push to get 0.3 out the door. From here, things look to mostly be performance work and ergonomic tweaks, and some project restructuring.</p>

<p>As a little treat; I currently plan to use Fusion and Luau inside of Daydream! I want to build a complete non-Roblox UI using it. I’ve even been floating the idea of rebuilding the whole documentation website in Fusion one day, but that’s more of a wishful thought.</p>

<h3 id="and-thats-it">And that’s it</h3>

<p>All of my other existing projects aren’t going to be actively developed. That doesn’t mean they’re over, and it doesn’t mean I won’t start more of them - it just means I won’t touch any particular project unless I have a motivation to.</p>

<p>I’m making a point of paring down my active projects to exactly two, because that’s where I think I’m happiest right now. If I don’t make progress on other projects, I’m fine with that. Daydream and Fusion are where I want to push right now.</p>

<p>This doesn’t mean I’m leaving the Roblox community, by the way. I have a <em>whole lot</em> of Roblox knowledge and I’m incredibly interested in the future of the platform. I want to help build Roblox up to be the best place it can be, so you’ll still see my username around.</p>

<p>It’s just that the user in question will be a hobbyist, not a full time creator :)</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m deciding to refocus my personal time on projects in the broader Luau ecosystem and my personal projects, rather than Roblox.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/towards-dedicated-luau-development/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/towards-dedicated-luau-development/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Framerate considered harmful</title><link href="https://fluff.blog/2025/01/21/framerate-considered-harmful.html" rel="alternate" type="text/html" title="Framerate considered harmful" /><published>2025-01-21T00:00:00+00:00</published><updated>2025-01-21T00:00:00+00:00</updated><id>https://fluff.blog/2025/01/21/framerate-considered-harmful</id><content type="html" xml:base="https://fluff.blog/2025/01/21/framerate-considered-harmful.html"><![CDATA[<p>I’ve spent the past little while moving Daydream away from framerate counters and towards “frame budgets”. Here’s why.</p>

<p><img src="/assets/posts/framerate-considered-harmful/frame-budget.png" alt="Daydream using frame and tick budgets." /></p>

<p>The primary reason is that it’s hard to properly read frame rates, because they are actually a reciprocal unit; to double your framerate, you must halve the amount of time taken to process a frame.</p>

<p>This can be fine in isolation - defining a “target framerate” is still a valid concept, because it’s reasonable to want to serve someone, say, 120 frames per second.</p>

<p>But when you try and compare frame rates with each other, or track frame rate through development, you’ll quickly find that framerate can be misleading. Targeting 240 FPS, but your prototype is hitting 2000 FPS? Plenty of headroom, surely - but actually, you’re already using 12% of the frame!</p>

<p>That’s why I’m moving away from the use of <em>framerate</em> as a debug metric. It just isn’t relevant during development. Instead, I’m measuring what percentage of the frame budget is being used, which I’ve set at 240 FPS for Daydream (since I have a good graphics card, and I feel like it should be possible).</p>

<p>Having a linear, easy-to-interpret percentage on-screen at all times makes performance usage visible and understandable in a way that - personally - I find framerate doesn’t offer. I derived the idea from a friend that used to work at Unity and Halfbrick; when they talk about performance, they always point to two things: set a budget early, and make expensive things visibly expensive.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve spent the past little while moving Daydream away from framerate counters and towards “frame budgets”. Here’s why.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/framerate-considered-harmful/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/framerate-considered-harmful/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>